KogenCredo.Integrity (KogenCredo v0.1.0)
View SourcePure helpers that keep the quality gates themselves honest.
Two independent tools live here. Neither runs Credo, touches git or writes files; callers pass source text or already-read file contents in.
Suppression scan
scan_suppressions/2 finds comments that switch a gate off: Credo pragmas
(# credo:disable-for-this-file, …-next-line, …-previous-line,
…-lines:N), Sobelow skips, coverage ignores and # dialyzer/# noqa style comment
pragmas. Only comments are scanned (attributes such as @dialyzer or @tag :skip are not
comments; see KogenCredo.Check.Warning.SkippedTest and friends for those). Comments are taken from the real tokenizer
(Code.string_to_quoted_with_comments/2), so a pragma-looking line inside a
string or heredoc is not reported. A file that does not parse yields
{:error, reason}; it is never reported as "no suppressions".
The default patterns are deliberately generic. Pass :patterns as a list of
{kind, ~r/regex/} to change what counts as a suppression.
Golden-config comparison
compare_credo_config/3 compares a trusted golden Credo config against the
one in the working tree and reports what was removed, disabled, loosened or
excluded. parse_config/1 reads a .credo.exs without evaluating it: only
literal terms, module aliases, ~r sigils and the %{}/list/tuple forms
Credo configs use are accepted, so a config cannot execute code while being
compared. Anything else (function calls, variables, File.read!, several top-level
expressions, an invalid regex) returns {:error, {:unsupported, snippet}}; a config containing an unsupported expression is therefore never reported as "unchanged". assert_matches_golden!/3
raises on any parse error, and compare_sources/3 returns it as a value.
Plugins, requires and checks added by code are not evaluated; a config that depends on them
is reported as unsupported when they appear as non-literals. Literal requires files and
plugins that the actual config adds are code Credo would load and run, so they are reported
(:requires_added, :plugin_added) and never pass as "unchanged". The comparison also reports
removed plugins and files.included entries and removed configs. A config whose checks,
plugins, requires or files have a shape Credo does not accept is
{:error, {side, :not_a_credo_config}}, never an exception. Limits: the comparison is
structural. Contents of referenced files are outside this comparison. It cannot tell whether a changed
parameter value is stricter or looser (it reports :params_changed and
leaves the judgement to the reviewer), and checks enabled through plugins
or requires code are visible only if they are listed in checks. Other top-level keys
(for example parsers) are not compared.
Summary
Types
A module written in the config, by name (see KogenCredo.Integrity.ModuleRef).
Functions
Raises ExUnit.AssertionError-style failure text when the configs differ.
Compares an actual Credo config against a golden one.
Parses both sources and compares them. Returns {:ok, differences} or the first parse error
({:error, {:golden | :actual, reason}}); an unreadable policy is an error, never "no change".
The default suppression patterns, as {kind, regex}.
Parses the text of a Credo config file into a term without evaluating code.
Scans many files given as %{path => source}. Returns
{suppressions_by_path, errors_by_path}; files that fail to parse are in
the second map and are never silently dropped.
Scans one source text for suppression comments.
Types
@type difference() :: {:config_removed, config :: String.t()} | {:plugin_removed, config :: String.t(), term()} | {:plugin_added, config :: String.t(), term()} | {:requires_added, config :: String.t(), term()} | {:requires_removed, config :: String.t(), term()} | {:inclusion_removed, config :: String.t(), term()} | {:check_removed, module_ref()} | {:check_disabled, module_ref()} | {:params_changed, module_ref(), golden :: term(), actual :: term()} | {:exclusion_added, config :: String.t(), term() | {module_ref(), term()}} | {:strict_disabled, config :: String.t()}
@type module_ref() :: KogenCredo.Integrity.ModuleRef.t()
A module written in the config, by name (see KogenCredo.Integrity.ModuleRef).
@type suppression() :: %{line: pos_integer(), kind: atom(), text: String.t()}
Functions
Raises ExUnit.AssertionError-style failure text when the configs differ.
Returns :ok when compare_credo_config/3 finds nothing. Meant for a test:
Integrity.assert_matches_golden!(File.read!("test/golden/credo.exs"), File.read!(".credo.exs"))
@spec compare_credo_config(term(), term(), keyword()) :: [difference()]
Compares an actual Credo config against a golden one.
Both arguments are config terms as returned by parse_config/1 (the
%{configs: [...]} map, a single config map, or a list of configs).
Configs are matched by :name (default "default"). Returns a list of
difference/0; [] means the actual config keeps everything the golden
one has. Extra checks in the actual config are fine.
Reported:
{:config_removed, config}— a golden config has no counterpart (by name) in actual{:plugin_removed, config, plugin}— a goldenpluginsentry is gone{:inclusion_removed, config, term}— a goldenfiles.includedentry is gone{:check_removed, mod}— in golden, absent in actual{:check_disabled, mod}— enabled in golden,falseor listed underdisabled:in actual{:params_changed, mod, golden, actual}— params differ (either direction){:exclusion_added, config, term}— the config'sfiles.excludedgrew{:exclusion_added, config, {mod, term}}— a check's ownfiles.excludedparam grew (any other change to that check's params is still:params_changed){:requires_removed, config, path}— a golden loader is gone (checks it provided could vanish){:requires_added, config, path}— actualrequireshas a file golden does not (Credo would load and run it){:plugin_added, config, plugin}— actualpluginshas an entry golden does not (a changed plugin entry is both removed and added){:strict_disabled, config}— golden hasstrict: true, actual does not
@spec compare_sources(String.t(), String.t(), keyword()) :: {:ok, [difference()]} | {:error, {:golden | :actual, term()}}
Parses both sources and compares them. Returns {:ok, differences} or the first parse error
({:error, {:golden | :actual, reason}}); an unreadable policy is an error, never "no change".
The default suppression patterns, as {kind, regex}.
Parses the text of a Credo config file into a term without evaluating code.
@spec scan_files( %{required(String.t()) => String.t()}, keyword() ) :: {%{required(String.t()) => [suppression()]}, %{required(String.t()) => term()}}
Scans many files given as %{path => source}. Returns
{suppressions_by_path, errors_by_path}; files that fail to parse are in
the second map and are never silently dropped.
@spec scan_suppressions( String.t(), keyword() ) :: {:ok, [suppression()]} | {:error, term()}
Scans one source text for suppression comments.
Options: :patterns (keyword of kind => regex, default default_patterns/0).
Each comment is reported once, under the first matching kind.