Firebreak.Coupling (Firebreak v0.1.0)

Copy Markdown View Source

Resolves the per-module coupling edges produced by Firebreak.Source into a global graph.

Resolution means turning a registered-name target (GenServer.call(:cache, …)) into the module that registers that name. Module-alias targets are already resolved; anything that can't be tied to a module statically is kept as an unresolved edge (still useful — it tells you a dependency exists even if we can't name the far end).

Wrapper-call edges (transitive inter-procedural)

Apps rarely scatter GenServer.call(Server, …) across the codebase; they wrap it in a public API (Server.fetch(id)). A direct-only scan misses the dependency from the API caller onto the server. After resolving the direct edges, resolve/1 pairs every module's api_calls (remote Mod.fun(...) sites) against an entry_points table built from every module's api_entries (public functions that themselves couple to a process), and synthesises the resulting edge onto the wrapped process — deduped against the direct edges.

The table is transitive: a wrapper's reachable processes include not just the ones it couples to directly, but those of the wrappers it calls. If A calls B.f, B.f calls C.g, and C.g wraps GenServer.call(P), then A gets the edge to P. The chain of plain function calls blocks A on P iff the terminal call is synchronous, so the terminal coupling's kind/sync? is what's recorded. The walk follows each module's api_fun_calls (the remote public-function calls each function makes), bounded to a fixed depth and cycle-guarded so a recursive or mutually-recursive API can't loop. Matching is by function name (not arity), an intentional over-approximation: any arity of a process API is treated as coupling to that process.

Summary

Functions

Maps every known name to the module behind it. Three sources, in increasing precedence

Resolves edges for every module. Returns {modules, name_index, edges} where modules have their :edges rewritten with resolution applied, name_index maps registered names to modules, and edges is the flat global edge list.

Functions

build_name_index(modules)

@spec build_name_index([Firebreak.ModuleInfo.t()]) :: %{optional(term()) => module()}

Maps every known name to the module behind it. Three sources, in increasing precedence:

  • child-spec names — a name a parent registers a child under ({Worker, name: :w}), bound to the child's module;
  • ETS tables — a table's name, bound to the module that calls :ets.new/2 (the owner a lookup from elsewhere actually depends on);
  • self-registrations — a module naming itself in start_link (name: __MODULE__), the most direct evidence, so it wins on conflict.

resolve(modules)

@spec resolve([Firebreak.ModuleInfo.t()]) ::
  {[Firebreak.ModuleInfo.t()], %{optional(term()) => module()},
   [Firebreak.Edge.t()]}

Resolves edges for every module. Returns {modules, name_index, edges} where modules have their :edges rewritten with resolution applied, name_index maps registered names to modules, and edges is the flat global edge list.