OeditusCredo

OeditusCredo

Custom Credo checks for detecting common Elixir/Phoenix anti-patterns, mistakes, and CWE Top 25 security vulnerabilities.

Overview

OeditusCredo provides 58 comprehensive custom Credo checks that detect common mistakes, risky code, and security vulnerabilities in Elixir and Phoenix projects:

Error Handling Anti-patterns

  • MissingErrorHandling - Detects {:ok, x} = pattern without error handling
  • SilentErrorCase - Detects case statements missing error branches
  • SwallowingException - Detects try/rescue blocks without logging or re-raising

Database & Performance Issues

  • InefficientFilter - Detects Repo.all followed by Enum filtering
  • NPlusOneQuery - Detects potential N+1 queries (Enum.map with Repo calls)
  • MissingPreload - Detects Ecto queries without proper preloading

LiveView & Concurrency Issues

  • UnmanagedTask - Detects unsupervised Task.async calls
  • SyncOverAsync - Detects blocking operations in LiveView/GenServer callbacks
  • MissingHandleAsync - Detects blocking in handle_event without async pattern
  • MissingThrottle - Detects form inputs without phx-debounce/throttle
  • InlineJavascript - Detects inline JS handlers instead of phx-* bindings

Readability

  • UnnecessaryInterpolatingSigil - Detects ~s/~c/~w without interpolation (suggests ~S/~C/~W)

Code Quality & Maintainability

  • DirectStructUpdate - Detects direct struct updates instead of changesets
  • CallbackHell - Detects deeply nested case statements (suggests with)
  • BlockingInPlug - Detects blocking operations in Plug functions
  • UnsafeMapAccess - Detects bracket access on maps where dot access is safer (requires typle + Elixir >= 1.20)

Code Organization & Idiomatic Refactoring

  • PreferCasePatternMatching - Detects if/cond where case pattern matching is preferred
  • PreferMultiHeadFunction - Detects parameter branching inside function body instead of multi-head clauses
  • PreferMultiHeadForNil - Detects is_nil/1 guards instead of multi-head clauses matching nil directly
  • PreferPipelineOperator - Detects sequential assignments instead of pipe operator |>
  • PreferInplaceMapMatching - Detects is_map/1 guard instead of inplace %{} = map pattern matching
  • PreferInplaceListMatching - Detects O(N) length/1 calls in guards instead of [_ | _] or []

  • PreferInplaceBinaryMatching - Detects is_binary non-empty guards instead of <<_::utf8, _::binary>>
  • PreferDestructuring - Detects elem/2 and Map.get/2 with literal keys instead of pattern match destructuring
  • PreferDotAccessForStructs - Detects struct[:field] bracket access instead of struct.field
  • PreferWithClause - Detects nested case statements (pyramid of doom) instead of with
  • PreferTaggedTuplesForErrors - Detects try...rescue used as control flow instead of {:ok, _} / {:error, _}
  • PreferForComprehensionOverFilterMap - Detects Enum.filter |> Enum.map instead of a for comprehension
  • PreferListPrepend - Detects O(N) list ++ [item] appends instead of [item | list]

  • PreferPatternMatchingForEmptiness - Detects Enum.count(list) > 0 instead of matching [_ | _] or []

  • PreferStringBoundariesOverRegex - Detects Regex.match? for prefix/suffix checks instead of String.starts_with?/2 and String.ends_with?/2
  • PreferFunctionCapture - Detects fn x -> Module.func(x) end instead of &Module.func/1
  • PreferShortFieldAccessCapture - Detects fn x -> x.field end instead of & &1.field
  • PreferMapMerge - Detects chained Map.put calls instead of a single Map.merge/2
  • AvoidSinglePipe - Detects single-stage pipes x |> f() instead of a direct call f(x)
  • AvoidUnawaitedTaskAsync - Detects fire-and-forget Task.async whose handle is discarded

Refactoring Suggestions

  • SuggestFSM - Detects imperative status/state management (suggests Finitomata or :gen_statem)
  • ChangeRiskAntiPatterns - Flags functions with a high CRAP score (both complex and under-tested). Opt-in/disabled by default; requires coverage data (see Change Risk Anti-Patterns (CRAP) score).

Telemetry & Observability

  • TelemetryInRecursiveFunction - Detects telemetry inside recursive functions (anti-pattern)
  • MissingTelemetryInAuthPlug - Detects auth/authz plugs without telemetry
  • MissingTelemetryForExternalHttp - Detects HTTP client calls without telemetry wrapper

