Current section

Files

Jump to
metastatic lib metastatic analysis dead_code_analyzer.ex
Raw

lib/metastatic/analysis/dead_code_analyzer.ex

defmodule Metastatic.Analysis.DeadCodeAnalyzer do
@moduledoc """
Plugin analyzer wrapper for dead code detection.
This module wraps the existing `Metastatic.Analysis.DeadCode` module as an
`Analyzer` behaviour plugin for use with the Runner system. It detects:
- **Unreachable code after returns** - Code following early_return nodes
- **Constant conditionals** - Branches that can never execute (if true/false)
- **Other dead patterns** - Unused assignments, etc.
## Usage
alias Metastatic.{Document, Analysis.Runner}
# Register as plugin
Registry.register(DeadCodeAnalyzer)
# Run via Runner
{:ok, report} = Runner.run(doc)
## Configuration
- `:min_confidence` - Minimum confidence to report (default: :low)
- `:low` - Report all dead code
- `:medium` - Report high-confidence issues only
- `:high` - Report only definite dead code
## Examples
# Unreachable after return
iex> ast = {:block, [], [
...> {:early_return, [], [{:literal, [subtype: :integer], 1}]},
...> {:literal, [subtype: :integer], 2}
...> ]}
iex> doc = Metastatic.Document.new(ast, :python)
iex> {:ok, report} = Metastatic.Analysis.Runner.run(doc,
...> analyzers: [Metastatic.Analysis.DeadCodeAnalyzer])
iex> length(report.issues)
1
iex> [issue | _] = report.issues
iex> issue.category
:correctness
"""
@behaviour Metastatic.Analysis.Analyzer
alias Metastatic.Analysis.{Analyzer, DeadCode}
# ----- Behaviour Callbacks -----
@impl true
def info do
%{
name: :dead_code,
category: :correctness,
description: "Detects unreachable and dead code patterns",
severity: :warning,
explanation: """
Dead code is code that can never be executed. This includes:
- Code after return statements
- Branches in constant conditionals
- Unreachable paths
Dead code should be removed to keep the codebase clean and maintainable.
""",
configurable: true
}
end
@impl true
def run_before(context) do
# Convert config from map to keyword list if needed
opts =
case context.config do
config when is_list(config) -> config
config when is_map(config) -> Map.to_list(config)
_other -> []
end
# Run the standalone DeadCode analyzer on the full document
case DeadCode.analyze(context.document, opts) do
{:ok, result} ->
# Store result for later use
context = Map.put(context, :dead_code_result, result)
{:ok, context}
{:error, reason} ->
{:skip, reason}
end
end
@impl true
def analyze(_node, _context) do
# Individual node analysis not needed; all analysis done in run_before
[]
end
@impl true
def run_after(context, issues) do
# Convert dead code results to analyzer issues
case Map.get(context, :dead_code_result) do
nil ->
issues
result ->
new_issues = convert_dead_code_results(result, context)
issues ++ new_issues
end
end
# ----- Private Helpers -----
defp convert_dead_code_results(result, context) do
dead_locations = Map.get(result, :dead_locations, [])
Enum.map(dead_locations, fn location ->
severity =
case Map.get(location, :confidence, :low) do
:high -> :warning
:medium -> :info
:low -> :info
end
node = Map.get(location, :context, %{}) |> Map.get(:ast)
# Let Analyzer.issue/1 extract location from node metadata automatically
# Don't override with explicit nil location
Analyzer.issue(
analyzer: __MODULE__,
category: :correctness,
severity: severity,
message: Map.get(location, :reason, "Dead code detected"),
node: node || context.document.ast,
suggestion:
Analyzer.suggestion(
type: :remove,
replacement: nil,
message: Map.get(location, :suggestion, "Remove dead code")
),
metadata: Map.put(location, :type, Map.get(location, :type))
)
end)
end
end