KogenCredo.Integrity (KogenCredo v0.1.0)

View Source

Pure 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

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

difference()

@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()}

module_ref()

@type module_ref() :: KogenCredo.Integrity.ModuleRef.t()

A module written in the config, by name (see KogenCredo.Integrity.ModuleRef).

suppression()

@type suppression() :: %{line: pos_integer(), kind: atom(), text: String.t()}

Functions

assert_matches_golden!(golden_source, actual_source, opts \\ [])

@spec assert_matches_golden!(String.t(), String.t(), keyword()) :: :ok

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"))

compare_credo_config(golden, actual, _ \\ [])

@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 golden plugins entry is gone
  • {:inclusion_removed, config, term} — a golden files.included entry is gone
  • {:check_removed, mod} — in golden, absent in actual
  • {:check_disabled, mod} — enabled in golden, false or listed under disabled: in actual
  • {:params_changed, mod, golden, actual} — params differ (either direction)
  • {:exclusion_added, config, term} — the config's files.excluded grew
  • {:exclusion_added, config, {mod, term}} — a check's own files.excluded param 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} — actual requires has a file golden does not (Credo would load and run it)
  • {:plugin_added, config, plugin} — actual plugins has an entry golden does not (a changed plugin entry is both removed and added)
  • {:strict_disabled, config} — golden has strict: true, actual does not

compare_sources(golden_source, actual_source, opts \\ [])

@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".

default_patterns()

@spec default_patterns() :: keyword(Regex.t())

The default suppression patterns, as {kind, regex}.

parse_config(source)

@spec parse_config(String.t()) :: {:ok, term()} | {:error, term()}

Parses the text of a Credo config file into a term without evaluating code.

scan_files(files, opts \\ [])

@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.

scan_suppressions(source, opts \\ [])

@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.