Security - Injection (CWE-89, CWE-78, CWE-94, CWE-79)

  • SQLInjection - Detects string interpolation/concatenation in Ecto queries
  • OSCommandInjection - Detects user input passed to System.cmd/os:cmd
  • CodeInjection - Detects dynamic code execution via Code.eval_string
  • XSSVulnerability - Detects raw/1 with user input in templates

Security - Authentication & Authorization (CWE-306, CWE-862, CWE-863, CWE-639)

  • MissingAuthentication - Detects controllers/routers without authentication plugs
  • MissingAuthorization - Detects Phoenix actions without authorization checks
  • IncorrectAuthorization - Detects role checks using negation/!= patterns
  • InsecureDirectObjectReference - Detects direct DB lookups from user params without ownership checks

Security - Data Protection (CWE-200, CWE-798, CWE-502)

  • SensitiveDataExposure - Detects sensitive fields in JSON responses and inspect output
  • HardcodedCredentials - Detects hardcoded passwords, API keys, tokens, and secrets
  • UnsafeDeserialization - Detects :erlang.binary_to_term without the :safe option

Security - Input & File Handling (CWE-20, CWE-22, CWE-434)

  • ImproperInputValidation - Detects missing validation of external input
  • PathTraversal - Detects user input in file paths without sanitization
  • UnrestrictedFileUpload - Detects file uploads without content-type validation

Security - Web (CWE-352, CWE-918)

  • MissingCSRFProtection - Detects API pipelines without CSRF protection
  • SSRFVulnerability - Detects HTTP requests with user-controlled URLs

Security - Race Conditions (CWE-367)

  • TOCTOU - Detects time-of-check/time-of-use patterns (File.exists? then File.read)

Important Note

All these checks are somewhat opinionated and might produce false positives. If a warning does not apply to your specific case, you can suppress it with # credo:disable-for-next-line or any other Credo config comment directive.

Installation

As a Project Dependency

Add oeditus_credo to your list of dependencies in mix.exs:

def deps do
  [
    {:oeditus_credo, "~> 0.10", only: [:dev, :test], runtime: false}
  ]
end

Standalone Installation (No Dependency Required)

You can also use OeditusCredo without adding it to your project dependencies:

# Install as a Hex archive (recommended for development)
mix archive.install hex oeditus_credo

# Or download and use the escript executable (best for CI/CD)
curl -L https://github.com/Oeditus/oeditus_credo/releases/latest/download/oeditus_credo -o oeditus_credo
chmod +x oeditus_credo

See STANDALONE.md for detailed standalone usage instructions.

Usage

With Standalone Installation

If you installed OeditusCredo as an archive or escript:

mix oeditus_credo              # Run with all checks enabled
mix oeditus_credo --strict     # Fail on any issues
mix oeditus_credo lib/my_app   # Analyze specific directory

With Project Dependency

Add the checks to your .credo.exs configuration:

