MetaCredo

MetaCredo

Cross-language static code analysis tool built on MetaAST.

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 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:

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

Usage

# Run all checks
$ mix metacredo

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

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

# JSON output
$ mix metacredo --format json

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

# Generate default configuration
$ mix metacredo.gen.config

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), 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

Security [S]—15 checks

Warning [W]—22 checks

Readability [R]—13 checks

Refactor [F]—10 checks

Design [D]—5 checks

Observability [O]—5 checks

Configuration

Create a .metacredo.exs file (or run mix metacredo.gen.config):

%{
  configs: [
    %{
      name: "default",
      files: %{
        included: ["lib/", "src/"],
        excluded: [~r"/_build/", ~r"/deps/"]
      },
      checks: %{
        enabled: :all,
        disabled: []
      }
    }
  ]
}

To selectively enable checks with parameters:

checks: %{
  enabled: [
    {MetaCredo.Check.Security.HardcodedValue, [exclude_localhost: true]},
    {MetaCredo.Check.Warning.MissingErrorHandling, []},
    {MetaCredo.Check.Readability.MagicNumber, [ignored_numbers: [0, 1, -1, 2]]}
  ],
  disabled: []
}

Inline Disable Comments

Use source comments to suppress specific checks:

# 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

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:

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

Relationship to Credo and OeditusCredo

Roadmap

The following items are planned for future releases:

License

MIT