Current section

Files

Jump to
metastatic lib metastatic analysis complexity loc.ex
Raw

lib/metastatic/analysis/complexity/loc.ex

defmodule Metastatic.Analysis.Complexity.LoC do
@moduledoc """
Lines of Code (LoC) metrics calculation.
Calculates logical lines of code by counting statements in the MetaAST,
and extracts physical/comment lines from metadata when available.
## Metrics
- **Logical LoC**: Count of executable statements at M2 level
- **Physical LoC**: Raw line count from source (from metadata)
- **Comment lines**: Lines containing only comments (from metadata)
- **Blank lines**: Physical - Logical - Comments
## What Counts as Logical Line
Each statement counts as one logical line:
- Assignments
- Function calls
- Early returns
- Conditionals
- Loops
- Pattern matches
- Exception handling
Expressions within statements don't count separately.
## Examples
iex> ast = {:literal, :integer, 42}
iex> metrics = Metastatic.Analysis.Complexity.LoC.calculate(ast)
iex> metrics.logical
0
"""
alias Metastatic.AST
@type t :: %{
physical: non_neg_integer(),
logical: non_neg_integer(),
comments: non_neg_integer(),
blank: non_neg_integer()
}
@doc """
Calculates LoC metrics for a MetaAST node.
Optionally takes metadata map which may contain:
- `:line_count` - Physical line count
- `:comment_lines` - Number of comment lines
## Examples
iex> ast = {:assignment, {:variable, "x"}, {:literal, :integer, 5}}
iex> metrics = Metastatic.Analysis.Complexity.LoC.calculate(ast)
iex> metrics.logical
1
iex> ast = {:block, [{:variable, "x"}, {:variable, "y"}]}
iex> metrics = Metastatic.Analysis.Complexity.LoC.calculate(ast)
iex> metrics.logical
0
"""
@spec calculate(AST.meta_ast(), map()) :: t()
def calculate(ast, metadata \\ %{}) do
logical = count_logical_lines(ast)
# Extract from metadata or estimate
physical = Map.get(metadata, :line_count, logical)
comments = Map.get(metadata, :comment_lines, 0)
blank = max(0, physical - logical - comments)
%{
physical: physical,
logical: logical,
comments: comments,
blank: blank
}
end
# Private implementation
defp count_logical_lines(ast) do
walk(ast, 0)
end
# Statements count as logical lines
defp walk({:assignment, target, value}, count) do
count = count + 1
count = walk_expr(target, count)
walk_expr(value, count)
end
defp walk({:inline_match, pattern, value}, count) do
count = count + 1
count = walk_expr(pattern, count)
walk_expr(value, count)
end
defp walk({:function_call, _name, args}, count) do
count = count + 1
Enum.reduce(args, count, fn arg, c -> walk_expr(arg, c) end)
end
defp walk({:early_return, value}, count) do
count = count + 1
walk_expr(value, count)
end
defp walk({:conditional, cond, then_br, else_br}, count) do
count = count + 1
count = walk_expr(cond, count)
count = walk(then_br, count)
walk(else_br, count)
end
defp walk({:loop, :while, cond, body}, count) do
count = count + 1
count = walk_expr(cond, count)
walk(body, count)
end
defp walk({:loop, _, iter, coll, body}, count) do
count = count + 1
count = walk_expr(iter, count)
count = walk_expr(coll, count)
walk(body, count)
end
defp walk({:exception_handling, try_block, catches, else_block}, count) do
count = count + 1
count = walk(try_block, count)
count =
Enum.reduce(catches, count, fn {_type, var, catch_body}, c ->
c = walk_expr(var, c)
walk(catch_body, c)
end)
walk(else_block, count)
end
defp walk({:pattern_match, value, branches}, count) do
count = count + 1
count = walk_expr(value, count)
Enum.reduce(branches, count, fn {pattern, branch}, c ->
c = walk_expr(pattern, c)
walk(branch, c)
end)
end
defp walk({:lambda, _params, body}, count) do
# Lambda definition is a statement
count = count + 1
walk(body, count)
end
defp walk({:block, stmts}, count) when is_list(stmts) do
Enum.reduce(stmts, count, fn stmt, c -> walk(stmt, c) end)
end
defp walk({:collection_op, _, func, coll}, count) do
count = walk_expr(func, count)
walk_expr(coll, count)
end
defp walk({:collection_op, _, func, coll, init}, count) do
count = walk_expr(func, count)
count = walk_expr(coll, count)
walk_expr(init, count)
end
defp walk({:async_operation, _type, body}, count) do
count = count + 1
walk(body, count)
end
defp walk({:language_specific, _, _}, count), do: count + 1
defp walk({:language_specific, _, _, _}, count), do: count + 1
# Expressions don't count
defp walk(expr, count), do: walk_expr(expr, count)
# walk_expr: traverse expressions without counting them as lines
defp walk_expr({:binary_op, _, _, left, right}, count) do
count = walk_expr(left, count)
walk_expr(right, count)
end
defp walk_expr({:unary_op, _, operand}, count) do
walk_expr(operand, count)
end
defp walk_expr({:tuple, elems}, count) when is_list(elems) do
Enum.reduce(elems, count, fn elem, c -> walk_expr(elem, c) end)
end
defp walk_expr({:list, elems}, count) when is_list(elems) do
Enum.reduce(elems, count, fn elem, c -> walk_expr(elem, c) end)
end
defp walk_expr({:literal, _, _}, count), do: count
defp walk_expr({:variable, _}, count), do: count
defp walk_expr(nil, count), do: count
defp walk_expr(_, count), do: count
end