KogenCredo.UnusedPublic (KogenCredo v0.1.0)
View SourceAdvisory finder for public functions that nothing else in the project calls.
It answers "which defs have no caller?" from the expanded code in compiled
BEAM debug info, not from source text, so aliases, imports and use-injected
code are already resolved. The pure core works on {module, beam_binary} pairs;
Mix.Tasks.Kogen.UnusedPublic feeds it the project's compiled modules.
Findings
:no_callers: nothing in the analysed modules calls, captures or references the function.:only_local: only other functions of the same module call it, so it may belong in adefp.
What is excluded
- behaviour callbacks: functions marked
@impl, and any function that is a callback of a declared@behaviour(loaded throughbehaviour_info/1); __name__functions,module_info,child_spec/1,main/1;- macros (
defmacro): a macro call is expanded away, so it leaves no call to find; - modules matching
:entrypoint_modules(default: controllers, routers, channels, sockets,Mix.Tasks.*) and anyModule.fun/aritylisted in:entrypoint_functions; - modules matching
:ignore_modules.
Functions with default arguments are treated as one group: the generated lower-arity heads are not reported and calls between them do not count.
What this does NOT prove
This is a heuristic over the modules you give it, not whole-program proof. A function can still be live when:
- it is called by name at runtime (
apply/3with computed values,String.to_existing_atom, Oban/telemetry/config MFAs stored in a database or non-literal data); literalapply(Mod, :fun, [..])calls,{Mod, :fun, [..]}tuples and&Mod.fun/ncaptures are counted; - it is part of a library's public API for other projects (do not run this on
a library, or list its public modules in
:ignore_modules); - it is called only from tests, other umbrella apps you did not include,
templates compiled elsewhere, or code in
priv/; - a framework dispatches to it (LiveView events, controller actions outside
the default patterns): add the module to
:entrypoint_modules.
Conversely a function can look used because a same-named call exists in a dead function; the analysis is one level deep and does not compute reachability from entry points.
Summary
Functions
Finds public functions without callers.
Reads module infos from {module, beam_binary} pairs.
Renders findings as lines file:line Mod.fun/arity (kind).
Types
@type finding() :: %{ module: module(), name: atom(), arity: arity(), line: pos_integer() | nil, file: String.t() | nil, kind: :no_callers | :only_local }
Functions
@spec analyze( [module_info()], keyword() ) :: [finding()]
Finds public functions without callers.
Options:
:entrypoint_modules- list ofRegexor module-name strings (exact match oninspect(module)); default controllers, routers, channels, sockets andMix.Tasks.*:entrypoint_functions- list of"Mod.fun/arity"strings or{mod, fun, arity}:ignore_modules- same shape as:entrypoint_modules, default[]
Returns findings sorted by file and line.
Reads module infos from {module, beam_binary} pairs.
Returns {infos, skipped} where skipped lists {module, reason} for beams
without debug info (compiled with debug_info: false) or unreadable chunks.
Those modules are neither analysed nor counted as callers, which can create
false findings; callers should surface skipped to the user.
Renders findings as lines file:line Mod.fun/arity (kind).