From d6645ac268af680db66c47b3f9b1a889d7884dfb Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Sun, 27 Feb 2022 21:30:47 +0100 Subject: [PATCH 1/2] 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 eb84a5e1d6..490c8b7b5a 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 b34026b08c..6e046557cc 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'); diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index e51d8f9e6e..424a1ab1e9 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 0000000000..f342fed93a --- /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 27bde886c0..58ddb60ec0 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); } }; From 83a00281a8601a6dfdaaec0e55612eb02297d5e1 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Mon, 28 Feb 2022 09:30:44 +0100 Subject: [PATCH 2/2] Extract common is_subclass_of and is_a logic --- conf/config.neon | 3 + .../IsAFunctionTypeSpecifyingExtension.php | 48 +++----------- .../Php/IsAFunctionTypeSpecifyingHelper.php | 62 +++++++++++++++++++ ...classOfFunctionTypeSpecifyingExtension.php | 48 +++----------- 4 files changed, 79 insertions(+), 82 deletions(-) create mode 100644 src/Type/Php/IsAFunctionTypeSpecifyingHelper.php diff --git a/conf/config.neon b/conf/config.neon index e0df3b916f..f89fc6ebf7 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1438,6 +1438,9 @@ services: tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - + class: PHPStan\Type\Php\IsAFunctionTypeSpecifyingHelper + - class: PHPStan\Type\Php\ArrayIsListFunctionTypeSpecifyingExtension tags: diff --git a/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php index 490c8b7b5a..1220eabab0 100644 --- a/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsAFunctionTypeSpecifyingExtension.php @@ -9,18 +9,9 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; -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; @@ -29,6 +20,12 @@ class IsAFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtens private TypeSpecifier $typeSpecifier; + public function __construct( + private IsAFunctionTypeSpecifyingHelper $isAFunctionTypeSpecifyingHelper, + ) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { return strtolower($functionReflection->getName()) === 'is_a' @@ -48,40 +45,9 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n return new SpecifiedTypes([], []); } - $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 $this->typeSpecifier->create( $node->getArgs()[0]->value, - $type, + $this->isAFunctionTypeSpecifyingHelper->determineType($classType, $allowString), $context, false, $scope, diff --git a/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php new file mode 100644 index 0000000000..8a689e6053 --- /dev/null +++ b/src/Type/Php/IsAFunctionTypeSpecifyingHelper.php @@ -0,0 +1,62 @@ +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(); + }, + ); + } + +} diff --git a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php index c463342194..15c773784e 100644 --- a/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/IsSubclassOfFunctionTypeSpecifyingExtension.php @@ -9,18 +9,9 @@ use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\FunctionReflection; -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; @@ -29,6 +20,12 @@ class IsSubclassOfFunctionTypeSpecifyingExtension implements FunctionTypeSpecify private TypeSpecifier $typeSpecifier; + public function __construct( + private IsAFunctionTypeSpecifyingHelper $isAFunctionTypeSpecifyingHelper, + ) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool { return strtolower($functionReflection->getName()) === 'is_subclass_of' @@ -48,40 +45,9 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n return new SpecifiedTypes([], []); } - $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 $this->typeSpecifier->create( $node->getArgs()[0]->value, - $type, + $this->isAFunctionTypeSpecifyingHelper->determineType($classType, $allowString), $context, false, $scope,