KogenCredo checks

View Source

These 35 checks are explicitly enabled through .credo.exs; see the example configuration. 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.

CheckBehaviorBase priorityDefault parameters
KogenCredo.Check.Readability.EmptySetupBlockEmpty setup blocks add noise to test files without providing any value. Remove them to keep tests concise.low[]
KogenCredo.Check.Readability.ErrorReasonStringReturn 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.JustifiedCoverageIgnoreCoverage ignores must document their reason so future audits can justify or remove them.high[allow_bare_until: nil]
KogenCredo.Check.Readability.MapGetAtomKeyDefine 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.DomainSizeA 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.DuplicateHelperThe 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.SizeLimitsSplit 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.BroadRescueRescuing 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.CtxBagPass 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.DiscardedResultA 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.DomainReachA 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.FailOpenWithLet 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.ForbiddenCallCalls 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.GlobalNameInventoryA 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.GlobalStateInAsyncAsync 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.LimitWithoutStableOrderlimit 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.NoGlobalTeardownTests must not delete or stop named, app-owned state.low[allowed_targets: [], case_templates: %{}, included_files: ["test/**/*_test.exs"]]
KogenCredo.Check.Warning.PathExpandPrivRelative 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.RepoAllThenTrimRepo.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.RepoInLoopA 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.RuntimeEnvDevDefaultHardcoded 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.SerialReasonA 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.SharedResourceGroupAsync 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.SkippedTestSkipped 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.StringKeyAccessDecode 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 nils.low[included_paths: ["lib/"], excluded_paths: [], codec_modules: [], codec_globs: []]
KogenCredo.Check.Warning.StubBodyA 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.TestHasNoAssertionsA 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.TestModuleShapeAsync 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.UnboundedWaitA 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.UnescapedLikeInputA 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.VacuousTestAn 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.VariableModuleCallmod.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.WeakAssertionassert 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: []]