Choreo.UML.Analysis (Choreo v0.10.0)

Copy Markdown View Source

Architectural analysis suite for Choreo.UML class/struct diagrams.

Provides static analysis tools optimized for functional and OOP software design:

Summary

Functions

Returns all classes that transitively depend on the given class.

Identifies components which have declared a :realizes or :inherits relationship to a :behavior, :protocol, or :interface component but do not implement all the functions specified by that contract.

Computes coupling and stability metrics for all components in the UML diagram.

Identifies all circular dependency paths in the UML diagram.

Returns all classes that the given class transitively depends on.

Identifies all Law of Demeter violations in the UML class diagram.

Identifies redundant relationships that are implied by a longer path.

Validates a UML diagram and returns a list of issues.

Functions

affected_by(uml, target)

@spec affected_by(Choreo.UML.t(), Choreo.UML.class_id()) :: [Choreo.UML.class_id()]

Returns all classes that transitively depend on the given class.

If you change target, these are the classes that could break.

Examples

iex> uml =
...>   Choreo.UML.new()
...>   |> Choreo.UML.add_class(:api)
...>   |> Choreo.UML.add_class(:service)
...>   |> Choreo.UML.add_class(:repo)
...>   |> Choreo.UML.add_relationship(:api, :service, type: :depends)
...>   |> Choreo.UML.add_relationship(:service, :repo, type: :depends)
iex> Enum.sort(Choreo.UML.Analysis.affected_by(uml, :repo))
[:api, :service]
iex> Choreo.UML.Analysis.affected_by(uml, :api)
[]

This analysis answers the question: "What breaks if I change this class?"

broken_contracts(uml)

@spec broken_contracts(Choreo.UML.t()) :: [
  {Choreo.UML.class_id(), Choreo.UML.class_id(), [map()]}
]

Identifies components which have declared a :realizes or :inherits relationship to a :behavior, :protocol, or :interface component but do not implement all the functions specified by that contract.

Returns a list of tuples [{component_id, contract_id, [missing_function_maps]}].

Examples

iex> contract = Choreo.UML.new()
...>   |> Choreo.UML.add_class(:auth, type: :behavior, functions: [%{name: "verify", arity: 1}])
...>   # Struct missing the arity 1 verify function:
...>   |> Choreo.UML.add_class(:provider, type: :struct, functions: [%{name: "verify", arity: 2}])
...>   |> Choreo.UML.add_relationship(:provider, :auth, type: :realizes)
iex> [{:provider, :auth, [missing]}] = Choreo.UML.Analysis.broken_contracts(contract)
iex> missing[:name]
"verify"
iex> missing[:arity]
1

coupling_metrics(uml)

@spec coupling_metrics(Choreo.UML.t()) :: %{
  optional(Choreo.UML.class_id()) => %{
    afferent: non_neg_integer(),
    efferent: non_neg_integer(),
    instability: float()
  }
}

Computes coupling and stability metrics for all components in the UML diagram.

Returns a map of component IDs to:

  • :afferent — number of incoming dependencies (who depends on me)
  • :efferent — number of outgoing dependencies (who do I depend on)
  • :instability — Ce / (Ca + Ce). Stable core modules approach 0.0, unstable leaf modules approach 1.0.

Examples

iex> uml =
...>   Choreo.UML.new()
...>   |> Choreo.UML.add_class(:a)
...>   |> Choreo.UML.add_class(:b)
...>   |> Choreo.UML.add_relationship(:a, :b, type: :associates)
iex> metrics = Choreo.UML.Analysis.coupling_metrics(uml)
iex> metrics[:a]
%{afferent: 0, efferent: 1, instability: 1.0}
iex> metrics[:b]
%{afferent: 1, efferent: 0, instability: 0.0}

cycles(uml)

@spec cycles(Choreo.UML.t()) :: [[Choreo.UML.class_id()]]

Identifies all circular dependency paths in the UML diagram.

Cycles in structural diagrams indicate high coupling and are a major source of compilation cascades in Elixir applications.

Returns a list of cycles, where each cycle is a list of node IDs starting at the canonical smallest node and listing each member once.

Examples

