yak(credo-strict-tuned-config-both-apps-x02d): credo strict, tuned config (both apps)

This commit is contained in:
Willem van den Ende
2026-10-29 10:13:02 +00:00
parent 13e3186f58
commit ef8407b272
13 changed files with 502 additions and 35 deletions
+19 -1
View File
@@ -4,6 +4,13 @@
# If you find anything wrong or unclear in this file, please report an
# issue on GitHub: https://github.com/rrrene/credo/issues
#
# The gate for this app is `mix credo --strict` on the command line (the
# `credo` target in Makefile, a prerequisite of `check`). The command line, not
# this file, decides: `--strict` overrides the inert `strict: false` key below.
# Editing `strict` here to silence a finding would change nothing - fix the
# finding, or write down why the check is wrong for this codebase (see
# `disabled:`).
#
%{
#
# You can have as many configs as you like in the `configs:` field.
@@ -126,7 +133,6 @@
#
{Credo.Check.Refactor.Apply, []},
{Credo.Check.Refactor.CondStatements, []},
{Credo.Check.Refactor.CyclomaticComplexity, []},
{Credo.Check.Refactor.FilterCount, []},
{Credo.Check.Refactor.FilterFilter, []},
{Credo.Check.Refactor.FunctionArity, []},
@@ -174,6 +180,18 @@
{Firehose.Checks.NoConnShadowing, []}
],
disabled: [
#
# Our one deliberate deviation from credo's defaults. CRAP (`ex_crap`,
# threshold 30, wired into the Lens pass in Makefile) is this app's
# complexity sensor, and it scores complexity against real coverage: a
# wide `case` that the tests exercise is not a problem - that was the
# itch behind ExCrap. A second, coverage-blind complexity sensor would
# just argue with the first, so complexity questions are settled by
# the CRAP target. Re-enabling this check without retiring the CRAP
# target is a policy change, not a cleanup.
#
{Credo.Check.Refactor.CyclomaticComplexity, []},
#
# Checks scheduled for next check update (opt-in for now)
{Credo.Check.Refactor.UtcNowTruncate, []},
+80
View File
@@ -0,0 +1,80 @@
defmodule Firehose.CredoConfigTest do
@moduledoc """
Pins the *policy* in `app/.credo.exs`, not just the file's presence.
The gate is `mix credo --strict` (the `credo` target in `app/Makefile`, a
prerequisite of `check`), so the config decides what every future commit has
to satisfy. Plain `ExUnit.Case`: this reads and evaluates a file, it does not
touch the database or boot the app's supervision tree.
The assertions are deliberately coarse - "this check sits in that list",
"the disable says why", "the custom check is still loaded" - so they survive
credo's own boilerplate churn but not a policy change.
"""
use ExUnit.Case, async: true
@config_path ".credo.exs"
@complexity_check "Credo.Check.Refactor.CyclomaticComplexity"
@custom_check "Firehose.Checks.NoConnShadowing"
@custom_check_load_path "lib_dev/firehose/checks/"
test "cyclomatic complexity is disabled in favour of CRAP" do
checks = default_config()[:checks]
assert @complexity_check in check_modules(checks[:disabled])
refute @complexity_check in check_modules(checks[:enabled])
end
test "the disable carries a rationale comment" do
rationale = rationale_above(@complexity_check)
refute rationale == [], "no comment block above the #{@complexity_check} entry"
assert Enum.join(rationale, "\n") =~ ~r/crap/i
end
# Regression guard: `mix credo.gen.config` would silently drop both of these,
# and the custom check would stop running without one word of warning.
test "the custom check and its load path survive" do
config = default_config()
assert @custom_check in check_modules(config[:checks][:enabled])
assert @custom_check_load_path in config[:requires]
end
defp default_config do
{config, _bindings} = Code.eval_file(@config_path)
Enum.find(config.configs, &(&1.name == "default"))
end
defp check_modules(checks) do
Enum.map(checks, fn
{module, _opts} -> module_name(module)
module -> module_name(module)
end)
end
# `Atom.to_string/1` keeps the "Elixir." prefix that `inspect/1` drops; the
# assertions read better (and diff better) without it.
defp module_name(module) do
module |> Atom.to_string() |> String.replace_prefix("Elixir.", "")
end
# The contiguous `#` block immediately above the check's own line. The entry
# line is the one that is not itself a comment: a comment mentioning the
# check by name must not be mistaken for the entry.
defp rationale_above(check_name) do
lines = @config_path |> File.read!() |> String.split("\n")
index =
Enum.find_index(lines, fn line ->
String.contains?(line, check_name) and not comment?(line)
end)
lines
|> Enum.take(index)
|> Enum.reverse()
|> Enum.take_while(&comment?/1)
end
defp comment?(line), do: line |> String.trim() |> String.starts_with?("#")
end