KogenCredo checks
View SourceThese 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.
| 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 nils. | 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: []] |