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_spec/1andstart_link/1— 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.
What this does not catch
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.