Skip to content

Respect @throws void on getIterator() when determining foreach Traversable throw points#5666

Open
phpstan-bot wants to merge 1 commit into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-t0v6vom
Open

Respect @throws void on getIterator() when determining foreach Traversable throw points#5666
phpstan-bot wants to merge 1 commit into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-t0v6vom

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When iterating over an IteratorAggregate with foreach inside a try block, PHPStan adds an implicit throw point representing the possibility that the iteration mechanism (particularly getIterator()) 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" in catch blocks.

The issue is that this implicit throw point was added unconditionally — it ignored both @throws void annotations on getIterator() and the exceptions.implicitThrows configuration parameter. This meant users had no way to suppress the false positive even when they explicitly annotated getIterator() with @throws void.

Changes

  • Extracted a new getTraversableForeachThrowPoint() method in src/Analyser/NodeScopeResolver.php that replaces the inline implicit throw point creation for foreach over Traversable types
  • For IteratorAggregate types, the method checks getIterator()'s @throws annotation:
    • @throws void → returns null (no throw point, iteration init cannot throw)
    • Explicit @throws SomeException → returns an explicit throw point with that type
    • No annotation → falls through to implicit throw point handling
  • The method also respects the implicitThrows configuration parameter, which was previously bypassed for the foreach Traversable throw point (unlike all other implicit throw points in NodeScopeResolver, MethodThrowPointHelper, and FuncCallHandler)
  • Added use IteratorAggregate; import

Root cause

The foreach Traversable implicit throw point at line 1440-1442 was added unconditionally:

if (!(new ObjectType(Traversable::class))->isSuperTypeOf($scope->getType($stmt->expr))->no()) {
    $throwPoints[] = InternalThrowPoint::createImplicit($scope, $stmt->expr);
}

This pattern differs from how all other method call throw points work (via MethodThrowPointHelper), which check @throws annotations and the implicitThrows config. 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.php covers:

  1. @throws void on getIterator() — no "might not be defined" error (the fix)
  2. No @throws annotation — "might not be defined" error (correct, getIterator() could throw)
  3. @throws \RuntimeException matching \Throwable catch — error (correct, explicit throw type matches)
  4. @throws \RuntimeException with \LogicException catch — no error (correct, throw type doesn't match catch)
  5. Array foreach — no error (correct, arrays don't call getIterator())

NSRT test (tests/PHPStan/Analyser/nsrt/bug-6833.php)

Verifies variable certainty directly with assertVariableCertainty():

  • @throws voidTrinaryLogic::createYes() in catch scope
  • No annotation → TrinaryLogic::createMaybe() in catch scope
  • Explicit throws → TrinaryLogic::createMaybe() in catch scope
  • Array foreach → TrinaryLogic::createYes() in catch scope

Fixes phpstan/phpstan#6833

…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
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