Avoid dead catch false positive for inconsistently overridden trait methods - #6199
Avoid dead catch false positive for inconsistently overridden trait methods#6199peter17 wants to merge 1 commit into
Conversation
|
Hi again @staabm here is a proposal to fix phpstan/phpstan#10315 |
|
@staabm any comment on this? Thanks! |
|
Hey, thanks for the PR. I will come back to this PR when time allows. there is quite a bit of work in my queue atm |
|
@SanderMuller please review |
|
Reviewed at Four things, one of which I would want resolved before merge. 1. 2. Trait errors lose their "in context of class" decoration. Routing through the collector means a dead catch inside a trait is now reported once on the trait instead of once per using class: Consistent with how isset/empty already behave, and arguably nicer (no duplicates for a trait used by twenty classes) - but it is user-visible, and baseline entries that carry 3. The collector key is built from $key = sprintf('%s:%d', $node->getOriginalCaughtType()->describe(VerbosityLevel::typeOnly()), $node->getOriginalNode()->getStartLine());
4. Only one of the two new tests is a regression test. CI, worth a look before merge. Otherwise: full suite 21343 green, self-analysis clean, phpcs clean on the four changed files, and the branch is up to date with |
|
Thanks — this was a very useful review. All four addressed; 1 and 3 in code. 1. 3. No more 2 + 3, now pinned by a test. There was no coverage of a dead catch in a trait at all, so both the new reporting shape and the key granularity could change unnoticed. Added
The second case discriminates the key granularity: collapsing 4. Which test guards the fix. Confirmed, and it still holds after the restructure: suppressing the new node's emission in CI. I could not attribute the old-PHPUnit failure to the diff either, and after this restructure the analyser-side change is a strictly additive node emission behind Verification after the changes: 122 tests in I updated the PR description above. |
|
Checked
Gates on my side: 122 tests in Non-blocking note for later: in the dead path the unchecked-exception early return happens before the trait bookkeeping, so that case emits no verdict at all while the alive path always emits one. Harmless today - a lone "no error" group reports nothing, which matches current behaviour - but if the emit ever moves above that return, the two paths would start disagreeing about a catch neither of them reports. |
|
Can this be merged? Thanks 😄 |
…ethods A trait's try/catch can be dead in the context of one class using the trait and alive in another, e.g. when it depends on whether an abstract method gets overridden without throwing. Apply the same ConstantConditionInTraitHelper mechanism already used for isset/empty/?? to CatchWithUnthrownExceptionRule, so disagreeing verdicts across classes using the trait suppress the error instead of reporting it. Closes phpstan/phpstan#10315
|
I rebased, but with significant changes: So I had to apply some of the the PR's changes to |
|
Reviewed the rebase at
Re-ran the behavioural checks on the new base rather than assuming the rebase was inert:
The 11 red checks are all base fallout, none of them yours. #6246, an unrelated PR sharing this base, fails the same cluster - and its Rector job fails on exactly the same two tests ( |
SanderMuller
left a comment
There was a problem hiding this comment.
Approving. The rebase is faithful and I closed the last two questions I had, both of which the test suite structurally cannot answer.
The rebase itself: ten of the eleven files are byte-identical to 7d163030b, the revision I verified before the split, so that verification carries over. The relocated block in TryCatchHandler keeps the same $matched then isInTrait() gate, the same node and the same position before the continue; CatchWithUnthrownExceptionNode is still byte-identical to 2.2.x; and TryCatchHandler is not #[ShadowedByTurboExtension], so nothing native follows. Re-ran the behavioural checks on the new base: fixture still exactly 36 and 67, the line-only key still makes the line 67 error vanish, and suppressing the emission still fails AbilityToDisableImplicitThrowsTest::testBug10315 while the other stays green.
Does the rule run outside the test harness? Your tests build CatchWithThrownExceptionInTraitRule by hand in a CompositeRule, so a green suite says nothing about the DI registration. Instrumented a real analysis of a symfony + doctrine vendor tree (3862 files): the rule is registered and invoked 43 times.
Do the verdicts survive a warm run? This is the one that worried me, because the mechanism needs verdicts from every using class in one run and RuleTestCase never touches the result cache - a warm-run-only false positive would escape the suite entirely. They are persisted: 43 collector entries in the cache file, for +7,779 bytes (+0.012%) on that corpus.
One discrepancy, resolved: the handler emits 52 nodes for 43 rule invocations, which looked like dropped verdicts. Logging the occurrence at both ends shows 20 distinct occurrences on each side and an empty difference - the extra emissions are repeats of the same catches from re-analysis passes, so no unique verdict is lost.
Gates: 122 tests in tests/PHPStan/Rules/Exceptions, full suite 21146, self-analysis clean, phpcs clean on all six changed source files.
CI: the eleven reds are all base fallout, not yours. #6246, an unrelated PR on this base, fails the same cluster with the same two Rector tests; the doctrine make phpstan jobs fail on Call to deprecated method toMutatingScope(); Test (PHP 7.4) / Test (PHP 8.5) are Benchmark against stale baselines. Also worth knowing for anyone comparing corpora here: this corpus reports 17685 or 17682 errors depending on the run, with FormErrorIterator generics flipping 6/3/6 across three identical cold runs - pre-existing non-determinism, unrelated to this PR.
A trait's try/catch can be dead in the context of one class using the trait and alive in another, e.g. when it depends on whether an abstract method gets overridden without throwing. Apply the same ConstantConditionInTraitHelper mechanism already used for isset/empty/?? to CatchWithUnthrownExceptionRule, so disagreeing verdicts across classes using the trait suppress the error instead of reporting it.
Closes phpstan/phpstan#10315
Edit: routing trait catches through the collector changes where they are reported. A dead catch inside a trait used to be reported once per class using the trait, decorated with
(in context of class …); it is now reported once on the trait itself:before
after
This matches how isset/empty/
??in traits already behave and removes the duplication for a trait used by many classes, but it is user-visible: baseline entries carrying(in context of class …)forcatch.neverThrownwill stop matching and need regenerating. Worth a release-notes line.