Skip to content

Detect dependencies that are declared but never referenced#2114

Closed
Flo0807 wants to merge 1 commit into
developfrom
feature/detect-unused-deps
Closed

Detect dependencies that are declared but never referenced#2114
Flo0807 wants to merge 1 commit into
developfrom
feature/detect-unused-deps

Conversation

@Flo0807

@Flo0807 Flo0807 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Why

The number incident (#2113) was not caused by a hard problem — it was caused by an invisible one. number stopped being used on 2024-08-26 (71757de), stayed declared in mix.exs, and kept being resolved for about two years until its decimal ~> 1.5 or ~> 2.0 pin blocked decimal 3.x and ecto_sql 3.14.

CI never had a chance to notice. The step was even called "Deps Unused":

- name: Deps Unused
  run: mix deps.unlock --check-unused

But mix deps.unlock --check-unused only compares mix.lock against mix.exs. A dependency that is declared but never called looks perfectly used to it.

Nothing off the shelf closes this gap: ex_check's :unused_deps wraps the same lock check, and mix xref operates on modules and files with no notion of which dependency they belong to.

What

scripts/check_unused_deps.exs, wired into the lint alias and CI as mix deps.unused:

  1. Resolve each declared dependency to its modules via its application spec.
  2. Collect every module reference in Backpex's own BEAM files — both the imports table (remote calls) and literal atoms (behaviours, structs, module names passed as values), which keeps false positives low.
  3. Report any dependency with zero referenced modules, and exit non-zero.

Dev/test-only tooling is skipped: it is never part of what a Backpex user resolves.

$ mix deps.unused
Checked 10 dependencies, all are referenced.
Allowlisted: phoenix_ecto — Provides protocol implementations (Phoenix.HTML.FormData for Ecto.Changeset) that are resolved at runtime and are never referenced by name.

What it found immediately

Two more instances of the same class of problem:

Dependency Finding Action
jason Appears nowhere outside mix.exs:54. Never used in lib/, not in git history either. Removed
ecto_sql Only Ecto.Query / Ecto.Changeset / Ecto.Repo are used — those live in :ecto. The only use Ecto.Migration hits are inside @moduledoc examples. Narrowed to {:ecto, "~> 3.6"}

phoenix_ecto is allowlisted with a reason (protocol implementations, resolved at runtime, never referenced by name). The allowlist is printed on every run — an exemption nobody sees is how an unused dependency hides in the first place.

Note on ecto_sql

This is the one user-facing change here: Backpex no longer pulls in ecto_sql, only ecto. Host applications bring their own SQL adapter (the demo declares ecto_sql itself), so in practice every Phoenix + Ecto app already has it. Flagging it explicitly in case you would rather keep ecto_sql declared and allowlist it instead — happy to switch.

Verification

  • mix deps.unused is green on this branch (10 dependencies checked, 1 allowlisted).
  • Verified it actually fails: re-adding an unused dependency makes it exit 1 with the offending name. (number itself can no longer serve as the fixture — it is now uninstallable against the locked decimal 3.1.1, which is precisely the conflict Remove unused number dependency to unblock decimal 3.x #2113 removed. castore was used instead.)
  • mix lint passes end to end (compile --warning-as-errors, deps.unlock --unused, deps.unused, format, credo, 100 doctests + 174 tests, 0 failures).
  • mix credo --strict, mix format --check-formatted and mix docs --warnings-as-errors pass; the demo still compiles against the narrowed dependency.

Scope

Backpex only. The demo has plenty of legitimately unreferenced runtime dependencies (bandit, phoenix_live_reload, …), so the check would be noisy there without value — the published package is where a stale dependency actually hurts users.

mix deps.unlock --check-unused only compares mix.lock against mix.exs, so a
dependency that is declared but never called looks perfectly used to it. That
blind spot let :number sit unused from 71757de (2024-08-26) until it started
blocking decimal 3.x, and with it ecto_sql, about two years later.

Nothing off the shelf covers this: ex_check's :unused_deps wraps the same lock
check, and mix xref works on modules and files without a notion of which
dependency they belong to.

Add scripts/check_unused_deps.exs, which resolves each declared dependency to
its modules via the application spec and matches them against the module
references in Backpex's own BEAM files, using both the imports table and
literal atoms so that behaviours and structs count as usage. Wire it into the
lint alias and CI as "mix deps.unused".

Running it surfaced two more instances of the same class of problem:

  * jason was declared but appears nowhere outside mix.exs. Removed.
  * ecto_sql was declared, but only Ecto.Query/Changeset/Repo are used, which
    live in :ecto. Narrowed to {:ecto, "~> 3.6"}; host applications bring their
    own SQL adapter.

phoenix_ecto is allowlisted with a reason: it provides protocol
implementations that are resolved at runtime and never referenced by name. The
allowlist is printed on every run, since an exemption nobody sees is how an
unused dependency hides in the first place.

Also add scripts/*.exs to the formatter inputs so the check stays formatted.
@Flo0807

Flo0807 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator Author

Closing for now — the two findings this check surfaced are being handled as dedicated PRs first (#2115 removes jason, #2116 switches ecto_sql to ecto). The check itself will come back as its own PR once those have landed, at which point it can be scoped purely to the tooling and will be green against develop.

The branch feature/detect-unused-deps is kept, nothing is lost.

@Flo0807 Flo0807 closed this Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant