Current section
Files
Jump to
Current section
Files
docs/checks.md
# KogenCredo checks
These 35 checks are explicitly enabled through `.credo.exs`; see
[the example configuration](configuration.md). This catalog describes version 0.1.0.
Each module documents its detection boundaries, examples and parameters.
Heuristic findings are advice, not proof that code is incorrect. Review findings
before deciding whether a check should fail your application's CI.
Parameters below are defaults, not a recommended policy for every application.
Checks requiring project-specific namespaces, paths, domains, codec modules or
allowed serial tests need those settings supplied deliberately.
| Check | Behavior | Base priority | Default parameters |
| --- | --- | --- | --- |
| `KogenCredo.Check.Readability.EmptySetupBlock` | Empty `setup` blocks add noise to test files without providing any value. Remove them to keep tests concise. | `low` | `[]` |
| `KogenCredo.Check.Readability.ErrorReasonString` | Return a machine-matchable reason, not prose: `{:error, :not_found}`, `{:error, {:invalid, :email}}` or an exception struct. Render human text at the edge (controller, CLI, UI). | `low` | `[included_paths: ["lib/"], excluded_paths: ["lib/mix/"], flag_interpolated: true]` |
| `KogenCredo.Check.Readability.JustifiedCoverageIgnore` | Coverage ignores must document their reason so future audits can justify or remove them. | `high` | `[allow_bare_until: nil]` |
| `KogenCredo.Check.Readability.MapGetAtomKey` | Define the shape once (`defstruct` with defaults) and use `x.key`, or pattern-match the required key. `Map.get(x, :key)` on a map you control hides missing keys and typos. | `low` | `[included_paths: ["lib/"], excluded_paths: [], flag_with_default: true, ignored_receivers: [:opts, :options, :kw, :keyword, :overrides, :assigns, :attrs, :params, :config, :metadata, :errors, :private]]` |
| `KogenCredo.Check.Refactor.DomainSize` | A domain over its ceiling does too much. Re-slice it into smaller domains instead of raising the limit. Configure each domain's `paths` and optional `max_lines`. | `low` | `[domains: [], max_lines: 3000, count_blank_and_comment_lines: true]` |
| `KogenCredo.Check.Refactor.DuplicateHelper` | The same private function pasted into several modules should live in one module and be called from the others. Duplicates drift apart and every copy needs the same fix. | `low` | `[included_paths: ["lib/"], excluded_paths: [], min_files: 3, min_nodes: 12, ignored_names: []]` |
| `KogenCredo.Check.Refactor.SizeLimits` | Split instead of growing. A module past a few hundred lines has more than one reason to change; a function past forty lines hides branches. Extract smaller functions or modules. | `low` | `[max_file_lines: 400, max_module_lines: 400, max_function_lines: 40, count_blank_and_comment_lines: true, count_template_lines: false, included_paths: ["lib/"], excluded_paths: []]` |
| `KogenCredo.Check.Warning.BroadRescue` | Rescuing every exception launders bugs (`FunctionClauseError`, `KeyError`, a typo) into a value and hides defects from supervisors and tests. Rescue the specific exception at the I/O edge (`rescue e in File.Error`) and return a typed `{:error, reason}`, or let it crash. | `low` | `[broad_exceptions: [Exception, RuntimeError, ErlangError], included_paths: ["lib/"], excluded_paths: []]` |
| `KogenCredo.Check.Warning.CtxBag` | Pass the values a function needs (`root`, `now`, a `%Project{}`), not a bag of services. A bag hides what a function depends on and lets every function reach every capability. Effects that tests replace belong behind a behaviour chosen at compile time. | `low` | `[banned_names: [:ctx, :context], allowed_names: [:conn, :socket], min_named_clauses: 3, min_clauses: 4, min_passed_ratio: 0.5, min_fields: 8, min_ambient: 2, ambient_fields: [:root, :cwd, :dir, :env, :tmp_dir, :tmp, :now, :clock, :time, :runner, :proc, :cmd, :git, :io, :shell, :config, :opts, :options, :log, :log_path, :logger, :harness, :provider, :http, :client, :repo, :timeout, :deadline, :registry, :store, :cache, :runtime, :deps, :services], included_paths: ["lib/"], excluded_paths: []]` |
| `KogenCredo.Check.Warning.DiscardedResult` | A fallible call whose result is dropped hides failures. | `low` | `[repo_functions: [:insert, :update, :delete, :insert_or_update], fallible_name_patterns: ["create_*", "insert_*", "update_*", "delete_*"], ignored_names: ["update_in", "update_change", "delete_change", "update_counter", "delete_csrf_token", "delete_session", "delete_req_header", "delete_resp_header", "delete_resp_cookie", "update_resp_header"], ignored_modules: ["Map", "Keyword", "Enum", "List", "Access", "Changeset", "Multi", "Kernel"], flag_bare_statement: true, flag_underscore_assignment: true, repo_names: ["Repo"], repo_modules: [], included_paths: [], excluded_paths: ["test/**"]]` |
| `KogenCredo.Check.Warning.DomainReach` | A domain should depend on shared contracts, not on the internals of another domain. Configure each domain's `namespace` and `paths`; references from one domain's files to another's namespace are reported. Reach other domains through a contract/port or an event. | `low` | `[domains: [], shared: [], root_namespace: nil, flag_unowned_root: false]` |
| `KogenCredo.Check.Warning.FailOpenWith` | Let `with` return the non-matching value as-is (no `else`), or match the exact error shapes the clauses can produce and translate them. Never invent a result for a value you did not expect, and never let `{:error, reason}` become `nil`, `[]` or `:ok`: the caller cannot tell "nothing found" from "the read failed". | `low` | `[included_paths: ["lib/"], excluded_paths: [], flag_case: true, flag_identity_else: true]` |
| `KogenCredo.Check.Warning.ForbiddenCall` | Calls that mutate global state (`System.put_env`, `File.cd!`), sleep for a fixed time or bypass an injected port should be impossible to add silently. Configure `rules` with the exact calls, a message that says what to use instead, and optional `paths`/`allow` scopes. | `low` | `[rules: [], presets: [], preset_allow: ["test/support/"]]` |
| `KogenCredo.Check.Warning.GlobalNameInventory` | A global name (PubSub topic, named ETS table, registered process name) makes every test that touches it share state with every other test. Accept an option with a default so tests can pass a unique name. | `low` | `[kinds: [:topic, :ets, :persistent_term, :process_name], allowed_names: [], included_files: ["lib/**/*.ex"]]` |
| `KogenCredo.Check.Warning.GlobalStateInAsync` | Async test modules must not write node-wide state. Application env, `System.put_env`, global mocks, named ETS tables, fixed file paths, `Logger.configure` and `:persistent_term.put` leak into concurrently running modules. | `low` | `[case_templates: %{}, kinds: [:env, :mock_global, :ets, :file, :cwd, :logger, :persistent_term], included_files: ["test/**/*_test.exs"]]` |
| `KogenCredo.Check.Warning.LimitWithoutStableOrder` | `limit` without a total `order_by` returns an arbitrary subset when sort values tie, so pagination repeats and skips rows. | `low` | `[unique_fields: [:id], ignore_limit_one: true, included_paths: [], excluded_paths: ["test/**"]]` |
| `KogenCredo.Check.Warning.NoCredoDisable` | `# credo:disable-for-...` hides a finding instead of fixing it. Fix the code, or change the check configuration in a reviewed place. Note: this check is advisory and can itself be suppressed or disabled, see the module documentation. | `low` | `[allowed_directives: [], allowed_checks: []]` |
| `KogenCredo.Check.Warning.NoGlobalTeardown` | Tests must not delete or stop named, app-owned state. | `low` | `[allowed_targets: [], case_templates: %{}, included_files: ["test/**/*_test.exs"]]` |
| `KogenCredo.Check.Warning.PathExpandPriv` | Relative `priv/...` paths depend on the working directory. They work under `mix` and fail in a release. Resolve them with `Application.app_dir(:my_app, "priv/...")` or `:code.priv_dir(:my_app)`. | `low` | `[included_paths: ["lib/", "config/"], excluded_paths: ["lib/mix/"], check_file_calls: true, file_functions: [:read, :read!, :stream!, :ls, :ls!, :exists?, :dir?, :regular?]]` |
| `KogenCredo.Check.Warning.RepoAllThenTrim` | `Repo.all/1` followed by `Enum.take/uniq_by/slice/filter/at` loads every matching row, then discards most of them in memory. Every call pays for the whole table slice. | `low` | `[trimmer_functions: [:take, :uniq_by, :slice, :filter, :at], repo_names: ["Repo"], repo_modules: [], included_paths: [], excluded_paths: ["test/**"]]` |
| `KogenCredo.Check.Warning.RepoInLoop` | A Repo call inside an iteration runs one query per element (N+1). | `low` | `[loop_functions: [:map, :each, :flat_map, :reduce, :reduce_while, :map_reduce, :filter, :reject, :find, :any?, :all?, :sort_by, :group_by, :min_by, :max_by], repo_functions: [:all, :stream, :one, :one!, :get, :get!, :get_by, :get_by!, :exists?, :aggregate, :preload, :reload, :reload!, :insert, :insert!, :update, :update!, :delete, :delete!, :insert_or_update, :insert_or_update!], max_depth: 3, repo_names: ["Repo"], repo_modules: [], included_paths: [], excluded_paths: ["test/**"]]` |
| `KogenCredo.Check.Warning.RuntimeEnvDevDefault` | Hardcoded developer-machine paths in `System.get_env/2` defaults will cause the application to silently read from a non-existent path on production servers. | `high` | `[]` |
| `KogenCredo.Check.Warning.SerialReason` | A serial test module adds its whole run time to the suite's serial phase. Declare why it must be serial with `@moduletag serial: :reason`, using the project's vocabulary, so the reason can be reviewed and later removed. | `low` | `[serial_vocabulary: [:env, :ets, :file, :cwd, :mock_global, :logger, :persistent_term, :pubsub, :named_process, :sandbox_shared, :external], case_templates: %{}, allowed_modules: [], included_files: ["test/**/*_test.exs"]]` |
| `KogenCredo.Check.Warning.SharedResourceGroup` | Async modules that touch the same global resource (env key, named ETS table, fixed path, PubSub topic) must be in the same `group:` so they run one after another. | `low` | `[case_templates: %{}, kinds: [:env, :ets, :file, :topic, :persistent_term], included_files: ["test/**/*_test.exs"]]` |
| `KogenCredo.Check.Warning.SkippedTest` | Skipped tests (`@tag :skip`, `@tag skip: true`, `@moduletag :skip`, etc.) hide failures and erode the test suite over time. | `high` | `[allow_with_comment: false]` |
| `KogenCredo.Check.Warning.StringKeyAccess` | Decode external JSON/YAML/params once, in a codec module, into a struct, and use struct fields inside the core. String-keyed maps spread the wire shape through the code base and turn typos into silent `nil`s. | `low` | `[included_paths: ["lib/"], excluded_paths: [], codec_modules: [], codec_globs: []]` |
| `KogenCredo.Check.Warning.StubBody` | A function that returns `:not_implemented` or raises "TODO" passes compilation and type checks while doing nothing. Implement the behaviour now, or leave the function out so the missing capability is a compile-time error rather than a runtime surprise. | `low` | `[included_paths: ["lib/"], excluded_paths: [], stub_atoms: [:not_implemented, :unimplemented, :not_yet_implemented], stub_exceptions: [NotImplementedError, UnimplementedError], stub_pattern: ~r/\b(not (yet )?implemented\|unimplemented\|todo\|stub(bed)?\|placeholder\|not supported yet)\b/i]` |
| `KogenCredo.Check.Warning.TestFsHygiene` | `@tag :tmp_dir` makes ExUnit create `tmp/<Module>/<test>` under the working directory, which is the project tree. Writes to project-relative paths or to `File.cwd!/0` pollute it as well, and a polluted tree breaks "clean tree after tests" gates. Build a unique directory under `System.tmp_dir!/0` and remove it in `on_exit/1`, or take the path from the test's setup. | `low` | `[included_paths: ["test/"], excluded_paths: [], allow_tmp_dir_paths: [], allow_write_paths: []]` |
| `KogenCredo.Check.Warning.TestHasNoAssertions` | A test without an assertion passes whenever the code does not crash. Add an assertion about the result or side effect that the test name promises, or delete the test. | `low` | `[file_suffixes: ["_test.exs"], test_macros: [:test], assertion_helpers: [], assertion_prefixes: ["verify", "expect", "flunk"], count_pattern_matches: true]` |
| `KogenCredo.Check.Warning.TestModuleShape` | Async test modules run concurrently and fail fast on hidden global state; large test modules hide unrelated behaviour. Use `async: true`, inject dependencies instead of mutating globals, and split modules by behaviour. Serial modules need an explicit, reviewed `serial_allowed` entry. | `low` | `[file_suffixes: ["_test.exs"], max_tests: 30, serial_allowed: [], case_suffix: "Case", require_async: true]` |
| `KogenCredo.Check.Warning.UnboundedWait` | A `receive` without `after`, or a call with timeout `:infinity`, turns a lost message or a crashed peer into a hang that only a CI timeout will end. Use a finite deadline (and handle the timeout) so the failure is reported where it happens. | `low` | `[included_paths: ["lib/", "test/"], excluded_paths: [], flag_receive_without_after: true, flag_timeout_option: true, infinity_calls: [{GenServer, :call, 2}, {GenServer, :stop, 2}, {Task, :await, 1}, {Task, :yield, 1}, {Task, :await_many, 1}, {Agent, :get, 2}, {Agent, :get_and_update, 2}, {Agent, :update, 2}, {Agent, :stop, 2}, {Supervisor, :stop, 2}, {Process, :sleep, 0}, {:timer, :sleep, 0}, {:gen_server, :call, 2}]]` |
| `KogenCredo.Check.Warning.UnescapedLikeInput` | A LIKE pattern built from input without escaping lets the input contain wildcards. | `low` | `[like_functions: [:like, :ilike], escape_functions: ["escape_like", "sanitize_like", "escape_like_pattern", "like_escape", "escape_wildcards"], included_paths: [], excluded_paths: ["test/**"]]` |
| `KogenCredo.Check.Warning.VacuousTest` | An assertion that cannot fail proves nothing and gives false confidence. Assert on the value the code under test produced: `assert result == expected`, or a pattern that can fail. | `low` | `[file_suffixes: ["_test.exs"], test_macros: [:test], allow_binding_match: false]` |
| `KogenCredo.Check.Warning.VariableModuleCall` | `mod.fun()` on a variable hides the callee from xref, Boundary and capability analysis. Use a behaviour whose implementation is picked at compile time (module attribute), or dispatch with an explicit `case` over the known modules. | `low` | `[included_paths: ["lib/"], excluded_paths: [], allowed_variables: [], ignored_functions: [:module_info, :__info__, :__struct__, :behaviour_info], check_apply: true]` |
| `KogenCredo.Check.Warning.WeakAssertion` | `assert is_map(result)` or `assert result != nil` passes for almost any bug. Assert the exact value or a pattern that pins the interesting fields: `assert %{status: :ok, id: ^id} = result`. | `low` | `[file_suffixes: ["_test.exs"], test_macros: [:test], only_when_all_weak: true, weak_predicates: [:is_map, :is_list, :is_binary, :is_atom, :is_integer, :is_float, :is_number, :is_tuple, :is_boolean, :is_function, :is_struct, :is_pid, :is_reference], weak_helpers: [], assertion_helpers: []]` |