diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index cbed5105ae..0bad74200b 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 = []; @@ -4051,10 +4051,7 @@ private function enterForeach(MutatingScope $scope, ExpressionResultStorage $sto ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)) && ($stmt->keyVar === null || ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name))) ) { - $keyVarName = null; - if ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)) { - $keyVarName = $stmt->keyVar->name; - } + $keyVarName = $stmt->keyVar instanceof Variable ? $stmt->keyVar->name : null; $scope = $scope->enterForeach( $originalScope, $stmt->expr, diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 9f7a86eb4a..972c5cd7d3 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -83,6 +83,7 @@ use PHPStan\Type\UnionType; use function array_key_exists; use function array_key_first; +use function array_keys; use function array_last; use function array_map; use function array_merge; @@ -596,7 +597,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 +755,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 +805,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 +2080,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 +2103,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; } @@ -2117,18 +2112,10 @@ private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes } $conditions = $conditionExpressionTypes; - foreach ($conditions as $conditionExprString => $conditionExprTypeHolder) { - $conditionExpr = $conditionExprTypeHolder->getExpr(); - if (!$conditionExpr instanceof Expr\Variable) { - continue; - } - if (!is_string($conditionExpr->name)) { + foreach (array_keys($conditions) as $conditionExprString) { + if ($conditionExprString !== $exprString) { continue; } - if ($conditionExpr->name !== $expr->name) { - continue; - } - unset($conditions[$conditionExprString]); } @@ -2136,9 +2123,10 @@ private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes 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 +2137,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 +2278,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 +2295,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; } @@ -2311,18 +2304,10 @@ private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTy } $conditions = $conditionExpressionTypes; - foreach ($conditions as $conditionExprString => $conditionExprTypeHolder) { - $conditionExpr = $conditionExprTypeHolder->getExpr(); - if (!$conditionExpr instanceof Expr\Variable) { + foreach (array_keys($conditions) as $conditionExprString) { + if ($conditionExprString !== $exprString) { continue; } - if (!is_string($conditionExpr->name)) { - continue; - } - if ($conditionExpr->name !== $expr->name) { - continue; - } - unset($conditions[$conditionExprString]); } @@ -2330,9 +2315,10 @@ private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTy 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; } 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-12517.php b/tests/PHPStan/Analyser/nsrt/bug-12517.php new file mode 100644 index 0000000000..d3e03e8b93 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12517.php @@ -0,0 +1,76 @@ +a !== null || $foo->b !== null) { + if ($foo->a === null) { + assertType('null', $foo->a); + assertType('mixed~null', $foo->b); + } + } + + $a = $foo->a; + $b = $foo->b; + if ($a !== null || $b !== null) { + if ($a === null) { + assertType('null', $a); + assertType('mixed~null', $b); + } + } + } +} + +class Test +{ + /** @var mixed */ + public static $a = null; + /** @var mixed */ + public static $b = null; + + public function sayHello(): void + { + if (Test::$a !== null || Test::$b !== null) { + if (Test::$a === null) { + assertType('null', Test::$a); + assertType('mixed~null', Test::$b); + } + } + + $a = Test::$a; + $b = Test::$b; + if ($a !== null || $b !== null) { + if ($a === null) { + assertType('null', $a); + assertType('mixed~null', $b); + } + } + } +} + +class WithArray +{ + public function sayHello(array $array): void + { + if ($array['a'] !== null || $array['b'] !== null) { + if ($array['a'] === null) { + assertType('null', $array['a']); + assertType('mixed~null', $array['b']); + } + } + + $a = $array['a']; + $b = $array['b']; + if ($a !== null || $b !== null) { + if ($a === null) { + assertType('null', $a); + assertType('mixed~null', $b); + } + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/pr-5596.php b/tests/PHPStan/Analyser/nsrt/pr-5596.php new file mode 100644 index 0000000000..7b53949871 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/pr-5596.php @@ -0,0 +1,42 @@ +name instanceof Identifier) { + assertType('PhpParser\Node\Expr\MethodCall', $node); + assertType('PhpParser\Node\Identifier', $node->name); + assertType('PhpParser\Node\Expr', $node->var); + } elseif ($node instanceof StaticCall && $node->name instanceof Identifier && $node->class instanceof Name) { + assertType('PhpParser\Node\Expr\StaticCall', $node); + assertType('PhpParser\Node\Identifier', $node->name); + assertType('PhpParser\Node\Name', $node->class); + } elseif ($node instanceof New_ && $node->class instanceof Name) { + assertType('PhpParser\Node\Expr\New_', $node); + assertType('PhpParser\Node\Name', $node->class); + } elseif ($node instanceof FuncCall && $node->name instanceof Name) { + assertType('PhpParser\Node\Expr\FuncCall', $node); + assertType('PhpParser\Node\Name', $node->name); + } elseif ($node instanceof FuncCall) { + assertType('PhpParser\Node\Expr\FuncCall', $node); + assertType('PhpParser\Node\Expr', $node->name); + } + } +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 9a2a8e1a9f..8b4e9fd5bc 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -4009,6 +4009,14 @@ public function testBug10422(): void $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10422.php'], []); } + public function testBug9155(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-9155.php'], []); + } + public function testBug13272(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index a931ed6379..85937c21b0 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -975,6 +975,12 @@ public function testBug12558(): void ]); } + public function testBug6486(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-6486.php'], []); + } + #[RequiresPhp('>= 8.5.0')] public function testPipeOperator(): void { diff --git a/tests/PHPStan/Rules/Methods/data/bug-6486.php b/tests/PHPStan/Rules/Methods/data/bug-6486.php new file mode 100644 index 0000000000..99f0819d1b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-6486.php @@ -0,0 +1,44 @@ +only && !$this->exclude) { + return true; + } + + if ($this->only) { + return Preg::isMatch($this->only, $name); + } + + return !Preg::isMatch($this->exclude, $name); + } +} + +class Preg { + /** + * @param non-empty-string $pattern + * @param string $subject + * @param array $matches Set by method + * @param int $flags PREG_UNMATCHED_AS_NULL, only available on PHP 7.2+ + * @param int $offset + * @return bool + */ + public static function isMatch($pattern, $subject, &$matches = null, $flags = 0, $offset = 0) + { + return true; + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9155.php b/tests/PHPStan/Rules/Methods/data/bug-9155.php new file mode 100644 index 0000000000..1b502f5109 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9155.php @@ -0,0 +1,36 @@ +foo && null === $this->bar) { + return; + } + + if (null === $this->foo && !$this->bar->barF()) { + echo 1; + } + } +}