%{
  configs: [
    %{
      name: "default",
      plugins: [],
      requires: [],
      checks: %{
        enabled: [
          # ... existing checks ...
          # Error Handling
          {OeditusCredo.Check.Warning.MissingErrorHandling, []},
          {OeditusCredo.Check.Warning.SilentErrorCase, []},
          {OeditusCredo.Check.Warning.SwallowingException, []},
          # Database & Performance
          {OeditusCredo.Check.Warning.InefficientFilter, []},
          {OeditusCredo.Check.Warning.NPlusOneQuery, []},
          {OeditusCredo.Check.Warning.MissingPreload, []},
          # LiveView & Concurrency
          {OeditusCredo.Check.Warning.UnmanagedTask, []},
          {OeditusCredo.Check.Warning.SyncOverAsync, []},
          {OeditusCredo.Check.Warning.MissingHandleAsync, []},
          {OeditusCredo.Check.Warning.MissingThrottle, []},
          {OeditusCredo.Check.Warning.InlineJavascript, []},
          # Readability
          {OeditusCredo.Check.Readability.UnnecessaryInterpolatingSigil, []},
          # Code Quality
          {OeditusCredo.Check.Warning.DirectStructUpdate, []},
          {OeditusCredo.Check.Warning.CallbackHell, [max_nesting: 2]},
          {OeditusCredo.Check.Warning.BlockingInPlug, []},
          # Code Organization & Idiomatic Refactoring
          {OeditusCredo.Check.Refactoring.PreferCasePatternMatching, []},
          {OeditusCredo.Check.Refactoring.PreferMultiHeadFunction, []},
          {OeditusCredo.Check.Refactoring.PreferMultiHeadForNil, []},
          {OeditusCredo.Check.Refactoring.PreferPipelineOperator, []},
          {OeditusCredo.Check.Refactoring.PreferInplaceMapMatching, []},
          {OeditusCredo.Check.Refactoring.PreferInplaceListMatching, []},
          {OeditusCredo.Check.Refactoring.PreferInplaceBinaryMatching, []},
          {OeditusCredo.Check.Refactoring.PreferDestructuring, []},
          {OeditusCredo.Check.Refactoring.PreferDotAccessForStructs, []},
          {OeditusCredo.Check.Refactoring.PreferWithClause, []},
          {OeditusCredo.Check.Refactoring.PreferTaggedTuplesForErrors, []},
          {OeditusCredo.Check.Refactoring.PreferForComprehensionOverFilterMap, []},
          {OeditusCredo.Check.Refactoring.PreferListPrepend, []},
          {OeditusCredo.Check.Refactoring.PreferPatternMatchingForEmptiness, []},
          {OeditusCredo.Check.Refactoring.PreferStringBoundariesOverRegex, []},
          {OeditusCredo.Check.Refactoring.PreferFunctionCapture, []},
          {OeditusCredo.Check.Refactoring.PreferShortFieldAccessCapture, []},
          {OeditusCredo.Check.Refactoring.PreferMapMerge, []},
          {OeditusCredo.Check.Refactoring.AvoidSinglePipe, []},
          {OeditusCredo.Check.Refactoring.AvoidUnawaitedTaskAsync, []},
          # Refactoring Suggestions
          {OeditusCredo.Check.Refactoring.SuggestFSM, []},
          # Telemetry & Observability
          {OeditusCredo.Check.Warning.TelemetryInRecursiveFunction, []},
          {OeditusCredo.Check.Warning.MissingTelemetryInAuthPlug, []},
          {OeditusCredo.Check.Warning.MissingTelemetryForExternalHttp, []},
          # Security - Injection
          {OeditusCredo.Check.Security.SQLInjection, []},
          {OeditusCredo.Check.Security.OSCommandInjection, []},
          {OeditusCredo.Check.Security.CodeInjection, []},
          {OeditusCredo.Check.Security.XSSVulnerability, []},
          # Security - Auth
          {OeditusCredo.Check.Security.MissingAuthentication, []},
          {OeditusCredo.Check.Security.MissingAuthorization, []},
          {OeditusCredo.Check.Security.IncorrectAuthorization, []},
          {OeditusCredo.Check.Security.InsecureDirectObjectReference, []},
          # Security - Data Protection
          {OeditusCredo.Check.Security.SensitiveDataExposure, []},
          {OeditusCredo.Check.Security.HardcodedCredentials, []},
          {OeditusCredo.Check.Security.UnsafeDeserialization, []},
          # Security - Input & File Handling
          {OeditusCredo.Check.Security.ImproperInputValidation, []},
          {OeditusCredo.Check.Security.PathTraversal, []},
          {OeditusCredo.Check.Security.UnrestrictedFileUpload, []},
          # Security - Web
          {OeditusCredo.Check.Security.MissingCSRFProtection, []},
          {OeditusCredo.Check.Security.SSRFVulnerability, []},
          # Security - Race Conditions
          {OeditusCredo.Check.Security.TOCTOU, []}
        ],
        disabled: [
          # Opt-in: needs persisted coverage data, see the CRAP section below
          {OeditusCredo.Check.Refactoring.ChangeRiskAntiPatterns, []},
          # Opt-in: needs the optional `typle` dependency and Elixir 1.20+
          {OeditusCredo.Check.Warning.UnsafeMapAccess, []}
        ]
      }
    ]
  ]
}

Then run:

mix credo

The same set of checks (with the two opt-in ones disabled) is what mix oeditus_credo enables automatically, so a standalone installation needs no .credo.exs at all.

AI Assistant & Automation Tasks

OeditusCredo includes built-in Mix tasks to generate AI coding rules, assistant skills, and GitHub Actions workflows for local PR shaping and automated CI reviews.

mix oeditus_assistant_rules

Generates coding rules in Markdown format (.aiassistant/rules/oeditus.md) derived from all OeditusCredo checks. Remote and local AI assistants (Cursor, Copilot, Cody, etc.) use this file to avoid producing code that triggers Credo warnings.

