# `DpExchange.Core.UnwiredCheck`
[🔗](https://github.com/DistortionPoint/dp-exchange-core/blob/v0.3.120/lib/dp_exchange/core/unwired_check.ex#L1)

Finds an internal function nothing in a package's own `lib/` ever calls.

## The defect this exists for

**"Mechanism built, documented, and never wired."** Six instances in one week across
this family, every one shipped green:

  * `rate_limit_blocking` plumbed through `Core.HttpClient` but never set by the
    caller — `dp_exchange_robinhood` (issue #16), `dp_exchange_webull` (issue #23,
    where it had to be threaded through three separate option allowlists and a fix
    stopping at the first layer still passed every test asserting the keyword was
    present), `dp_exchange_coinbase` (issue #26).
  * `FrameSender`'s retry path in `dp_exchange_coinbase` — documented as the thing a
    caller does after a failed batch; the caller reported and never retried
    (issue #22).
  * `dp_exchange_schwab`'s `subscribe_notices/1` facade — built, nothing ever emitted
    to it, because the internal registry it should have delegated to
    (`Feed.subscribe_notices/2`) had no caller anywhere in `lib/`.
  * `dp_exchange_schwab`'s `Auth.refresh/2` — zero call sites anywhere in `lib/`,
    while `Socket` captured `access_token` once at `start_link/1` and `websockex`
    reconnects synchronously with no delay, so an expired token became an undelayed
    loop hammering the venue with a credential the package could not replace.

In every case a test called the function directly, so coverage stayed high and the
suite stayed green — a test is not a caller. **An internal function with no caller in
the package's own `lib/` is either dead code or an unwired mechanism, and both are
defects.**

## Real call-graph data, not a grep

Built on `:xref`, the same OTP cross-reference tool `Core.AdapterContract`'s "7.
purity" assertion already reads `imports` chunks through. A grep trips on
`apply(Mod, :fun, args)` and on a captured `&Mod.fun/1` — both are ordinary, correct
ways to call a function and neither looks like a call textually. `:xref`'s edge
relation (`E`, queried below as `E || M:F/A` — restrict the range to a specific
callee) resolves both correctly, including a call from a function back to its own
module. What it cannot resolve is genuinely dynamic dispatch — a module or function
name assembled at runtime — and no static tool can; that limit is inherent to the
problem, not particular to this check.

`:xref.analyze(:exports_not_used)` was considered and rejected: its own semantics are
"not called from outside the defining module," so a public function called only by a
sibling function in the same module reads as unused even though something in `lib/`
plainly calls it. Querying the raw edge relation and building the "was this ever a
callee, from anywhere in the analysed set" answer ourselves avoids that false
positive.

## What counts as a caller

Anything reachable through `:xref`'s edge relation from a module whose compiled
`:compile_info` source path is under `lib_root` — regardless of which Mix environment
produced the `.beam` files. A package compiled under `MIX_ENV=test` (`elixirc_paths`
commonly adding `test/support`) still only has its `lib/`-sourced modules added to the
`:xref` server here, so a function reachable only from `test/support` — a fake, a
fixture, a helper only the suite imports — is **not** treated as wired. That is
deliberate: it is exactly the shape of six known instances of this defect, where a
test calling the function directly was the reason nothing looked wrong.

## What is excluded, and why each is safe to exclude without an allowlist that rots

  * **`excluded_modules`** — whole modules the caller names explicitly, not inferred.
    `Core.AdapterContract`'s "16. internal wiring" assertion passes `[@venue, @fake]`:
    the facade (called only by consumers, by the family's own contract — see
    `usage-rules/adapter.md`) and the in-process fake (called only by a consumer's
    tests, never by this package's own `lib/`). Both bindings already exist for other
    assertions in the same suite; nothing new is hand-maintained here.
  * **Behaviour callbacks** — read from the module's own `:attributes` chunk
    (`@behaviour` targets) and each target's own `behaviour_info(:callbacks)`, so
    `GenServer`, `WebSockex`, `Supervisor`, `DpExchange.Core.Venue` and any other
    behaviour a module declares excuses exactly the callbacks that behaviour defines —
    never a function the module merely happens to export. A callback's default body,
    injected by `use` and never overridden, is still excused: it satisfies the same
    name/arity whether hand-written or macro-generated.
  * **`child_spec` and `start_link`, at any arity** — always excused, on every module, because a
    `Supervisor` calls them by convention rather than through any call `:xref` can see
    in the analysed set.
  * **Compiler-injected exports** — `module_info/0`, `module_info/1`, and any export
    whose name is wrapped in double underscores (`__struct__`, `__info__`, `__impl__`,
    …), the compiler's own naming convention for exactly this category.
  * **Macros** — every export named `MACRO-<name>`, which is how `defmacro` compiles. A
    macro call is expanded where it is written and leaves no call behind in the caller's
    beam, so a macro used a hundred times inside `lib/` has no edge in the call graph and
    read as unwired. The price is stated rather than hidden: a macro nothing uses is not
    reported. (None of the five venues defines one.)

## What this does not catch

**Anything reached only through data.** A `{Mod, :fun, args}` tuple handed to
`:timer.apply_after/4`, `Task.Supervisor.start_child/4` or a child spec's `:start` is a
literal MFA, but it is a tuple and not a call, so `:xref` sees no edge to `Mod.fun/n`
and the function reads as unwired though it runs. The remedy is the one the rule already
names — call it where it is wired (a captured `&Mod.fun/1` or a literal `apply/3` is
resolved) — never an allowlist.

**Dead code that calls itself in a ring.** "Has a caller" is the test, not "is
reachable from an entry point": two dead functions that call each other each have one,
and neither is reported. A dead chain is reported at its top, and the next run reports
the next link once that one is deleted. Reachability from the facade, the fake and the
behaviour callbacks would be the stricter question; it was not built because it cannot
be checked against the five venues without running them.

A function reachable only through the package's own `Fake` module and never through
any real production path is not flagged — `Fake` is part of `lib/`, so a call from it
satisfies the letter of "something in `lib/` calls it," even though the real code path
may still be dead. That is a narrower, rarer defect shape than the one this check was
built for, and asserting it would mean inferring which of a module's callers are
"real," which is not mechanically separable from the fake/real distinction this family
already draws by hand in each package's own fixtures.

# `violation`

```elixir
@type violation() :: %{
  module: module(),
  function: atom(),
  arity: non_neg_integer(),
  file: String.t() | nil,
  line: pos_integer() | nil
}
```

# `format`

```elixir
@spec format([violation()]) :: String.t()
```

Renders `run/3` violations as one line per finding, for a failure message.

# `run`

```elixir
@spec run(Path.t(), Path.t(), [module()]) :: {:ok, [violation()]} | {:error, term()}
```

Finds internal functions with no caller under `lib_root`.

`beam_dir` is a directory of compiled `.beam` files (typically
`Mix.Project.build_path() |> Path.join("lib/#{app}/ebin")`). Only the ones among them
whose recorded `:compile_info` source path falls under `lib_root` are analysed; the
rest are silently ignored, whichever Mix environment produced them.

`excluded_modules` are analysed for call-graph purposes (so a function they call still
counts as wired) but never themselves reported as violations — pass the facade and any
fake/reference implementation the caller exposes as public surface in its own right.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
