-
Couldn't load subscription status.
- Fork 544
Avoid false error on is_subclass_of #4472
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,35 @@ public function determineType( | |
| $objectOrClassTypeClassNames = array_values(array_unique($objectOrClassTypeClassNames)); | ||
| } | ||
|
|
||
| return TypeTraverser::map( | ||
| $isUncertain = $classType->getConstantStrings() === []; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From the original check |
||
|
|
||
| $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) { | ||
| // 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 ( | ||
| // 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 +96,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; | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -393,9 +393,31 @@ public function testBug6305(): void | |
| 'Call to function is_subclass_of() with Bug6305\B and \'Bug6305\\\A\' will always evaluate to true.', | ||
| 11, | ||
| ], | ||
| // [ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently this is too complicated to be detected since we don't make a difference in ObjectType between the exact class and one of the children. |
||
| // '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'], [ | ||
| [ | ||
| '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 arguments Bug13713\\test, 'stdClass' and false will always evaluate to true.", | ||
| 12, | ||
| ], | ||
| [ | ||
| "Call to function is_subclass_of() with arguments class-string<Bug13713\\test>, '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 <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.', | ||
| ], | ||
| ]); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types = 1); | ||
|
|
||
| namespace Bug13713; | ||
|
|
||
| function debug(object $object): void { | ||
| if ($object instanceof \stdClass) { | ||
| echo var_export(\is_subclass_of($object, \stdClass::class, false), true) . \PHP_EOL; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With this PR there is no error here which fix phpstan/phpstan#13713 |
||
| } | ||
| 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> $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); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| <?php | ||
|
|
||
| namespace Bug6305b; | ||
|
|
||
| class A {} | ||
|
|
||
| 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, B::class)) { | ||
| } | ||
|
|
||
| $b = mt_rand(0, 1) === 0 ? A::class : B::class; | ||
|
|
||
| if (is_subclass_of($b, A::class)) { | ||
| } | ||
|
|
||
| if (is_subclass_of($b, B::class)) { | ||
| } |