From ec44d068e484fdb7085e0e6eea6ea4fc3f1dccc1 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:33:27 +0000 Subject: [PATCH 1/3] Fix generic type inference for new assigned to property via ??= - Added handling for AssignOp\Coalesce in NewAssignedToPropertyVisitor - The visitor now marks `new` expressions in `$this->prop ??= new Foo()` with the property attribute, so MutatingScope::exactInstantiation() can infer generic type parameters from the property's declared type - New regression test in tests/PHPStan/Rules/Properties/data/bug-12250.php Closes https://github.com/phpstan/phpstan/issues/12250 --- src/Parser/NewAssignedToPropertyVisitor.php | 2 +- .../TypesAssignedToPropertiesRuleTest.php | 6 ++++++ .../PHPStan/Rules/Properties/data/bug-12250.php | 17 +++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Properties/data/bug-12250.php diff --git a/src/Parser/NewAssignedToPropertyVisitor.php b/src/Parser/NewAssignedToPropertyVisitor.php index 60fd8fd72e..4eb7b93bff 100644 --- a/src/Parser/NewAssignedToPropertyVisitor.php +++ b/src/Parser/NewAssignedToPropertyVisitor.php @@ -16,7 +16,7 @@ final class NewAssignedToPropertyVisitor extends NodeVisitorAbstract #[Override] public function enterNode(Node $node): ?Node { - if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignRef) { + if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignRef || $node instanceof Node\Expr\AssignOp\Coalesce) { if ( ($node->var instanceof Node\Expr\PropertyFetch || $node->var instanceof Node\Expr\StaticPropertyFetch) && $node->expr instanceof Node\Expr\New_ diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 5f3578f946..7d34042dc4 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -1019,4 +1019,10 @@ public function testCloneWith(): void ]); } + #[RequiresPhp('>= 8.0')] + public function testBug12250(): void + { + $this->analyse([__DIR__ . '/data/bug-12250.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-12250.php b/tests/PHPStan/Rules/Properties/data/bug-12250.php new file mode 100644 index 0000000000..4c0ebea53e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-12250.php @@ -0,0 +1,17 @@ += 8.0 + +namespace Bug12250; + +class HelloWorld +{ + /** + * @var \WeakMap<\stdClass, \stdClass> + */ + protected \WeakMap $bug, $ok; + + public function bug(): void + { + $this->bug ??= new \WeakMap(); + $this->ok = new \WeakMap(); + } +} From b40d11319c84f2d8ef20569f4805092f71770eb9 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 16 Feb 2026 20:43:09 +0000 Subject: [PATCH 2/3] Add regression test for #4525 Closes https://github.com/phpstan/phpstan/issues/4525 --- .../TypesAssignedToPropertiesRuleTest.php | 5 ++++ .../Rules/Properties/data/bug-4525.php | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 tests/PHPStan/Rules/Properties/data/bug-4525.php diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 7d34042dc4..cf7136dd1f 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -1025,4 +1025,9 @@ public function testBug12250(): void $this->analyse([__DIR__ . '/data/bug-12250.php'], []); } + public function testBug4525(): void + { + $this->analyse([__DIR__ . '/data/bug-4525.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-4525.php b/tests/PHPStan/Rules/Properties/data/bug-4525.php new file mode 100644 index 0000000000..3441b25c88 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-4525.php @@ -0,0 +1,24 @@ += 7.4 + +namespace Bug4525; + +use SplObjectStorage; + +class HelloWorld +{ + /** + * @var SplObjectStorage<\DateTime, \DateTimeImmutable> + */ + private SplObjectStorage $map; + + public function sayHello(): void + { + $this->map = new SplObjectStorage(); + } + + /** @phpstan-return SplObjectStorage<\DateTime, \DateTimeImmutable> */ + public function getMap(): SplObjectStorage + { + return $this->map ??= new SplObjectStorage(); + } +} From 962b6d6a6abcc77729e277f50d8e2b860646e1a2 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 19 Feb 2026 14:21:43 +0100 Subject: [PATCH 3/3] Add similar behavior for all Assign nodes --- src/Parser/NewAssignedToPropertyVisitor.php | 2 +- src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php | 6 +++- .../PhpDoc/InvalidPhpDocTagValueRule.php | 6 +++- .../PhpDoc/WrongVariableNameInVarTagRule.php | 6 +++- .../PhpDoc/InvalidPHPStanDocTagRuleTest.php | 10 ++++++ .../PhpDoc/InvalidPhpDocTagValueRuleTest.php | 10 ++++++ .../WrongVariableNameInVarTagRuleTest.php | 22 +++++++++++++ .../data/invalid-phpdoc-assign-operator.php | 11 +++++++ .../invalid-phpstan-doc-assign-operator.php | 12 +++++++ .../wrong-variable-name-var-assign-op.php | 31 +++++++++++++++++++ 10 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-assign-operator.php create mode 100644 tests/PHPStan/Rules/PhpDoc/data/invalid-phpstan-doc-assign-operator.php create mode 100644 tests/PHPStan/Rules/PhpDoc/data/wrong-variable-name-var-assign-op.php diff --git a/src/Parser/NewAssignedToPropertyVisitor.php b/src/Parser/NewAssignedToPropertyVisitor.php index 4eb7b93bff..7808d82fce 100644 --- a/src/Parser/NewAssignedToPropertyVisitor.php +++ b/src/Parser/NewAssignedToPropertyVisitor.php @@ -16,7 +16,7 @@ final class NewAssignedToPropertyVisitor extends NodeVisitorAbstract #[Override] public function enterNode(Node $node): ?Node { - if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignRef || $node instanceof Node\Expr\AssignOp\Coalesce) { + if ($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignRef || $node instanceof Node\Expr\AssignOp) { if ( ($node->var instanceof Node\Expr\PropertyFetch || $node->var instanceof Node\Expr\StaticPropertyFetch) && $node->expr instanceof Node\Expr\New_ diff --git a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php index 2a75a6c8c6..d0d20f4ba9 100644 --- a/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php +++ b/src/Rules/PhpDoc/InvalidPHPStanDocTagRule.php @@ -88,7 +88,11 @@ public function processNode(Node $node, Scope $scope): array return []; } if ($node instanceof Node\Stmt\Expression) { - if (!$node->expr instanceof Node\Expr\Assign && !$node->expr instanceof Node\Expr\AssignRef) { + if ( + !$node->expr instanceof Node\Expr\Assign + && !$node->expr instanceof Node\Expr\AssignRef + && !$node->expr instanceof Node\Expr\AssignOp + ) { return []; } } diff --git a/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php b/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php index 4b18ca789a..edf59cd62f 100644 --- a/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php +++ b/src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php @@ -47,7 +47,11 @@ public function processNode(Node $node, Scope $scope): array return []; } if ($node instanceof Node\Stmt\Expression) { - if (!$node->expr instanceof Node\Expr\Assign && !$node->expr instanceof Node\Expr\AssignRef) { + if ( + !$node->expr instanceof Node\Expr\Assign + && !$node->expr instanceof Node\Expr\AssignRef + && !$node->expr instanceof Node\Expr\AssignOp + ) { return []; } } diff --git a/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php b/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php index 4c78688ff1..6296e12ca6 100644 --- a/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php +++ b/src/Rules/PhpDoc/WrongVariableNameInVarTagRule.php @@ -288,7 +288,11 @@ private function processForeach(Scope $scope, Node\Expr $iterateeExpr, ?Node\Exp */ private function processExpression(Scope $scope, Expr $expr, array $varTags): array { - if ($expr instanceof Node\Expr\Assign || $expr instanceof Node\Expr\AssignRef) { + if ( + $expr instanceof Node\Expr\Assign + || $expr instanceof Node\Expr\AssignRef + || $expr instanceof Node\Expr\AssignOp + ) { return $this->processAssign($scope, $expr->var, $expr->expr, $varTags); } diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php index ba9b73ad41..e6005045ac 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPHPStanDocTagRuleTest.php @@ -54,6 +54,16 @@ public function testBug8697(): void $this->analyse([__DIR__ . '/data/bug-8697.php'], []); } + public function testAssignOperator(): void + { + $this->analyse([__DIR__ . '/data/invalid-phpstan-doc-assign-operator.php'], [ + [ + 'Unknown PHPDoc tag: @phpstan-va', + 9, + ], + ]); + } + #[RequiresPhp('>= 8.4')] public function testPropertyHooks(): void { diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php index dcb6f03bbf..0fbe4ced57 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocTagValueRuleTest.php @@ -126,6 +126,16 @@ public function testIgnoreWithinPhpDoc(): void $this->analyse([__DIR__ . '/data/ignore-line-within-phpdoc.php'], []); } + public function testAssignOperator(): void + { + $this->analyse([__DIR__ . '/data/invalid-phpdoc-assign-operator.php'], [ + [ + 'PHPDoc tag @var has invalid value (\\\\Foo|\Bar $test): Unexpected token "\\\\\\\\Foo|\\\\Bar", expected type at offset 9 on line 1', + 8, + ], + ]); + } + public function testBug6299(): void { $this->analyse([__DIR__ . '/data/bug-6299.php'], [ diff --git a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php index b0d9948e37..838211838e 100644 --- a/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php @@ -548,6 +548,28 @@ public function testReportWrongType( $this->analyse([__DIR__ . '/data/wrong-var-native-type.php'], $expectedErrors); } + public function testAssignOperator(): void + { + $this->analyse([__DIR__ . '/data/wrong-variable-name-var-assign-op.php'], [ + [ + 'PHPDoc tag @var with type int is not subtype of native type void.', + 11, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type void.', + 14, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type void.', + 20, + ], + [ + 'PHPDoc tag @var with type int is not subtype of native type void.', + 23, + ], + ]); + } + public function testBug12457(): void { $this->checkTypeAgainstPhpDocType = true; diff --git a/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-assign-operator.php b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-assign-operator.php new file mode 100644 index 0000000000..c45c91135a --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/invalid-phpdoc-assign-operator.php @@ -0,0 +1,11 @@ +