From b6249f6642e2b6f69921174a7fedf6bc33d9bd07 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 10 Mar 2025 15:35:01 +0100 Subject: [PATCH 1/2] Fix false positives on non-existing-offset's --- .../NonexistentOffsetInArrayDimFetchRule.php | 36 ++++++++++++++++ ...nexistentOffsetInArrayDimFetchRuleTest.php | 12 ++++++ ...rray-dim-fetch-on-array-key-first-last.php | 42 +++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 tests/PHPStan/Rules/Arrays/data/array-dim-fetch-on-array-key-first-last.php diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php index d3ef021189..4d234e59f6 100644 --- a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php @@ -13,6 +13,7 @@ use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function count; +use function in_array; use function sprintf; /** @@ -96,6 +97,41 @@ public function processNode(Node $node, Scope $scope): array return []; } + if ( + $node->dim instanceof Node\Expr\FuncCall + && $node->dim->name instanceof Node\Name + && in_array($node->dim->name->toLowerString(), ['array_key_first', 'array_key_last'], true) + && count($node->dim->getArgs()) >= 1 + ) { + $arrayArg = $node->dim->getArgs()[0]->value; + $arrayType = $scope->getType($arrayArg); + if ( + $arrayType->isArray()->yes() + && $arrayType->isIterableAtLeastOnce()->yes() + ) { + return []; + } + } + + if ( + $node->dim instanceof Node\Expr\BinaryOp\Minus + && $node->dim->left instanceof Node\Expr\FuncCall + && $node->dim->left->name instanceof Node\Name + && in_array($node->dim->left->name->toLowerString(), ['count', 'sizeof'], true) + && count($node->dim->left->getArgs()) >= 1 + && $node->dim->right instanceof Node\Scalar\Int_ + && $node->dim->right->value === 1 + ) { + $arrayArg = $node->dim->left->getArgs()[0]->value; + $arrayType = $scope->getType($arrayArg); + if ( + $arrayType->isList()->yes() + && $arrayType->isIterableAtLeastOnce()->yes() + ) { + return []; + } + } + return $this->nonexistentOffsetInArrayDimFetchCheck->check( $scope, $node->var, diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 55da1ddeb3..9eac02405b 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -829,6 +829,18 @@ public function testArrayDimFetchAfterArraySearch(): void ]); } + public function testArrayDimFetchOnArrayKeyFirsOrLastOrCount(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/array-dim-fetch-on-array-key-first-last.php'], [ + [ + 'Offset 0|null might not exist on list.', + 12, + ], + ]); + } + public function testBug8649(): void { $this->reportPossiblyNonexistentGeneralArrayOffset = true; diff --git a/tests/PHPStan/Rules/Arrays/data/array-dim-fetch-on-array-key-first-last.php b/tests/PHPStan/Rules/Arrays/data/array-dim-fetch-on-array-key-first-last.php new file mode 100644 index 0000000000..4e0cad39e1 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/array-dim-fetch-on-array-key-first-last.php @@ -0,0 +1,42 @@ + $hellos + */ + public function first(array $hellos): string + { + if (rand(0,1)) { + return $hellos[array_key_first($hellos)]; + } + if ($hellos !== []) { + return $hellos[array_key_first($hellos)]; + } + return ''; + } + + /** + * @param array $hellos + */ + public function last(array $hellos): string + { + if ($hellos !== []) { + return $hellos[array_key_last($hellos)]; + } + return ''; + } + + /** + * @param list $hellos + */ + public function works(array $hellos): string + { + if ($hellos === []) { + return 'nothing'; + } + + return $hellos[count($hellos) - 1]; + } +} From 3b6ddca024548af43a137dbf9fa6e826b754557c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 10 Mar 2025 15:50:56 +0100 Subject: [PATCH 2/2] more tests --- .../Arrays/NonexistentOffsetInArrayDimFetchRule.php | 13 +++++++++++-- .../NonexistentOffsetInArrayDimFetchRuleTest.php | 8 ++++++++ .../array-dim-fetch-on-array-key-first-last.php | 12 ++++++++++-- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php index 4d234e59f6..b0edfcadf9 100644 --- a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php @@ -14,6 +14,7 @@ use PHPStan\Type\VerbosityLevel; use function count; use function in_array; +use function is_string; use function sprintf; /** @@ -106,7 +107,11 @@ public function processNode(Node $node, Scope $scope): array $arrayArg = $node->dim->getArgs()[0]->value; $arrayType = $scope->getType($arrayArg); if ( - $arrayType->isArray()->yes() + $arrayArg instanceof Node\Expr\Variable + && $node->var instanceof Node\Expr\Variable + && is_string($arrayArg->name) + && $arrayArg->name === $node->var->name + && $arrayType->isArray()->yes() && $arrayType->isIterableAtLeastOnce()->yes() ) { return []; @@ -125,7 +130,11 @@ public function processNode(Node $node, Scope $scope): array $arrayArg = $node->dim->left->getArgs()[0]->value; $arrayType = $scope->getType($arrayArg); if ( - $arrayType->isList()->yes() + $arrayArg instanceof Node\Expr\Variable + && $node->var instanceof Node\Expr\Variable + && is_string($arrayArg->name) + && $arrayArg->name === $node->var->name + && $arrayType->isList()->yes() && $arrayType->isIterableAtLeastOnce()->yes() ) { return []; diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 9eac02405b..78e8d86ccf 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -838,6 +838,14 @@ public function testArrayDimFetchOnArrayKeyFirsOrLastOrCount(): void 'Offset 0|null might not exist on list.', 12, ], + [ + 'Offset (int|string) might not exist on non-empty-list.', + 16, + ], + [ + 'Offset int<-1, max> might not exist on non-empty-list.', + 45, + ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/data/array-dim-fetch-on-array-key-first-last.php b/tests/PHPStan/Rules/Arrays/data/array-dim-fetch-on-array-key-first-last.php index 4e0cad39e1..82fac73327 100644 --- a/tests/PHPStan/Rules/Arrays/data/array-dim-fetch-on-array-key-first-last.php +++ b/tests/PHPStan/Rules/Arrays/data/array-dim-fetch-on-array-key-first-last.php @@ -6,12 +6,16 @@ class Hello { /** * @param list $hellos */ - public function first(array $hellos): string + public function first(array $hellos, array $anotherArray): string { if (rand(0,1)) { return $hellos[array_key_first($hellos)]; } if ($hellos !== []) { + if ($anotherArray !== []) { + return $hellos[array_key_first($anotherArray)]; + } + return $hellos[array_key_first($hellos)]; } return ''; @@ -31,12 +35,16 @@ public function last(array $hellos): string /** * @param list $hellos */ - public function works(array $hellos): string + public function countOnArray(array $hellos, array $anotherArray): string { if ($hellos === []) { return 'nothing'; } + if (rand(0,1)) { + return $hellos[count($anotherArray) - 1]; + } + return $hellos[count($hellos) - 1]; } }