Packages
metastatic
0.7.7
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/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