Skip to content

Fix phpstan/phpstan#13920: Anonymous class causes "variable might not be defined" issue#5205

Merged
staabm merged 3 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-oi90985
Mar 17, 2026
Merged

Fix phpstan/phpstan#13920: Anonymous class causes "variable might not be defined" issue#5205
staabm merged 3 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-oi90985

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

When an anonymous class with an explicit constructor calling parent::__construct() was used inside a try block, PHPStan incorrectly reported that variables defined before the try block "might not be defined" in the finally block.

Changes

  • Modified src/Analyser/ExprHandler/NewHandler.php to replace the scope on throw points extracted from the anonymous class constructor's statement result with the outer scope (the scope at the new expression site)
  • Added replaceScope() method to src/Analyser/InternalThrowPoint.php
  • Added regression test in tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php and tests/PHPStan/Rules/Variables/data/bug-13920.php

Root cause

When processing new class() extends Foo { public function __construct() { parent::__construct(); } }, the NewHandler extracted throw points from the constructor's body analysis. These throw points carried scopes from within the constructor method, which naturally didn't contain the outer method's variables. When the try/finally handling merged these throw point scopes into the finallyScope via mergeWith(), any outer variable present in the finally scope but absent from the inner constructor scope was downgraded from "definitely defined" (Yes certainty) to "might not be defined" (Maybe certainty).

The fix replaces the inner scope on these throw points with the outer $scope at the call site, matching the behavior already used for inherited constructors (the else branch at line 196, which uses getConstructorThrowPoint() with the outer scope).

Test

Added a regression test that verifies no errors are reported when a variable is defined before a try block containing an anonymous class with parent::__construct(), and that variable is used in the finally block.

Fixes phpstan/phpstan#13920

- Throw points from anonymous class constructor body carried inner method scopes
  instead of the outer scope, causing variables defined before try blocks to be
  reported as "might not be defined" in finally blocks
- Added replaceScope() method to InternalThrowPoint to allow scope replacement
- New regression test in tests/PHPStan/Rules/Variables/data/bug-13920.php

Closes phpstan/phpstan#13920
@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-oi90985 branch from 7b4cead to 4afa331 Compare March 14, 2026 09:07
@VincentLanglet VincentLanglet requested a review from staabm March 14, 2026 09:10
@phpstan-bot
Copy link
Collaborator Author

The branch is already up to date with the remote. Everything looks good — tests pass, static analysis passes, the review is approved, and the code is pushed.

}, StatementContext::createTopLevel());
if ($constructorResult !== null) {
$throwPoints = array_map(static fn (ThrowPoint $point) => InternalThrowPoint::createFromPublic($point), $constructorResult->getStatementResult()->getThrowPoints());
$throwPoints = array_map(static fn (ThrowPoint $point): InternalThrowPoint => InternalThrowPoint::createFromPublic($point)->replaceScope($scope), $constructorResult->getStatementResult()->getThrowPoints());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems to be the only call-site where we use InternalThrowPoint::createFromPublic.

instead of adding replaceScope I think we can just add a Scope $scope parameter to InternalThrowPoint::createFromPublic - we don't need to look for backwards compatibility, because InternalThrowPoint is not annotated @api.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I addressed staabm's review by:

  • Adding an optional ?MutatingScope $scope = null parameter to InternalThrowPoint::createFromPublic()
  • Removing the replaceScope() method
  • Updating the call site in NewHandler.php to pass $scope directly to createFromPublic()

Tests and static analysis both pass.

…f replaceScope

Move the scope replacement into createFromPublic() as an optional parameter,
removing the separate replaceScope() method as suggested in code review.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
}

public static function createFromPublic(ThrowPoint $throwPoint): self
public static function createFromPublic(ThrowPoint $throwPoint, ?MutatingScope $scope = null): self
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the $scope parameter is not optional and cannot be null.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already handled — all 11,571 tests passed, and the fix has been committed and pushed.

…f replaceScope

Make the $scope parameter required (non-nullable) since it is always
provided at the only call site.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Collaborator Author

The test passes. The PR is already in the approved state with all review feedback addressed. There's nothing more to change — the code correctly uses a required MutatingScope $scope parameter on createFromPublic() as staabm requested.

@staabm staabm merged commit 7861fe4 into phpstan:2.1.x Mar 17, 2026
651 of 653 checks passed
@staabm staabm deleted the create-pull-request/patch-oi90985 branch March 17, 2026 18:46
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.

3 participants