Summary
mix reach.check --dead-code reports compile-time DSL macros as "result unused" — most notably
Phoenix.Component.attr/3, slot/3, and Phoenix.Router.{scope, pipe_through, get, post, ...}. These calls
don't produce a value to consume; they register attributes/routes at compile time. The dead-code pass
appears to treat any top-level call with a discarded return as dead, regardless of macro semantics.
Minimal repro
defmodule ReproComponent do
use Phoenix.Component
attr :id, :string, required: true # line 4 — flagged
attr :label, :string, default: "" # line 5 — flagged
slot :inner_block, required: true # line 6 — flagged
def card(assigns) do
~H"""
<div id={@id}>{@label}{render_slot(@inner_block)}</div>
"""
end
end
defmodule ReproRouter do
use Phoenix.Router
pipeline :browser do
plug :accepts, ["html"]
end
scope "/" do # line 21 — flagged
pipe_through :browser # line 22 — flagged
get "/health", ReproController, :health # line 23 — flagged
end
end
mix reach.check --dead-code output:
Dead code
─────────
lib/repro_component.ex:4 attr result unused ⚠
lib/repro_component.ex:5 attr result unused ⚠
lib/repro_component.ex:6 slot result unused ⚠
lib/repro_router.ex:21 scope result unused ⚠
lib/repro_router.ex:22 pipe_through result unused ⚠
lib/repro_router.ex:23 get result unused ⚠
None of these are dead — they all have compile-time side effects (attribute registration, route
registration). The same noise appears for every component file and the router.
Environment
- reach 2.3.0
- Elixir 1.19.5
- phoenix_live_view ~> 1.0
Impact
Low severity per item, but high volume: in a real Phoenix app this category dominates the dead-code output.
In our codebase it produces hundreds of findings, drowning out genuine unused-result calls (e.g.
Gettext.put_locale/1, Zelo.Auth.Cache.renew/1 where the result actually should be checked or ignored
explicitly).
Possible cause (speculation — haven't read reach source yet)
Two angles:
- Top-level module-body expressions are always "result unused" by definition (there's nothing to bind to),
so flagging them is structurally meaningless — could just be skipped.
- Even inside do blocks (e.g. inside scope/2), known compile-time DSLs like Phoenix.Component.attr/3,
Phoenix.Router.*, Ecto.Schema.field/2, etc. need an allowlist or a "treat as side-effecting" annotation.
Either fix would clean up the bulk of the false positives. A user-configurable dead_code: [ignore_calls:
[...]] in .reach.exs would also work as an escape hatch.
Happy to PR a fix if you point me at the relevant analyzer pass.
Summary
mix reach.check --dead-code reports compile-time DSL macros as "result unused" — most notably
Phoenix.Component.attr/3, slot/3, and Phoenix.Router.{scope, pipe_through, get, post, ...}. These calls
don't produce a value to consume; they register attributes/routes at compile time. The dead-code pass
appears to treat any top-level call with a discarded return as dead, regardless of macro semantics.
Minimal repro
defmodule ReproComponent do
use Phoenix.Component
end
defmodule ReproRouter do
use Phoenix.Router
end
mix reach.check --dead-code output:
Dead code
─────────
lib/repro_component.ex:4 attr result unused ⚠
lib/repro_component.ex:5 attr result unused ⚠
lib/repro_component.ex:6 slot result unused ⚠
lib/repro_router.ex:21 scope result unused ⚠
lib/repro_router.ex:22 pipe_through result unused ⚠
lib/repro_router.ex:23 get result unused ⚠
None of these are dead — they all have compile-time side effects (attribute registration, route
registration). The same noise appears for every component file and the router.
Environment
Impact
Low severity per item, but high volume: in a real Phoenix app this category dominates the dead-code output.
In our codebase it produces hundreds of findings, drowning out genuine unused-result calls (e.g.
Gettext.put_locale/1, Zelo.Auth.Cache.renew/1 where the result actually should be checked or ignored
explicitly).
Possible cause (speculation — haven't read reach source yet)
Two angles:
so flagging them is structurally meaningless — could just be skipped.
Phoenix.Router.*, Ecto.Schema.field/2, etc. need an allowlist or a "treat as side-effecting" annotation.
Either fix would clean up the bulk of the false positives. A user-configurable dead_code: [ignore_calls:
[...]] in .reach.exs would also work as an escape hatch.
Happy to PR a fix if you point me at the relevant analyzer pass.