From f2663f1231e14264c291a13a56a6c21c1cb94e89 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: 2025年10月23日 23:54:40 +0200 Subject: [PATCH 1/4] Avoid false error on is_subclass_of --- .../Php/IsAFunctionTypeSpecifyingHelper.php | 9 +++-- ...classOfFunctionTypeSpecifyingExtension.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-6305.php | 2 +- .../PHPStan/Analyser/nsrt/is-subclass-of.php | 2 +- ...mpossibleCheckTypeFunctionCallRuleTest.php | 28 ++++++++++----- .../Rules/Comparison/data/bug-13713.php | 14 ++++++++ .../Rules/Comparison/data/bug-6305b.php | 35 +++++++++++++++++++ 7 files changed, 79 insertions(+), 13 deletions(-) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-13713.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-6305b.php diff --git a/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php index df33262d8d..f24a5fc439 100644 --- a/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php +++ b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php @@ -38,13 +38,18 @@ public function determineType( return 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): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } if ($type instanceof ConstantStringType) { if (!$allowSameClass && $objectOrClassTypeClassNames === [$type->getValue()]) { - return new NeverType(); + // For objectType we cannot be sure since 'Foo' is used for both + // - the Foo class + // - a child of foo class + if ($objectOrClassType->isString()->yes()) { + return new NeverType(); + } } if ($allowString) { return TypeCombinator::union( diff --git a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php index e910bd5ea1..f0be7cb94d 100644 --- a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php @@ -53,7 +53,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $resultType = $this->isAFunctionTypeSpecifyingHelper->determineType($objectOrClassType, $classType, $allowString, false); // prevent false-positives in IsAFunctionTypeSpecifyingHelper - if ($classType->getConstantStrings() === [] && $resultType->isSuperTypeOf($objectOrClassType)->yes()) { + if ($resultType->isSuperTypeOf($objectOrClassType)->yes()) { return new SpecifiedTypes([], []); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-6305.php b/tests/PHPStan/Analyser/nsrt/bug-6305.php index 89bfea9c62..80e8cc4a2e 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6305.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6305.php @@ -15,5 +15,5 @@ class B extends A {} } if (is_subclass_of($b, B::class)) { - assertType('*NEVER*', $b); + assertType('Bug6305Types\B', $b); // Could be NEVER } diff --git a/tests/PHPStan/Analyser/nsrt/is-subclass-of.php b/tests/PHPStan/Analyser/nsrt/is-subclass-of.php index 1469236ea5..25c462f875 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)) { diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index b6f79eeeeb..5540084ebe 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -389,17 +389,29 @@ public function testBug6305(): void { $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, - ], + // [ + // '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 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'], []); + } + 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..2f1aa7432f --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-13713.php @@ -0,0 +1,14 @@ + Date: 2025年10月24日 19:14:35 +0200 Subject: [PATCH 2/4] Rework --- phpstan-baseline.neon | 6 ---- .../IsAFunctionTypeSpecifyingExtension.php | 4 +-- .../Php/IsAFunctionTypeSpecifyingHelper.php | 33 ++++++++++++++++--- ...classOfFunctionTypeSpecifyingExtension.php | 10 +----- ...mpossibleCheckTypeFunctionCallRuleTest.php | 20 ++++++++--- .../Rules/Comparison/data/bug-13713.php | 20 +++++++++-- .../Rules/Comparison/data/bug-6305b.php | 12 ------- 7 files changed, 62 insertions(+), 43 deletions(-) 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 f24a5fc439..2b4a943ab8 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,20 +37,35 @@ 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 ($objectOrClassType, $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()]) { + if (!$allowSameClass) { // For objectType we cannot be sure since 'Foo' is used for both // - the Foo class // - a child of foo class - if ($objectOrClassType->isString()->yes()) { + if ( + $objectOrClassTypeClassNames === [$type->getValue()] + && $objectOrClassType->isString()->yes() + ) { 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( @@ -80,6 +96,13 @@ static function (Type $type, callable $traverse) use ($objectOrClassType, $objec 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 f0be7cb94d..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 ($resultType->isSuperTypeOf($objectOrClassType)->yes()) { + if ($resultType === null) { return new SpecifiedTypes([], []); } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 5540084ebe..d819237215 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -389,10 +389,10 @@ public function testBug6305(): void { $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\\\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, @@ -409,7 +409,17 @@ public function testBug6305b(): void public function testBug13713(): void { $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/bug-13713.php'], []); + $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 diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13713.php b/tests/PHPStan/Rules/Comparison/data/bug-13713.php index 2f1aa7432f..166d811ced 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-13713.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-13713.php @@ -4,11 +4,25 @@ namespace Bug13713; -function debug(object $test): void { - if ($test instanceof \stdClass) { - echo var_export(\is_subclass_of($test, \stdClass::class, false), true) . \PHP_EOL; +function debug(object $object): void { + if ($object instanceof \stdClass) { + echo var_export(\is_subclass_of($object, \stdClass::class, false), true) . \PHP_EOL; + } + if ($object instanceof test) { + echo var_export(\is_subclass_of($object, \stdClass::class, false), true) . \PHP_EOL; } } class test extends \stdClass {} debug(new test); + +/** + * @param class-string<\stdclass> $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 index 44ff8fbcc8..61e758f0a3 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-6305b.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-6305b.php @@ -9,27 +9,15 @@ class B extends A {} $b = mt_rand(0, 1) === 0 ? new B() : new A(); if (is_subclass_of($b, A::class)) { - if (is_subclass_of($b, A::class)) { - echo 'x'; - } } if (is_subclass_of($b, B::class)) { - if (is_subclass_of($b, B::class)) { - echo 'y'; - } } $b = mt_rand(0, 1) === 0 ? A::class : B::class; if (is_subclass_of($b, A::class)) { - if (is_subclass_of($b, A::class)) { - echo 'x'; - } } if (is_subclass_of($b, B::class)) { - if (is_subclass_of($b, B::class)) { - echo 'y'; - } } From 95c28b0e86ec40e26407a9204f4d48220951dc23 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: 2025年10月29日 22:28:52 +0100 Subject: [PATCH 3/4] Improve --- .../Php/IsAFunctionTypeSpecifyingHelper.php | 20 +++++++++++-------- tests/PHPStan/Analyser/nsrt/bug-6305.php | 2 +- ...mpossibleCheckTypeFunctionCallRuleTest.php | 8 ++++---- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php index 2b4a943ab8..dfd50d83d7 100644 --- a/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php +++ b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php @@ -47,14 +47,18 @@ static function (Type $type, callable $traverse) use ($objectOrClassType, $objec } if ($type instanceof ConstantStringType) { if (!$allowSameClass) { - // For objectType we cannot be sure since 'Foo' is used for both - // - the Foo class - // - a child of foo class - if ( - $objectOrClassTypeClassNames === [$type->getValue()] - && $objectOrClassType->isString()->yes() - ) { - return new NeverType(); + if ($objectOrClassTypeClassNames === [$type->getValue()]) { + $isSameClass = true; + foreach ($objectOrClassType->getObjectClassReflections() as $classReflection) { + if (!$classReflection->isFinal()) { + $isSameClass = false; + break; + } + } + + if ($isSameClass) { + return new NeverType(); + } } if ( diff --git a/tests/PHPStan/Analyser/nsrt/bug-6305.php b/tests/PHPStan/Analyser/nsrt/bug-6305.php index 80e8cc4a2e..89bfea9c62 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6305.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6305.php @@ -15,5 +15,5 @@ class B extends A {} } if (is_subclass_of($b, B::class)) { - assertType('Bug6305Types\B', $b); // Could be NEVER + assertType('*NEVER*', $b); } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index d819237215..94a9f26501 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -393,10 +393,10 @@ public function testBug6305(): void '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, - // ], + [ + 'Call to function is_subclass_of() with Bug6305\B and \'Bug6305\\\B\' will always evaluate to false.', + 14, + ], ]); } From 05462d0514d5d2d0c3d5b3830e68b66db1f76d04 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: 2025年10月31日 16:27:39 +0100 Subject: [PATCH 4/4] Add final test --- tests/PHPStan/Analyser/nsrt/is-subclass-of.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/is-subclass-of.php b/tests/PHPStan/Analyser/nsrt/is-subclass-of.php index 25c462f875..08602c6279 100644 --- a/tests/PHPStan/Analyser/nsrt/is-subclass-of.php +++ b/tests/PHPStan/Analyser/nsrt/is-subclass-of.php @@ -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); + } +};

AltStyle によって変換されたページ (->オリジナル) /