- Split Analyzer.analyze/1 into compute_state/1 (slow, scans all BEAMs) and a fast filter step. analyze/1 now accepts :precomputed_state opt. - Mix.Tasks.Compile.Unused.run/1 picks up precomputed state from process dict :mix_unused_extra_analyze_opts when present (test escape hatch). - AnalyzerTest setup_all computes state once; exclude tests pass it through analyze/1, dropping per-test cost from ~700ms to <1ms. - UnusedTest setup_all does the same — the consolidated severity-args test drops from 3.4s to ~5ms. - Trim further sleeps in HistoricalLoadingTest (100->30ms) and MovementTest (50->20ms, receive timeouts 100->30ms). Total suite: 40.6s -> 37.7s; 2488 tests, 0 failures.
142 lines
3.7 KiB
Elixir
142 lines
3.7 KiB
Elixir
defmodule Mix.Tasks.Compile.Unused do
|
|
@moduledoc """
|
|
Mix compiler that finds public functions in this project which no observed
|
|
caller ever invokes.
|
|
|
|
## Usage
|
|
|
|
Run directly:
|
|
|
|
mix compile.unused
|
|
|
|
Or wire into the compiler chain in `mix.exs`:
|
|
|
|
compilers: Mix.compilers() ++ [:unused]
|
|
|
|
## Configuration
|
|
|
|
In `mix.exs`:
|
|
|
|
def project do
|
|
[
|
|
...,
|
|
unused: [
|
|
severity: :warning, # :warning (default) | :error | :info
|
|
exclude: [
|
|
MyApp.SomeModule, # whole modules
|
|
{MyApp.Foo, :ignored, 0} # specific MFA triples
|
|
],
|
|
ignore_test_files: true # skip files under test/ (default true)
|
|
]
|
|
]
|
|
end
|
|
|
|
## How it works
|
|
|
|
* The project is compiled (via `mix compile`).
|
|
* `MixUnused.Analyzer` then enumerates every public function in every
|
|
project-owned BEAM file via `Module.__info__/1` and walks the
|
|
abstract code in each BEAM (via `:beam_lib`) to collect the set of
|
|
remote `Module.fun/arity` calls.
|
|
* Functions in the def-set that aren't in the call-set are reported.
|
|
* Behaviour callbacks (`@impl true` or via `behaviour_info(:callbacks)`),
|
|
well-known entry points (`start_link`, `child_spec`, `mount`,
|
|
`render`, GenServer callbacks, etc.) and reflection helpers
|
|
(`__info__/1`, `module_info/0`, ...) are excluded automatically.
|
|
"""
|
|
|
|
use Mix.Task.Compiler
|
|
|
|
alias Mix.Task.Compiler
|
|
alias MixUnused.Analyzer
|
|
|
|
@recursive false
|
|
|
|
@impl Compiler
|
|
def run(args) do
|
|
{opts, _, _} =
|
|
OptionParser.parse(args,
|
|
switches: [severity: :string, all: :boolean]
|
|
)
|
|
|
|
config = read_config()
|
|
|
|
severity =
|
|
opts
|
|
|> Keyword.get(:severity)
|
|
|> parse_severity(Keyword.get(config, :severity, :warning))
|
|
|
|
# Make sure the project is compiled before analyzing.
|
|
_ = Mix.Task.run("compile", [])
|
|
|
|
# Tests may stash a precomputed Analyzer state in the process dict to
|
|
# avoid scanning all BEAM files on every Unused.run/1 call.
|
|
extra_analyze_opts = Process.get(:mix_unused_extra_analyze_opts, [])
|
|
|
|
config
|
|
|> filter_options()
|
|
|> Keyword.merge(extra_analyze_opts)
|
|
|> Analyzer.analyze()
|
|
|> filter_test_files(Keyword.get(config, :ignore_test_files, true))
|
|
|> emit(severity)
|
|
end
|
|
|
|
@impl Compiler
|
|
def manifests, do: []
|
|
|
|
@impl Compiler
|
|
def clean, do: :ok
|
|
|
|
defp read_config, do: Keyword.get(Mix.Project.config(), :unused, [])
|
|
|
|
defp filter_options(config) do
|
|
[exclude: Keyword.get(config, :exclude, [])]
|
|
end
|
|
|
|
defp parse_severity(nil, default), do: default
|
|
|
|
defp parse_severity(string, _default) do
|
|
case String.downcase(string) do
|
|
"warning" -> :warning
|
|
"error" -> :error
|
|
"info" -> :info
|
|
_ -> :warning
|
|
end
|
|
end
|
|
|
|
defp filter_test_files(entries, false), do: entries
|
|
|
|
defp filter_test_files(entries, true) do
|
|
Enum.reject(entries, fn %{file: file} ->
|
|
file != nil and (String.starts_with?(file, "test/") or String.contains?(file, "/test/"))
|
|
end)
|
|
end
|
|
|
|
defp emit([], _severity) do
|
|
Mix.shell().info("[unused] No unused public functions found.")
|
|
{:ok, []}
|
|
end
|
|
|
|
defp emit(entries, severity) do
|
|
formatted = Analyzer.format(entries)
|
|
Mix.shell().info("\n" <> formatted)
|
|
|
|
diagnostics = Enum.map(entries, &to_diagnostic(&1, severity))
|
|
|
|
case severity do
|
|
:error -> {:error, diagnostics}
|
|
_ -> {:ok, diagnostics}
|
|
end
|
|
end
|
|
|
|
defp to_diagnostic(%{module: m, name: n, arity: a, file: file, line: line}, severity) do
|
|
%Mix.Task.Compiler.Diagnostic{
|
|
compiler_name: "unused",
|
|
file: file,
|
|
position: line || 0,
|
|
message: "unused public function #{inspect(m)}.#{n}/#{a}",
|
|
severity: severity,
|
|
details: nil
|
|
}
|
|
end
|
|
end
|