mix oeditus_assistant_rules            # Writes to .aiassistant/rules/oeditus.md
mix oeditus_assistant_rules --stdout   # Prints rules to stdout
mix oeditus_assistant_rules -o PATH    # Writes to a custom file path

mix oeditus_assistant_skill

Generates an AI assistant skill file (SKILL.md) for running OeditusCredo checks in a lightweight local mode. It empowers AI assistants to evaluate local files or PR diffs and propose idiomatic refactorings directly without needing full CI runs.

Supported target assistants: claude, openai, gemini.

# Generate skill for specific assistant target
mix oeditus_assistant_skill claude    # Writes to .claude/skills/oeditus/SKILL.md
mix oeditus_assistant_skill openai    # Writes to .openai/skills/oeditus/SKILL.md
mix oeditus_assistant_skill gemini    # Writes to .gemini/skills/oeditus/SKILL.md

# Options
mix oeditus_assistant_skill claude --stdout   # Prints skill to stdout
mix oeditus_assistant_skill claude -o PATH    # Writes to custom file path

mix oeditus_assistant_gha

Generates a GitHub Action workflow (.github/workflows/oeditus.yml) and composite action runner (.github/actions/oeditus/) that automatically review Elixir PR diffs using Claude or OpenAI models and post inline GitHub PR comments with code suggestions.

Generated files:

  • .github/actions/oeditus/action.yml (Composite action metadata)
  • .github/actions/oeditus/review_pr.py (Standalone Python 3 PR reviewer)
  • .github/workflows/oeditus.yml (GitHub Actions workflow trigger)
mix oeditus_assistant_gha            # Writes action and workflow files
mix oeditus_assistant_gha --stdout   # Prints generated workflow files to stdout
mix oeditus_assistant_gha -o DIR     # Writes files to a custom base directory

Change Risk Anti-Patterns (CRAP) score

ChangeRiskAntiPatterns is opt-in and disabled by default because it needs test-coverage data that Credo cannot produce on its own. It combines each function's cyclomatic complexity with its test coverage:

CRAP = complexity^2 * (1 - coverage/100)^3 + complexity

A fully covered function scores its complexity; a complex, untested function scores much higher. The default maximum is 30 (the historical CRAP convention).

IMPORTANT: This check only works when run after generating persisted coverage data. It reads cover/default.coverdata, which is produced by --export-coverage:

mix test --cover --export-coverage default
mix credo

Plain mix test --cover prints a coverage report but does not leave an importable coverage file, so it is not sufficient. When no coverage data is found, the check does nothing by default (so it never breaks a mix credo run launched without coverage). Set require_coverage: true to turn missing coverage into a reported issue instead (useful in CI).

Enable it in .credo.exs:

{OeditusCredo.Check.Refactoring.ChangeRiskAntiPatterns, []}

Parameters: max_score (default 30), coverdata (default "cover/default.coverdata"), exclude_test_files (default true), require_coverage (default false).

Configuration Options

All checks support configuration parameters. Pass them in .credo.exs:

General (Credo-standard) Parameters

Every check accepts the following general parameters provided by Credo:

  • false — Disable a check entirely. When a check tuple uses false instead of a keyword list, the check is skipped and produces no issues.

    # Disable a check
    {OeditusCredo.Check.Warning.NPlusOneQuery, false}
  • exit_status (integer()) — Override the exit status contributed by issues from this check. By default, all checks in the :warning category contribute exit status 16. Setting exit_status: 0 means the check still runs and reports issues, but they will not cause a non-zero exit code.

    # Run the check but don't fail CI on its issues
    {OeditusCredo.Check.Warning.NPlusOneQuery, exit_status: 0}
    
    # Custom exit status
    {OeditusCredo.Check.Security.SQLInjection, exit_status: 2}
  • priority — Override the base priority for the check (:low, :normal, :high, :higher, or :ignore).

  • files — Restrict which files the check runs on:

    {OeditusCredo.Check.Security.SQLInjection, files: %{included: ["lib/my_app/repo.ex"]}}

These parameters can be combined with any check-specific parameters.

Common Check-Specific Parameters

Every OeditusCredo check additionally accepts:

  • exclude_test_files (boolean()) — When set to true, files ending in _test.exs or located under a /test/ directory are skipped. The default is false for every check except ChangeRiskAntiPatterns, where it defaults to true.