iex> uml =
...>   Choreo.UML.new()
...>   |> Choreo.UML.add_class(:a)
...>   |> Choreo.UML.add_class(:b)
...>   |> Choreo.UML.add_relationship(:a, :b, type: :associates)
...>   |> Choreo.UML.add_relationship(:b, :a, type: :depends)
iex> Choreo.UML.Analysis.cycles(uml)
[[:a, :b]]

depends_on(uml, target)

@spec depends_on(Choreo.UML.t(), Choreo.UML.class_id()) :: [Choreo.UML.class_id()]

Returns all classes that the given class transitively depends on.

These are the classes target cannot function without, following the directed relationships in the diagram.

Examples

iex> uml =
...>   Choreo.UML.new()
...>   |> Choreo.UML.add_class(:api)
...>   |> Choreo.UML.add_class(:service)
...>   |> Choreo.UML.add_class(:repo)
...>   |> Choreo.UML.add_relationship(:api, :service, type: :depends)
...>   |> Choreo.UML.add_relationship(:service, :repo, type: :depends)
iex> Enum.sort(Choreo.UML.Analysis.depends_on(uml, :api))
[:repo, :service]
iex> Choreo.UML.Analysis.depends_on(uml, :repo)
[]

This analysis answers the question: "What does this class depend on?"

law_of_demeter_violations(uml)

@spec law_of_demeter_violations(Choreo.UML.t()) :: [
  {Choreo.UML.class_id(), Choreo.UML.class_id(), Choreo.UML.class_id()}
]

Identifies all Law of Demeter violations in the UML class diagram.

A violation occurs when a class A has a direct relationship to a class C, but also has a path through an intermediate class B (i.e., A -> B, B -> C, and A -> C).

Returns a list of triplets [{class_a, class_b, class_c}] representing the violations.

Examples

iex> uml =
...>   Choreo.UML.new()
...>   |> Choreo.UML.add_class(:a)
...>   |> Choreo.UML.add_class(:b)
...>   |> Choreo.UML.add_class(:c)
...>   |> Choreo.UML.add_relationship(:a, :b, type: :associates)
...>   |> Choreo.UML.add_relationship(:b, :c, type: :associates)
...>   |> Choreo.UML.add_relationship(:a, :c, type: :associates)
iex> Choreo.UML.Analysis.law_of_demeter_violations(uml)
[{:a, :b, :c}]

transitive_reduction(uml)

@spec transitive_reduction(Choreo.UML.t()) :: [
  {Choreo.UML.class_id(), Choreo.UML.class_id()}
]

Identifies redundant relationships that are implied by a longer path.

If api -> service, service -> repo, and api -> repo all exist, the direct api -> repo edge is redundant because it is implied by the transitive path.

Returns a list of {from, to} tuples. On cyclic diagrams returns an empty list because every edge in a cycle is structurally required.

Examples

iex> uml =
...>   Choreo.UML.new()
...>   |> Choreo.UML.add_class(:api)
...>   |> Choreo.UML.add_class(:service)
...>   |> Choreo.UML.add_class(:repo)
...>   |> Choreo.UML.add_relationship(:api, :service, type: :depends)
...>   |> Choreo.UML.add_relationship(:service, :repo, type: :depends)
...>   |> Choreo.UML.add_relationship(:api, :repo, type: :depends)
iex> Choreo.UML.Analysis.transitive_reduction(uml)
[{:api, :repo}]

This analysis answers the question: "Which explicit relationships are redundant?"

validate(uml)

@spec validate(Choreo.UML.t()) :: [{:error | :warning, String.t()}]

Validates a UML diagram and returns a list of issues.

Checks for:

  • Circular dependencies (:error)
  • Broken contracts (:error)
  • Isolated classes (:warning)
  • Law of Demeter violations (:warning)

Examples

iex> uml =
...>   Choreo.UML.new()
...>   |> Choreo.UML.add_class(:a)
...>   |> Choreo.UML.add_class(:b)
...>   |> Choreo.UML.add_relationship(:a, :b, type: :associates)
...>   |> Choreo.UML.add_relationship(:b, :a, type: :depends)
iex> Enum.any?(Choreo.UML.Analysis.validate(uml), fn {sev, _} -> sev == :error end)
true