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}
]
endUsage
# 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.configHow 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
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 literalsSQLInjection—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 handlingSilentErrorCase—case matching {:ok, } without {:error, } branchSwallowingException—try/rescue without logging or re-raisingNPlusOneQuery—Database calls inside collection operations (N+1)MissingPreload—Collection ops over DB results without eager loadingUnmanagedTask—Task.async without Task.SupervisorSyncOverAsync—Blocking calls in GenServer/LiveView callbacksMissingHandleAsync—Blocking in handle_event without async delegationDirectStructUpdate—Struct updates bypassing changesetsCallbackHell—Deeply nested conditionals exceeding thresholdBlockingInPlug—Blocking I/O in Plug call/init middlewareMissingThrottle—Expensive operations without rate limitingInefficientFilter—Repo.all then Enum.filter (filter in memory)ImperativeStatusHandling—Imperative if/else chains on status codesUnusedOperation—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 functionDebugLeftover—IO.inspect, console.log, dbg() left in production codeRaiseInsideRescue—raise/throw inside rescue without re-raise semantics
Readability [R]—13 checks
MagicNumber—Numeric literals in expressions without named constantsDeepNesting—Functions with nesting depth exceeding thresholdLongFunction—Functions with too many statementsComplexConditional—Deeply nested boolean operationsLongParameterList—Functions with too many parametersFunctionNames—Function names not in snake_caseModuleNames—Module/container names not in PascalCaseVariableNames—Variable names not in snake_caseModuleDoc—Modules without documentationSinglePipe—Single-step pipe chains (unnecessary|>)NestedFunctionCalls—Deeply nested calls likefoo(bar(baz(x)))Specs—Public functions without type specificationsLargeNumbers—Large integers without underscore separators
Refactor [F]—10 checks
SimplifyConditional—if x do true else false endpatternsDeadCode—Unreachable code after early returnsCodeDuplication—Duplicate function bodies (same AST structure)NegatedConditionWithElse—if !x do...else(swap branches)DoubleBooleanNegation—!!xpattern (simplify to boolean cast)AppendSingleItem—list ++ [item](use[item | list]orList.insert_at)PipeChainStart—Pipe chains starting with a literal valueFilterCount—Enum.filter |> Enum.count(useEnum.count/2)UnlessWithElse—unless...else(useifinstead)VariableRebinding—Same variable assigned multiple times in a block
Design [D]—5 checks
HighComplexity—Functions with cyclomatic complexity exceeding thresholdLowCohesion—Modules where functions share no common dataHighCoupling—Modules with too many external dependenciesTagTODO—TODO comments that should be addressedTagFIXME—FIXME comments indicating known bugs
Observability [O]—5 checks
MissingTelemetryInObanWorker—Oban worker perform/1 without telemetryMissingTelemetryInLiveviewMount—LiveView mount/3 without telemetryMissingTelemetryInAuthPlug—Auth plug call/2 without telemetryMissingTelemetryForExternalHttp—HTTP client calls without telemetry wrapperTelemetryInRecursiveFunction—Telemetry inside recursive functions (anti-pattern)
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
- Credo operates on Elixir's native AST (
Macromodule). 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.
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.
- 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