DpExchange.Core.UnwiredCheck (DpExchangeCore v0.3.28)

Copy Markdown View Source

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/1 and start_link/1 — 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 exportsmodule_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

Functions

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

Finds internal functions with no caller under lib_root.

Types

violation()

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

Functions

format(violations)

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

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

run(beam_dir, lib_root, excluded_modules \\ [])

@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.