Packages

Cross-language static code analysis tool built on MetaAST. Provides 72 checks covering security (CWE Top 25), code quality, readability, design, consistency, observability, and refactoring.

Current section

Files

Jump to
metacredo README.md
Raw

README.md

<img src="https://raw.githubusercontent.com/Oeditus/metacredo/v0.4.4/priv/images/logo-200.png" alt="MetaCredo" width="128" align="right">

# MetaCredo

Cross-language static code analysis tool built on
[`MetaAST`](https://github.com/Oeditus/metastatic).  
  
Write a check once, run it across Elixir, Erlang, Ruby,
Python, Haskell, and all other languages supported by Metastatic.

## Credits

MetaCredo stands on the shoulders of
[Credo](https://github.com/rrrene/credo) by Rene Foehring—an
exceptional static analysis tool that has shaped how the entire Elixir
community thinks about code quality, consistency, and teaching through
tooling. Credo's design—its check behaviour, category system,
configuration format, and the philosophy that a linter should *teach*
rather than merely scold—served as the direct architectural
inspiration for MetaCredo. We are grateful for the years of thoughtful
work that went into Credo and the high bar it set for developer
experience in static analysis.

MetaCredo extends that vision across language boundaries: every check
operates on Metastatic's unified MetaAST, so the same insight that
helps an Elixir developer can help a Python, Ruby, or Haskell developer
just as well.

## Installation

Add `metacredo` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:metacredo, "~> 0.4", only: [:dev, :test], runtime: false}
  ]
end
```

## Usage

```sh
# Run all checks
$ mix metacredo

# Strict mode (only normal+ priority issues)
$ mix metacredo --strict

# Filter by category
$ mix metacredo --only security,warning

# Umbrella switches to turn off DB or user input checks
$ mix metacredo --no-db
$ mix metacredo --no-user

# JSON output
$ mix metacredo --format json

# Explain a specific check
$ mix metacredo explain MetaCredo.Check.Security.HardcodedValue

