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_blockingplumbed throughCore.HttpClientbut 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 indp_exchange_coinbase— documented as the thing a caller does after a failed batch; the caller reported and never retried (issue #22).dp_exchange_schwab'ssubscribe_notices/1facade — built, nothing ever emitted to it, because the internal registry it should have delegated to (Feed.subscribe_notices/2) had no caller anywhere inlib/.dp_exchange_schwab'sAuth.refresh/2— zero call sites anywhere inlib/, whileSocketcapturedaccess_tokenonce atstart_link/1andwebsockexreconnects 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 — seeusage-rules/adapter.md) and the in-process fake (called only by a consumer's tests, never by this package's ownlib/). 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
:attributeschunk (@behaviourtargets) and each target's ownbehaviour_info(:callbacks), soGenServer,WebSockex,Supervisor,DpExchange.Core.Venueand 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 byuseand never overridden, is still excused: it satisfies the same name/arity whether hand-written or macro-generated. child_specandstart_link, at any arity — always excused, on every module, because aSupervisorcalls them by convention rather than through any call:xrefcan 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 howdefmacrocompiles. 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 insidelib/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.
Summary
Types
@type violation() :: %{ module: module(), function: atom(), arity: non_neg_integer(), file: String.t() | nil, line: pos_integer() | nil }
Functions
Renders run/3 violations as one line per finding, for a failure message.
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.