Current section

Files

Jump to
metastatic lib metastatic analysis complexity cyclomatic.ex
Raw

lib/metastatic/analysis/complexity/cyclomatic.ex

defmodule Metastatic.Analysis.Complexity.Cyclomatic do
@moduledoc """
Cyclomatic complexity calculation (McCabe metric).
Cyclomatic complexity measures the number of linearly independent paths
through a program's source code. It is calculated as the number of
decision points plus one.
## Decision Points
- `conditional` - if/else statements (+1)
- `loop` - while/for loops (+1)
- `binary_op` with `:boolean` and `:or`/`:and` operators (+1 each)
- `exception_handling` - try/catch blocks (+1 per catch clause)
- `pattern_match` - case statements (+1 per branch)
## Thresholds
- 1-10: Simple, low risk
- 11-20: More complex, moderate risk
- 21-50: Complex, high risk
- 51+: Untestable, very high risk
## Examples
# Simple sequence: complexity = 1
iex> ast = {:literal, :integer, 42}
iex> Metastatic.Analysis.Complexity.Cyclomatic.calculate(ast)
1
# Single conditional: complexity = 2
iex> ast = {:conditional, {:variable, "x"}, {:literal, :integer, 1}, {:literal, :integer, 2}}
iex> Metastatic.Analysis.Complexity.Cyclomatic.calculate(ast)
2
# Conditional with loop: complexity = 3
iex> ast = {:conditional, {:variable, "x"},
...> {:loop, :while, {:variable, "y"}, {:literal, :integer, 1}},
...> {:literal, :integer, 2}}
iex> Metastatic.Analysis.Complexity.Cyclomatic.calculate(ast)
3
"""
alias Metastatic.AST
@doc """
Calculates cyclomatic complexity for a MetaAST node.
Returns the complexity as a non-negative integer (minimum 1).
## Examples
iex> ast = {:binary_op, :arithmetic, :+, {:variable, "x"}, {:literal, :integer, 5}}
iex> Metastatic.Analysis.Complexity.Cyclomatic.calculate(ast)
1
iex> ast = {:conditional, {:variable, "condition"},
...> {:literal, :integer, 1},
...> {:literal, :integer, 2}}
iex> Metastatic.Analysis.Complexity.Cyclomatic.calculate(ast)
2
"""
@spec calculate(AST.meta_ast()) :: non_neg_integer()
def calculate(ast) do
1 + count_decision_points(ast)
end
# Private implementation
defp count_decision_points(ast) do
walk(ast, 0)
end
# Conditional: +1 decision point
defp walk({:conditional, cond, then_br, else_br}, count) do
count = count + 1
count = walk(cond, count)
count = walk(then_br, count)
walk(else_br, count)
end
# Loop: +1 decision point
defp walk({:loop, :while, cond, body}, count) do
count = count + 1
count = walk(cond, count)
walk(body, count)
end
defp walk({:loop, _, iter, coll, body}, count) do
count = count + 1
count = walk(iter, count)
count = walk(coll, count)
walk(body, count)
end
# Boolean operators (and/or): +1 each
defp walk({:binary_op, :boolean, op, left, right}, count) when op in [:and, :or] do
count = count + 1
count = walk(left, count)
walk(right, count)
end
# Other binary operators: no decision point
defp walk({:binary_op, _, _, left, right}, count) do
count = walk(left, count)
walk(right, count)
end
# Unary operator
defp walk({:unary_op, _, operand}, count) do
walk(operand, count)
end
# Exception handling: +1 per catch clause
defp walk({:exception_handling, try_block, catches, else_block}, count) do
count = count + length(catches)
count = walk(try_block, count)
count = Enum.reduce(catches, count, fn catch_clause, c -> walk(catch_clause, c) end)
walk(else_block, count)
end
# Pattern match: +1 per branch
defp walk({:pattern_match, value, branches}, count) do
count = count + length(branches)
count = walk(value, count)
Enum.reduce(branches, count, fn
{:match_arm, _pattern, _guard, body}, c -> walk(body, c)
{_pattern, branch}, c -> walk(branch, c)
end)
end
# Match arm (used in pattern matching and guarded functions)
defp walk({:match_arm, _pattern, guard, body}, count) do
count = if guard, do: walk(guard, count), else: count
walk(body, count)
end
# Block: walk statements
defp walk({:block, stmts}, count) when is_list(stmts) do
Enum.reduce(stmts, count, fn stmt, c -> walk(stmt, c) end)
end
# Function call: walk arguments
defp walk({:function_call, _name, args}, count) do
Enum.reduce(args, count, fn arg, c -> walk(arg, c) end)
end
# Lambda: walk body
defp walk({:lambda, _params, body}, count) do
walk(body, count)
end
# Collection operations
defp walk({:collection_op, _, func, coll}, count) do
count = walk(func, count)
walk(coll, count)
end
defp walk({:collection_op, _, func, coll, init}, count) do
count = walk(func, count)
count = walk(coll, count)
walk(init, count)
end
# Assignment
defp walk({:assignment, target, value}, count) do
count = walk(target, count)
walk(value, count)
end
# Inline match
defp walk({:inline_match, pattern, value}, count) do
count = walk(pattern, count)
walk(value, count)
end
# Early return
defp walk({:early_return, value}, count) do
walk(value, count)
end
# Tuple
defp walk({:tuple, elems}, count) when is_list(elems) do
Enum.reduce(elems, count, fn elem, c -> walk(elem, c) end)
end
# List
defp walk({:list, elems}, count) when is_list(elems) do
Enum.reduce(elems, count, fn elem, c -> walk(elem, c) end)
end
# Map
defp walk({:map, pairs}, count) when is_list(pairs) do
Enum.reduce(pairs, count, fn {key, value}, c ->
c = walk(key, c)
walk(value, c)
end)
end
# Async operation
defp walk({:async_operation, type, body}, count) do
_ = type
walk(body, count)
end
# M2.2s: Structural/Organizational types
# Container: walk body (7-tuple format)
defp walk({:container, _type, _name, _parent, _type_params, _implements, body}, count) do
if is_list(body) do
Enum.reduce(body, count, fn member, c -> walk(member, c) end)
else
walk(body, count)
end
end
# Function definition: walk body (6-tuple format)
defp walk({:function_def, _name, params, _ret_type, opts, body}, count)
when is_list(params) do
# Walk parameters (for pattern params with embedded conditionals)
count =
Enum.reduce(params, count, fn
{:param, _name, pattern, default}, c ->
c = if pattern, do: walk(pattern, c), else: c
if default, do: walk(default, c), else: c
_simple_param, c ->
c
end)
# Walk guards if present in opts
count =
if is_map(opts) do
case Map.get(opts, :guards) do
nil -> count
guard -> walk(guard, count)
end
else
count
end
# Walk body
walk(body, count)
end
# Attribute access: walk receiver
defp walk({:attribute_access, receiver, _attribute}, count) do
walk(receiver, count)
end
# Augmented assignment: walk target and value
defp walk({:augmented_assignment, _op, target, value}, count) do
count = walk(target, count)
walk(value, count)
end
# Property: walk getter and setter
defp walk({:property, _name, getter, setter, _metadata}, count) do
count = if getter, do: walk(getter, count), else: count
if setter, do: walk(setter, count), else: count
end
# Language-specific: traverse embedded body if present
defp walk({:language_specific, _, _, _, metadata}, count) when is_map(metadata) do
# If there's a body in metadata (e.g., module/function definitions), traverse it
case Map.get(metadata, :body) do
nil -> count
body -> walk(body, count)
end
end
defp walk({:language_specific, _, _, _}, count), do: count
defp walk({:language_specific, _, _}, count), do: count
# Literals and variables: no decision points
defp walk({:literal, _, _}, count), do: count
defp walk({:variable, _}, count), do: count
# Nil (else branch)
defp walk(nil, count), do: count
# Fallback for unknown nodes
defp walk(_, count), do: count
end