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/cohesion.ex
defmodule Metastatic.Analysis.Cohesion do
@moduledoc """
Cohesion analysis for containers (modules/classes).
Measures how well the members (methods/functions) of a container work together.
High cohesion indicates that members are closely related and work toward a
common purpose, which is a desirable property in object-oriented design.
## Supported Metrics
### LCOM (Lack of Cohesion of Methods)
Measures the number of disjoint sets of methods. Lower is better.
- LCOM = 0: Perfect cohesion (all methods share state)
- LCOM > 0: Poor cohesion (methods form disconnected groups)
### TCC (Tight Class Cohesion)
Ratio of directly connected method pairs. Range: 0.0-1.0, higher is better.
- TCC = 1.0: All methods directly share state
- TCC > 0.5: Good cohesion
- TCC < 0.3: Poor cohesion
### LCC (Loose Class Cohesion)
Ratio of directly or indirectly connected method pairs. Range: 0.0-1.0.
- LCC >= TCC always
- High LCC but low TCC: Methods connected through intermediaries
## Algorithm
1. Extract all methods/functions from container
2. For each method, identify which instance variables it accesses
3. Build a connection graph between methods based on shared variables
4. Calculate LCOM (number of disconnected components)
5. Calculate TCC (direct connections / total possible pairs)
6. Calculate LCC (transitive closure / total possible pairs)
## Examples
# High cohesion - all methods use shared state
ast = {:container, :class, \"BankAccount\", %{}, [
{:function_def, :public, \"deposit\", [\"amount\"], %{},
{:augmented_assignment, :+, {:attribute_access, {:variable, \"self\"}, \"balance\"}, {:variable, \"amount\"}}},
{:function_def, :public, \"withdraw\", [\"amount\"], %{},
{:augmented_assignment, :-, {:attribute_access, {:variable, \"self\"}, \"balance\"}, {:variable, \"amount\"}}},
{:function_def, :public, \"get_balance\", [], %{},
{:attribute_access, {:variable, \"self\"}, \"balance\"}}
]}
doc = Document.new(ast, :python)
{:ok, result} = Cohesion.analyze(doc)
result.lcom # => 0 (perfect cohesion)
result.tcc # => 1.0 (all methods connected)
result.assessment # => :excellent
# Low cohesion - methods don't share state
ast = {:container, :class, \"Utilities\", %{}, [
{:function_def, :public, \"format_date\", [\"date\"], %{}, ...},
{:function_def, :public, \"calculate_tax\", [\"amount\"], %{}, ...},
{:function_def, :public, \"send_email\", [\"to\", \"msg\"], %{}, ...}
]}
{:ok, result} = Cohesion.analyze(doc)
result.lcom # => 3 (three disjoint methods)
result.tcc # => 0.0 (no shared state)
result.assessment # => :very_poor
"""
alias Metastatic.{Analysis.Cohesion.Result, Document}
use Metastatic.Document.Analyzer,
doc: """
Analyze cohesion of a container (module/class/namespace).
Returns `{:ok, result}` if the AST contains a container, or `{:error, reason}` otherwise.
## Examples
iex> ast = {:container, :class, \"Calculator\", %{}, [
...> {:function_def, :public, \"add\", [\"x\"], %{},
...> {:augmented_assignment, :+, {:attribute_access, {:variable, \"self\"}, \"total\"}, {:variable, \"x\"}}},
...> {:function_def, :public, \"get_total\", [], %{},
...> {:attribute_access, {:variable, \"self\"}, \"total\"}}
...> ]}
iex> doc = Metastatic.Document.new(ast, :python)
iex> {:ok, result} = Metastatic.Analysis.Cohesion.analyze(doc)
iex> result.lcom
0
iex> result.tcc
1.0
"""
@impl Metastatic.Document.Analyzer
def handle_analyze(%Document{ast: ast}, _opts \\ []) do
with {:ok, container_type, container_name, members} <- extract_container(ast),
do: {:ok, analyze_container(container_type, container_name, members)}
end
# Private implementation
defp extract_container({:container, type, name, _metadata, members}) do
{:ok, type, name, members}
end
defp extract_container(_), do: {:error, "AST does not contain a container"}
defp analyze_container(container_type, container_name, members) do
# Extract only function definitions (not properties or other members)
methods = Enum.filter(members, &match?({:function_def, _, _, _, _, _}, &1))
# Extract state variables accessed by each method
method_vars = Enum.map(methods, fn method -> {method, extract_accessed_state(method)} end)
# Calculate metrics
method_count = length(methods)
method_pairs = calculate_method_pairs(method_count)
# Build connection graph and calculate metrics
{lcom, tcc, lcc, connected_pairs} = calculate_cohesion_metrics(method_vars, method_pairs)
# Extract all unique shared state variables
shared_state =
method_vars
|> Enum.flat_map(fn {_method, vars} -> vars end)
|> Enum.uniq()
|> Enum.sort()
# Assess quality
assessment = Result.assess(lcom, tcc)
warnings = Result.generate_warnings(lcom, tcc, method_count)
recommendations = Result.generate_recommendations(assessment, method_count, tcc)
%Result{
container_name: container_name,
container_type: container_type,
lcom: lcom,
tcc: tcc,
lcc: lcc,
method_count: method_count,
method_pairs: method_pairs,
connected_pairs: connected_pairs,
shared_state: shared_state,
assessment: assessment,
warnings: warnings,
recommendations: recommendations
}
end
# Calculate number of possible method pairs: n * (n - 1) / 2
defp calculate_method_pairs(method_count) when method_count < 2, do: 0
defp calculate_method_pairs(method_count), do: div(method_count * (method_count - 1), 2)
# Extract state variables accessed by a method (attribute_access nodes)
defp extract_accessed_state({:function_def, _vis, _name, _params, _meta, body}) do
extract_state_accesses(body, MapSet.new())
|> MapSet.to_list()
end
defp extract_state_accesses(ast, acc) do
case ast do
# Attribute access on self/this -> instance variable
{:attribute_access, {:variable, var}, attr} when var in ["self", "this", "@"] ->
MapSet.put(acc, attr)
# Augmented assignment with attribute access
{:augmented_assignment, _op, target, value} ->
acc = extract_state_accesses(target, acc)
extract_state_accesses(value, acc)
# Property access
{:property, _name, getter, setter, _metadata} ->
acc = if getter, do: extract_state_accesses(getter, acc), else: acc
if setter, do: extract_state_accesses(setter, acc), else: acc
# Recurse into compound structures
{:binary_op, _, _, left, right} ->
acc = extract_state_accesses(left, acc)
extract_state_accesses(right, acc)
{:unary_op, _, _, operand} ->
extract_state_accesses(operand, acc)
{:conditional, cond, then_branch, else_branch} ->
acc = extract_state_accesses(cond, acc)
acc = extract_state_accesses(then_branch, acc)
if else_branch, do: extract_state_accesses(else_branch, acc), else: acc
{:assignment, target, value} ->
acc = extract_state_accesses(target, acc)
extract_state_accesses(value, acc)
{:function_call, _name, args} ->
Enum.reduce(args, acc, fn arg, a -> extract_state_accesses(arg, a) end)
{:block, stmts} when is_list(stmts) ->
Enum.reduce(stmts, acc, fn stmt, a -> extract_state_accesses(stmt, a) end)
{:early_return, value} ->
extract_state_accesses(value, acc)
# Loops, lambdas, collections
{:loop, :while, condition, body} ->
acc = extract_state_accesses(condition, acc)
extract_state_accesses(body, acc)
{:loop, _, _iter, coll, body} ->
acc = extract_state_accesses(coll, acc)
extract_state_accesses(body, acc)
{:lambda, _params, body} ->
extract_state_accesses(body, acc)
{:collection_op, _, func, coll} ->
acc = extract_state_accesses(func, acc)
extract_state_accesses(coll, acc)
{:collection_op, _, func, coll, init} ->
acc = extract_state_accesses(func, acc)
acc = extract_state_accesses(coll, acc)
extract_state_accesses(init, acc)
# Containers and functions (nested)
{:container, _type, _name, _metadata, members} when is_list(members) ->
Enum.reduce(members, acc, fn member, a -> extract_state_accesses(member, a) end)
{:function_def, _vis, _name, params, metadata, body} ->
# Walk parameters
acc =
Enum.reduce(params, acc, fn
{:pattern, pattern}, a -> extract_state_accesses(pattern, a)
{:default, _name, default}, a -> extract_state_accesses(default, a)
_simple_param, a -> a
end)
# Walk guards
acc =
case Map.get(metadata, :guards) do
nil -> acc
guard -> extract_state_accesses(guard, acc)
end
# Walk body
extract_state_accesses(body, acc)
# Literals, variables, etc. - no state access
_ ->
acc
end
end
# Calculate LCOM, TCC, and LCC metrics
defp calculate_cohesion_metrics(_method_vars, method_pairs) when method_pairs == 0 do
# Less than 2 methods - no cohesion to measure
{0, 0.0, 0.0, 0}
end
defp calculate_cohesion_metrics(method_vars, method_pairs) do
# Build connection matrix: which methods share variables?
connections = build_connection_matrix(method_vars)
# Count directly connected pairs (TCC)
connected_pairs = count_connected_pairs(connections)
tcc = connected_pairs / method_pairs
# Calculate LCOM using union-find algorithm
lcom = calculate_lcom(connections, length(method_vars))
# Calculate LCC (transitive closure)
transitive_connections = calculate_transitive_closure(connections, length(method_vars))
transitive_pairs = count_connected_pairs(transitive_connections)
lcc = transitive_pairs / method_pairs
{lcom, tcc, lcc, connected_pairs}
end
# Build connection matrix: true if methods i and j share at least one variable
defp build_connection_matrix(method_vars) do
indexed = Enum.with_index(method_vars)
for {{_method1, vars1}, i} <- indexed,
{{_method2, vars2}, j} <- indexed,
i < j,
into: %{} do
# Check if methods share any variables
shared = MapSet.new(vars1) |> MapSet.intersection(MapSet.new(vars2))
{{i, j}, MapSet.size(shared) > 0}
end
end
# Count how many method pairs are directly connected
defp count_connected_pairs(connections) do
Enum.count(connections, fn {_pair, connected} -> connected end)
end
# Calculate LCOM using union-find to count disconnected components
defp calculate_lcom(connections, method_count) do
# Initialize union-find with each method in its own set
uf = Enum.reduce(0..(method_count - 1), %{}, fn i, acc -> Map.put(acc, i, i) end)
# Union methods that are connected
uf =
Enum.reduce(connections, uf, fn {{i, j}, connected}, acc ->
if connected do
union(acc, i, j)
else
acc
end
end)
# Count number of distinct roots (disconnected components)
roots =
Enum.map(0..(method_count - 1), fn i -> find(uf, i) end)
|> Enum.uniq()
|> length()
# LCOM is number of components minus 1 (0 = fully connected)
max(0, roots - 1)
end
# Union-find: find root of element
defp find(uf, i) do
parent = Map.get(uf, i, i)
if parent == i do
i
else
find(uf, parent)
end
end
# Union-find: merge two sets
defp union(uf, i, j) do
root_i = find(uf, i)
root_j = find(uf, j)
if root_i != root_j do
Map.put(uf, root_i, root_j)
else
uf
end
end
# Calculate transitive closure using Floyd-Warshall
defp calculate_transitive_closure(connections, method_count) do
# Build adjacency matrix
adj =
for i <- 0..(method_count - 1),
j <- 0..(method_count - 1),
into: %{} do
cond do
i == j -> {{i, j}, true}
i < j -> {{i, j}, Map.get(connections, {i, j}, false)}
true -> {{i, j}, Map.get(connections, {j, i}, false)}
end
end
# Floyd-Warshall: find transitive connections
adj =
for k <- 0..(method_count - 1), reduce: adj do
acc ->
for i <- 0..(method_count - 1),
j <- 0..(method_count - 1),
reduce: acc do
inner_acc ->
ik = Map.get(inner_acc, {i, k}, false)
kj = Map.get(inner_acc, {k, j}, false)
ij = Map.get(inner_acc, {i, j}, false)
Map.put(inner_acc, {i, j}, ij or (ik and kj))
end
end
# Extract upper triangle (i < j pairs)
for {i, j} <- Map.keys(connections), into: %{} do
{{i, j}, Map.get(adj, {i, j}, false)}
end
end
end