Respect @throws void on getIterator() when determining foreach Traversable throw points#5666
Conversation
VincentLanglet
left a comment
There was a problem hiding this comment.
Merge the two bug-6833 fixtures files into a single one
VincentLanglet
left a comment
There was a problem hiding this comment.
Add test for array|IteratorAggregate where IteratorAggregate throws void or throws exception ; since iterating an array does not produce exception it should only be impacted by IteratorAggregate throws type.
|
Done. Merged the two separate bug-6833 test fixture files ( |
|
Pushed. Here's what was done to address VincentLanglet's review: Implementation fix ( Tests added (3 new cases in
Each test includes both |
VincentLanglet
left a comment
There was a problem hiding this comment.
Solve
FILE: tests/PHPStan/Analyser/NodeScopeResolverTest.php
--------------------------------------------------------------------------------
FOUND 1 ERROR AFFECTING 1 LINE
--------------------------------------------------------------------------------
288 | ERROR | [x] Use early exit to reduce code nesting.
| | (SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed)
--------------------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
--------------------------------------------------------------------------------
|
Done. Replaced the |
|
Please check again with the new changes @staabm |
…aversable throw points - Extract `getTraversableForeachThrowPoint()` from inline implicit throw point creation in the `Foreach_` handler of `NodeScopeResolver` - For `IteratorAggregate` types, check `getIterator()` throw type: - `@throws void` → no throw point (iteration init cannot throw) - Explicit `@throws SomeException` → explicit throw point with that type - No annotation → fall through to implicit throw point - Also respect the `implicitThrows` config parameter, which was previously ignored for the foreach Traversable throw point (unlike all other implicit throw points in the codebase) - Probed analogous cases: direct `Iterator` types, union types, array foreach, `@throws` with non-matching catch types — all behave correctly
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ection Decompose union types to check each member individually. Non-Traversable members (like arrays) are skipped. IteratorAggregate members have their getIterator() @throws annotation checked. This ensures @throws void is respected even when the iteratee is a union like array|IteratorAggregate. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…each throw point detection Replace manual union decomposition (`$exprType instanceof UnionType ? $exprType->getTypes() : [...]`) with `TypeCombinator::intersect($exprType, $traversableType)` to extract the Traversable part, then check if it's entirely IteratorAggregate via `isSuperTypeOf`. This follows the codebase convention of never using `instanceof` to check types and lets the type system handle union/intersection decomposition internally. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The implicitThrows config should only gate the no-annotation case for IteratorAggregate types. For non-IteratorAggregate Traversables, the implicit throw point should always be added since there is no getIterator() to inspect. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The test file belongs in tests/PHPStan/Analyser/nsrt/ since it contains assertVariableCertainty() calls. The nsrt directory auto-discovers test files, removing the need for the manual yield and PHP version guard in NodeScopeResolverTest. The DefinedVariableRuleTest path is updated to reference the new location. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
880195f to
b2409b8
Compare
Summary
When iterating over an
IteratorAggregatewithforeachinside atryblock, PHPStan adds an implicit throw point representing the possibility that the iteration mechanism (particularlygetIterator()) could throw. This throw point uses the pre-foreach scope (without the loop variable), causing the loop variable to be reported as "might not be defined" incatchblocks.The issue is that this implicit throw point was added unconditionally — it ignored both
@throws voidannotations ongetIterator()and theexceptions.implicitThrowsconfiguration parameter. This meant users had no way to suppress the false positive even when they explicitly annotatedgetIterator()with@throws void.Changes
getTraversableForeachThrowPoint()method insrc/Analyser/NodeScopeResolver.phpthat replaces the inline implicit throw point creation forforeachover Traversable typesIteratorAggregatetypes, the method checksgetIterator()'s@throwsannotation:@throws void→ returnsnull(no throw point, iteration init cannot throw)@throws SomeException→ returns an explicit throw point with that typeimplicitThrowsconfiguration parameter, which was previously bypassed for the foreach Traversable throw point (unlike all other implicit throw points inNodeScopeResolver,MethodThrowPointHelper, andFuncCallHandler)use IteratorAggregate;importRoot cause
The foreach Traversable implicit throw point at line 1440-1442 was added unconditionally:
This pattern differs from how all other method call throw points work (via
MethodThrowPointHelper), which check@throwsannotations and theimplicitThrowsconfig. The fix brings the foreach Traversable throw point in line with the established pattern.Test
Rule test (
tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php::testBug6833)Test data file
tests/PHPStan/Rules/Variables/data/bug-6833.phpcovers:@throws voidongetIterator()— no "might not be defined" error (the fix)@throwsannotation — "might not be defined" error (correct,getIterator()could throw)@throws \RuntimeExceptionmatching\Throwablecatch — error (correct, explicit throw type matches)@throws \RuntimeExceptionwith\LogicExceptioncatch — no error (correct, throw type doesn't match catch)getIterator())NSRT test (
tests/PHPStan/Analyser/nsrt/bug-6833.php)Verifies variable certainty directly with
assertVariableCertainty():@throws void→TrinaryLogic::createYes()in catch scopeTrinaryLogic::createMaybe()in catch scopeTrinaryLogic::createMaybe()in catch scopeTrinaryLogic::createYes()in catch scopeFixes phpstan/phpstan#6833