diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 2c49112e41..311f70d4c6 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2664,11 +2664,29 @@ 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(), ); + + $scope = $this->processAssignVar( + $scope, + $stmt, + $arrayArg, + new TypeExpr($newType), + 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 ( @@ -2681,6 +2699,23 @@ static function (): void { $arrayArg = $expr->getArgs()[0]->value; $scope = $scope->invalidateExpression($arrayArg)->assignExpression($arrayArg, $arrayType, $arrayNativeType); + + $scope = $this->processAssignVar( + $scope, + $stmt, + $arrayArg, + new TypeExpr($arrayType), + 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 ( @@ -2695,11 +2730,29 @@ static function (): 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(), ); + + $scope = $this->processAssignVar( + $scope, + $stmt, + $arrayArg, + new TypeExpr($newType), + 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 ( @@ -2715,11 +2768,29 @@ static function (): 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), ); + + $scope = $this->processAssignVar( + $scope, + $stmt, + $arrayArg, + new TypeExpr($newType), + 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 ( @@ -2728,11 +2799,29 @@ static function (): 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)), ); + + $scope = $this->processAssignVar( + $scope, + $stmt, + $arrayArg, + new TypeExpr($newType), + 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 ( @@ -2741,11 +2830,29 @@ static function (): 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)), ); + + $scope = $this->processAssignVar( + $scope, + $stmt, + $arrayArg, + new TypeExpr($newType), + 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/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' + + } +} 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/Arrays/DeadForeachRuleTest.php b/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php index d6c1445dfa..9e272b5802 100644 --- a/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/DeadForeachRuleTest.php @@ -45,4 +45,14 @@ public function testBug13248(): void $this->analyse([__DIR__ . '/data/bug-13248.php'], []); } + 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 @@ +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(); + } + } +}; diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 310e25d728..d9916e1e13 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -819,4 +819,103 @@ 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'], []); + } + + 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, + ], + ]); + } + + 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; + $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-13438.php b/tests/PHPStan/Rules/Properties/data/bug-13438.php new file mode 100644 index 0000000000..4abf3e09e9 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13438.php @@ -0,0 +1,28 @@ += 8.0 + +namespace Bug13438; + +use LogicException; + +class Test +{ + /** + * @param non-empty-list $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..78378d2fe5 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13438b.php @@ -0,0 +1,28 @@ += 8.0 + +namespace Bug13438b; + +use LogicException; + +class Test +{ + /** + * @param non-empty-list $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..d12ed84ef1 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13438c.php @@ -0,0 +1,34 @@ += 8.0 + +namespace Bug13438c; + +use LogicException; + +class Test +{ + /** + * @var list + */ + 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); + } +} 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..5e25e051eb --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13438d.php @@ -0,0 +1,20 @@ += 8.0 + +namespace Bug13438d; + +class Test +{ + /** + * @param array{} $queue + */ + public function __construct( + private array $queue, + ) + { + } + + public function test1(): int + { + return array_push($this->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..ec885da21f --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13438e.php @@ -0,0 +1,20 @@ += 8.0 + +namespace Bug13438e; + +class Test +{ + /** + * @param array{} $queue + */ + public function __construct( + private array $queue, + ) + { + } + + public function test1(): int + { + return array_unshift($this->queue, 1); + } +} 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..e053ec3275 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-13438f.php @@ -0,0 +1,28 @@ += 8.0 + +namespace Bug13438f; + +use function PHPStan\dumpType; +use function PHPStan\Testing\assertType; + +class Test +{ + /** + * @param array> $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 + } + +} 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'; + } +} 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, + ], ]); }