Packages
metastatic
0.14.1
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/encapsulation.ex
defmodule Metastatic.Analysis.Encapsulation do
@moduledoc """
Encapsulation analysis for containers (modules/classes).
Detects violations of encapsulation principles - the practice of hiding internal
state and requiring all interaction to go through well-defined interfaces.
## Principles Checked
1. **Information Hiding**: Internal state should be private
2. **Controlled Access**: State access should go through methods
3. **Interface Segregation**: Public interface should be minimal and focused
4. **Implementation Hiding**: Internal details should not leak
## Detected Violations
### Public State (Critical)
Instance variables that are publicly accessible without getters/setters.
Breaks encapsulation by allowing uncontrolled state modification.
### Missing Accessors (High)
Direct state access/modification without using accessor methods.
Bypasses potential validation and business logic.
### Excessive Accessors (Medium)
Too many getter/setter pairs relative to behavior methods.
May indicate a "data class" that lacks encapsulation of behavior.
### Leaky Abstraction (Medium)
Methods that expose internal implementation details.
Makes refactoring difficult and couples clients to implementation.
## Examples
# Good encapsulation
ast = {:container, :class, "BankAccount", %{}, [
{:function_def, :public, "deposit", ["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} = Encapsulation.analyze(doc)
result.violations # => []
result.score # => 100
result.assessment # => :excellent
# Poor encapsulation - direct state access
ast = {:container, :class, "User", %{}, [
{:function_def, :public, "process", [], %{},
{:assignment, {:attribute_access, {:variable, "self"}, "status"}, {:literal, :string, "done"}}}
]}
{:ok, result} = Encapsulation.analyze(doc)
result.violations # => [%{type: :missing_accessor, ...}]
result.assessment # => :poor
"""
alias Metastatic.{Analysis.Encapsulation.Result, Document}
use Metastatic.Document.Analyzer,
doc: """
Analyze encapsulation of a container (module/class/namespace).
Returns `{:ok, result}` if the AST contains a container, or `{:error, reason}` otherwise.
## Examples
iex> ast = {:container, :class, "Example", %{}, [
...> {:function_def, :public, "get_value", [], %{},
...> {:attribute_access, {:variable, "self"}, "value"}}
...> ]}
iex> doc = Metastatic.Document.new(ast, :python)
iex> {:ok, result} = Metastatic.Analysis.Encapsulation.analyze(doc)
iex> result.score
100
iex> result.assessment
:excellent
"""
@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 methods and properties
methods = Enum.filter(members, &match?({:function_def, _, _, _, _, _}, &1))
properties = Enum.filter(members, &match?({:property, _, _, _, _}, &1))
# Count visibility
public_methods =
Enum.count(methods, fn {:function_def, vis, _, _, _, _} -> vis == :public end)
private_methods =
Enum.count(methods, fn {:function_def, vis, _, _, _, _} -> vis == :private end)
# Identify accessors (getters/setters)
accessor_count = count_accessors(methods)
# Extract all state variables accessed in the container
state_variables = extract_state_variables(members)
# Detect violations
violations = detect_violations(members, methods, properties)
# Calculate metrics
score = Result.calculate_score(violations)
assessment = Result.assess(score)
recommendations = Result.generate_recommendations(violations, public_methods, accessor_count)
%Result{
container_name: container_name,
container_type: container_type,
violations: violations,
score: score,
assessment: assessment,
public_methods: public_methods,
private_methods: private_methods,
accessor_count: accessor_count,
state_variables: Enum.sort(state_variables),
recommendations: recommendations
}
end
# Count methods that are simple accessors (getters/setters)
defp count_accessors(methods) do
Enum.count(methods, &accessor?/1)
end
# Check if a function is a simple accessor
defp accessor?({:function_def, :public, name, params, _meta, body}) do
# Getter: no params, returns attribute access
getter? = params == [] and getter_body?(body)
# Setter: one param, assigns to attribute
setter? = match?([_], params) and setter_body?(body)
# Check if name suggests accessor (get_*, set_*, or property-style)
name_suggests_accessor =
String.starts_with?(name, ["get_", "set_"]) or
not String.contains?(name, "_")
(getter? or setter?) and name_suggests_accessor
end
defp accessor?(_), do: false
# Check if body is a getter (returns attribute access)
defp getter_body?({:attribute_access, _, _}), do: true
defp getter_body?(_), do: false
# Check if body is a setter (assigns to attribute)
defp setter_body?({:assignment, {:attribute_access, _, _}, _}), do: true
defp setter_body?(_), do: false
# Extract all state variables accessed in members
defp extract_state_variables(members) do
members
|> Enum.flat_map(&extract_state_from_member/1)
|> Enum.uniq()
end
defp extract_state_from_member({:function_def, _, _, _, _, body}) do
extract_state_accesses(body, [])
end
defp extract_state_from_member({:property, _name, getter, setter, _metadata}) do
getter_vars = if getter, do: extract_state_from_member(getter), else: []
setter_vars = if setter, do: extract_state_from_member(setter), else: []
getter_vars ++ setter_vars
end
defp extract_state_from_member(_), do: []
defp extract_state_accesses(ast, acc) do
case ast do
{:attribute_access, {:variable, var}, attr} when var in ["self", "this", "@"] ->
[attr | acc]
{:augmented_assignment, _op, target, value} ->
acc = extract_state_accesses(target, acc)
extract_state_accesses(value, acc)
{:assignment, target, value} ->
acc = extract_state_accesses(target, acc)
extract_state_accesses(value, acc)
{:binary_op, _, _, left, right} ->
acc = extract_state_accesses(left, acc)
extract_state_accesses(right, acc)
{:conditional, cond, then_br, else_br} ->
acc = extract_state_accesses(cond, acc)
acc = extract_state_accesses(then_br, acc)
if else_br, do: extract_state_accesses(else_br, acc), else: acc
{:block, stmts} when is_list(stmts) ->
Enum.reduce(stmts, acc, fn stmt, a -> extract_state_accesses(stmt, a) end)
{:function_call, _name, args} ->
Enum.reduce(args, acc, fn arg, a -> extract_state_accesses(arg, a) end)
{: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)
_ ->
acc
end
end
# Detect encapsulation violations
defp detect_violations(_members, methods, _properties) do
violations = []
# Check for direct state assignments without accessors
violations = violations ++ detect_missing_accessors(methods)
# Check for excessive accessors (data class smell)
violations = violations ++ detect_excessive_accessors(methods)
# Check for public methods accessing too much state
violations = violations ++ detect_leaky_abstractions(methods)
violations
end
# Detect methods that directly modify state without proper encapsulation
defp detect_missing_accessors(methods) do
methods
|> Enum.flat_map(fn {:function_def, visibility, name, _params, _meta, body} = method ->
# Skip if it's already an accessor
if accessor?(method) do
[]
else
# Check if method directly assigns to state
if visibility == :public and has_direct_state_assignment?(body) do
[
Result.violation(
:missing_accessor,
:high,
"Method '#{name}' directly modifies state - consider using setter methods",
name,
"Extract state modification into private methods or setters"
)
]
else
[]
end
end
end)
end
# Check if AST contains direct state assignments
defp has_direct_state_assignment?(ast) do
case ast do
{:assignment, {:attribute_access, {:variable, var}, _attr}, _value}
when var in ["self", "this", "@"] ->
true
{:augmented_assignment, _op, {:attribute_access, {:variable, var}, _attr}, _value}
when var in ["self", "this", "@"] ->
true
{:block, stmts} when is_list(stmts) ->
Enum.any?(stmts, &has_direct_state_assignment?/1)
{:conditional, _cond, then_br, else_br} ->
has_direct_state_assignment?(then_br) or
(else_br != nil and has_direct_state_assignment?(else_br))
{:loop, :while, _condition, body} ->
has_direct_state_assignment?(body)
{:loop, _, _iter, _coll, body} ->
has_direct_state_assignment?(body)
_ ->
false
end
end
# Detect excessive accessors (data class smell)
defp detect_excessive_accessors(methods) do
accessor_count = count_accessors(methods)
total_public = Enum.count(methods, fn {:function_def, vis, _, _, _, _} -> vis == :public end)
# If more than 70% of public methods are accessors, it's a data class smell
if total_public > 0 and accessor_count / total_public > 0.7 and accessor_count > 5 do
[
Result.violation(
:excessive_accessors,
:medium,
"Container has #{accessor_count} accessors out of #{total_public} public methods - may be a data class",
nil,
"Add behavior methods that encapsulate business logic, not just state access"
)
]
else
[]
end
end
# Detect methods that expose too much internal state (leaky abstraction)
defp detect_leaky_abstractions(methods) do
# Check for public methods that return internal collections or complex state
methods
|> Enum.flat_map(fn {:function_def, visibility, name, _params, _meta, body} ->
if visibility == :public and returns_internal_structure?(body) do
[
Result.violation(
:leaky_abstraction,
:medium,
"Method '#{name}' may expose internal implementation details",
name,
"Consider returning copies or immutable views of internal state"
)
]
else
[]
end
end)
end
# Check if method returns internal collections or complex structures
defp returns_internal_structure?(ast) do
case ast do
# Returning a list/tuple of internal state
{:list, elems} when is_list(elems) ->
Enum.any?(elems, fn elem ->
case elem do
{:attribute_access, {:variable, var}, _} when var in ["self", "this", "@"] -> true
_ -> false
end
end)
{:tuple, elems} when is_list(elems) ->
Enum.any?(elems, fn elem ->
case elem do
{:attribute_access, {:variable, var}, _} when var in ["self", "this", "@"] -> true
_ -> false
end
end)
# Early return
{:early_return, value} ->
returns_internal_structure?(value)
# Conditional
{:conditional, _cond, then_br, _else_br} ->
returns_internal_structure?(then_br)
# Block - check last statement
{:block, [_ | _] = stmts} ->
returns_internal_structure?(List.last(stmts))
_ ->
false
end
end
end