diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 297cb84a9e..c4f86aba74 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -469,6 +469,7 @@ public function processAssignVar( if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) { if ($var instanceof Variable && is_string($var->name)) { + $preservedDimFetchTypes = $scope->getDynamicArrayDimFetchExpressionTypes($var->name); $nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, new TypeExpr($valueToWrite)), $scopeBeforeAssignEval, $storage); $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes()); } else { @@ -505,6 +506,11 @@ public function processAssignVar( $scope = $scope->assignExpression($expr, $type, $nativeType); } + if (isset($preservedDimFetchTypes)) { + $scope = $scope->restoreExpressionTypes($preservedDimFetchTypes); + unset($preservedDimFetchTypes); + } + $setVarType = $scope->getType($originalVar->var); if ( !$setVarType instanceof ErrorType diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 4ce247488f..810cd2025b 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2857,6 +2857,68 @@ public function assignInitializedProperty(Type $fetchedOnType, string $propertyN return $this->assignExpression(new PropertyInitializationExpr($propertyName), new MixedType(), new MixedType()); } + /** + * @return list + */ + public function getDynamicArrayDimFetchExpressionTypes(string $variableName): array + { + $result = []; + foreach ($this->expressionTypes as $exprString => $exprTypeHolder) { + $expr = $exprTypeHolder->getExpr(); + if (!$expr instanceof Expr\ArrayDimFetch) { + continue; + } + if (!$expr->var instanceof Variable || !is_string($expr->var->name) || $expr->var->name !== $variableName) { + continue; + } + if ($expr->dim === null || $expr->dim instanceof Scalar) { + continue; + } + $nativeHolder = $this->nativeExpressionTypes[$exprString] ?? $exprTypeHolder; + $result[] = [$exprString, $exprTypeHolder, $nativeHolder]; + } + return $result; + } + + /** + * @param list $holders + */ + public function restoreExpressionTypes(array $holders): self + { + $expressionTypes = $this->expressionTypes; + $nativeExpressionTypes = $this->nativeExpressionTypes; + $changed = false; + foreach ($holders as [$exprString, $holder, $nativeHolder]) { + if (isset($expressionTypes[$exprString])) { + continue; + } + $expressionTypes[$exprString] = $holder; + $nativeExpressionTypes[$exprString] = $nativeHolder; + $changed = true; + } + if (!$changed) { + return $this; + } + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $expressionTypes, + $nativeExpressionTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + $this->currentlyAssignedExpressions, + $this->currentlyAllowedUndefinedExpressions, + $this->inFunctionCallsStack, + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); + } + public function invalidateExpression(Expr $expressionToInvalidate, bool $requireMoreCharacters = false, ?ClassReflection $invalidatingClass = null): self { $expressionTypes = $this->expressionTypes; diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 00783f481b..a58f0cfadc 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1263,4 +1263,12 @@ public function testBug14308(): void $this->analyse([__DIR__ . '/data/bug-14308.php'], []); } + #[RequiresPhp('>= 8.1')] + public function testBug14347(): void + { + $this->reportPossiblyNonexistentConstantArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-14347.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-14347.php b/tests/PHPStan/Rules/Arrays/data/bug-14347.php new file mode 100644 index 0000000000..1de995e91a --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-14347.php @@ -0,0 +1,38 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug14347; + +enum Suit: string { + case Clubs = 'c'; + case Diamonds = 'd'; + case Hearts = 'h'; + case Spades = 's'; +} +/** + * @param array $cards + * @return array + */ +function countCards(array $cards): array { + $cardCounts = ['all' => 0]; + foreach ($cards as $card) { + $cardCounts['all']++; + $cardCounts[$card->value] ??= 0; + $cardCounts[$card->value]++; + } + return $cardCounts; +} +/** + * @param array $cards + * @return array + */ +function countCardsBroken(array $cards): array { + $cardCounts = ['all' => 0]; + foreach ($cards as $card) { + $cardCounts[$card->value] ??= 0; + $cardCounts['all']++; + $cardCounts[$card->value]++; + } + return $cardCounts; +}