From c33c748a2d5f110f1f2304dfaf00d93b16188413 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Sun, 27 Feb 2022 21:30:47 +0100 Subject: [PATCH] Duplicate and adapt solution from is_subclass_of for is_a --- .../IsAFunctionTypeSpecifyingExtension.php | 89 ++++++++++--------- .../Analyser/NodeScopeResolverTest.php | 1 + tests/PHPStan/Analyser/TypeSpecifierTest.php | 12 +-- tests/PHPStan/Analyser/data/bug-6704.php | 22 +++++ tests/PHPStan/Analyser/data/is-a.php | 2 +- 5 files changed, 79 insertions(+), 47 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/bug-6704.php diff --git a/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php index eb84a5e1d61..490c8b7b5ae 100644 --- a/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php @@ -2,24 +2,26 @@ namespace PHPStan\Type\Php; -use PhpParser\Node; -use PhpParser\Node\Expr\ClassConstFetch; use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; -use PHPStan\ShouldNotHappenException; use PHPStan\Type\ClassStringType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\Generic\GenericClassStringType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeTraverser; +use PHPStan\Type\UnionType; +use function count; use function strtolower; class IsAFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension @@ -30,53 +32,60 @@ class IsAFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtens public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { return strtolower($functionReflection->getName()) === 'is_a' - && isset($node->getArgs()[0]) - && isset($node->getArgs()[1]) && !$context->null(); } public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - if ($context->null()) { - throw new ShouldNotHappenException(); + if (count($node->getArgs()) < 2) { + return new SpecifiedTypes(); } + $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)); - $classNameArgExpr = $node->getArgs()[1]->value; - $classNameArgExprType = $scope->getType($classNameArgExpr); - if ( - $classNameArgExpr instanceof ClassConstFetch - && $classNameArgExpr->class instanceof Name - && $classNameArgExpr->name instanceof Node\Identifier - && strtolower($classNameArgExpr->name->name) === 'class' - ) { - $objectType = $scope->resolveTypeByName($classNameArgExpr->class); - $types = $this->typeSpecifier->create($node->getArgs()[0]->value, $objectType, $context, false, $scope); - } elseif ($classNameArgExprType instanceof ConstantStringType) { - $objectType = new ObjectType($classNameArgExprType->getValue()); - $types = $this->typeSpecifier->create($node->getArgs()[0]->value, $objectType, $context, false, $scope); - } elseif ($classNameArgExprType instanceof GenericClassStringType) { - $objectType = $classNameArgExprType->getGenericType(); - $types = $this->typeSpecifier->create($node->getArgs()[0]->value, $objectType, $context, false, $scope); - } elseif ($context->true()) { - $objectType = new ObjectWithoutClassType(); - $types = $this->typeSpecifier->create($node->getArgs()[0]->value, $objectType, $context, false, $scope); - } else { - $types = new SpecifiedTypes(); + if (!$classType instanceof ConstantStringType && !$context->truthy()) { + return new SpecifiedTypes([], []); } - if (isset($node->getArgs()[2]) && $context->true()) { - if (!$scope->getType($node->getArgs()[2]->value)->isSuperTypeOf(new ConstantBooleanType(true))->no()) { - $types = $types->intersectWith($this->typeSpecifier->create( - $node->getArgs()[0]->value, - isset($objectType) ? new GenericClassStringType($objectType) : new ClassStringType(), - $context, - false, - $scope, - )); + $type = TypeTraverser::map($classType, static function (Type $type, callable $traverse) use ($allowString): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); } - } + 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 GenericClassStringType) { + if ($allowString) { + return TypeCombinator::union( + $type->getGenericType(), + $type, + ); + } + return $type->getGenericType(); + } + if ($allowString) { + return TypeCombinator::union( + new ObjectWithoutClassType(), + new ClassStringType(), + ); + } + return new ObjectWithoutClassType(); + }); - return $types; + return $this->typeSpecifier->create( + $node->getArgs()[0]->value, + $type, + $context, + false, + $scope, + ); } public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index d02a995b156..71ba9740013 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -726,6 +726,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-6704.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/filter-iterator-child-class.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/smaller-than-benevolent.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 e51d8f9e6eb..424a1ab1e9b 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -445,7 +445,7 @@ public function dataCondition(): array )), ]), ['$foo' => 'static(DateTime)'], - ['$foo' => '~static(DateTime)'], + [], ], [ new FuncCall(new Name('is_a'), [ @@ -461,7 +461,7 @@ public function dataCondition(): array new Arg(new Variable('genericClassString')), ]), ['$foo' => 'Bar'], - ['$foo' => '~Bar'], + [], ], [ new FuncCall(new Name('is_a'), [ @@ -470,7 +470,7 @@ public function dataCondition(): array new Arg(new Expr\ConstFetch(new Name('true'))), ]), ['$foo' => 'class-string|Foo'], - ['$foo' => '~Foo'], + ['$foo' => '~class-string|Foo'], ], [ new FuncCall(new Name('is_a'), [ @@ -478,7 +478,7 @@ public function dataCondition(): array new Arg(new Variable('className')), new Arg(new Expr\ConstFetch(new Name('true'))), ]), - ['$foo' => 'class-string|object'], + ['$foo' => 'class-string|object'], [], ], [ @@ -488,7 +488,7 @@ public function dataCondition(): array new Arg(new Variable('unknown')), ]), ['$foo' => 'class-string|Foo'], - ['$foo' => '~Foo'], + ['$foo' => '~class-string|Foo'], ], [ new FuncCall(new Name('is_a'), [ @@ -496,7 +496,7 @@ public function dataCondition(): array new Arg(new Variable('className')), new Arg(new Variable('unknown')), ]), - ['$foo' => 'class-string|object'], + ['$foo' => 'class-string|object'], [], ], [ diff --git a/tests/PHPStan/Analyser/data/bug-6704.php b/tests/PHPStan/Analyser/data/bug-6704.php new file mode 100644 index 00000000000..f342fed93ae --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-6704.php @@ -0,0 +1,22 @@ +|class-string $a + * @param DateTimeImmutable|stdClass $b + */ +function foo(string $a, object $b): void +{ + if (!is_a($a, stdClass::class, true)) { + assertType('class-string', $a); + } + + if (!is_a($b, stdClass::class)) { + assertType('DateTimeImmutable', $b); + } +} diff --git a/tests/PHPStan/Analyser/data/is-a.php b/tests/PHPStan/Analyser/data/is-a.php index 27bde886c02..58ddb60ec0e 100644 --- a/tests/PHPStan/Analyser/data/is-a.php +++ b/tests/PHPStan/Analyser/data/is-a.php @@ -23,6 +23,6 @@ function (string $foo) { function (string $foo, string $someString) { if (is_a($foo, $someString, true)) { - \PHPStan\Testing\assertType('class-string', $foo); + \PHPStan\Testing\assertType('class-string', $foo); } };