fix: respawn on same-file reflection staleness and post-boot classes#5
Open
fdaviddpt wants to merge 1 commit into
Open
fix: respawn on same-file reflection staleness and post-boot classes#5fdaviddpt wants to merge 1 commit into
fdaviddpt wants to merge 1 commit into
Conversation
0.6.0 excluded the analysis target from the staleness check, reasoning that phpstan re-reads the analysed file. That holds for the file's AST and not for its reflection. Editing a class-level @extends generic left the worker resolving $this->options against the shape captured at first analysis, so a correct fix kept reporting the error it had just fixed — and a newly introduced shape error would have been missed just as silently. The target is now fingerprinted by structure: declarations, signatures, property types and docblocks, with function bodies stripped. A changed fingerprint respawns the worker; a body edit does not, so the edit-check-edit loop on one file stays warm (measured 0.0s warm, ~6s only when the structure actually moves). Second bug, same root: the worker's analysable file set is fixed at boot, so a class created afterwards reads as "unknown class" for a file that a cold run resolves fine, with phantom errors at stale line numbers on top. Content alone cannot separate that from a genuinely missing class, so a result containing class.notFound now buys one respawn and reports the fresh worker's verdict. Third, found while building the end-to-end harness: findPhpstanBin resolved phpstan from the server's own vendor directory, so a global install analyses with the wrong phpstan — no project extensions, no custom rules, far too few errors, and no complaint. MCP_PHPSTAN_PHPSTAN_BIN now takes precedence and fails loudly on a bad path. Verified against a real project (~14k PHP files): both failures reproduce on 0.6.0 and are clean after, with a control proving the harness actually analyses — the first version of it reported CLEAN for everything because it was silently using the wrong phpstan. Co-Authored-By: Max <noreply>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What broke
Two ways a warm worker served answers that disagreed with a cold
phpstanrun on the same file. Both were found from the consumer side — the validator kept reporting errors I had just fixed, and a fresh run said PASS.1. Same-file reflection staleness. 0.6.0 deliberately excluded the analysis target from the staleness check:
That is true of the AST and false of the reflection. After editing a class-level
@extendsgeneric, the worker kept resolving$this->optionsagainst the shape captured at first analysis — so a correct fix kept reporting the error it had just fixed. The symmetric case is worse and silent: a newly introduced shape error would not be reported at all.2. Classes created after boot are invisible. The worker's analysable file set is fixed at boot, so a class added afterwards reads as
extends unknown class Foofor a file sitting on disk that a cold run resolves fine — with phantom errors at stale line numbers on top.The fix
Structural fingerprint for the target. Declarations, signatures, property types and docblocks, with function bodies stripped (
token_get_all, skipping from a function's{to its matching}). A changed fingerprint respawns the worker. A body edit leaves it identical, so the edit-check-edit loop on one file stays warm — which is the whole point of the package.One respawn on
class.notFound. Content alone cannot distinguish "created after boot" from "genuinely missing", so pay one respawn and report the fresh worker's verdict. Bounded to a single retry per call.MCP_PHPSTAN_PHPSTAN_BIN. Found while building the harness, and a real bug on its own:findPhpstanBin()resolved phpstan from the server's vendor directory, so a global install analyses a project with the wrong phpstan — none of the project's extensions or custom rules, far fewer errors, no complaint.Evidence
Against a real project (~14k PHP files), same config and paths, one warm server per run:
class.notFound×2 + phantomreturn.missingoffsetAccess.notFound— still caught@extendsgenericWarm-path cost, measured on the same server:
A note on the harness. Its first version reported CLEAN for every scenario, which looked like a total fix. It was a broken harness: the dev-repo server resolved its own phpstan, which loads none of the project's extensions, so it analysed almost nothing. A control case — a blatant
return 42;in a: stringmethod — caught it. That control is whyMCP_PHPSTAN_PHPSTAN_BINexists in this PR, and it is worth keeping in mind for any future benchmark here: a warm-process server that quietly does less work looks exactly like a fast one.Tests
11 new unit cases in
PhpstanRunnerStalenessTest— body-only edits keep the fingerprint stable; class-level generics, method signatures and property types change it; formatting and line comments do not; a bodyless abstract method does not derail the token scan; plus the binary-override contract. Full suite 25/25, including the 0.6.0 cross-file regression test.