Packages
metastatic
0.14.2
0.26.0
0.25.0
0.24.1
0.24.0
0.23.0
0.22.2
0.22.1
0.22.0
0.21.3
0.21.2
0.21.1
0.21.0
0.20.3
0.20.2
0.20.1
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.1
0.15.0
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.0
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.2
0.4.1
0.4.0
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
Cross-language code meta-model library using unified MetaAST representation. Parse, transform, and translate code across Python, Elixir, Ruby, Erlang, Haskell, and more via a shared three-tuple AST format.
Current section
Files
Jump to
Current section
Files
lib/metastatic/analysis/smells.ex
defmodule Metastatic.Analysis.Smells do
@moduledoc """
Code smell detection at the MetaAST level.
Identifies design and maintainability issues that indicate poor code quality.
Works across all supported languages by operating on the unified MetaAST
representation and leveraging existing complexity metrics.
## Detected Smells
- **Long function** - Too many statements (threshold: 50)
- **Deep nesting** - Excessive nesting depth (threshold: 4)
- **Magic numbers** - Unexplained numeric literals in expressions
- **Complex conditionals** - Deeply nested boolean operations
- **Long parameter list** - Too many parameters (threshold: 5)
## Usage
alias Metastatic.{Document, Analysis.Smells}
# Analyze for code smells
ast = {:block, [], (for i <- 1..100, do: {:literal, [subtype: :integer], i})}
doc = Document.new(ast, :python)
{:ok, result} = Smells.analyze(doc)
result.has_smells? # => true
result.total_smells # => 1
result.smells # => [%{type: :long_function, ...}]
## Examples
# No code smells - using common constants 0, 1, -1
iex> ast = {:binary_op, [category: :arithmetic, operator: :+], [{:literal, [subtype: :integer], 1}, {:literal, [subtype: :integer], 0}]}
iex> doc = Metastatic.Document.new(ast, :python)
iex> {:ok, result} = Metastatic.Analysis.Smells.analyze(doc)
iex> result.has_smells?
false
# Magic number detected
iex> ast = {:binary_op, [category: :arithmetic, operator: :+], [{:variable, [], "x"}, {:literal, [subtype: :integer], 42}]}
iex> doc = Metastatic.Document.new(ast, :python)
iex> {:ok, result} = Metastatic.Analysis.Smells.analyze(doc)
iex> result.has_smells?
true
iex> [smell | _] = result.smells
iex> smell.type
:magic_number
"""
alias Metastatic.Analysis.{Complexity, Smells.Result}
alias Metastatic.Document
# Default thresholds
@default_thresholds %{
max_statements: 50,
max_nesting: 4,
max_parameters: 5,
max_cognitive: 15
}
use Metastatic.Document.Analyzer,
doc: """
Analyzes a document for code smells.
Returns `{:ok, result}` where result is a `Metastatic.Analysis.Smells.Result` struct.
## Options
- `:thresholds` - Map of threshold overrides (see default thresholds)
- `:detect` - List of smell types to detect (default: all)
## Examples
iex> ast = {:literal, [subtype: :integer], 42}
iex> doc = Metastatic.Document.new(ast, :elixir)
iex> {:ok, result} = Metastatic.Analysis.Smells.analyze(doc)
iex> result.has_smells?
false
"""
@impl Metastatic.Document.Analyzer
def handle_analyze(%Document{ast: ast} = doc, opts \\ []) do
thresholds = Keyword.get(opts, :thresholds, %{}) |> merge_thresholds()
smells =
[]
|> detect_long_function(doc, thresholds)
|> detect_deep_nesting(doc, thresholds)
|> detect_magic_numbers(ast, thresholds)
|> detect_complex_conditionals(ast, thresholds)
{:ok, Result.new(smells)}
end
# Private implementation
defp merge_thresholds(overrides) do
Map.merge(@default_thresholds, overrides)
end
# Detect long functions using per-function complexity metrics
defp detect_long_function(smells, doc, thresholds) do
case Complexity.analyze(doc) do
{:error, _} ->
smells
{:ok, complexity} ->
# Check per-function metrics for long functions
per_function_smells =
complexity.per_function
|> Enum.filter(&(&1.statements > thresholds.max_statements))
|> Enum.map(fn func ->
%{
type: :long_function,
severity:
determine_severity(:long_function, func.statements, thresholds.max_statements),
description:
"Function '#{func.name}' has #{func.statements} statements (threshold: #{thresholds.max_statements})",
suggestion: "Break this function into smaller, focused functions",
context: %{
function_name: func.name,
statement_count: func.statements,
threshold: thresholds.max_statements
},
location: %{function: func.name, line: Map.get(func, :line)}
}
end)
# Fallback: if no per-function metrics, check the whole document
document_smells =
with true <- Enum.empty?(complexity.per_function),
statement_count when statement_count > thresholds.max_statements <-
complexity.function_metrics.statement_count do
location = extract_location(doc)
[
%{
type: :long_function,
severity:
determine_severity(:long_function, statement_count, thresholds.max_statements),
description:
"Code has #{statement_count} statements (threshold: #{thresholds.max_statements})",
suggestion: "Break this function into smaller, focused functions",
context: %{
statement_count: statement_count,
threshold: thresholds.max_statements
},
location: location
}
]
else
_ -> []
end
per_function_smells ++ document_smells ++ smells
end
end
# Detect deep nesting using per-function complexity metrics
defp detect_deep_nesting(smells, doc, thresholds) do
case Complexity.analyze(doc) do
{:error, _} ->
smells
{:ok, complexity} ->
# Check per-function metrics for deep nesting
per_function_smells =
complexity.per_function
|> Enum.filter(fn func ->
func.max_nesting > thresholds.max_nesting
end)
|> Enum.map(fn func ->
%{
type: :deep_nesting,
severity:
determine_severity(:deep_nesting, func.max_nesting, thresholds.max_nesting),
description:
"Function '#{func.name}' has nesting depth of #{func.max_nesting} (threshold: #{thresholds.max_nesting})",
suggestion: "Reduce nesting by extracting functions or using early returns",
context: %{
function_name: func.name,
max_nesting: func.max_nesting,
threshold: thresholds.max_nesting
},
location: %{function: func.name, line: Map.get(func, :line)}
}
end)
# Fallback: if no per-function metrics, check the whole document
document_smells =
with true <- Enum.empty?(complexity.per_function),
max_nesting when max_nesting > thresholds.max_nesting <-
complexity.max_nesting do
location = extract_location(doc)
[
%{
type: :deep_nesting,
severity: determine_severity(:deep_nesting, max_nesting, thresholds.max_nesting),
description:
"Nesting depth of #{max_nesting} exceeds threshold (threshold: #{thresholds.max_nesting})",
suggestion: "Reduce nesting by extracting functions or using early returns",
context: %{
max_nesting: max_nesting,
threshold: thresholds.max_nesting
},
location: location
}
]
else
_ -> []
end
per_function_smells ++ document_smells ++ smells
end
end
# Detect magic numbers (numeric literals in complex expressions)
defp detect_magic_numbers(smells, ast, _thresholds) do
magic_numbers = find_magic_numbers(ast, [], nil)
Enum.map(magic_numbers, fn {value, context, line} ->
%{
type: :magic_number,
severity: :low,
description: "Magic number #{value} should be a named constant",
suggestion: "Extract #{value} to a named constant",
context: context,
location: if(line, do: %{line: line}, else: nil)
}
end) ++ smells
end
# 3-tuple format
defp find_magic_numbers(ast, context, current_line) do
case ast do
{:binary_op, meta, [left, right]} when is_list(meta) ->
line = Keyword.get(meta, :line, current_line)
# Check for numeric literals in binary operations
find_magic_numbers(left, [:binary_op | context], line) ++
find_magic_numbers(right, [:binary_op | context], line)
{:literal, meta, value} when is_list(meta) ->
subtype = Keyword.get(meta, :subtype)
cond do
subtype == :integer and is_integer(value) and value not in [0, 1, -1] ->
# Report integers other than 0, 1, -1 in operation context
if :binary_op in context or :unary_op in context do
[{value, %{value: value, in_expression: true}, current_line}]
else
[]
end
subtype == :float and is_float(value) ->
# Report all float literals in operation context
if :binary_op in context or :unary_op in context do
[{value, %{value: value, in_expression: true}, current_line}]
else
[]
end
true ->
[]
end
{:unary_op, meta, [operand]} when is_list(meta) ->
line = Keyword.get(meta, :line, current_line)
find_magic_numbers(operand, [:unary_op | context], line)
{:conditional, meta, [cond_expr, then_br, else_br]} when is_list(meta) ->
line = Keyword.get(meta, :line, current_line)
find_magic_numbers(cond_expr, context, line) ++
find_magic_numbers(then_br, context, line) ++
find_magic_numbers(else_br, context, line)
{:block, _meta, statements} when is_list(statements) ->
Enum.flat_map(statements, &find_magic_numbers(&1, context, current_line))
{:function_call, meta, args} when is_list(meta) and is_list(args) ->
line = Keyword.get(meta, :line, current_line)
Enum.flat_map(args, &find_magic_numbers(&1, context, line))
{:language_specific, meta, native_ast} when is_list(meta) ->
# Extract line information from language-specific wrapper
line = extract_line_from_metadata(native_ast)
find_magic_numbers_in_metadata(native_ast, context, line)
_ ->
[]
end
end
defp find_magic_numbers_in_metadata(metadata, context, line) when is_map(metadata) do
# Check if metadata has a body field to recurse into
case Map.get(metadata, :body) do
nil -> []
body -> find_magic_numbers(body, context, line)
end
end
# Detect complex conditionals (nested boolean operations)
defp detect_complex_conditionals(smells, ast, _thresholds) do
complex_conditions = find_complex_conditionals(ast, 0, nil)
Enum.map(complex_conditions, fn {depth, line} ->
%{
type: :complex_conditional,
severity: if(depth > 3, do: :high, else: :medium),
description: "Complex conditional with #{depth} nested boolean operations",
suggestion: "Extract condition logic into well-named boolean variables",
context: %{complexity_depth: depth},
location: if(line, do: %{line: line}, else: nil)
}
end) ++ smells
end
# 3-tuple format
defp find_complex_conditionals(ast, depth, current_line) do
case ast do
{:conditional, meta, [cond_expr, then_br, else_br]} when is_list(meta) ->
line = Keyword.get(meta, :line, current_line)
cond_depth = count_boolean_depth(cond_expr, 0)
results =
if cond_depth > 2 do
[{cond_depth, line}]
else
[]
end
results ++
find_complex_conditionals(then_br, depth, line) ++
find_complex_conditionals(else_br, depth, line)
{:block, _meta, statements} when is_list(statements) ->
Enum.flat_map(statements, &find_complex_conditionals(&1, depth, current_line))
{:loop, meta, children} when is_list(meta) and is_list(children) ->
loop_type = Keyword.get(meta, :loop_type, :for)
case {loop_type, children} do
{:while, [cond_expr | rest]} ->
cond_depth = count_boolean_depth(cond_expr, 0)
results =
if cond_depth > 2 do
[{cond_depth, current_line}]
else
[]
end
results ++ Enum.flat_map(rest, &find_complex_conditionals(&1, depth, current_line))
_ ->
Enum.flat_map(children, &find_complex_conditionals(&1, depth, current_line))
end
{:language_specific, meta, native_ast} when is_list(meta) ->
# Extract line information from language-specific wrapper
line = extract_line_from_metadata(native_ast)
find_complex_conditionals_in_metadata(native_ast, depth, line)
_ ->
[]
end
end
defp find_complex_conditionals_in_metadata(metadata, depth, line) when is_map(metadata) do
case Map.get(metadata, :body) do
nil -> []
body -> find_complex_conditionals(body, depth, line)
end
end
# 3-tuple format
defp count_boolean_depth({:binary_op, meta, [left, right]}, depth) when is_list(meta) do
category = Keyword.get(meta, :category)
if category == :boolean do
max(
count_boolean_depth(left, depth + 1),
count_boolean_depth(right, depth + 1)
)
else
max(
count_boolean_depth(left, depth),
count_boolean_depth(right, depth)
)
end
end
defp count_boolean_depth({:unary_op, meta, [operand]}, depth) when is_list(meta) do
category = Keyword.get(meta, :category)
if category == :boolean do
count_boolean_depth(operand, depth + 1)
else
count_boolean_depth(operand, depth)
end
end
defp count_boolean_depth(_, depth), do: depth
# Determine severity based on how much threshold is exceeded
defp determine_severity(_type, value, threshold) do
ratio = value / threshold
cond do
ratio >= 3.0 -> :critical
ratio >= 2.0 -> :high
ratio >= 1.5 -> :medium
true -> :low
end
end
# Extract location information from document metadata
defp extract_location(%Document{metadata: metadata, ast: ast}) do
# Try to extract from metadata first
location = extract_location_from_metadata(metadata)
# If not in metadata, try to walk AST for language_specific nodes
if location do
location
else
extract_location_from_ast(ast)
end
end
defp extract_location_from_metadata(metadata) when is_map(metadata) do
cond do
# Function name and line from metadata
Map.has_key?(metadata, :function_name) and Map.has_key?(metadata, :line) ->
%{
function: metadata.function_name,
line: metadata.line
}
# Just line number
Map.has_key?(metadata, :line) ->
%{line: metadata.line}
# Check elixir_meta for line info
Map.has_key?(metadata, :elixir_meta) ->
case extract_line_from_elixir_meta(metadata.elixir_meta) do
nil -> nil
line -> %{line: line}
end
true ->
nil
end
end
defp extract_location_from_metadata(_), do: nil
defp extract_line_from_metadata(metadata) when is_map(metadata) do
cond do
Map.has_key?(metadata, :line) -> metadata.line
Map.has_key?(metadata, :elixir_meta) -> extract_line_from_elixir_meta(metadata.elixir_meta)
true -> nil
end
end
defp extract_line_from_metadata(_), do: nil
defp extract_line_from_elixir_meta(elixir_meta) when is_list(elixir_meta) do
Keyword.get(elixir_meta, :line)
end
defp extract_line_from_elixir_meta(_), do: nil
defp extract_location_from_ast(ast) do
case find_first_language_specific_node(ast) do
{:ok, metadata} -> extract_location_from_metadata(metadata)
:not_found -> nil
end
end
# 3-tuple format
defp find_first_language_specific_node(ast) do
case ast do
{:language_specific, meta, native_ast} when is_list(meta) and is_map(native_ast) ->
{:ok, native_ast}
tuple when is_tuple(tuple) ->
tuple
|> Tuple.to_list()
|> Enum.find_value(:not_found, &find_first_language_specific_node/1)
list when is_list(list) ->
Enum.find_value(list, :not_found, &find_first_language_specific_node/1)
%{} = map ->
map
|> Map.values()
|> Enum.find_value(:not_found, &find_first_language_specific_node/1)
_ ->
:not_found
end
end
end