From aa8d7799d776313ed60777ccd01c0727c1ee8547 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 7 Sep 2025 19:07:13 +0200 Subject: [PATCH 01/12] Fix incorrect analyzing of array_shift with non-empty-list property --- src/Analyser/NodeScopeResolver.php | 3 ++ .../TypesAssignedToPropertiesRuleTest.php | 41 +++++++++++++++++++ .../Rules/Properties/data/bug-13438.php | 28 +++++++++++++ .../Rules/Properties/data/bug-13438b.php | 28 +++++++++++++ .../Rules/Properties/data/bug-13438c.php | 34 +++++++++++++++ 5 files changed, 134 insertions(+) create mode 100644 tests/PHPStan/Rules/Properties/data/bug-13438.php create mode 100644 tests/PHPStan/Rules/Properties/data/bug-13438b.php create mode 100644 tests/PHPStan/Rules/Properties/data/bug-13438c.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 2c49112e41..7723d8ac0e 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2669,6 +2669,9 @@ static function (): void { $isArrayPop ? $arrayArgType->popArray() : $arrayArgType->shiftArray(), $isArrayPop ? $arrayArgNativeType->popArray() : $arrayArgNativeType->shiftArray(), ); + if ($arrayArg instanceof PropertyFetch || $arrayArg instanceof StaticPropertyFetch) { + $nodeCallback(new PropertyAssignNode($arrayArg, new TypeExpr($scope->getType($arrayArg)), false), $scope); + } } if ( diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 310e25d728..2ad6707cad 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -819,4 +819,45 @@ public function testBug7824(): void $this->analyse([__DIR__ . '/data/bug-7824.php'], []); } + public function testBug13438(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-13438.php'], [ + [ + 'Property Bug13438\Test::$queue (non-empty-list) does not accept list.', + 20, + 'list might be empty.', + ], + [ + 'Property Bug13438\Test::$queue (non-empty-list) does not accept list.', + 26, + 'list might be empty.', + ], + ]); + } + + public function testBug13438b(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-13438b.php'], [ + [ + 'Property Bug13438b\Test::$queue (non-empty-list) does not accept list.', + 20, + 'list might be empty.', + ], + [ + 'Property Bug13438b\Test::$queue (non-empty-list) does not accept list.', + 26, + 'list might be empty.', + ], + + ]); + } + + public function testBug13438c(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-13438c.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-13438.php b/tests/PHPStan/Rules/Properties/data/bug-13438.php new file mode 100644 index 0000000000..bfb764b97f --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13438.php @@ -0,0 +1,28 @@ + $queue + */ + public function __construct( + private array $queue, + ) + { + } + + public function test1(): int + { + return array_shift($this->queue) + ?? throw new LogicException('queue is empty'); + } + + public function test2(): int + { + return array_shift($this->queue); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-13438b.php b/tests/PHPStan/Rules/Properties/data/bug-13438b.php new file mode 100644 index 0000000000..8d0e539611 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13438b.php @@ -0,0 +1,28 @@ + $queue + */ + public function __construct( + private array $queue, + ) + { + } + + public function test1(): int + { + return array_pop($this->queue) + ?? throw new LogicException('queue is empty'); + } + + public function test2(): int + { + return array_pop($this->queue); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-13438c.php b/tests/PHPStan/Rules/Properties/data/bug-13438c.php new file mode 100644 index 0000000000..b5db487a8c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13438c.php @@ -0,0 +1,34 @@ + + */ + private $queue; + + /** + * @param non-empty-list $queue + */ + public function __construct( + array $queue, + ) + { + $this->queue = $queue; + } + + public function test1(): int + { + return array_shift($this->queue) + ?? throw new LogicException('queue is empty'); + } + + public function test2(): int + { + return array_shift($this->queue); + } +} From 9d2f659871b0a86bdbbd20280ed12fa599ec6803 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 7 Sep 2025 19:17:55 +0200 Subject: [PATCH 02/12] fix array_push()/array_pop() --- src/Analyser/NodeScopeResolver.php | 4 ++++ .../TypesAssignedToPropertiesRuleTest.php | 22 +++++++++++++++++++ .../Rules/Properties/data/bug-13438d.php | 20 +++++++++++++++++ .../Rules/Properties/data/bug-13438e.php | 20 +++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 tests/PHPStan/Rules/Properties/data/bug-13438d.php create mode 100644 tests/PHPStan/Rules/Properties/data/bug-13438e.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 7723d8ac0e..2d0e95fea6 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2684,6 +2684,10 @@ static function (): void { $arrayArg = $expr->getArgs()[0]->value; $scope = $scope->invalidateExpression($arrayArg)->assignExpression($arrayArg, $arrayType, $arrayNativeType); + + if ($arrayArg instanceof PropertyFetch || $arrayArg instanceof StaticPropertyFetch) { + $nodeCallback(new PropertyAssignNode($arrayArg, new TypeExpr($scope->getType($arrayArg)), false), $scope); + } } if ( diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 2ad6707cad..1942577876 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -860,4 +860,26 @@ public function testBug13438c(): void $this->analyse([__DIR__ . '/data/bug-13438c.php'], []); } + public function testBug13438d(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-13438d.php'], [ + [ + 'Property Bug13438d\Test::$queue (array{}) does not accept array{1}.', + 18, + ], + ]); + } + + public function testBug13438e(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-13438e.php'], [ + [ + 'Property Bug13438e\Test::$queue (array{}) does not accept array{1}.', + 18, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-13438d.php b/tests/PHPStan/Rules/Properties/data/bug-13438d.php new file mode 100644 index 0000000000..40853f0abc --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13438d.php @@ -0,0 +1,20 @@ +queue, 1); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-13438e.php b/tests/PHPStan/Rules/Properties/data/bug-13438e.php new file mode 100644 index 0000000000..81be5c6755 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13438e.php @@ -0,0 +1,20 @@ +queue, 1); + } +} From 4998c741b48bfbf5371fa88e89f5bdc3e1af0bee Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 7 Sep 2025 19:29:02 +0200 Subject: [PATCH 03/12] fix more functions --- src/Analyser/NodeScopeResolver.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 2d0e95fea6..c674c9b4b7 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2707,6 +2707,10 @@ static function (): void { $scope->getType($arrayArg)->shuffleArray(), $scope->getNativeType($arrayArg)->shuffleArray(), ); + + if ($arrayArg instanceof PropertyFetch || $arrayArg instanceof StaticPropertyFetch) { + $nodeCallback(new PropertyAssignNode($arrayArg, new TypeExpr($scope->getType($arrayArg)), false), $scope); + } } if ( @@ -2727,6 +2731,10 @@ static function (): void { $arrayArgType->spliceArray($offsetType, $lengthType, $replacementType), $arrayArgNativeType->spliceArray($offsetType, $lengthType, $replacementType), ); + + if ($arrayArg instanceof PropertyFetch || $arrayArg instanceof StaticPropertyFetch) { + $nodeCallback(new PropertyAssignNode($arrayArg, new TypeExpr($scope->getType($arrayArg)), false), $scope); + } } if ( @@ -2740,6 +2748,10 @@ static function (): void { $this->getArraySortPreserveListFunctionType($scope->getType($arrayArg)), $this->getArraySortPreserveListFunctionType($scope->getNativeType($arrayArg)), ); + + if ($arrayArg instanceof PropertyFetch || $arrayArg instanceof StaticPropertyFetch) { + $nodeCallback(new PropertyAssignNode($arrayArg, new TypeExpr($scope->getType($arrayArg)), false), $scope); + } } if ( @@ -2753,6 +2765,10 @@ static function (): void { $this->getArraySortDoNotPreserveListFunctionType($scope->getType($arrayArg)), $this->getArraySortDoNotPreserveListFunctionType($scope->getNativeType($arrayArg)), ); + + if ($arrayArg instanceof PropertyFetch || $arrayArg instanceof StaticPropertyFetch) { + $nodeCallback(new PropertyAssignNode($arrayArg, new TypeExpr($scope->getType($arrayArg)), false), $scope); + } } if ( From f37c06c77d93aa8b858e1477f618e706a85c0fd5 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 7 Sep 2025 19:38:36 +0200 Subject: [PATCH 04/12] added regression test --- src/Analyser/NodeScopeResolver.php | 1 + .../TypesAssignedToPropertiesRuleTest.php | 19 +++++++++++++ .../Rules/Properties/data/bug-2888.php | 28 +++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 tests/PHPStan/Rules/Properties/data/bug-2888.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index c674c9b4b7..f9caf6b3aa 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2669,6 +2669,7 @@ static function (): void { $isArrayPop ? $arrayArgType->popArray() : $arrayArgType->shiftArray(), $isArrayPop ? $arrayArgNativeType->popArray() : $arrayArgNativeType->shiftArray(), ); + if ($arrayArg instanceof PropertyFetch || $arrayArg instanceof StaticPropertyFetch) { $nodeCallback(new PropertyAssignNode($arrayArg, new TypeExpr($scope->getType($arrayArg)), false), $scope); } diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 1942577876..e632ff5622 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -882,4 +882,23 @@ public function testBug13438e(): void ]); } + public function testBug2888(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-2888.php'], [ + [ + 'Property Bug2888\MyClass::$prop (array) does not accept array.', + 17, + ], + [ + 'Property Bug2888\MyClass::$prop (array) does not accept array.', + 18, + ], + [ + 'Property Bug2888\MyClass::$prop (array) does not accept array.', + 26, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-2888.php b/tests/PHPStan/Rules/Properties/data/bug-2888.php new file mode 100644 index 0000000000..27de191b2b --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-2888.php @@ -0,0 +1,28 @@ +prop, 'string'); + array_unshift($this->prop, 'string'); + } + + /** + * @return void + */ + public function bar() + { + $this->prop[] = 'string'; + } +} From 4fe77cf727ff3fa00c127b1a0a0dd2b53275290a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 7 Sep 2025 19:46:54 +0200 Subject: [PATCH 05/12] fix php 7.4 lint --- tests/PHPStan/Rules/Properties/data/bug-13438.php | 2 +- tests/PHPStan/Rules/Properties/data/bug-13438b.php | 2 +- tests/PHPStan/Rules/Properties/data/bug-13438c.php | 2 +- tests/PHPStan/Rules/Properties/data/bug-13438d.php | 2 +- tests/PHPStan/Rules/Properties/data/bug-13438e.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/PHPStan/Rules/Properties/data/bug-13438.php b/tests/PHPStan/Rules/Properties/data/bug-13438.php index bfb764b97f..4abf3e09e9 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-13438.php +++ b/tests/PHPStan/Rules/Properties/data/bug-13438.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug13438; diff --git a/tests/PHPStan/Rules/Properties/data/bug-13438b.php b/tests/PHPStan/Rules/Properties/data/bug-13438b.php index 8d0e539611..78378d2fe5 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-13438b.php +++ b/tests/PHPStan/Rules/Properties/data/bug-13438b.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug13438b; diff --git a/tests/PHPStan/Rules/Properties/data/bug-13438c.php b/tests/PHPStan/Rules/Properties/data/bug-13438c.php index b5db487a8c..d12ed84ef1 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-13438c.php +++ b/tests/PHPStan/Rules/Properties/data/bug-13438c.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug13438c; diff --git a/tests/PHPStan/Rules/Properties/data/bug-13438d.php b/tests/PHPStan/Rules/Properties/data/bug-13438d.php index 40853f0abc..5e25e051eb 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-13438d.php +++ b/tests/PHPStan/Rules/Properties/data/bug-13438d.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug13438d; diff --git a/tests/PHPStan/Rules/Properties/data/bug-13438e.php b/tests/PHPStan/Rules/Properties/data/bug-13438e.php index 81be5c6755..ec885da21f 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-13438e.php +++ b/tests/PHPStan/Rules/Properties/data/bug-13438e.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug13438e; From cf9bf2cf331aee1e1b829ca980131f8fadd9d2fe Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 8 Sep 2025 10:22:02 +0200 Subject: [PATCH 06/12] use MutatingScope->processAssignVar() instead --- src/Analyser/NodeScopeResolver.php | 114 +++++++++++++++--- .../TypesAssignedToPropertiesRuleTest.php | 17 +++ .../Rules/Properties/data/bug-13438f.php | 28 +++++ 3 files changed, 141 insertions(+), 18 deletions(-) create mode 100644 tests/PHPStan/Rules/Properties/data/bug-13438f.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index f9caf6b3aa..23131855bd 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2670,9 +2670,22 @@ static function (): void { $isArrayPop ? $arrayArgNativeType->popArray() : $arrayArgNativeType->shiftArray(), ); - if ($arrayArg instanceof PropertyFetch || $arrayArg instanceof StaticPropertyFetch) { - $nodeCallback(new PropertyAssignNode($arrayArg, new TypeExpr($scope->getType($arrayArg)), false), $scope); - } + $scope = $this->processAssignVar( + $scope, + $stmt, + $arrayArg, + $arrayArg, + static function (Node $node, Scope $scope) use ($nodeCallback): void { + if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { + return; + } + + $nodeCallback($node, $scope); + }, + $context, + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []), + true, + )->getScope(); } if ( @@ -2686,9 +2699,22 @@ static function (): void { $arrayArg = $expr->getArgs()[0]->value; $scope = $scope->invalidateExpression($arrayArg)->assignExpression($arrayArg, $arrayType, $arrayNativeType); - if ($arrayArg instanceof PropertyFetch || $arrayArg instanceof StaticPropertyFetch) { - $nodeCallback(new PropertyAssignNode($arrayArg, new TypeExpr($scope->getType($arrayArg)), false), $scope); - } + $scope = $this->processAssignVar( + $scope, + $stmt, + $arrayArg, + $arrayArg, + static function (Node $node, Scope $scope) use ($nodeCallback): void { + if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { + return; + } + + $nodeCallback($node, $scope); + }, + $context, + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []), + true, + )->getScope(); } if ( @@ -2709,9 +2735,22 @@ static function (): void { $scope->getNativeType($arrayArg)->shuffleArray(), ); - if ($arrayArg instanceof PropertyFetch || $arrayArg instanceof StaticPropertyFetch) { - $nodeCallback(new PropertyAssignNode($arrayArg, new TypeExpr($scope->getType($arrayArg)), false), $scope); - } + $scope = $this->processAssignVar( + $scope, + $stmt, + $arrayArg, + $arrayArg, + static function (Node $node, Scope $scope) use ($nodeCallback): void { + if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { + return; + } + + $nodeCallback($node, $scope); + }, + $context, + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []), + true, + )->getScope(); } if ( @@ -2733,9 +2772,22 @@ static function (): void { $arrayArgNativeType->spliceArray($offsetType, $lengthType, $replacementType), ); - if ($arrayArg instanceof PropertyFetch || $arrayArg instanceof StaticPropertyFetch) { - $nodeCallback(new PropertyAssignNode($arrayArg, new TypeExpr($scope->getType($arrayArg)), false), $scope); - } + $scope = $this->processAssignVar( + $scope, + $stmt, + $arrayArg, + $arrayArg, + static function (Node $node, Scope $scope) use ($nodeCallback): void { + if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { + return; + } + + $nodeCallback($node, $scope); + }, + $context, + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []), + true, + )->getScope(); } if ( @@ -2750,9 +2802,22 @@ static function (): void { $this->getArraySortPreserveListFunctionType($scope->getNativeType($arrayArg)), ); - if ($arrayArg instanceof PropertyFetch || $arrayArg instanceof StaticPropertyFetch) { - $nodeCallback(new PropertyAssignNode($arrayArg, new TypeExpr($scope->getType($arrayArg)), false), $scope); - } + $scope = $this->processAssignVar( + $scope, + $stmt, + $arrayArg, + $arrayArg, + static function (Node $node, Scope $scope) use ($nodeCallback): void { + if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { + return; + } + + $nodeCallback($node, $scope); + }, + $context, + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []), + true, + )->getScope(); } if ( @@ -2767,9 +2832,22 @@ static function (): void { $this->getArraySortDoNotPreserveListFunctionType($scope->getNativeType($arrayArg)), ); - if ($arrayArg instanceof PropertyFetch || $arrayArg instanceof StaticPropertyFetch) { - $nodeCallback(new PropertyAssignNode($arrayArg, new TypeExpr($scope->getType($arrayArg)), false), $scope); - } + $scope = $this->processAssignVar( + $scope, + $stmt, + $arrayArg, + $arrayArg, + static function (Node $node, Scope $scope) use ($nodeCallback): void { + if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { + return; + } + + $nodeCallback($node, $scope); + }, + $context, + static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, false, [], []), + true, + )->getScope(); } if ( diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index e632ff5622..d9916e1e13 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -882,6 +882,23 @@ public function testBug13438e(): void ]); } + public function testBug13438f(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-13438f.php'], [ + [ + 'Property Bug13438f\Test::$queue (array>) does not accept non-empty-array>.', + 20, + 'list might be empty.', + ], + [ + 'Property Bug13438f\Test::$queue (array>) does not accept non-empty-array>.', + 25, + 'list might be empty.', + ], + ]); + } + public function testBug2888(): void { $this->checkExplicitMixed = true; diff --git a/tests/PHPStan/Rules/Properties/data/bug-13438f.php b/tests/PHPStan/Rules/Properties/data/bug-13438f.php new file mode 100644 index 0000000000..2a36743647 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13438f.php @@ -0,0 +1,28 @@ +> $queue + */ + public function __construct( + private array $queue, + ) { + } + + public function test1(): void + { + array_shift($this->queue[5]); // no longer is non-empty-list after this + } + + public function test2(): void + { + $this->queue[5] = []; // normally it works thanks to processAssignVar + } + +} From b872a7302d441d94432c1a7465e0d20860f48283 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 8 Sep 2025 10:40:24 +0200 Subject: [PATCH 07/12] Added regression test --- ...isonOperatorsConstantConditionRuleTest.php | 6 ++++ .../Rules/Comparison/data/bug-3387.php | 33 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-3387.php diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index f05edf6b3d..a6981ba1b9 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -257,4 +257,10 @@ public function testBug12716(): void $this->analyse([__DIR__ . '/data/bug-12716.php'], []); } + public function testBug3387(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-3387.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-3387.php b/tests/PHPStan/Rules/Comparison/data/bug-3387.php new file mode 100644 index 0000000000..ef10797eb8 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-3387.php @@ -0,0 +1,33 @@ + []]; + foreach ($items as $item) { + $array[$key][] = $item; + if (count($array[$key]) > 1) { + throw new RuntimeException(); + } + } +}; + +function (array $items, string $key) { + $array = [$key => []]; + foreach ($items as $item) { + array_unshift($array[$key], $item); + if (count($array[$key]) > 1) { + throw new RuntimeException(); + } + } +}; + +function (array $items, string $key) { + $array = [$key => []]; + foreach ($items as $item) { + array_push($array[$key], $item); + if (count($array[$key]) > 1) { + throw new RuntimeException(); + } + } +}; From 15bb2f773245903654ed3771a5bba23965b98b11 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 8 Sep 2025 10:43:54 +0200 Subject: [PATCH 08/12] Added regression test --- .../Rules/Arrays/DeadForeachRuleTest.php | 5 +++++ tests/PHPStan/Rules/Arrays/data/bug-2560.php | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 tests/PHPStan/Rules/Arrays/data/bug-2560.php diff --git a/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php b/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php index d6c1445dfa..83c17d989a 100644 --- a/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php @@ -45,4 +45,9 @@ public function testBug13248(): void $this->analyse([__DIR__ . '/data/bug-13248.php'], []); } + public function testBug2560(): void + { + $this->analyse([__DIR__ . '/data/bug-2560.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-2560.php b/tests/PHPStan/Rules/Arrays/data/bug-2560.php new file mode 100644 index 0000000000..e7e6fd4124 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-2560.php @@ -0,0 +1,20 @@ + Date: Mon, 8 Sep 2025 10:45:27 +0200 Subject: [PATCH 09/12] Added regression test --- .../Rules/Arrays/DeadForeachRuleTest.php | 5 ++++ tests/PHPStan/Rules/Arrays/data/bug-2457.php | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 tests/PHPStan/Rules/Arrays/data/bug-2457.php diff --git a/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php b/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php index 83c17d989a..9e272b5802 100644 --- a/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php @@ -50,4 +50,9 @@ public function testBug2560(): void $this->analyse([__DIR__ . '/data/bug-2560.php'], []); } + public function testBug2457(): void + { + $this->analyse([__DIR__ . '/data/bug-2457.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-2457.php b/tests/PHPStan/Rules/Arrays/data/bug-2457.php new file mode 100644 index 0000000000..88811feff4 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-2457.php @@ -0,0 +1,29 @@ + Date: Mon, 8 Sep 2025 10:58:38 +0200 Subject: [PATCH 10/12] fix php 7.4 build --- tests/PHPStan/Rules/Properties/data/bug-13438f.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Properties/data/bug-13438f.php b/tests/PHPStan/Rules/Properties/data/bug-13438f.php index 2a36743647..e053ec3275 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-13438f.php +++ b/tests/PHPStan/Rules/Properties/data/bug-13438f.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug13438f; From 6d77e92f1c32145ec75290262dc921666649a781 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 8 Sep 2025 11:26:45 +0200 Subject: [PATCH 11/12] Create bug-11846.php --- tests/PHPStan/Analyser/nsrt/bug-11846.php | 28 +++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-11846.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-11846.php b/tests/PHPStan/Analyser/nsrt/bug-11846.php new file mode 100644 index 0000000000..06fbcd8313 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11846.php @@ -0,0 +1,28 @@ +', $outerList); + + foreach ($outerList as $key => $outerElement) { + $result = false; + + assertType('array{}|array{array{}}', $outerElement); + foreach ($outerElement as $innerElement) { + $result = true; + } + assertType('bool', $result); // could be 'true' + + } +} From 4702c2b2e2ff562e887571b492a62c34fdcb41de Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 8 Sep 2025 11:32:34 +0200 Subject: [PATCH 12/12] use newType() as expr --- src/Analyser/NodeScopeResolver.php | 27 +++++++++++-------- tests/PHPStan/Analyser/nsrt/shuffle.php | 8 +++--- tests/PHPStan/Analyser/nsrt/sort.php | 6 ++--- .../PHPStan/Rules/Variables/EmptyRuleTest.php | 4 +++ 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 23131855bd..311f70d4c6 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2664,9 +2664,10 @@ static function (): void { $arrayArgNativeType = $scope->getNativeType($arrayArg); $isArrayPop = $functionReflection->getName() === 'array_pop'; + $newType = $isArrayPop ? $arrayArgType->popArray() : $arrayArgType->shiftArray(); $scope = $scope->invalidateExpression($arrayArg)->assignExpression( $arrayArg, - $isArrayPop ? $arrayArgType->popArray() : $arrayArgType->shiftArray(), + $newType, $isArrayPop ? $arrayArgNativeType->popArray() : $arrayArgNativeType->shiftArray(), ); @@ -2674,7 +2675,7 @@ static function (): void { $scope, $stmt, $arrayArg, - $arrayArg, + new TypeExpr($newType), static function (Node $node, Scope $scope) use ($nodeCallback): void { if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { return; @@ -2703,7 +2704,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $scope, $stmt, $arrayArg, - $arrayArg, + new TypeExpr($arrayType), static function (Node $node, Scope $scope) use ($nodeCallback): void { if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { return; @@ -2729,9 +2730,10 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { && $functionReflection->getName() === 'shuffle' ) { $arrayArg = $expr->getArgs()[0]->value; + $newType = $scope->getType($arrayArg)->shuffleArray(); $scope = $scope->assignExpression( $arrayArg, - $scope->getType($arrayArg)->shuffleArray(), + $newType, $scope->getNativeType($arrayArg)->shuffleArray(), ); @@ -2739,7 +2741,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $scope, $stmt, $arrayArg, - $arrayArg, + new TypeExpr($newType), static function (Node $node, Scope $scope) use ($nodeCallback): void { if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { return; @@ -2766,9 +2768,10 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $lengthType = isset($expr->getArgs()[2]) ? $scope->getType($expr->getArgs()[2]->value) : new NullType(); $replacementType = isset($expr->getArgs()[3]) ? $scope->getType($expr->getArgs()[3]->value) : new ConstantArrayType([], []); + $newType = $arrayArgType->spliceArray($offsetType, $lengthType, $replacementType); $scope = $scope->invalidateExpression($arrayArg)->assignExpression( $arrayArg, - $arrayArgType->spliceArray($offsetType, $lengthType, $replacementType), + $newType, $arrayArgNativeType->spliceArray($offsetType, $lengthType, $replacementType), ); @@ -2776,7 +2779,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $scope, $stmt, $arrayArg, - $arrayArg, + new TypeExpr($newType), static function (Node $node, Scope $scope) use ($nodeCallback): void { if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { return; @@ -2796,9 +2799,10 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { && count($expr->getArgs()) >= 1 ) { $arrayArg = $expr->getArgs()[0]->value; + $newType = $this->getArraySortPreserveListFunctionType($scope->getType($arrayArg)); $scope = $scope->assignExpression( $arrayArg, - $this->getArraySortPreserveListFunctionType($scope->getType($arrayArg)), + $newType, $this->getArraySortPreserveListFunctionType($scope->getNativeType($arrayArg)), ); @@ -2806,7 +2810,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $scope, $stmt, $arrayArg, - $arrayArg, + new TypeExpr($newType), static function (Node $node, Scope $scope) use ($nodeCallback): void { if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { return; @@ -2826,9 +2830,10 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { && count($expr->getArgs()) >= 1 ) { $arrayArg = $expr->getArgs()[0]->value; + $newType = $this->getArraySortDoNotPreserveListFunctionType($scope->getType($arrayArg)); $scope = $scope->assignExpression( $arrayArg, - $this->getArraySortDoNotPreserveListFunctionType($scope->getType($arrayArg)), + $newType, $this->getArraySortDoNotPreserveListFunctionType($scope->getNativeType($arrayArg)), ); @@ -2836,7 +2841,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void { $scope, $stmt, $arrayArg, - $arrayArg, + new TypeExpr($newType), static function (Node $node, Scope $scope) use ($nodeCallback): void { if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) { return; diff --git a/tests/PHPStan/Analyser/nsrt/shuffle.php b/tests/PHPStan/Analyser/nsrt/shuffle.php index 6b699e598a..cf85e3528b 100644 --- a/tests/PHPStan/Analyser/nsrt/shuffle.php +++ b/tests/PHPStan/Analyser/nsrt/shuffle.php @@ -13,7 +13,7 @@ public function normalArrays1(array $arr): void /** @var mixed[] $arr */ shuffle($arr); assertType('list', $arr); - assertNativeType('list', $arr); + assertNativeType('list', $arr); assertType('list>', array_keys($arr)); assertType('list', array_values($arr)); } @@ -23,7 +23,7 @@ public function normalArrays2(array $arr): void /** @var non-empty-array $arr */ shuffle($arr); assertType('non-empty-list', $arr); - assertNativeType('list', $arr); + assertNativeType('non-empty-list', $arr); assertType('non-empty-list>', array_keys($arr)); assertType('non-empty-list', array_values($arr)); } @@ -67,7 +67,7 @@ public function constantArrays2(array $arr): void /** @var array{0?: 1, 1?: 2, 2?: 3} $arr */ shuffle($arr); assertType('list<1|2|3>', $arr); - assertNativeType('list', $arr); + assertNativeType('list<1|2|3>', $arr); assertType('list<0|1|2>', array_keys($arr)); assertType('list<1|2|3>', array_values($arr)); } @@ -107,7 +107,7 @@ public function constantArrays6(array $arr): void /** @var array{foo?: 1, bar: 2, }|array{baz: 3, foobar?: 4} $arr */ shuffle($arr); assertType('non-empty-list<1|2|3|4>', $arr); - assertNativeType('list', $arr); + assertNativeType('non-empty-list<1|2|3|4>', $arr); assertType('non-empty-list<0|1>', array_keys($arr)); assertType('non-empty-list<1|2|3|4>', array_values($arr)); } diff --git a/tests/PHPStan/Analyser/nsrt/sort.php b/tests/PHPStan/Analyser/nsrt/sort.php index 93dfe0d147..88dd95b55a 100644 --- a/tests/PHPStan/Analyser/nsrt/sort.php +++ b/tests/PHPStan/Analyser/nsrt/sort.php @@ -91,17 +91,17 @@ public function normalArray(array $arr): void $arr1 = $arr; sort($arr1); assertType('list', $arr1); - assertNativeType('list', $arr1); + assertNativeType('list', $arr1); $arr2 = $arr; rsort($arr2); assertType('list', $arr2); - assertNativeType('list', $arr2); + assertNativeType('list', $arr2); $arr3 = $arr; usort($arr3, fn(int $a, int $b) => $a <=> $b); assertType('list', $arr3); - assertNativeType('list', $arr3); + assertNativeType('list', $arr3); } public function mixed($arr): void diff --git a/tests/PHPStan/Rules/Variables/EmptyRuleTest.php b/tests/PHPStan/Rules/Variables/EmptyRuleTest.php index 98a52dfae7..d6ca36af5d 100644 --- a/tests/PHPStan/Rules/Variables/EmptyRuleTest.php +++ b/tests/PHPStan/Rules/Variables/EmptyRuleTest.php @@ -96,6 +96,10 @@ public function testBug6974(): void 'Variable $a in empty() always exists and is always falsy.', 12, ], + [ + 'Variable $a in empty() always exists and is not falsy.', + 30, + ], ]); }