Firebreak.Runtime (Firebreak v0.1.0)

Copy Markdown View Source

Exact supervision-tree extraction by the canonical OTP method.

OTP never parses supervision trees from source. supervisor.erl calls Mod:init(Args) at runtime and reads the returned child specs and restart flags as data — child specs are runtime values, not syntax. When the target modules are compiled and loadable, Firebreak does the same: it calls init/1 to obtain the exact {SupFlags, child_specs} and tags the result :exact.

Crucially, init/1 does not start any children — in OTP, start_link calls init/1 first and only afterwards iterates the returned specs to start them. So evaluating init/1 is side-effect-light and tells us the real tree, including child lists assembled with pipes, ++, List.flatten/1, conditionals, or builder calls that defeat any static pass.

What stays static

  • Application modules. Their start/2 callback actually boots the tree (it calls Supervisor.start_link), so we must not invoke it. The static parser reads the child list out of start/2 instead.
  • Un-loadable modules. Analysing source that isn't compiled (no _build), we fall back to the static ModuleInfo as parsed.
  • The coupling graph (edges, registers, unsupervised_spawns, traps_exit?). Those describe who-calls-whom; init/1 does not reveal them, and they remain pure static analysis.

The init argument

init/1 receives whatever start_link passed it, which we can't know without running the app. We try a short list of common arguments (:ok, [], nil, %{}) and take the first that returns a well-formed supervisor result. The overwhelming majority of supervisors ignore the argument (def init(_)), so this is exact in practice.

When init/1 pattern-matches a struct (def init(%Config{} = conf)), the simple arguments can't match, so we synthesise one via the struct module's new/1 (the Elixir convention for a populated, valid struct) — trying Config.new([]), Config.new(%{}), Config.new(), then a raw struct/2 — before falling back. This reads config-driven supervisors (Oban, etc.) exactly instead of statically. The struct type is detected from the source (Firebreak.ModuleInfo.init_arg_struct).

A supervisor whose child set branches on its argument is the one case where the tree reflects the probe/synthesised argument rather than production — notably a synthesised default config can omit config-driven children (queues, plugins); --observe gives the real runtime shape. Anything that raises for every candidate falls back to static.

Summary

Functions

Replace the supervision-tree fields of loadable supervisor modules with exact data read from init/1. Application and un-loadable modules are returned unchanged (still :static). Pass runtime: false to disable entirely.

Functions

enrich(modules, opts \\ [])

@spec enrich(
  [Firebreak.ModuleInfo.t()],
  keyword()
) :: [Firebreak.ModuleInfo.t()]

Replace the supervision-tree fields of loadable supervisor modules with exact data read from init/1. Application and un-loadable modules are returned unchanged (still :static). Pass runtime: false to disable entirely.