diff --git a/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php index 1220eabab0..2e03074b37 100644 --- a/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php @@ -37,6 +37,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n if (count($node->getArgs()) < 2) { return new SpecifiedTypes(); } + $objectOrClassType = $scope->getType($node->getArgs()[0]->value); $classType = $scope->getType($node->getArgs()[1]->value); $allowStringType = isset($node->getArgs()[2]) ? $scope->getType($node->getArgs()[2]->value) : new ConstantBooleanType(false); $allowString = !$allowStringType->equals(new ConstantBooleanType(false)); @@ -47,7 +48,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n return $this->typeSpecifier->create( $node->getArgs()[0]->value, - $this->isAFunctionTypeSpecifyingHelper->determineType($classType, $allowString), + $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, true), $context, false, $scope, diff --git a/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php index 8a689e6053..c608156f84 100644 --- a/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php +++ b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php @@ -6,28 +6,37 @@ use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; +use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; final class IsAFunctionTypeSpecifyingHelper { public function determineType( + Type $objectOrClassType, Type $classType, bool $allowString, + bool $allowSameClass, ): Type { + $objectOrClassTypeClassName = $this->determineClassNameFromObjectOrClassType($objectOrClassType, $allowString); + return TypeTraverser::map( $classType, - static function (Type $type, callable $traverse) use ($allowString): Type { + static function (Type $type, callable $traverse) use ($objectOrClassTypeClassName, $allowString, $allowSameClass): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } if ($type instanceof ConstantStringType) { + if (!$allowSameClass && $type->getValue() === $objectOrClassTypeClassName) { + return new NeverType(); + } if ($allowString) { return TypeCombinator::union( new ObjectType($type->getValue()), @@ -59,4 +68,17 @@ static function (Type $type, callable $traverse) use ($allowString): Type { ); } + private function determineClassNameFromObjectOrClassType(Type $type, bool $allowString): ?string + { + if ($type instanceof TypeWithClassName) { + return $type->getClassName(); + } + + if ($allowString && $type instanceof ConstantStringType) { + return $type->getValue(); + } + + return null; + } + } diff --git a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php index 15c773784e..2740401e4f 100644 --- a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php @@ -37,6 +37,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n if (count($node->getArgs()) < 2) { return new SpecifiedTypes(); } + $objectOrClassType = $scope->getType($node->getArgs()[0]->value); $classType = $scope->getType($node->getArgs()[1]->value); $allowStringType = isset($node->getArgs()[2]) ? $scope->getType($node->getArgs()[2]->value) : new ConstantBooleanType(true); $allowString = !$allowStringType->equals(new ConstantBooleanType(false)); @@ -47,7 +48,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n return $this->typeSpecifier->create( $node->getArgs()[0]->value, - $this->isAFunctionTypeSpecifyingHelper->determineType($classType, $allowString), + $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, false), $context, false, $scope, diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 7adb46e867..2620bf7552 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -80,6 +80,7 @@ public function dataFileAsserts(): iterable } yield from $this->gatherAssertTypes(__DIR__ . '/data/is-numeric.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/is-a.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/is-subclass-of.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3142.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-shapes-keys-strings.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1216.php'); @@ -754,6 +755,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6505.php'); } + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6305.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6699.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6715.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6682.php'); diff --git a/tests/PHPStan/Analyser/data/bug-6305.php b/tests/PHPStan/Analyser/data/bug-6305.php new file mode 100644 index 0000000000..5cf5d353b4 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6305.php @@ -0,0 +1,19 @@ +', $foo); + \PHPStan\Testing\assertType('class-string', $foo); } }; @@ -26,3 +28,55 @@ function (string $foo, string $someString) { \PHPStan\Testing\assertType('class-string', $foo); } }; + +function (Bar $a, Bar $b, Bar $c, Bar $d) { + if (is_a($a, Bar::class)) { + \PHPStan\Testing\assertType('IsA\Bar', $a); + } + + if (is_a($b, Foo::class)) { + \PHPStan\Testing\assertType('IsA\Bar', $b); + } + + /** @var class-string $barClassString */ + $barClassString = 'Bar'; + if (is_a($c, $barClassString)) { + \PHPStan\Testing\assertType('IsA\Bar', $c); + } + + /** @var class-string $fooClassString */ + $fooClassString = 'Foo'; + if (is_a($d, $fooClassString)) { + \PHPStan\Testing\assertType('IsA\Bar', $d); + } +}; + +function (string $a, string $b, string $c, string $d) { + /** @var class-string $a */ + if (is_a($a, Bar::class, true)) { + \PHPStan\Testing\assertType('class-string', $a); + } + + /** @var class-string $b */ + if (is_a($b, Foo::class, true)) { + \PHPStan\Testing\assertType('class-string', $b); + } + + /** @var class-string $c */ + /** @var class-string $barClassString */ + $barClassString = 'Bar'; + if (is_a($c, $barClassString, true)) { + \PHPStan\Testing\assertType('class-string', $c); + } + + /** @var class-string $d */ + /** @var class-string $fooClassString */ + $fooClassString = 'Foo'; + if (is_a($d, $fooClassString, true)) { + \PHPStan\Testing\assertType('class-string', $d); + } +}; + +class Foo {} + +class Bar extends Foo {} diff --git a/tests/PHPStan/Analyser/data/is-subclass-of.php b/tests/PHPStan/Analyser/data/is-subclass-of.php new file mode 100644 index 0000000000..1469236ea5 --- /dev/null +++ b/tests/PHPStan/Analyser/data/is-subclass-of.php @@ -0,0 +1,55 @@ + $barClassString */ + $barClassString = 'Bar'; + if (is_subclass_of($c, $barClassString)) { + \PHPStan\Testing\assertType('IsSubclassOf\Bar', $c); + } + + /** @var class-string $fooClassString */ + $fooClassString = 'Foo'; + if (is_subclass_of($d, $fooClassString)) { + \PHPStan\Testing\assertType('IsSubclassOf\Bar', $d); + } +}; + +function (string $a, string $b, string $c, string $d) { + /** @var class-string $a */ + if (is_subclass_of($a, Bar::class)) { + \PHPStan\Testing\assertType('class-string', $a); + } + + /** @var class-string $b */ + if (is_subclass_of($b, Foo::class)) { + \PHPStan\Testing\assertType('class-string', $b); + } + + /** @var class-string $c */ + /** @var class-string $barClassString */ + $barClassString = 'Bar'; + if (is_subclass_of($c, $barClassString)) { + \PHPStan\Testing\assertType('class-string', $c); + } + + /** @var class-string $d */ + /** @var class-string $fooClassString */ + $fooClassString = 'Foo'; + if (is_subclass_of($d, $fooClassString)) { + \PHPStan\Testing\assertType('class-string', $d); + } +}; + +class Foo {} + +class Bar extends Foo {} diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 02ef452f79..f057e1aa81 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -448,6 +448,22 @@ public function testBug3766(): void $this->analyse([__DIR__ . '/data/bug-3766.php'], []); } + public function testBug6305(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6305.php'], [ + [ + 'Call to function is_subclass_of() with Bug6305\B and \'Bug6305\\\A\' will always evaluate to true.', + 11, + ], + [ + 'Call to function is_subclass_of() with Bug6305\B and \'Bug6305\\\B\' will always evaluate to false.', + 14, + ], + ]); + } + public function testBug6698(): void { $this->checkAlwaysTrueCheckTypeFunctionCall = true; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6305.php b/tests/PHPStan/Rules/Comparison/data/bug-6305.php new file mode 100644 index 0000000000..c4d142a276 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6305.php @@ -0,0 +1,15 @@ +