Skip to content

Dim the code that cannot be reached - #430

Open
petrovo-as wants to merge 1 commit into
PHPantom-dev:mainfrom
petrovo-as:diagnostics-unreachable-code
Open

Dim the code that cannot be reached#430
petrovo-as wants to merge 1 commit into
PHPantom-dev:mainfrom
petrovo-as:diagnostics-unreachable-code

Conversation

@petrovo-as

@petrovo-as petrovo-as commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Note

I am not a Rust developer. This was written with AI assistance and reviewed line by line, including two rounds of adversarial review that between them caught two false positives around goto: one where a jump was treated as leaving the block it stayed inside, and one where a return the jump skipped over dimmed the live code after that block. Please read it as a proposal rather than as finished work.

The problem

A statement after one that always leaves the block never runs, and nothing said so. This is D6 in the backlog, which the change implements and removes.

The change

return, throw, exit, die, continue, break, and an if whose every branch does one of those all end a block. An if without an else never does, since a path through it always falls through.

Reported as a Hint tagged Unnecessary, per D6, so editors grey the text out rather than underlining it. The check reads the shape of the statement list and nothing else, which keeps it in the fast phase.

Two things only look dead. A declaration at the top level of a file is hoisted and holds wherever it sits, while the same declaration inside a function body is created by running the statement and after a return never comes into being — they are told apart by where they sit, not by what they are. A goto label is an entry point that ends the dead run. Either can sit in the middle of otherwise dead code, so one block may hold several runs, each reported on its own.

Not included

A call to a function declared never also ends a block. Recognising it needs the type engine, which would move the check into the expensive phase to catch a case the reader can already see.

Two gaps are filed rather than fixed, as D24 and D25: the "Remove unreachable code" action reads PHPStan's diagnostics only and deletes to the next closing brace instead of using the reported range, so wiring the native code to it needs more than adding the code; and a braced namespace or declare body is treated as a fresh statement list, so reachability neither flows in nor out.

Testing

36 integration tests in diagnostics_unreachable_code.rs, asserting on the text that was dimmed rather than on line numbers. Demos added to examples/php/diagnostics.php. Full suite, clippy --all-targets -D warnings, and fmt --check pass.

Checklist

If applicable:

  • I have updated CHANGELOG.md
  • I have updated the documentation (README.md, docs/, examples/)
  • I have updated the config schema (config-schema.json)
  • I have added/updated tests to cover my changes
  • I fully understand the code that I am submitting (what it does,
    how it works, how it's organized), including any code drafted by an LLM.
  • For any prose generated by an LLM, I have proof-read and copy-edited with
    an eye towards deleting anything that is irrelevant, clarifying anything
    that is confusing, and adding details that are relevant. This includes,
    for example, commit descriptions, PR descriptions, and code comments.

🤖 Generated with Claude Code

@petrovo-as
petrovo-as force-pushed the diagnostics-unreachable-code branch from 5aca710 to b23db4f Compare September 8, 2026 14:40
@codecov-commenter

codecov-commenter commented Sep 8, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 98.19005% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/diagnostics/unreachable_code.rs 98.17% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

A statement after one that always leaves the block never runs, and
nothing said so. `return`, `throw`, `exit`, `die`, `continue`, `break`,
and an `if` whose every branch does one of those all end a block; an
`if` without an `else` never does, since a path through it always falls
through.

Reported as a Hint tagged `Unnecessary`, so editors grey the text out
rather than underlining it — dead code is tidying, not a defect, and it
is rendered the way an unused import already is. The check reads the
shape of the statement list and nothing else, which keeps it in the
fast phase alongside the syntax and unused-symbol checks.

Two things only look dead. A declaration at the top level of a file is
hoisted, so it holds whether or not control reaches the line it is
written on, while the same declaration inside a function body is
created by running the statement and after a `return` never comes into
being at all — the two are told apart by where they sit rather than by
what they are. A `goto` label is an entry point, so it ends the dead
run rather than being swallowed by it. Either can sit in the middle of
otherwise dead code, so one block may hold several runs and each is
reported on its own.

A call to a function declared `never` also ends a block. Recognising it
takes the type engine, which would move this check into the expensive
phase to catch a case the reader can already see, so it is left to the
narrowing code that already has the types in hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@petrovo-as
petrovo-as force-pushed the diagnostics-unreachable-code branch from b23db4f to c65dfc6 Compare September 8, 2026 14:58
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.

2 participants