diff --git a/src/Analyser/ExpressionTypeHolder.php b/src/Analyser/ExpressionTypeHolder.php index 7477dbf3dc..c7994b6db1 100644 --- a/src/Analyser/ExpressionTypeHolder.php +++ b/src/Analyser/ExpressionTypeHolder.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr; use PHPStan\TrinaryLogic; +use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -50,6 +51,23 @@ public function equals(self $other): bool return $this->type === $other->type || $this->type->equals($other->type); } + public function isSuperTypeOf(self $other): IsSuperTypeOfResult + { + if ($this === $other) { + return IsSuperTypeOfResult::createYes(); + } + + if (!$this->certainty->equals($other->certainty)) { + return IsSuperTypeOfResult::createNo(); + } + + if ($this->type === $other->type) { + return IsSuperTypeOfResult::createYes(); + } + + return $this->type->isSuperTypeOf($other->type); + } + public function and(self $other): self { if ($this->type === $other->type || $this->type->equals($other->type)) { diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index a43ba35dda..68a570bfce 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3216,15 +3216,42 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self if (array_key_exists($conditionalExprString, $conditions)) { continue; } + + $exactMatches = []; + $superTypeMatches = []; foreach ($conditionalExpressions as $conditionalExpression) { + $allExact = true; + $allSuperType = true; foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) { - if (!array_key_exists($holderExprString, $specifiedExpressions) || !$specifiedExpressions[$holderExprString]->equals($conditionalTypeHolder)) { - continue 2; + if (!array_key_exists($holderExprString, $specifiedExpressions)) { + $allExact = false; + $allSuperType = false; + break; + } + if ($conditionalTypeHolder->equals($specifiedExpressions[$holderExprString])) { + continue; + } + + $allExact = false; + if (!$conditionalTypeHolder->isSuperTypeOf($specifiedExpressions[$holderExprString])->yes()) { + $allSuperType = false; + break; } } + if ($allExact) { + $exactMatches[] = $conditionalExpression; + } elseif ($allSuperType) { + $superTypeMatches[] = $conditionalExpression; + } + } - $conditions[$conditionalExprString][] = $conditionalExpression; - $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder(); + if (count($exactMatches) > 0) { + foreach ($exactMatches as $exactMatch) { + $conditions[$conditionalExprString][] = $exactMatch; + $specifiedExpressions[$conditionalExprString] = $exactMatch->getTypeHolder(); + } + } elseif (count($superTypeMatches) === 1 && count($conditionalExpressions) >= 2) { + $conditions[$conditionalExprString][] = $superTypeMatches[0]; } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-10422.php b/tests/PHPStan/Analyser/nsrt/bug-10422.php new file mode 100644 index 0000000000..3b910a253b --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10422.php @@ -0,0 +1,42 @@ +something()) { + $error = 'another'; + } + if ($error) { + die('Done'); + } + assertType(Foo::class, $test); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4090-regression.php b/tests/PHPStan/Analyser/nsrt/bug-4090-regression.php new file mode 100644 index 0000000000..0b254291da --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4090-regression.php @@ -0,0 +1,37 @@ + $aggregation + */ + private function parseAggregation(array $aggregation): ?string + { + $type = $aggregation['type'] ?? null; + if (!\is_string($type) || empty($type) || is_numeric($type)) { + return null; + } + + if (empty($aggregation['field']) && $type !== 'filter') { + return null; + } + + $field = ''; + if ($type !== 'filter') { + $field = self::buildFieldName(); + } + + assertType('non-falsy-string', $type); + + return $field; + } + + private static function buildFieldName(): string + { + return 'field'; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4090.php b/tests/PHPStan/Analyser/nsrt/bug-4090.php new file mode 100644 index 0000000000..1617f42f42 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4090.php @@ -0,0 +1,50 @@ + 1) { + assertType('non-empty-array', $a); + echo implode(',', $a); + } elseif (count($a) === 1) { + assertType('non-empty-array', $a); + echo trim(current($a)); + } +} + + +/** @param string[] $a */ +function bar(array $a): void +{ + $count = count($a); + if ($count > 1) { + assertType('non-empty-array', $a); + echo implode(',', $a); + } elseif ($count === 1) { + assertType('non-empty-array', $a); + echo trim(current($a)); + } +} + + +/** @param string[] $a */ +function qux(array $a): void +{ + switch (count($a)) { + case 0: + assertType('array{}', $a); + break; + case 1: + assertType('non-empty-array', $a); + echo trim(current($a)); + break; + default: + assertType('non-empty-array', $a); + echo implode(',', $a); + break; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-5051.php b/tests/PHPStan/Analyser/nsrt/bug-5051.php index 6c3e80dce1..94ffc4711c 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-5051.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5051.php @@ -60,27 +60,27 @@ public function testWithBooleans($data): void assertType('bool', $update); } else { assertType('1|2', $data); - assertType('bool', $update); + assertType('false', $update); } if ($data === 1) { - assertType('bool', $update); - assertType('bool', $foo); + assertType('false', $update); + assertType('false', $foo); } else { assertType('bool', $update); assertType('bool', $foo); } if ($data === 2) { - assertType('bool', $update); - assertType('bool', $foo); + assertType('false', $update); + assertType('false', $foo); } else { assertType('bool', $update); assertType('bool', $foo); } if ($data === 3) { - assertType('bool', $update); + assertType('false', $update); assertType('true', $foo); } else { assertType('bool', $update); @@ -88,7 +88,7 @@ public function testWithBooleans($data): void } if ($data === 1 || $data === 2) { - assertType('bool', $update); + assertType('false', $update); assertType('false', $foo); } else { assertType('bool', $update); diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index ee754a40d9..e3f03f7683 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1851,6 +1851,11 @@ public function testBug10527(): void $this->analyse([__DIR__ . '/data/bug-10527.php'], []); } + public function testBug10055(): void + { + $this->analyse([__DIR__ . '/data/bug-10055.php'], []); + } + public function testBug10626(): void { $this->analyse([__DIR__ . '/data/bug-10626.php'], [ diff --git a/tests/PHPStan/Rules/Functions/data/bug-10055.php b/tests/PHPStan/Rules/Functions/data/bug-10055.php new file mode 100644 index 0000000000..a2caa08b73 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-10055.php @@ -0,0 +1,26 @@ + expectInt($param2), + 'value2' => expectInt($param2), + 'value3' => expectBool($param2), + }; +} diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index b6161df233..7549009c10 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1476,6 +1476,25 @@ public function testBug14227(): void $this->analyse([__DIR__ . '/data/bug-14227.php'], []); } + public function testBug12597(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-12597.php'], []); + } + + #[RequiresPhp('>= 8.0')] + public function testBug12597NonFinite(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-12597-non-finite.php'], []); + } + public function testBug14117(): void { $this->cliArgumentsVariablesRegistered = true; @@ -1499,4 +1518,14 @@ public function testBug14117(): void ]); } + public function testBug11218(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-11218.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-11218.php b/tests/PHPStan/Rules/Variables/data/bug-11218.php new file mode 100644 index 0000000000..a0b27708dd --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-11218.php @@ -0,0 +1,29 @@ +message($message); + } + } + + public function message(string $message): void {} +} + +class Foo {} +class Bar {} + +class HelloWorld2 +{ + public function test(mixed $type): void + { + if (is_int($type) || is_object($type)) { + $message = 'Hello!'; + } + + if (is_int($type)) { + $this->message($message); + } + } + + public function test2(mixed $type): void + { + if ($type instanceof Foo || $type instanceof Bar) { + $message = 'Hello!'; + } + + if ($type instanceof Foo) { + $this->message($message); + } + } + + public function message(string $message): void + { + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-12597.php b/tests/PHPStan/Rules/Variables/data/bug-12597.php new file mode 100644 index 0000000000..2cfdf45974 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-12597.php @@ -0,0 +1,22 @@ +message($message); + } + } + + public function message(string $message): void {} +}