# Generate default configuration (local or global)
$ mix metacredo.gen.config
$ mix metacredo.gen.config --global
```

## How It Works

MetaCredo operates on the **MetaAST** representation provided by Metastatic.
Source files are parsed into a language-agnostic AST using Metastatic's adapters
(Elixir, Python, Ruby, Haskell, Erlang, Cure, March, JavaScript, TypeScript), and then checks pattern-match against
the uniform `{type, keyword_meta, children}` node structure. This means every
check is cross-language by default.

## Check Categories (72 checks)

### Consistency `[C]`—2 checks

- `ExceptionNames`—Exception/error classes not ending in "Error" or "Exception"
- `ParameterPatternMatching`—Destructuring in body instead of params

### Security `[S]`—15 checks

- `HardcodedValue`—Hardcoded URLs, IPs, and sensitive values in string literals
- `SQLInjection`—SQL string concatenation/interpolation with variables (CWE-89)
- `XSSVulnerability`—raw(), html_safe, innerHTML, dangerouslySetInnerHTML (CWE-79)
- `PathTraversal`—File operations with user-controlled paths (CWE-22)
- `SSRFVulnerability`—HTTP requests with user-controlled URLs (CWE-918)
- `SensitiveDataExposure`—Logging/inspecting passwords, tokens, PII (CWE-200)
- `MissingCSRFProtection`—State-changing actions without CSRF validation (CWE-352)
- `InsecureDirectObjectReference`—Direct DB lookups from user params (CWE-639)
- `UnrestrictedFileUpload`—File uploads without type/size validation (CWE-434)
- `TOCTOU`—File.exists? followed by file operations (CWE-367)
- `MissingAuthentication`—Controllers/handlers without auth middleware (CWE-306)
- `MissingAuthorization`—Sensitive operations without authorization (CWE-862)
- `IncorrectAuthorization`—Auth-after-action bugs, negation patterns (CWE-863)
- `ImproperInputValidation`—User input to sensitive ops without validation (CWE-20)
- `InlineJavascript`—Inline script tags, onclick handlers, javascript: URIs

### Warning `[W]`—22 checks

- `MissingErrorHandling`—`{:ok, _} = call()` without error handling
- `SilentErrorCase`—case matching {:ok, _} without {:error, _} branch
- `SwallowingException`—try/rescue without logging or re-raising
- `NPlusOneQuery`—Database calls inside collection operations (N+1)
- `MissingPreload`—Collection ops over DB results without eager loading
- `UnmanagedTask`—Task.async without Task.Supervisor
- `SyncOverAsync`—Blocking calls in GenServer/LiveView callbacks
- `MissingHandleAsync`—Blocking in handle_event without async delegation
- `DirectStructUpdate`—Struct updates bypassing changesets
- `CallbackHell`—Deeply nested conditionals exceeding threshold
- `BlockingInPlug`—Blocking I/O in Plug call/init middleware
- `MissingThrottle`—Expensive operations without rate limiting
- `InefficientFilter`—Repo.all then Enum.filter (filter in memory)
- `ImperativeStatusHandling`—Imperative if/else chains on status codes
- `UnusedOperation`—Function call result discarded (not assigned or returned)
- `UnsafeExec`—System.cmd/exec with user-controlled arguments (CWE-78)
- `BoolOperationOnSameValues`—`x && x`, `x || x` (always same result)
- `OperationOnSameValues`—`x - x` (always 0), `x / x` (always 1)
- `OperationWithConstantResult`—`x * 0` (always 0)
- `LazyLogging`—Logger with string interpolation instead of anonymous function
- `DebugLeftover`—IO.inspect, console.log, dbg() left in production code
- `RaiseInsideRescue`—raise/throw inside rescue without re-raise semantics

### Readability `[R]`—13 checks

- `MagicNumber`—Numeric literals in expressions without named constants
- `DeepNesting`—Functions with nesting depth exceeding threshold
- `LongFunction`—Functions with too many statements
- `ComplexConditional`—Deeply nested boolean operations
- `LongParameterList`—Functions with too many parameters
- `FunctionNames`—Function names not in snake_case
- `ModuleNames`—Module/container names not in PascalCase
- `VariableNames`—Variable names not in snake_case
- `ModuleDoc`—Modules without documentation
- `SinglePipe`—Single-step pipe chains (unnecessary `|>`)
- `NestedFunctionCalls`—Deeply nested calls like `foo(bar(baz(x)))`
- `Specs`—Public functions without type specifications
- `LargeNumbers`—Large integers without underscore separators

### Refactor `[F]`—10 checks

- `SimplifyConditional`—`if x do true else false end` patterns
- `DeadCode`—Unreachable code after early returns
- `CodeDuplication`—Duplicate function bodies (same AST structure)
- `NegatedConditionWithElse`—`if !x do...else` (swap branches)
- `DoubleBooleanNegation`—`!!x` pattern (simplify to boolean cast)
- `AppendSingleItem`—`list ++ [item]` (use `[item | list]` or `List.insert_at`)
- `PipeChainStart`—Pipe chains starting with a literal value
- `FilterCount`—`Enum.filter |> Enum.count` (use `Enum.count/2`)
- `UnlessWithElse`—`unless...else` (use `if` instead)
- `VariableRebinding`—Same variable assigned multiple times in a block

### Design `[D]`—5 checks

- `HighComplexity`—Functions with cyclomatic complexity exceeding threshold
- `LowCohesion`—Modules where functions share no common data
- `HighCoupling`—Modules with too many external dependencies
- `TagTODO`—TODO comments that should be addressed (configurable via `tags`)
- `TagFIXME`—FIXME comments indicating known bugs (configurable via `tags`)

### Observability `[O]`—5 checks

- `MissingTelemetryInObanWorker`—Oban worker perform/1 without telemetry
- `MissingTelemetryInLiveviewMount`—LiveView mount/3 without telemetry
- `MissingTelemetryInAuthPlug`—Auth plug call/2 without telemetry
- `MissingTelemetryForExternalHttp`—HTTP client calls without telemetry wrapper
- `TelemetryInRecursiveFunction`—Telemetry inside recursive functions (anti-pattern)

Generate a default `.metacredo.exs` file in your project directory by running:

```sh
$ mix metacredo.gen.config
```

Or generate a user-wide global configuration in `~/.config/metacredo/.metacredo.exs` (or `$XDG_CONFIG_HOME/metacredo/.metacredo.exs`):

```sh
$ mix metacredo.gen.config --global
```

MetaCredo resolves configuration files in order:
1. Local `.metacredo.exs` in the project root
2. `config/.metacredo.exs`
3. Global user configuration (`~/.config/metacredo/.metacredo.exs`)
4. Internal defaults

### Typical `.metacredo.exs` Example

Here is an example of a typical `.metacredo.exs` configuration file:

```elixir
# Generated by `mix metacredo.gen.config`.
# Move any check to the `disabled` list or set its second element to `false`
# to turn it off. Add `{ModuleName, [param: value]}` to customize parameters.
%{
  configs: [
    %{
      name: "default",
      no_db: false,
      no_user: false,
      files: %{
        included: ["lib/", "src/", "web/"],
        excluded: [
          ~r"(^|/)_build/",
          ~r"(^|/)deps/",
          ~r"(^|/)node_modules/",
          ~r"(^|/)\.git/",
          ~r"(^|/)test/"
        ]
      },
      checks: %{
        enabled: [
          # -- Consistency --
          {MetaCredo.Check.Consistency.ExceptionNames, []},
          {MetaCredo.Check.Consistency.ParameterPatternMatching, []},

          # -- Security --
          {MetaCredo.Check.Security.HardcodedValue, [exclude_localhost: true]},
          {MetaCredo.Check.Security.SQLInjection, []},
          {MetaCredo.Check.Security.XSSVulnerability, []},
          {MetaCredo.Check.Security.PathTraversal, []},
          {MetaCredo.Check.Security.SSRFVulnerability, []},
          {MetaCredo.Check.Security.SensitiveDataExposure, []},
          {MetaCredo.Check.Security.MissingCSRFProtection, []},

          # -- Warning --
          {MetaCredo.Check.Warning.MissingErrorHandling, []},
          {MetaCredo.Check.Warning.SilentErrorCase, []},
          {MetaCredo.Check.Warning.SwallowingException, []},
          {MetaCredo.Check.Warning.NPlusOneQuery, []},
          {MetaCredo.Check.Warning.UnmanagedTask, []},

          # -- Readability --
          {MetaCredo.Check.Readability.MagicNumber, [ignored_numbers: [0, 1, -1, 2]]},
          {MetaCredo.Check.Readability.DeepNesting, [max_nesting: 3]},
          {MetaCredo.Check.Readability.LongFunction, [max_length: 50]},
          {MetaCredo.Check.Readability.FunctionNames, []},
          {MetaCredo.Check.Readability.SinglePipe, []},

          # -- Refactor --
          {MetaCredo.Check.Refactor.SimplifyConditional, []},
          {MetaCredo.Check.Refactor.DeadCode, []},
          {MetaCredo.Check.Refactor.CodeDuplication, []},

          # -- Design --
          {MetaCredo.Check.Design.HighComplexity, [max_complexity: 10]},
          {MetaCredo.Check.Design.TagTodo, []},
          {MetaCredo.Check.Design.TagFixme, []},

          # -- Observability --
          {MetaCredo.Check.Observability.MissingTelemetryInObanWorker, []},
          {MetaCredo.Check.Observability.MissingTelemetryInLiveviewMount, []}
        ],
        disabled: [
          {MetaCredo.Check.Readability.ModuleDoc, []}
        ]
      }
    }
  ]
}
```

Alternatively, set `enabled: :all` to run all available checks by default and explicitly list disabled checks in `disabled: [...]`.

### Configuring TODO / FIXME tags

The `Design.TagTodo` and `Design.TagFixme` checks accept a `tags` param: a
list of tag patterns to search for inside comments. Each entry may be a
literal string (matched exactly, case-insensitively) or a regex:

```elixir
# -- Design --
# Report TODO plus the custom XXX tag, and FIXME plus HACK:
{MetaCredo.Check.Design.TagTodo, [tags: ["TODO", ~r/XXX/]]},
{MetaCredo.Check.Design.TagFixme, [tags: ["FIXME", ~r/HACK/]]},
```

Defaults are `tags: ["TODO"]` and `tags: ["FIXME"]` respectively. Passing
`tags: []` disables the check. Only matches that occur **inside a comment**
are reported -- a tag appearing in a string literal, variable/function name,
or `@moduledoc` prose is ignored.

## Default Noise Policy

MetaCredo ships with a **curated, low-noise default set**. The goal is that
*every* finding reported by a default run is actionable without any
configuration. Checks are disabled by default for one of two reasons:

1. **They are project-specific** and only make sense with explicit opt-in
   (e.g. web-only security checks in a CLI/library codebase).
2. **They rely on unsound heuristics** (typically substring matching on
   function or variable names) that produce a high false-positive rate on
   idiomatic code.

The following checks are disabled by default:

| Check | Why it is off by default |
|---|---|
| `Security.MissingAuthentication` | Project-specific (web apps only). |
| `Security.MissingCSRFProtection` | Project-specific (web apps only). |
| `Security.IncorrectAuthorization` | Project-specific (web apps only). |
| `Security.ImproperInputValidation` | Project-specific (web apps only). |
| `Security.PathTraversal` | Name-substring heuristic: `format_issue` matches `rm`, `source_file` matches `file`. |
| `Security.SSRFVulnerability` | Name-substring heuristic: `params_get` matches `get`. |
| `Security.InsecureDirectObjectReference` | Name-substring heuristic: any variable containing `id`/`param`. |
| `Security.MissingAuthorization` | Name-substring heuristic: `Map.update` flagged via `id` in args. |
| `Security.XSSVulnerability` | Name-substring heuristic: `has_raw_content?` matches `raw`. |
| `Security.UnrestrictedFileUpload` | Name-substring heuristic on helper functions. |
| `Security.InlineJavascript` | Flags any string containing `<script>`/`onclick=`, including its own doc examples. |
| `Warning.MissingErrorHandling` | Noisy on idiomatic `{:ok, _} = ...` discard patterns. |
| `Warning.UnusedOperation` | Noisy on idiomatic discard patterns. |
| `Warning.NPlusOneQuery` | `database_function?/1` matches any call containing `get`/`find`/`fetch`/`load`/`select`, so `Map.get` in a `reduce` trips it. |
| `Observability.MissingTelemetryInAuthPlug` | Name-substring heuristic: `check_loc`, `tokens`, `validate_*` flagged as "auth". |
| `Warning.ImperativeStatusHandling` | Name-substring heuristic: `block`, `lock`, `check` flagged as state transitions. |
| `Design.LowCohesion` | Variable-sharing heuristic trips on nearly every module with helpers. |
| `Warning.CallbackHell` | Redundant with `DeepNesting` + `ComplexConditional` + `HighComplexity`. |
| `Warning.DebugLeftover` | Flags legitimate output primitives (`IO.puts`, `IO.write`). |
| `Readability.Specs` | Noisy on behaviour callbacks and Mix-task `run/1`. |

### Default Exclusions

Beyond the checks above, MetaCredo also excludes whole directory trees from
analysis by default via `files.excluded`:

| Pattern | Why it is excluded |
|---|---|
| `~r"(^|/)_build/"` | Compiled build artifacts. |
| `~r"(^|/)deps/"` | Third-party dependencies. |
| `~r"(^|/)node_modules/"` | JavaScript dependencies. |
| `~r"(^|/)\.git/"` | Version-control internals. |
| `~r"(^|/)test/"` | Test trees (including nested `lib/test/` and umbrella `apps/*/test/`). |

The `test/` exclusion keeps checks that are tuned for production code from
firing on test-only patterns (hardcoded credentials, `assert_raise`, magic
numbers, etc.). Paths that merely *contain* the substring `test` — such as
`lib/latest/`, `lib/contest/`, or a root-level `test_helper.exs` — are **not**
excluded. The `test/` exclusion is a *default*: explicitly targeting a path
lifts it, so the tests are analysed. Add the tree back via `files.included`
in `.metacredo.exs`, or target it explicitly on the command line:

```bash
$ mix metacredo --path test/
$ mix metacredo --files-included test/
```
To enable any of these, add it to the `enabled` list (or remove it from
`disabled`) in `.metacredo.exs`. For web projects, a good starting point is:

```elixir
checks: %{
  enabled: :all,
  disabled: [
    # keep the genuinely noisy ones off
    {MetaCredo.Check.Warning.CallbackHell, []},
    {MetaCredo.Check.Warning.DebugLeftover, []},
    {MetaCredo.Check.Design.LowCohesion, []}
  ]
}
```

`mix metacredo.gen.config` writes a configuration file that already reflects
this policy: curated checks land in `enabled`, and the rest in `disabled`.
## Inline Disable Comments

Use source comments to suppress specific checks:

```elixir
# metacredo:disable-for-next-line MetaCredo.Check.Security.HardcodedValue
@test_url "https://api.example.com"

# metacredo:disable-for-this-file
```

The comment must be represented as a `:comment` node in the MetaAST for
inline disabling to work. Metastatic's adapters that preserve comments
(e.g., the Cure adapter) support this out of the box.

## Writing Custom Checks

```elixir
defmodule MyApp.Check.CustomCheck do
  use MetaCredo.Check,
    category: :warning,
    base_priority: :normal,
    explanations: [
      check: "Detects a custom anti-pattern.",
      params: [threshold: "Maximum allowed occurrences (default: 3)"]
    ],
    param_defaults: [threshold: 3]

  @impl true
  def run(%SourceFile{} = source_file, params) do
    threshold = params_get(params, :threshold)

    source_file
    |> SourceFile.ast()
    |> Metastatic.AST.prewalk([], fn node, acc ->
      # ... detection logic ...
      {node, acc}
    end)
    |> elem(1)
  end
end
```

Register custom checks in `.metacredo.exs`:

```elixir
checks: %{
  enabled: [
    {MyApp.Check.CustomCheck, [threshold: 5]}
  ]
}
```

## Relationship to Credo and OeditusCredo

- **Credo** operates on Elixir's native AST (`Macro` module). MetaCredo
  operates on the language-agnostic MetaAST.
- **OeditusCredo** provides Credo plugin checks for the Elixir community
  and remains available for Elixir-only projects.
- **MetaCredo** covers the same detection patterns as OeditusCredo but
  works across all languages supported by Metastatic.

## CI / Diff-Based Analysis

MetaCredo can analyze only the files changed in a pull request, making it
ideal for CI pipelines:

```sh
# Analyze only changed files (default: origin/main...HEAD)
$ mix metacredo --diff --strict

# GitHub Actions with inline PR annotations
$ mix metacredo --diff --format github --strict

# Custom base branch
$ mix metacredo --diff --base origin/develop --format github
```

`--format github` emits GitHub Actions workflow commands that produce
inline annotations directly on the PR diff:

```
::error file=lib/repo.ex,line=42::HardcodedValue: Hardcoded URL found
::warning file=lib/worker.ex,line=15::MissingErrorHandling: Missing error handling
metacredo: 2 issue(s) found
```

See [CI.md](CI.md) for the complete CI integration guide, including
GitHub Actions workflows, GitLab CI examples, the `MetaCredo.Git` API,
and troubleshooting.

## Roadmap

The following items are planned for future releases:

- [ ] **Plugin system** for third-party checks (mirrors Credo plugins).
- [ ] **LSP integration** for in-editor diagnostics.
- [ ] **Auto-fix / code modification** via MetaAST transformations.
- [x] **CI/CD integrations** (GitHub Actions, GitLab CI, etc.).
- [ ] **Extract analysis modules from metastatic core** in the next major
   release of metastatic, using deprecated re-exports to bridge the
   transition.

## License

MIT