Code Quality

  • CallbackHell: max_nesting — Maximum allowed case nesting depth (default: 2)
  • DirectStructUpdate: extra_struct_patterns — Additional regex strings for struct-like variable names (default: [])
  • BlockingInPlug: extra_blocking_modules — Additional module atoms to treat as blocking (default: [])

Code Organization & Idiomatic Refactoring

The idiomatic refactoring checks (PreferCasePatternMatching, PreferMultiHeadFunction, PreferMultiHeadForNil, PreferPipelineOperator, PreferInplaceMapMatching, PreferInplaceListMatching, PreferInplaceBinaryMatching, PreferDestructuring, PreferDotAccessForStructs, PreferWithClause, PreferTaggedTuplesForErrors, PreferForComprehensionOverFilterMap, PreferListPrepend, PreferPatternMatchingForEmptiness, PreferStringBoundariesOverRegex, PreferFunctionCapture, PreferShortFieldAccessCapture, PreferMapMerge, AvoidSinglePipe, AvoidUnawaitedTaskAsync) take no check-specific parameters beyond the general Credo ones and exclude_test_files. Since they are the most opinionated checks in the suite, exclude_test_files: true and exit_status: 0 are the usual ways to soften them:

{OeditusCredo.Check.Refactoring.AvoidSinglePipe, [exclude_test_files: true, exit_status: 0]}

Refactoring Suggestions

  • SuggestFSM: status_field_names — Field names to watch (default: [:status, :state]); min_states — Minimum distinct status values before flagging (default: 3)
  • ChangeRiskAntiPatterns: max_score — Maximum CRAP score before a function is reported (default: 30); coverdata — Path to the persisted coverage file (default: "cover/default.coverdata"); exclude_test_files — Skip test files (default: true); require_coverage — Report an issue when coverage data is missing instead of skipping (default: false). See Change Risk Anti-Patterns (CRAP) score for the required workflow.

LiveView & Concurrency

  • SyncOverAsync: extra_blocking_modules — Additional blocking module atoms (default: []); callback_functions — Callback names to check (default: [:handle_event, :handle_call, :handle_info, :handle_cast, :handle_continue])
  • MissingHandleAsync: extra_blocking_modules — Additional blocking module atoms (default: [])

Telemetry & Observability

  • MissingTelemetryForExternalHttp: extra_http_modules — Additional {module_parts, [functions]} tuples (default: [])
  • MissingTelemetryInAuthPlug: extra_auth_plug_names — Additional auth plug name substrings (default: [])

Security — Injection

  • CodeInjection: extra_dangerous_functions — Additional Code.* function atoms to flag (default: [])

Security — Auth

  • MissingAuthentication: sensitive_actions — Controller actions requiring auth (default: [:index, :show, :create, :new, :update, :edit, :delete, :destroy])
  • MissingAuthorization: extra_auth_indicators — Additional authorization indicator substrings (default: [])
  • IncorrectAuthorization: extra_auth_indicators — Additional authorization indicator substrings (default: [])
  • InsecureDirectObjectReference: extra_ownership_indicators — Additional ownership/auth indicator substrings (default: [])

Security — Data Protection

  • SensitiveDataExposure: extra_sensitive_terms — Additional sensitive field substrings (default: [])
  • HardcodedCredentials: extra_credential_terms — Additional credential name substrings (default: [])

Security — Web

  • SSRFVulnerability: extra_http_modules — Additional HTTP module atom lists, e.g. [[:MyHTTP]] (default: [])

Example

# Customise check-specific params
{OeditusCredo.Check.Warning.CallbackHell, [max_nesting: 3]},
{OeditusCredo.Check.Warning.SyncOverAsync, [extra_blocking_modules: [:ExternalAPI]]},
{OeditusCredo.Check.Security.CodeInjection, [extra_dangerous_functions: [:compile_string]]},
{OeditusCredo.Check.Security.HardcodedCredentials, [exclude_test_files: true, extra_credential_terms: ["conn_string"]]},
{OeditusCredo.Check.Warning.MissingTelemetryForExternalHttp, [
  extra_http_modules: [{[:MyApp, :HTTP], [:get, :post]}]
]},

# Run check as advisory only (won't affect exit code)
{OeditusCredo.Check.Warning.DirectStructUpdate, exit_status: 0},

# Disable a check entirely
{OeditusCredo.Check.Warning.InlineJavascript, false}

Test Coverage

The library includes comprehensive tests for all 58 checks. Run tests with:

mix test

Run mix test to see the current test count and coverage.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT License. See LICENSE for details.