Packages
metastatic
0.5.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/complexity/cognitive.ex
defmodule Metastatic.Analysis.Complexity.Cognitive do
@moduledoc """
Cognitive complexity calculation.
Cognitive complexity measures how difficult code is to understand, taking into
account nested structures that increase the mental burden on readers.
## Algorithm
Based on the Sonar cognitive complexity specification:
- **Base Increments:** Structural elements add to complexity
- `conditional` - +1 (+nesting level)
- `loop` - +1 (+nesting level)
- `binary_op` with `:boolean` and `:and`/`:or` - +1
- `pattern_match` - +1 (+nesting level per branch)
- `exception_handling` - +1 (+nesting level)
- **Nesting Penalty:** Each level of nesting adds to the increment
- Conditional at level 0: +1
- Conditional at level 1: +2
- Conditional at level 2: +3
## Difference from Cyclomatic
Cognitive complexity differs from cyclomatic in that it:
- Applies nesting penalties (deeper = more complex)
- Doesn't count boolean operators in conditions the same way
- Emphasizes understandability over testability
## Examples
# Simple conditional: cognitive = 1
iex> ast = {:conditional, {:variable, "x"}, {:literal, :integer, 1}, {:literal, :integer, 2}}
iex> Metastatic.Analysis.Complexity.Cognitive.calculate(ast)
1
# Nested conditional: cognitive = 3 (1 + 2)
iex> ast = {:conditional, {:variable, "x"},
...> {:conditional, {:variable, "y"}, {:literal, :integer, 1}, {:literal, :integer, 2}},
...> {:literal, :integer, 3}}
iex> Metastatic.Analysis.Complexity.Cognitive.calculate(ast)
3
"""
alias Metastatic.AST
@doc """
Calculates cognitive complexity for a MetaAST node.
Returns the complexity as a non-negative integer (minimum 0).
## Examples
iex> ast = {:literal, :integer, 42}
iex> Metastatic.Analysis.Complexity.Cognitive.calculate(ast)
0
iex> ast = {:conditional, {:variable, "x"},
...> {:literal, :integer, 1},
...> {:literal, :integer, 2}}
iex> Metastatic.Analysis.Complexity.Cognitive.calculate(ast)
1
"""
@spec calculate(AST.meta_ast()) :: non_neg_integer()
def calculate(ast) do
walk(ast, 0, 0)
end
# Private implementation
# walk(ast, nesting_level, accumulator) -> accumulator
# Conditional: +1 +nesting_level
defp walk({:conditional, cond, then_br, else_br}, nesting, acc) do
acc = acc + 1 + nesting
acc = walk(cond, nesting, acc)
# Increment nesting for branches
acc = walk(then_br, nesting + 1, acc)
walk(else_br, nesting + 1, acc)
end
# Loop: +1 +nesting_level
defp walk({:loop, :while, cond, body}, nesting, acc) do
acc = acc + 1 + nesting
acc = walk(cond, nesting, acc)
walk(body, nesting + 1, acc)
end
defp walk({:loop, _, iter, coll, body}, nesting, acc) do
acc = acc + 1 + nesting
acc = walk(iter, nesting, acc)
acc = walk(coll, nesting, acc)
walk(body, nesting + 1, acc)
end
# Boolean operators (and/or): +1 (no nesting penalty)
defp walk({:binary_op, :boolean, op, left, right}, nesting, acc) when op in [:and, :or] do
acc = acc + 1
acc = walk(left, nesting, acc)
walk(right, nesting, acc)
end
# Other binary operators: no complexity
defp walk({:binary_op, _, _, left, right}, nesting, acc) do
acc = walk(left, nesting, acc)
walk(right, nesting, acc)
end
# Unary operator
defp walk({:unary_op, _, operand}, nesting, acc) do
walk(operand, nesting, acc)
end
# Exception handling: +1 +nesting_level
defp walk({:exception_handling, try_block, catches, else_block}, nesting, acc) do
acc = acc + 1 + nesting
acc = walk(try_block, nesting + 1, acc)
acc =
Enum.reduce(catches, acc, fn {_type, _var, catch_body}, a ->
walk(catch_body, nesting + 1, a)
end)
walk(else_block, nesting + 1, acc)
end
# Pattern match: +1 +nesting_level per branch
defp walk({:pattern_match, value, branches}, nesting, acc) do
acc = walk(value, nesting, acc)
Enum.reduce(branches, acc, fn
{:match_arm, _pattern, _guard, body}, a ->
a = a + 1 + nesting
walk(body, nesting + 1, a)
{_pattern, branch}, a ->
a = a + 1 + nesting
walk(branch, nesting + 1, a)
end)
end
# Match arm (used in pattern matching and guarded functions)
defp walk({:match_arm, _pattern, guard, body}, nesting, acc) do
acc = if guard, do: walk(guard, nesting, acc), else: acc
walk(body, nesting + 1, acc)
end
# Block: walk statements at same nesting level
defp walk({:block, stmts}, nesting, acc) when is_list(stmts) do
Enum.reduce(stmts, acc, fn stmt, a -> walk(stmt, nesting, a) end)
end
# Function call: walk arguments
defp walk({:function_call, _name, args}, nesting, acc) do
Enum.reduce(args, acc, fn arg, a -> walk(arg, nesting, a) end)
end
# Lambda: increase nesting for body
defp walk({:lambda, _params, body}, nesting, acc) do
walk(body, nesting + 1, acc)
end
# Collection operations
defp walk({:collection_op, _, func, coll}, nesting, acc) do
acc = walk(func, nesting, acc)
walk(coll, nesting, acc)
end
defp walk({:collection_op, _, func, coll, init}, nesting, acc) do
acc = walk(func, nesting, acc)
acc = walk(coll, nesting, acc)
walk(init, nesting, acc)
end
# Assignment
defp walk({:assignment, target, value}, nesting, acc) do
acc = walk(target, nesting, acc)
walk(value, nesting, acc)
end
# Inline match
defp walk({:inline_match, pattern, value}, nesting, acc) do
acc = walk(pattern, nesting, acc)
walk(value, nesting, acc)
end
# Early return
defp walk({:early_return, value}, nesting, acc) do
walk(value, nesting, acc)
end
# Tuple
defp walk({:tuple, elems}, nesting, acc) when is_list(elems) do
Enum.reduce(elems, acc, fn elem, a -> walk(elem, nesting, a) end)
end
# List
defp walk({:list, elems}, nesting, acc) when is_list(elems) do
Enum.reduce(elems, acc, fn elem, a -> walk(elem, nesting, a) end)
end
# Map
defp walk({:map, pairs}, nesting, acc) when is_list(pairs) do
Enum.reduce(pairs, acc, fn {key, value}, a ->
a = walk(key, nesting, a)
walk(value, nesting, a)
end)
end
# Async operation
defp walk({:async_operation, _type, body}, nesting, acc) do
walk(body, nesting, acc)
end
# M2.2s: Structural/Organizational types
# Container: walk body (7-tuple format)
defp walk({:container, _type, _name, _parent, _type_params, _implements, body}, nesting, acc) do
if is_list(body) do
Enum.reduce(body, acc, fn member, a -> walk(member, nesting, a) end)
else
walk(body, nesting, acc)
end
end
# Function definition: walk body (6-tuple format)
defp walk({:function_def, _name, params, _ret_type, opts, body}, nesting, acc)
when is_list(params) do
acc =
Enum.reduce(params, acc, fn
{:param, _name, pattern, default}, a ->
a = if pattern, do: walk(pattern, nesting, a), else: a
if default, do: walk(default, nesting, a), else: a
_simple_param, a ->
a
end)
acc =
if is_map(opts) do
case Map.get(opts, :guards) do
nil -> acc
guard -> walk(guard, nesting, acc)
end
else
acc
end
walk(body, nesting, acc)
end
defp walk({:attribute_access, receiver, _attribute}, nesting, acc) do
walk(receiver, nesting, acc)
end
defp walk({:augmented_assignment, _op, target, value}, nesting, acc) do
acc = walk(target, nesting, acc)
walk(value, nesting, acc)
end
defp walk({:property, _name, getter, setter, _metadata}, nesting, acc) do
acc = if getter, do: walk(getter, nesting, acc), else: acc
if setter, do: walk(setter, nesting, acc), else: acc
end
# Language-specific: traverse embedded body if present
defp walk({:language_specific, _, _, _, metadata}, nesting, acc) when is_map(metadata) do
case Map.get(metadata, :body) do
nil -> acc
body -> walk(body, nesting, acc)
end
end
defp walk({:language_specific, _, _, _}, _nesting, acc), do: acc
defp walk({:language_specific, _, _}, _nesting, acc), do: acc
# Literals and variables: no complexity
defp walk({:literal, _, _}, _nesting, acc), do: acc
defp walk({:variable, _}, _nesting, acc), do: acc
# Nil
defp walk(nil, _nesting, acc), do: acc
# Fallback
defp walk(_, _nesting, acc), do: acc
end