diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index cbed5105ae..abd0066be9 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3917,7 +3917,7 @@ private function tryProcessUnrolledConstantArrayForeach( $matchedNativeArrays = count($nativeConstantArrays) === count($constantArrays) ? $nativeConstantArrays : null; $valueVarName = $stmt->valueVar->name; - $keyVarName = $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name) ? $stmt->keyVar->name : null; + $keyVarName = $stmt->keyVar instanceof Variable ? $stmt->keyVar->name : null; $allBodyScopes = []; $allChainScopes = []; @@ -4052,7 +4052,7 @@ private function enterForeach(MutatingScope $scope, ExpressionResultStorage $sto && ($stmt->keyVar === null || ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name))) ) { $keyVarName = null; - if ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)) { + if ($stmt->keyVar instanceof Variable) { $keyVarName = $stmt->keyVar->name; } $scope = $scope->enterForeach( diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 9f7a86eb4a..0649379fdf 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -596,7 +596,7 @@ public function specifyTypesInCondition( } return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); - } elseif ($expr instanceof FuncCall && !($expr->name instanceof Name)) { + } elseif ($expr instanceof FuncCall) { $specifiedTypes = $this->specifyTypesFromCallableCall($context, $expr, $scope); if ($specifiedTypes !== null) { return $specifiedTypes; @@ -754,10 +754,10 @@ public function specifyTypesInCondition( $result = $result->setAlwaysOverwriteTypes(); } return $result->setNewConditionalExpressionHolders(array_merge( - $this->processBooleanNotSureConditionalTypes($scope, $leftTypesForHolders, $rightTypesForHolders), - $this->processBooleanNotSureConditionalTypes($scope, $rightTypesForHolders, $leftTypesForHolders), - $this->processBooleanSureConditionalTypes($scope, $leftTypesForHolders, $rightTypesForHolders), - $this->processBooleanSureConditionalTypes($scope, $rightTypesForHolders, $leftTypesForHolders), + $this->processBooleanNotSureConditionalTypes($scope, $leftTypesForHolders, $rightTypesForHolders, $rightScope), + $this->processBooleanNotSureConditionalTypes($scope, $rightTypesForHolders, $leftTypesForHolders, $scope), + $this->processBooleanSureConditionalTypes($scope, $leftTypesForHolders, $rightTypesForHolders, $rightScope), + $this->processBooleanSureConditionalTypes($scope, $rightTypesForHolders, $leftTypesForHolders, $scope), ))->setRootExpr($expr); } @@ -804,10 +804,10 @@ public function specifyTypesInCondition( $result = $result->setAlwaysOverwriteTypes(); } return $result->setNewConditionalExpressionHolders(array_merge( - $this->processBooleanNotSureConditionalTypes($scope, $leftTypes, $rightTypes), - $this->processBooleanNotSureConditionalTypes($scope, $rightTypes, $leftTypes), - $this->processBooleanSureConditionalTypes($scope, $leftTypes, $rightTypes), - $this->processBooleanSureConditionalTypes($scope, $rightTypes, $leftTypes), + $this->processBooleanNotSureConditionalTypes($scope, $leftTypes, $rightTypes, $rightScope), + $this->processBooleanNotSureConditionalTypes($scope, $rightTypes, $leftTypes, $scope), + $this->processBooleanSureConditionalTypes($scope, $leftTypes, $rightTypes, $rightScope), + $this->processBooleanSureConditionalTypes($scope, $rightTypes, $leftTypes, $scope), ))->setRootExpr($expr); } @@ -2079,14 +2079,11 @@ private function augmentBooleanOrTruthyWithConditionalHolders(MutatingScope $sco /** * @return array */ - private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): array + private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes, Scope $rightScope): array { $conditionExpressionTypes = []; foreach ($leftTypes->getSureTypes() as $exprString => [$expr, $type]) { - if (!$expr instanceof Expr\Variable) { - continue; - } - if (!is_string($expr->name)) { + if (!$this->isTrackableExpression($expr)) { continue; } @@ -2105,10 +2102,7 @@ private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes if (count($conditionExpressionTypes) > 0) { $holders = []; foreach ($rightTypes->getSureTypes() as $exprString => [$expr, $type]) { - if (!$expr instanceof Expr\Variable) { - continue; - } - if (!is_string($expr->name)) { + if (!$this->isTrackableExpression($expr)) { continue; } @@ -2118,27 +2112,19 @@ private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes $conditions = $conditionExpressionTypes; foreach ($conditions as $conditionExprString => $conditionExprTypeHolder) { - $conditionExpr = $conditionExprTypeHolder->getExpr(); - if (!$conditionExpr instanceof Expr\Variable) { - continue; + if ($conditionExprString === $exprString) { + unset($conditions[$conditionExprString]); } - if (!is_string($conditionExpr->name)) { - continue; - } - if ($conditionExpr->name !== $expr->name) { - continue; - } - - unset($conditions[$conditionExprString]); } if (count($conditions) === 0) { continue; } + $targetScope = $expr instanceof Expr\Variable ? $scope : $rightScope; $holder = new ConditionalExpressionHolder( $conditions, - ExpressionTypeHolder::createYes($expr, TypeCombinator::intersect($scope->getType($expr), $type)), + ExpressionTypeHolder::createYes($expr, TypeCombinator::intersect($targetScope->getType($expr), $type)), ); $holders[$exprString][$holder->getKey()] = $holder; } @@ -2149,6 +2135,17 @@ private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes return []; } + private function isTrackableExpression(Expr $expr): bool + { + if ($expr instanceof Expr\Variable) { + return is_string($expr->name); + } + + return $expr instanceof Expr\PropertyFetch + || $expr instanceof Expr\ArrayDimFetch + || $expr instanceof Expr\StaticPropertyFetch; + } + /** * Flatten a deep BooleanOr chain into leaf expressions and process them * without recursive filterByFalseyValue calls. This reduces O(n^2) to O(n) @@ -2279,14 +2276,11 @@ private function specifyTypesForFlattenedBooleanAnd( /** * @return array */ - private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): array + private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes, Scope $rightScope): array { $conditionExpressionTypes = []; foreach ($leftTypes->getSureNotTypes() as $exprString => [$expr, $type]) { - if (!$expr instanceof Expr\Variable) { - continue; - } - if (!is_string($expr->name)) { + if (!$this->isTrackableExpression($expr)) { continue; } @@ -2299,10 +2293,7 @@ private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTy if (count($conditionExpressionTypes) > 0) { $holders = []; foreach ($rightTypes->getSureNotTypes() as $exprString => [$expr, $type]) { - if (!$expr instanceof Expr\Variable) { - continue; - } - if (!is_string($expr->name)) { + if (!$this->isTrackableExpression($expr)) { continue; } @@ -2312,27 +2303,19 @@ private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTy $conditions = $conditionExpressionTypes; foreach ($conditions as $conditionExprString => $conditionExprTypeHolder) { - $conditionExpr = $conditionExprTypeHolder->getExpr(); - if (!$conditionExpr instanceof Expr\Variable) { - continue; - } - if (!is_string($conditionExpr->name)) { - continue; + if ($conditionExprString === $exprString) { + unset($conditions[$conditionExprString]); } - if ($conditionExpr->name !== $expr->name) { - continue; - } - - unset($conditions[$conditionExprString]); } if (count($conditions) === 0) { continue; } + $targetScope = $expr instanceof Expr\Variable ? $scope : $rightScope; $holder = new ConditionalExpressionHolder( $conditions, - ExpressionTypeHolder::createYes($expr, TypeCombinator::remove($scope->getType($expr), $type)), + ExpressionTypeHolder::createYes($expr, TypeCombinator::remove($targetScope->getType($expr), $type)), ); $holders[$exprString][$holder->getKey()] = $holder; } @@ -2586,6 +2569,49 @@ private function createForExpr( } } + if ( + $expr instanceof ArrayDimFetch + && $expr->dim !== null + && !$context->null() + ) { + $dimType = $scope->getType($expr->dim)->toArrayKey(); + if ($dimType instanceof ConstantIntegerType || $dimType->getConstantStrings() !== []) { + $varType = $scope->getType($expr->var); + $constantArrays = $varType->getConstantArrays(); + if ($constantArrays !== []) { + $refinedArrays = []; + $changed = false; + foreach ($constantArrays as $constantArray) { + if (!$constantArray->hasOffsetValueType($dimType)->yes()) { + $refinedArrays[] = $constantArray; + continue; + } + $offsetValueType = $constantArray->getOffsetValueType($dimType); + if ($context->false()) { + $narrowedValueType = TypeCombinator::remove($offsetValueType, $type); + } else { + $narrowedValueType = TypeCombinator::intersect($offsetValueType, $type); + } + if ($narrowedValueType instanceof NeverType) { + $changed = true; + continue; + } + if (!$narrowedValueType->equals($offsetValueType)) { + $changed = true; + $refinedArrays[] = $constantArray->setExistingOffsetValueType($dimType, $narrowedValueType); + } else { + $refinedArrays[] = $constantArray; + } + } + + if ($changed && $refinedArrays !== []) { + $varExprString = $this->exprPrinter->printExpr($expr->var); + $sureTypes[$varExprString] = [$expr->var, TypeCombinator::union(...$refinedArrays)]; + } + } + } + } + $types = new SpecifiedTypes($sureTypes, $sureNotTypes); if (isset($containsNull) && !$containsNull) { return $this->createNullsafeTypes($originalExpr, $scope, $context, $type)->unionWith($types); diff --git a/src/Type/Constant/ConstantArrayTypeBuilder.php b/src/Type/Constant/ConstantArrayTypeBuilder.php index 894fbfc4f1..cd7f5aa026 100644 --- a/src/Type/Constant/ConstantArrayTypeBuilder.php +++ b/src/Type/Constant/ConstantArrayTypeBuilder.php @@ -120,10 +120,6 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt } if ($offsetType === null) { - if (count($this->nextAutoIndexes) === 0) { - return; - } - $newAutoIndexes = $optional ? $this->nextAutoIndexes : []; $hasOptional = false; foreach ($this->keyTypes as $i => $keyType) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-14566.php b/tests/PHPStan/Analyser/nsrt/bug-14566.php new file mode 100644 index 0000000000..ec6005247c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14566.php @@ -0,0 +1,133 @@ +name when $node is CallLike (which has no $name + * property) produces ErrorType. + */ +function fooElseifPropertyNarrowing(\PhpParser\Node\Expr\CallLike $node, \PHPStan\Analyser\Scope $scope): void { + if ($node instanceof \PhpParser\Node\Expr\MethodCall && $node->name instanceof \PhpParser\Node\Identifier) { + assertType('PhpParser\Node\Expr\MethodCall', $node); + assertType('PhpParser\Node\Identifier', $node->name); + } elseif ($node instanceof \PhpParser\Node\Expr\StaticCall && $node->name instanceof \PhpParser\Node\Identifier && $node->class instanceof \PhpParser\Node\Name) { + assertType('PhpParser\Node\Expr\StaticCall', $node); + assertType('PhpParser\Node\Identifier', $node->name); + assertType('PhpParser\Node\Name', $node->class); + } elseif ($node instanceof \PhpParser\Node\Expr\New_ && $node->class instanceof \PhpParser\Node\Name) { + assertType('PhpParser\Node\Expr\New_', $node); + assertType('PhpParser\Node\Name', $node->class); + } elseif ($node instanceof \PhpParser\Node\Expr\FuncCall && $node->name instanceof \PhpParser\Node\Name) { + assertType('PhpParser\Node\Expr\FuncCall', $node); + assertType('PhpParser\Node\Name', $node->name); + } elseif ($node instanceof \PhpParser\Node\Expr\FuncCall) { + assertType('PhpParser\Node\Expr\FuncCall', $node); + assertType('PhpParser\Node\Expr', $node->name); + } +} + +class FooContainer { + /** @var \stdClass|string */ + public $x; + /** @var \stdClass|int */ + public $y; +} + +function fooPropertyFetchInstanceof(FooContainer $c): void { + if ($c->x instanceof \stdClass && $c->y instanceof \stdClass) { + return; + } + if ($c->x instanceof \stdClass) { + assertType('int', $c->y); + } +} + +function fooPropertyFetchInstanceofOr(FooContainer $c): void { + if (!$c->x instanceof \stdClass || !$c->y instanceof \stdClass) { + if ($c->x instanceof \stdClass) { + assertType('int', $c->y); + } + return; + } + assertType('stdClass', $c->x); + assertType('stdClass', $c->y); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7716.php b/tests/PHPStan/Analyser/nsrt/bug-7716.php index 63280c3b46..bd4d38f66e 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7716.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7716.php @@ -15,13 +15,13 @@ public function sayHello(array $array): int $hasBar = isset($array['bar']) && $array['bar'] > 1; if ($hasFoo) { - assertType('array{foo: int, bar?: int}', $array); + assertType('array{foo: int<2, max>, bar?: int}', $array); assertType('int<2, max>', $array['foo']); return $array['foo']; } if ($hasBar) { - assertType('array{foo?: int, bar: int}', $array); + assertType('array{foo?: int, bar: int<2, max>}', $array); assertType('int<2, max>', $array['bar']); return $array['bar']; } @@ -38,13 +38,13 @@ public function sayHello2(array $array): int $hasFoo = isset($array['foo']) && $array['foo'] > 1; if ($hasFoo) { - assertType('array{foo: int, bar?: int}', $array); + assertType('array{foo: int<2, max>, bar?: int}', $array); assertType('int<2, max>', $array['foo']); return $array['foo']; } if ($hasBar) { - assertType('array{foo?: int, bar: int}', $array); + assertType('array{foo?: int, bar: int<2, max>}', $array); assertType('int<2, max>', $array['bar']); return $array['bar']; }