diff --git a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php index ce90c1b610e..30bc1ba4fb8 100644 --- a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php @@ -15,15 +15,11 @@ use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\IntersectionType; -use PHPStan\Type\MixedType; -use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; -use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; use function count; use function strtolower; @@ -49,58 +45,43 @@ 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)); - if (!$classType instanceof ConstantStringType) { - if ($context->truthy()) { - if ($allowString) { - $type = TypeCombinator::union( - new ObjectWithoutClassType(), - new ClassStringType(), - ); - } else { - $type = new ObjectWithoutClassType(); - } - - return $this->typeSpecifier->create( - $node->getArgs()[0]->value, - $type, - $context, - false, - $scope, - ); - } - - return new SpecifiedTypes(); + if (!$classType instanceof ConstantStringType && !$context->truthy()) { + return new SpecifiedTypes([], []); } - $type = TypeTraverser::map($objectType, static function (Type $type, callable $traverse) use ($classType, $allowString): Type { - if ($type instanceof UnionType) { - return $traverse($type); - } - if ($type instanceof IntersectionType) { + $possibleType = TypeTraverser::map($classType, static function (Type $type, callable $traverse) use ($allowString): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); } - if ($allowString) { - if ($type instanceof StringType) { - return new GenericClassStringType(new ObjectType($classType->getValue())); + if ($type instanceof ConstantStringType) { + if ($allowString) { + return TypeCombinator::union( + new ObjectType($type->getValue()), + new GenericClassStringType(new ObjectType($type->getValue())), + ); } + return new ObjectType($type->getValue()); } - if ($type instanceof ObjectWithoutClassType || $type instanceof TypeWithClassName) { - return new ObjectType($classType->getValue()); - } - if ($type instanceof MixedType) { - $objectType = new ObjectType($classType->getValue()); + if ($type instanceof GenericClassStringType) { if ($allowString) { return TypeCombinator::union( - new GenericClassStringType($objectType), - $objectType, + $type->getGenericType(), + $type, ); } - - return $objectType; + return $type->getGenericType(); } - return new NeverType(); + if ($allowString) { + return TypeCombinator::union( + new ObjectWithoutClassType(), + new ClassStringType(), + ); + } + return new ObjectWithoutClassType(); }); + $type = TypeCombinator::intersect($objectType, $possibleType); + return $this->typeSpecifier->create( $node->getArgs()[0]->value, $type, diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index bcf743e8454..765dc97959a 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -722,6 +722,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/countable.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6696.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6698.php'); } /** diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index 8d16a1692dd..37e3f40dd6f 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -26,6 +26,7 @@ use PHPStan\Type\MixedType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StringType; use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; @@ -66,6 +67,7 @@ protected function setUp(): void $this->scope = $this->scope->assignVariable('foo', new MixedType()); $this->scope = $this->scope->assignVariable('classString', new ClassStringType()); $this->scope = $this->scope->assignVariable('genericClassString', new GenericClassStringType(new ObjectType('Bar'))); + $this->scope = $this->scope->assignVariable('object', new ObjectWithoutClassType()); } /** @@ -951,7 +953,18 @@ public function dataCondition(): array new Arg(new Variable('stringOrNull')), ]), [ - '$string' => 'class-string|object', + '$string' => 'class-string', + ], + [], + ], + [ + new FuncCall(new Name('is_subclass_of'), [ + new Arg(new Variable('object')), + new Arg(new Variable('stringOrNull')), + new Arg(new Expr\ConstFetch(new Name('false'))), + ]), + [ + '$object' => 'object', ], [], ], @@ -962,7 +975,39 @@ public function dataCondition(): array new Arg(new Expr\ConstFetch(new Name('false'))), ]), [ - '$string' => 'object', + '$string' => '*NEVER*', + ], + [], + ], + [ + new FuncCall(new Name('is_subclass_of'), [ + new Arg(new Variable('string')), + new Arg(new Variable('genericClassString')), + ]), + [ + '$string' => 'class-string', + ], + [], + ], + [ + new FuncCall(new Name('is_subclass_of'), [ + new Arg(new Variable('object')), + new Arg(new Variable('genericClassString')), + new Arg(new Expr\ConstFetch(new Name('false'))), + ]), + [ + '$object' => 'Bar', + ], + [], + ], + [ + new FuncCall(new Name('is_subclass_of'), [ + new Arg(new Variable('string')), + new Arg(new Variable('genericClassString')), + new Arg(new Expr\ConstFetch(new Name('false'))), + ]), + [ + '$string' => '*NEVER*', ], [], ], diff --git a/tests/PHPStan/Analyser/data/bug-6698.php b/tests/PHPStan/Analyser/data/bug-6698.php new file mode 100644 index 00000000000..65b81ade24f --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6698.php @@ -0,0 +1,33 @@ + + */ + public function getClasses(): iterable; +} + +class Y +{ + public X $x; + + /** + * @template T of object + * + * @param class-string $type + * @return iterable> + */ + public function findImplementations(string $type): iterable + { + foreach ($this->x->getClasses() as $class) { + if (is_subclass_of($class, $type)) { + assertType('class-string', $class); + yield $class; + } + } + } +} diff --git a/tests/PHPStan/Analyser/data/generic-class-string.php b/tests/PHPStan/Analyser/data/generic-class-string.php index 45f1dfaddf9..d808a0fcf84 100644 --- a/tests/PHPStan/Analyser/data/generic-class-string.php +++ b/tests/PHPStan/Analyser/data/generic-class-string.php @@ -20,15 +20,22 @@ function testMixed($a) { if (is_subclass_of($a, 'DateTimeInterface')) { assertType('class-string|DateTimeInterface', $a); assertType('DateTimeInterface', new $a()); + } else { + assertType('mixed~class-string|DateTimeInterface', $a); } if (is_subclass_of($a, 'DateTimeInterface') || is_subclass_of($a, 'stdClass')) { assertType('class-string|class-string|DateTimeInterface|stdClass', $a); assertType('DateTimeInterface|stdClass', new $a()); + } else { + // could also exclude stdClass + assertType('mixed~class-string|DateTimeInterface', $a); } if (is_subclass_of($a, C::class)) { assertType('int', $a::f()); + } else { + assertType('mixed~class-string|PHPStan\Generics\GenericClassStringType\C', $a); } } @@ -40,6 +47,8 @@ function testObject($a) { if (is_subclass_of($a, 'DateTimeInterface')) { assertType('DateTimeInterface', $a); + } else { + assertType('object~DateTimeInterface', $a); } } @@ -52,10 +61,14 @@ function testString($a) { if (is_subclass_of($a, 'DateTimeInterface')) { assertType('class-string', $a); assertType('DateTimeInterface', new $a()); + } else { + assertType('string', $a); } if (is_subclass_of($a, C::class)) { assertType('int', $a::f()); + } else { + assertType('string', $a); } } @@ -68,10 +81,14 @@ function testStringObject($a) { if (is_subclass_of($a, 'DateTimeInterface')) { assertType('class-string|DateTimeInterface', $a); assertType('DateTimeInterface', new $a()); + } else { + assertType('object~DateTimeInterface|string', $a); } if (is_subclass_of($a, C::class)) { assertType('int', $a::f()); + } else { + assertType('object~PHPStan\Generics\GenericClassStringType\C|string', $a); } } @@ -84,6 +101,29 @@ function testClassString($a) { if (is_subclass_of($a, 'DateTime')) { assertType('class-string', $a); assertType('DateTime', new $a()); + } else { + assertType('class-string', $a); + } +} + +/** + * @param object|string $a + * @param class-string<\DateTimeInterface> $b + */ +function testClassStringAsClassName($a, string $b) { + assertType('object', new $a()); + + if (is_subclass_of($a, $b)) { + assertType('class-string|DateTimeInterface', $a); + assertType('DateTimeInterface', new $a()); + } else { + assertType('object|string', $a); + } + + if (is_subclass_of($a, $b, false)) { + assertType('DateTimeInterface', $a); + } else { + assertType('object|string', $a); } } diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 6cf86a184bc..5587459ac40 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -448,4 +448,11 @@ public function testBug3766(): void $this->analyse([__DIR__ . '/data/bug-3766.php'], []); } + public function testBug6698(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-6698.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-6698.php b/tests/PHPStan/Rules/Comparison/data/bug-6698.php new file mode 100644 index 00000000000..c65e7e977ab --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-6698.php @@ -0,0 +1,31 @@ + + */ + public function getClasses(): iterable; +} + +class Y +{ + /** @var X */ + public $x; + + /** + * @template T of object + * + * @param class-string $type + * @return iterable> + */ + public function findImplementations(string $type): iterable + { + foreach ($this->x->getClasses() as $class) { + if (is_subclass_of($class, $type)) { + yield $class; + } + } + } +}