Current section

Files

Jump to
optimum_credo docs checks.md
Raw

docs/checks.md

# OptimumCredo checks

These 74 checks are explicitly enabled through `.credo.exs`; see
[the example configuration](configuration.md). This catalog describes version 0.5.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 |
| --- | --- | --- | --- |
| `OptimumCredo.Check.Readability.AssignOrder` | Keep chained assigns in alphabetical order, with pinned keys such as `:module` first. | `low` | `[min_run: 3, pinned_assigns: [:module]]` |
| `OptimumCredo.Check.Readability.AssignThree` | Use `assign(socket, :key, value)` in a pipeline, one assign per line, instead of `assign(socket, key: value)` or `assign(socket, %{key: value})` with a literal. | `low` | `[]` |
| `OptimumCredo.Check.Readability.BannedAbbreviations` | Prefer complete words over abbreviations, using the business-domain term. | `low` | `[banned_abbreviations: [acct: "account", btn: "button", cfg: "config", cust: "customer", idx: "index", mgr: "manager", msg: "message", pwd: "password", qty: "quantity", tmp: "temporary", usr: "user"], allowed_names: []]` |
| `OptimumCredo.Check.Readability.BooleanFunctionName` | Functions that return a boolean end in `?`. | `low` | `[ignore_functions: []]` |
| `OptimumCredo.Check.Readability.CastListOrder` | Keep field lists passed to `cast/3` and `validate_required/2` alphabetical. | `low` | `[min_run: 3, functions: [:cast, :validate_required], local_calls: :imported, changeset_imports: ["Ecto.Changeset"]]` |
| `OptimumCredo.Check.Readability.DefaultCallbackDefined` | Do not define `mount/3`, `mount/1` or `update/2` when the body equals the default behaviour. Only single-clause, guard-free definitions with ignored arguments are reported. Opt-in, low priority. | `low` | `[]` |
| `OptimumCredo.Check.Readability.DepsOrder` | Alphabetically ordered dependencies are more easily scannable by the reader. | `low` | `[sort_method: :alpha]` |
| `OptimumCredo.Check.Readability.DescribeFunctionArity` | Name unit-test `describe` blocks after the function and arity, and web tests after the route. | `low` | `[describe_pattern: "^\\w+[?!]?/\\d+( [^\\n]+)?$", web_describe_pattern: "^([A-Z]+ )?/", web_paths: ["_web/"], unit_paths: [], ignore_paths: ["test/support/", "/features/", "/integration/"], allowed_patterns: ["^on_mount :\\w+$"]]` |
| `OptimumCredo.Check.Readability.DocumentationFormatting` | Function documentation should have proper spacing for readability. | `low` | `[]` |
| `OptimumCredo.Check.Readability.EventNameSnakeCase` | Event names use `snake_case`, ideally `<verb>_<noun>`. Dashes and uppercase letters are reported in `handle_event/3` heads, `push_event/3` calls, and `phx-*` string attributes in `~H` templates. Dynamic names are not inspected. Opt-in, low priority. | `low` | `[allowed_events: [], check_heex: true]` |
| `OptimumCredo.Check.Readability.ExtractableSpecTypes` | Common types in @spec declarations should be extracted to module-level semantic type aliases for better readability, maintainability and consistency. | `low` | `[]` |
| `OptimumCredo.Check.Readability.FunctionOrder` | Keep function definitions in a predictable order. Choose one convention with `function_order`: alphabetical within public and private groups, or private functions below their first caller. | `low` | `[function_order: :off, min_run: 3]` |
| `OptimumCredo.Check.Readability.GenericModuleName` | Name modules for what they are, not for being a collection of leftovers. | `low` | `[banned_names: ["Common", "Helper", "Helpers", "Misc", "Stuff", "Util", "Utilities", "Utils"], allowed_modules: []]` |
| `OptimumCredo.Check.Readability.GettextDomain` | Use a per-feature Gettext domain. | `low` | `[ash: false, allowed_domains: [], bare_functions: [:gettext, :ngettext, :pgettext, :npgettext, :gettext_noop]]` |
| `OptimumCredo.Check.Readability.ImplementationHelperInAssertion` | A test that builds its expectation with the same function the implementation uses proves nothing. Hardcode the expected value. | `low` | `[helper_pattern: "^(format\|humanize\|display\|label\|render\|title\|slug\|translate\|to_)", modules: [], watch_namespace: true]` |
| `OptimumCredo.Check.Readability.ImportOrder` | Alphabetically ordered imports are more easily scannable by the reader. | `low` | `[sort_method: :alpha]` |
| `OptimumCredo.Check.Readability.LlmScaffoldingComment` | Remove assistant chatter and scaffolding before review; comments should explain why. | `low` | `[patterns: ["^(here'?s\|here is\|here are) (the \|an? \|your )?(updated\|revised\|fixed\|complete\|final\|new\|modified\|refactored\|corrected\|improved\|implementation\|code\|solution\|function)\\b", "^(certainly\|sure\|of course\|absolutely)[!,.]", "\\bas an ai\\b", "^(i'?ve\|i have) (added\|updated\|created\|fixed\|implemented\|removed\|refactored)\\b", "^let me\\b", "\\b(rest\|remainder) of (the )?(code\|file\|function\|module\|implementation)\\b.*\\b(remains?\|unchanged\|same\|here)\\b", "^\\.{3}\\s*(existing\|rest of\|previous\|other)\\b", "\\b(generated\|written\|created\|authored) (by\|with) (claude\|chatgpt\|gpt\|copilot\|cursor\|an? ai\|ai)\\b", "\\b(implementation\|code\|logic) (goes\|should go\|belongs) here\\b", "\\byour (code\|implementation\|logic) here\\b"], extra_patterns: []]` |
| `OptimumCredo.Check.Readability.MaybePrefix` | Functions that may or may not act on their first argument start with `maybe_`. | `low` | `[prefix: "maybe_", allowed_prefixes: [], ignore_functions: []]` |
| `OptimumCredo.Check.Readability.ModuleAttributeListOrder` | Keep literal lists in module attributes alphabetical. | `low` | `[min_run: 3, ignore_attributes: []]` |
| `OptimumCredo.Check.Readability.ModuleOrganization` | Module organization should follow Phoenix conventions with proper blank line separation between different statement types (use, import, alias, require). | `high` | `[]` |
| `OptimumCredo.Check.Readability.ModulePathMismatch` | The module namespace should mirror the directory and the file name should mirror the module, as `mix phx.gen.*` would generate it. | `low` | `[ignore_paths: [], web_layer_dirs: ["controllers", "live", "components", "channels", "plugs"], additional_modules: :nested, allowed_modules: []]` |
| `OptimumCredo.Check.Readability.ModuleSelfPrefix` | Call local functions directly instead of through `__MODULE__`. | `low` | `[allow_self_prefix_for: []]` |
| `OptimumCredo.Check.Readability.PrivateFunctionSpecs` | Private functions should not have @spec declarations. Specs are for public API documentation and type checking, not internal implementation details. | `low` | `[]` |
| `OptimumCredo.Check.Readability.RedundantBooleanComparison` | Comparing a boolean-valued expression with `true` or `false` is redundant: use the expression itself, or `not` / `!` for the negation. | `low` | `[strict: false, trust_question_mark: false]` |
| `OptimumCredo.Check.Readability.RedundantFinalAssignment` | Do not bind a value to a variable only to return that variable on the next line. Reported for `def`/`defp`/`defmacro`/`defmacrop` bodies with a plain variable match. Opt-in, low priority. | `low` | `[]` |
| `OptimumCredo.Check.Readability.SchemaFieldOrder` | Keep schema fields in alphabetical order so entries are predictable. | `low` | `[min_run: 3, group_associations: false]` |
| `OptimumCredo.Check.Readability.SingleElementTupleMessage` | Send an atom rather than a one-element tuple: `send(pid, :refresh)` instead of `send(pid, {:refresh})`. Covers `send/2`, `Process.send/3` and `Process.send_after/3,4`. Opt-in, low priority. | `low` | `[]` |
| `OptimumCredo.Check.Readability.SpecWithImpl` | A function annotated with `@impl` implements a callback, whose spec is already defined by the behaviour. Remove the `@spec`. | `low` | `[]` |
| `OptimumCredo.Check.Readability.TestNameLowercase` | Test names complete a sentence about behaviour and start lowercase. | `low` | `[allow_acronyms: true, allow_camel_case: true, allowed_prefixes: [], ignore_paths: ["test/support/"]]` |
| `OptimumCredo.Check.Readability.TestNameRepeatsSubject` | The `describe` already names the function, so the test name should state the behaviour only. | `low` | `[min_subject_length: 5, ignore_paths: ["test/support/"]]` |
| `OptimumCredo.Check.Readability.TestPathMismatch` | A test file mirrors its subject: `lib/my_app/x.ex` is tested by `test/my_app/x_test.exs`, and the test module is named `<Module>Test`. | `low` | `[ignore_paths: [], require_test_counterpart: true, web_layer_dirs: ["controllers", "live", "components", "channels", "plugs"]]` |
| `OptimumCredo.Check.Readability.TypeParenthesesConsistency` | Keep type syntax consistent: by default definitions have no parentheses and references do. | `low` | `[definition_style: :bare, reference_style: :parens]` |
| `OptimumCredo.Check.Readability.TypespecOrder` | Alphabetically ordered typespecs are more easily scannable by the reader. | `low` | `[sort_method: :alpha]` |
| `OptimumCredo.Check.Readability.UnusedTypes` | Type definitions should be removed if they're not used anywhere in the module. | `low` | `[ignored_types: [:t]]` |
| `OptimumCredo.Check.Readability.VerboseAssertions` | Assertion patterns should use explicit `is_nil/1` predicates instead of equality comparisons with nil. | `low` | `[]` |
| `OptimumCredo.Check.Refactor.AdjacentClauseOneLiteral` | Adjacent clauses that differ only in one literal value can often become one clause with a parameter. Reported when the only differences are a single literal pair; small clauses are skipped. Readable separate clauses are a valid choice. Opt-in, low priority. | `low` | `[min_nodes: 6]` |
| `OptimumCredo.Check.Refactor.CaseTrueFalse` | A `case` expression with only `true ->` and `false ->` clauses should be replaced with an `if` expression. | `normal` | `[]` |
| `OptimumCredo.Check.Refactor.ConditionalOnParameter` | Prefer several function clauses or guards over a single body that is just a `case` or `if` on one of the parameters. Opt-in, low priority; only single-expression bodies are inspected. | `low` | `[min_case_clauses: 2, check_if: true]` |
| `OptimumCredo.Check.Refactor.DuplicateHandleEvent` | Identical `handle_event/3` clauses in several LiveViews usually belong in an `on_mount` hook (`attach_hook/4`) or a shared function. Compared across all files, after alpha-renaming variables; trivial clauses are skipped. Opt-in, low priority. | `low` | `[min_nodes: 4, min_modules: 2]` |
| `OptimumCredo.Check.Refactor.LiveViewBareMatch` | LiveView modules must use `case` for operations that return `{:ok, _} \| {:error, _}`. | `high` | `[]` |
| `OptimumCredo.Check.Refactor.RepeatedLiteral` | A string literal repeated in a module is a candidate for a named module attribute. Single obvious literals stay inline; this check only counts repeated plain strings outside patterns, attributes, sigils and tests. Opt-in, low priority. | `low` | `[min_occurrences: 3, min_length: 3, ignore_literals: [], ignore_paths: ["test/", "priv/", "config/", "_test.exs"]]` |
| `OptimumCredo.Check.Refactor.SchemaTableNameMismatch` | Name the schema after its table (singular of the table name). | `low` | `[custom_inflections: %{}, allow_context_prefix: false]` |
| `OptimumCredo.Check.Refactor.ToggleOnlyHandleEvent` | A handler that only toggles a boolean read by one `:if` can use `JS.toggle/1` or `JS.show/hide` instead of a server round trip. Reported only for recognisable toggle-only handlers in modules whose inline template has a single `:if` consumer. Suggestion-grade; opt-in, low priority. | `low` | `[require_inline_template: true]` |
| `OptimumCredo.Check.Refactor.WithElseManyClauses` | A `with` with a long `else` usually means inner calls return ambiguous errors. Return exact `{:error, reason}` values from the inner functions so the `else` shrinks or disappears. Opt-in, low priority; counts clauses only. | `low` | `[min_else_clauses: 3]` |
| `OptimumCredo.Check.Warning.AshCallWithoutActor` | Pass the actor to Ash calls so policies can authorize them. | `low` | `[ash: false, actorless_resources: [], resource_modules: [], ash_functions: [:get, :get!, :read, :read!, :read_one, :read_one!, :create, :create!, :update, :update!, :destroy, :destroy!, :load, :load!, :count, :count!, :exists?, :bulk_create, :bulk_create!, :bulk_update, :bulk_update!, :bulk_destroy, :bulk_destroy!]]` |
| `OptimumCredo.Check.Warning.AshUpsertOptions` | `upsert_identity` and `upsert_fields` need `upsert?: true`. | `low` | `[ash: false]` |
| `OptimumCredo.Check.Warning.AsyncFalseWithoutComment` | `async: false` serialises the suite. Keep tests async and isolate the one test that needs global state; when `async: false` is truly required, say why in a comment. | `low` | `[require_comment: true, case_suffix: "Case", exempt_moduletag: :timing]` |
| `OptimumCredo.Check.Warning.AuthorizeFalseWithoutComment` | Explain every `authorize?: false` with a comment on the same line. | `low` | `[ash: false, ignore_paths: ["test/", "priv/repo/seeds"]]` |
| `OptimumCredo.Check.Warning.CallerModuleAttributes` | Avoid injecting generic module attributes such as `@opts`, `@pos`, or `@app` into the caller from a quote. Those attributes belong to the caller's module and can collide with another macro. Use an owner-specific name, optionally wrapped in leading/trailing underscores, or explicitly allow intentional caller configuration. | `normal` | `[required_prefix: nil, allow_attributes: []]` |
| `OptimumCredo.Check.Warning.CommentedOutCode` | Delete commented-out code; version control remembers it. Use `# TODO:`/`# FIXME:` for real follow-ups. | `low` | `[min_lines: 2, ignore_prefixes: ["TODO:", "FIXME:", "NOTE:"]]` |
| `OptimumCredo.Check.Warning.DescribeWithoutErrorCase` | Test the sad path next to the happy path: not-found, invalid and unauthorised cases. This heuristic only looks for textual hints and is meant as a prompt for review. | `low` | `[min_tests: 2, error_words: ["error", "invalid", "fail", "not", "not found", "unauthorized", "forbidden", "reject", "denied", "missing", "cannot", "raise", "non-existent", "nonexistent", "non-existing", "nonexisting", "does not exist", "doesn't exist", "expired", "unknown"], error_atoms: [:error, :unauthorized, :forbidden, :not_found, :invalid, :unprocessable_entity, :bad_request, :conflict, :internal_server_error], ignore_paths: ["test/support/"]]` |
| `OptimumCredo.Check.Warning.EnvVarWithoutFailure` | Fail loudly on missing production environment variables. | `low` | `[optional_vars: ["DNS_CLUSTER_QUERY"]]` |
| `OptimumCredo.Check.Warning.ExplicitIndexName` | Let Ecto infer index names for single-column indexes. | `low` | `[allow_multi_column: true, allow_partial_indexes: true, ignore_files: []]` |
| `OptimumCredo.Check.Warning.ExternalCallInMigration` | Do not call external services from migrations. | `low` | `[external_modules: ["HTTPoison", "Req", "Tesla", "Finch", "Mint", "ExAws", "Stripe", "Swoosh", ":httpc", ":hackney"], ignore_files: []]` |
| `OptimumCredo.Check.Warning.HardcodedAssetPath` | Reference static assets through verified routes (`~p"/images/logo.png"`) or `static_path/2`, not a hard-coded string in `src` or `href`. Only HEEx text is inspected. Opt-in, low priority. | `low` | `[prefixes: ["/images/", "/assets/", "/css/", "/js/", "/fonts/"]]` |
| `OptimumCredo.Check.Warning.HardcodedYearInTest` | A fixed year can make a time-relative test expire. Derive time-relative dates from `Date.utc_today/0` or use an explicit frozen clock. Fixed historical fixtures are legitimate; configure `allow_years` for those intentional cases. | `low` | `[min_year: 2020, max_year: 2100, allow_years: []]` |
| `OptimumCredo.Check.Warning.HiddenIdInput` | Do not rely on hidden `*_id` inputs for authorisation. Derive the owner from the session/socket and scope queries by it. Reported for `<input type="hidden">` and `<.input type="hidden">` whose name, id or field ends in `_id`. Partial heuristic; Sobelow and review cover the semantics. Opt-in, low priority. | `low` | `[allowed_fields: []]` |
| `OptimumCredo.Check.Warning.HttpClientInTest` | Tests must not hit the network. Stub external services with Mox, Bypass, `Req.Test` or fixtures. | `low` | `[client_modules: [HTTPoison, HTTPotion, Req, Tesla, Finch, Mojito, Mint.HTTP, :httpc, :hackney], mock_wrappers: [:expect, :expect_once, :stub, :stub_with, :mock, :mock_global, :with_mock, :with_mocks], non_request_functions: [:new, :build, :client, :build_client, :start, :start_link, :child_spec, :merge, :update, :attach, :put_header, :put_headers, :put_option, :put_private], allow_local_urls: true]` |
| `OptimumCredo.Check.Warning.LoggerErrorWithoutContext` | `Logger.error("failed")` in an error branch hides what failed. Include the relevant values: `Logger.error("sync failed for #{id}: #{inspect(reason)}")`, or pass metadata. Only constant string messages are reported. Opt-in, low priority. | `low` | `[scope: :error_branches]` |
| `OptimumCredo.Check.Warning.MixOutsideBuildConfig` | `Mix` does not exist in releases; do not call it from application code. | `low` | `[allowed_paths: ["lib/mix/tasks"], allow_ensure_loaded_guard: true]` |
| `OptimumCredo.Check.Warning.NarrowRandomRange` | A small random range collides sooner or later and causes flaky tests. Use UUIDs, or a timestamp plus a large random value, for uniqueness. | `low` | `[min_range: 1000000]` |
| `OptimumCredo.Check.Warning.NotNullWithoutDefault` | Adding a NOT NULL column to an existing table needs a default or a backfill. | `low` | `[ignore_files: []]` |
| `OptimumCredo.Check.Warning.RawSqlIndexInMigration` | Prefer the migration DSL for indexes over raw SQL in `execute/1`. | `low` | `[allow_concurrent: true, allow_expression_indexes: true, ignore_files: []]` |
| `OptimumCredo.Check.Warning.ReferenceWithoutOnDelete` | Declare what happens to child rows when the referenced row is deleted. | `low` | `[require_on_delete: true, ignore_files: []]` |
| `OptimumCredo.Check.Warning.RepoCallInEnumeration` | Do not query the database once per element. | `low` | `[repo_functions: [:get, :get!, :get_by, :get_by!, :one, :one!, :all, :exists?, :aggregate, :reload, :reload!, :preload], enumerator_functions: [:map, :flat_map, :each, :filter, :reject, :reduce, :reduce_while, :map_reduce, :find, :find_value, :any?, :all?, :count, :sort_by, :group_by, :min_by, :max_by, :map_every], ignore_paths: ["test/", "priv/"]]` |
| `OptimumCredo.Check.Warning.RequiredFieldConsistency` | A column that is `null: false` in the migration should be required by the changeset. | `low` | `[migrations_path: "priv/repo/migrations", ignore_fields: [:id, :inserted_at, :updated_at]]` |
| `OptimumCredo.Check.Warning.SchemaModuleInMigration` | Migrations must not depend on application modules such as Ecto schemas. Those modules evolve (or disappear) after the migration is written, which breaks it when it runs against production data later. | `low` | `[app_namespace: nil, allowed_modules: [], ignore_files: []]` |
| `OptimumCredo.Check.Warning.SeedInsertWithoutOnConflict` | Seed inserts should be idempotent. | `low` | `[seed_paths: ["priv/repo/seeds.exs", "priv/repo/seeds/"]]` |
| `OptimumCredo.Check.Warning.SleepInTest` | Sleeping in tests slows the suite and hides races. Synchronise on a message (`assert_receive`) or an observable state change instead. | `low` | `[functions: [{Process, :sleep}, {:timer, :sleep}], wait_functions: [:wait, :wait_until], max_wait_block_lines: 5, exempt_moduletag: :timing]` |
| `OptimumCredo.Check.Warning.SleepOutsideTest` | Avoid `Process.sleep/1` and `:timer.sleep/1` outside tests. Prefer messages, timers (`Process.send_after/3`), or an explicit retry/backoff library. `sleep(:infinity)` is allowed. Opt-in, low priority. | `low` | `[ignore_paths: ["test/", "priv/", "_test.exs"]]` |
| `OptimumCredo.Check.Warning.StaleRenderBinding` | An assertion on HTML captured before an interaction can pass (or fail) for the wrong reason. Re-render or bind the interaction result. | `low` | `[producers: [:live, :live_isolated, :render, :render_component, :render_click, :render_submit, :render_change, :render_keydown, :render_keyup, :render_blur, :render_focus, :render_hook, :render_async, :render_patch, :follow_redirect], interactions: [:render_click, :render_submit, :render_change, :render_keydown, :render_keyup, :render_blur, :render_focus, :render_hook, :render_upload, :render_async, :render_patch, :follow_redirect, :send], modules: [Phoenix.LiveViewTest, Phoenix.ConnTest, Kernel]]` |
| `OptimumCredo.Check.Warning.TestWithoutAssertion` | A test without an assertion cannot fail for the behaviour it names. | `low` | `[assertion_helpers: [], treat_bang_call_as_assertion: false, treat_mock_expectations_as_assertions: true, treat_match_as_assertion: true]` |
| `OptimumCredo.Check.Warning.UnsafeAtomConversion` | Do not create atoms from runtime data. | `low` | `[allow_in_paths: []]` |
| `OptimumCredo.Check.Warning.WebNamespaceInCore` | Core modules should not depend on the web namespace. Web code may use core modules, never the other way around. | `low` | `[web_namespace: nil, allowed_modules: [], allow_application: true]` |