diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4b4096c92c..2df1d19fa3 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1587,12 +1587,6 @@ parameters: count: 1 path: src/Type/Php/IsAFunctionTypeSpecifyingExtension.php - - - rawMessage: 'Doing instanceof PHPStan\Type\Generic\GenericClassStringType is error-prone and deprecated. Use Type::isClassStringType() and Type::getClassStringObjectType() instead.' - identifier: phpstanApi.instanceofType - count: 2 - path: src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php - - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' identifier: phpstanApi.instanceofType diff --git a/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php index 5d19e4950f..feecd449be 100644 --- a/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php @@ -50,9 +50,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $allowString = !$allowStringType->equals(new ConstantBooleanType(false)); $resultType = $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, true); - - // prevent false-positives in IsAFunctionTypeSpecifyingHelper - if ($classType->getConstantStrings() === [] && $resultType->isSuperTypeOf($objectOrClassType)->yes()) { + if ($resultType === null) { return new SpecifiedTypes([], []); } diff --git a/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php index df33262d8d..dfd50d83d7 100644 --- a/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php +++ b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php @@ -16,6 +16,7 @@ use PHPStan\Type\UnionType; use function array_unique; use function array_values; +use function in_array; #[AutowiredService] final class IsAFunctionTypeSpecifyingHelper @@ -26,7 +27,7 @@ public function determineType( Type $classType, bool $allowString, bool $allowSameClass, - ): Type + ): ?Type { $objectOrClassTypeClassNames = $objectOrClassType->getObjectClassNames(); if ($allowString) { @@ -36,15 +37,39 @@ public function determineType( $objectOrClassTypeClassNames = array_values(array_unique($objectOrClassTypeClassNames)); } - return TypeTraverser::map( + $isUncertain = $classType->getConstantStrings() === []; + + $resultType = TypeTraverser::map( $classType, - static function (Type $type, callable $traverse) use ($objectOrClassTypeClassNames, $allowString, $allowSameClass): Type { + static function (Type $type, callable $traverse) use ($objectOrClassType, $objectOrClassTypeClassNames, $allowString, $allowSameClass, &$isUncertain): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } if ($type instanceof ConstantStringType) { - if (!$allowSameClass && $objectOrClassTypeClassNames === [$type->getValue()]) { - return new NeverType(); + if (!$allowSameClass) { + if ($objectOrClassTypeClassNames === [$type->getValue()]) { + $isSameClass = true; + foreach ($objectOrClassType->getObjectClassReflections() as $classReflection) { + if (!$classReflection->isFinal()) { + $isSameClass = false; + break; + } + } + + if ($isSameClass) { + return new NeverType(); + } + } + + if ( + // For object, as soon as the exact same type is provided + // in the list we cannot be sure of the result + in_array($type->getValue(), $objectOrClassTypeClassNames, true) + // This also occurs for generic class string + || ($allowString && $objectOrClassTypeClassNames === [] && $objectOrClassType->isSuperTypeOf($type)->yes()) + ) { + $isUncertain = true; + } } if ($allowString) { return TypeCombinator::union( @@ -75,6 +100,13 @@ static function (Type $type, callable $traverse) use ($objectOrClassTypeClassNam return new ObjectWithoutClassType(); }, ); + + // prevent false-positives + if ($isUncertain && $resultType->isSuperTypeOf($objectOrClassType)->yes()) { + return null; + } + + return $resultType; } } diff --git a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php index e910bd5ea1..001247124d 100644 --- a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php @@ -12,7 +12,6 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\FunctionTypeSpecifyingExtension; -use PHPStan\Type\Generic\GenericClassStringType; use function count; use function strtolower; @@ -45,15 +44,8 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $allowStringType = isset($node->getArgs()[2]) ? $scope->getType($node->getArgs()[2]->value) : new ConstantBooleanType(true); $allowString = !$allowStringType->equals(new ConstantBooleanType(false)); - // prevent false-positives in IsAFunctionTypeSpecifyingHelper - if ($objectOrClassType instanceof GenericClassStringType && $classType instanceof GenericClassStringType) { - return new SpecifiedTypes([], []); - } - $resultType = $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, false); - - // prevent false-positives in IsAFunctionTypeSpecifyingHelper - if ($classType->getConstantStrings() === [] && $resultType->isSuperTypeOf($objectOrClassType)->yes()) { + if ($resultType === null) { return new SpecifiedTypes([], []); } diff --git a/tests/PHPStan/Analyser/nsrt/is-subclass-of.php b/tests/PHPStan/Analyser/nsrt/is-subclass-of.php index 1469236ea5..08602c6279 100644 --- a/tests/PHPStan/Analyser/nsrt/is-subclass-of.php +++ b/tests/PHPStan/Analyser/nsrt/is-subclass-of.php @@ -4,7 +4,7 @@ function (Bar $a, Bar $b, Bar $c, Bar $d) { if (is_subclass_of($a, Bar::class)) { - \PHPStan\Testing\assertType('*NEVER*', $a); + \PHPStan\Testing\assertType('IsSubclassOf\Bar', $a); // Can still be a Bar child } if (is_subclass_of($b, Foo::class)) { @@ -53,3 +53,11 @@ function (string $a, string $b, string $c, string $d) { class Foo {} class Bar extends Foo {} + +final class FinalFoo {} + +function (FinalFoo $a) { + if (is_subclass_of($a, FinalFoo::class)) { + \PHPStan\Testing\assertType('*NEVER*', $a); + } +}; diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index b6f79eeeeb..94a9f26501 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -400,6 +400,28 @@ public function testBug6305(): void ]); } + public function testBug6305b(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6305b.php'], []); + } + + public function testBug13713(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-13713.php'], [ + [ + "Call to function is_subclass_of() with arguments Bug13713\\test, 'stdClass' and false will always evaluate to true.", + 12, + ], + [ + "Call to function is_subclass_of() with arguments class-string, 'stdClass' and true will always evaluate to true.", + 25, + 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', + ], + ]); + } + public function testBug6698(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13713.php b/tests/PHPStan/Rules/Comparison/data/bug-13713.php new file mode 100644 index 0000000000..166d811ced --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-13713.php @@ -0,0 +1,28 @@ + $stdClass + * @param class-string $test + */ +function debugWithClass(string $stdClass, string $test): void { + echo var_export(\is_subclass_of($stdClass, \stdClass::class, true), true) . \PHP_EOL; + echo var_export(\is_subclass_of($test, \stdClass::class, true), true) . \PHP_EOL; +} + +debugWithClass(test::class, test::class); diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6305b.php b/tests/PHPStan/Rules/Comparison/data/bug-6305b.php new file mode 100644 index 0000000000..61e758f0a3 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6305b.php @@ -0,0 +1,23 @@ +