Current section

Files

Jump to
metastatic lib metastatic analysis coupling.ex
Raw

lib/metastatic/analysis/coupling.ex

defmodule Metastatic.Analysis.Coupling do
@moduledoc """
Coupling analysis for containers (modules/classes).
Measures dependencies between containers. Low coupling is desirable for maintainability.
## Coupling Types
- **Afferent Coupling (Ca)**: Number of containers that depend on this container
- **Efferent Coupling (Ce)**: Number of containers this container depends on
- **Instability (I)**: Ce / (Ca + Ce) - ranges from 0 (stable) to 1 (unstable)
## Assessment
- Low coupling (Ce < 5): Excellent - easy to test and maintain
- Moderate coupling (5 <= Ce < 10): Good - acceptable dependencies
- High coupling (10 <= Ce < 20): Fair - consider refactoring
- Very high coupling (Ce >= 20): Poor - difficult to maintain
## Examples
# Low coupling - no external dependencies
ast = {:container, [container_type: :class, name: "Calculator"], [
{:function_def, [name: "add", params: [{:param, [], "x"}, {:param, [], "y"}], visibility: :public], [
{:binary_op, [category: :arithmetic, operator: :+], [{:variable, [], "x"}, {:variable, [], "y"}]}]}
]}
doc = Document.new(ast, :python)
{:ok, result} = Coupling.analyze(doc)
result.efferent_coupling # => 0
result.assessment # => :excellent
"""
alias Metastatic.Document
@type result :: %{
container_name: String.t() | nil,
efferent_coupling: non_neg_integer(),
dependencies: [String.t()],
instability: float(),
assessment: :excellent | :good | :fair | :poor,
warnings: [String.t()],
recommendations: [String.t()]
}
use Metastatic.Document.Analyzer,
doc: """
Analyzes coupling in a document.
Accepts either a `Metastatic.Document` struct or a `{language, native_ast}` tuple.
"""
@impl Metastatic.Document.Analyzer
def handle_analyze(%Document{ast: ast}, _opts \\ []) do
with {:ok, _type, name, members} <- extract_container(ast),
do: {:ok, analyze_coupling(name, members)}
end
# 3-tuple format
defp extract_container({:container, meta, [members]}) when is_list(meta) do
type = Keyword.get(meta, :container_type, :class)
name = Keyword.get(meta, :name, "anonymous")
members_list = if is_list(members), do: members, else: [members]
{:ok, type, name, members_list}
end
defp extract_container(_), do: {:error, "AST does not contain a container"}
defp analyze_coupling(name, members) do
# Extract dependencies (external types/modules referenced)
dependencies = extract_dependencies(members) |> Enum.uniq() |> Enum.sort()
efferent = length(dependencies)
# Calculate instability (assuming Ca=0 since we don't have cross-file analysis yet)
instability = if efferent > 0, do: 1.0, else: 0.0
{assessment, warnings, recommendations} = assess_coupling(efferent)
%{
container_name: name,
efferent_coupling: efferent,
dependencies: dependencies,
instability: instability,
assessment: assessment,
warnings: warnings,
recommendations: recommendations
}
end
defp extract_dependencies(members) do
members
|> Enum.flat_map(&extract_deps_from_member/1)
|> Enum.uniq()
end
# 3-tuple format
defp extract_deps_from_member({:function_def, meta, [body]}) when is_list(meta),
do: extract_deps_from_ast(body)
defp extract_deps_from_member({:property, meta, [getter, setter]}) when is_list(meta) do
extract_deps_from_ast(getter) ++ extract_deps_from_ast(setter)
end
defp extract_deps_from_member(_), do: []
defp extract_deps_from_ast(nil), do: []
defp extract_deps_from_ast(ast) do
case ast do
# Function calls to external modules (Module.function pattern) - 3-tuple
{:attribute_access, _meta, [{:variable, _, module}, _func]}
when module not in ["self", "this", "@"] ->
[module]
# Function call - 3-tuple
{:function_call, meta, args} when is_list(meta) and is_list(args) ->
name = Keyword.get(meta, :name, "")
# Check if it's a qualified call (e.g., "Math.sqrt")
deps =
if is_binary(name) and String.contains?(name, ".") do
[name |> String.split(".") |> hd()]
else
[]
end
deps ++ Enum.flat_map(args, &extract_deps_from_ast/1)
# Binary op - 3-tuple
{:binary_op, _meta, [left, right]} ->
extract_deps_from_ast(left) ++ extract_deps_from_ast(right)
# Conditional - 3-tuple
{:conditional, _meta, [cond_expr, then_br, else_br]} ->
extract_deps_from_ast(cond_expr) ++
extract_deps_from_ast(then_br) ++
extract_deps_from_ast(else_br)
# Block - 3-tuple
{:block, _meta, stmts} when is_list(stmts) ->
Enum.flat_map(stmts, &extract_deps_from_ast/1)
# Assignment - 3-tuple
{:assignment, _meta, [_target, value]} ->
extract_deps_from_ast(value)
# Augmented assignment - 3-tuple
{:augmented_assignment, _meta, [_target, value]} ->
extract_deps_from_ast(value)
# Loop - 3-tuple
{:loop, meta, children} when is_list(meta) and is_list(children) ->
Enum.flat_map(children, &extract_deps_from_ast/1)
_ ->
[]
end
end
defp assess_coupling(efferent) do
{assessment, warnings, recommendations} =
cond do
efferent < 5 ->
{:excellent, [], ["Low coupling - excellent for maintainability and testing"]}
efferent < 10 ->
{:good, [],
[
"Moderate coupling - acceptable but monitor growth",
"Consider dependency injection for better testability"
]}
efferent < 20 ->
{:fair,
["High coupling (#{efferent} dependencies) - may be difficult to test and maintain"],
[
"Refactor to reduce dependencies",
"Use interfaces/protocols to decouple from concrete implementations",
"Consider the Dependency Inversion Principle"
]}
true ->
{:poor,
[
"Very high coupling (#{efferent} dependencies) - serious maintainability issue",
"Changes to dependencies will likely require changes to this container"
],
[
"Major refactoring recommended",
"Split container into smaller, focused units",
"Introduce abstractions to reduce direct dependencies",
"Apply SOLID principles, especially Single Responsibility"
]}
end
{assessment, warnings, recommendations}
end
end