diff --git a/conf/config.neon b/conf/config.neon index e60841c20b..b35118a943 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1420,6 +1420,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\GetDebugTypeFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\GetParentClassDynamicFunctionReturnTypeExtension tags: diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 71a2693874..c2d959661e 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1844,17 +1844,7 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif if ( $exprNode instanceof FuncCall && $exprNode->name instanceof Name - && strtolower($exprNode->name->toString()) === 'gettype' - && isset($exprNode->getArgs()[0]) - && $constantType->isString()->yes() - ) { - return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context, $rootExpr); - } - - if ( - $exprNode instanceof FuncCall - && $exprNode->name instanceof Name - && strtolower($exprNode->name->toString()) === 'get_class' + && in_array(strtolower($exprNode->name->toString()), ['gettype', 'get_class', 'get_debug_type'], true) && isset($exprNode->getArgs()[0]) && $constantType->isString()->yes() ) { @@ -1952,7 +1942,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty $context->true() && $unwrappedLeftExpr instanceof FuncCall && $unwrappedLeftExpr->name instanceof Name - && strtolower($unwrappedLeftExpr->name->toString()) === 'get_class' + && in_array(strtolower($unwrappedLeftExpr->name->toString()), ['get_class', 'get_debug_type'], true) && isset($unwrappedLeftExpr->getArgs()[0]) ) { if ($rightType->getClassStringObjectType()->isObject()->yes()) { diff --git a/src/Type/Php/GetDebugTypeFunctionReturnTypeExtension.php b/src/Type/Php/GetDebugTypeFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..40cc595f1c --- /dev/null +++ b/src/Type/Php/GetDebugTypeFunctionReturnTypeExtension.php @@ -0,0 +1,93 @@ +getName() === 'get_debug_type'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 1) { + return null; + } + + $argType = $scope->getType($functionCall->getArgs()[0]->value); + if ($argType instanceof UnionType) { + return new UnionType(array_map(Closure::fromCallable([self::class, 'resolveOneType']), $argType->getTypes())); + } + return self::resolveOneType($argType); + } + + /** + * @see https://www.php.net/manual/en/function.get-debug-type.php#refsect1-function.get-debug-type-returnvalues + */ + private static function resolveOneType(Type $type): Type + { + if ($type->isNull()->yes()) { + return new ConstantStringType('null'); + } + if ($type->isBoolean()->yes()) { + return new ConstantStringType('bool'); + } + if ($type->isInteger()->yes()) { + return new ConstantStringType('int'); + } + if ($type->isFloat()->yes()) { + return new ConstantStringType('float'); + } + if ($type->isString()->yes()) { + return new ConstantStringType('string'); + } + if ($type->isArray()->yes()) { + return new ConstantStringType('array'); + } + + // "resources" type+state is skipped since we cannot infer the state + + if ($type->isObject()->yes()) { + $classNames = $type->getObjectClassNames(); + $reflections = $type->getObjectClassReflections(); + + $types = []; + foreach ($classNames as $index => $className) { + // if the class is not final, the actual returned string might be of a child class + if ($reflections[$index]->isFinal() && !$reflections[$index]->isAnonymous()) { + $types[] = new ConstantStringType($className); + } + + if ($reflections[$index]->isAnonymous()) { // phpcs:ignore + $types[] = new ConstantStringType('class@anonymous'); + } + } + + switch (count($types)) { + case 0: + return new StringType(); + case 1: + return $types[0]; + default: + return new UnionType($types); + } + } + + return new StringType(); + } + +} diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 63f60450b0..ef657dc012 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1424,6 +1424,10 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/call-user-func-php7.php'); } + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/get-debug-type.php'); + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/gettype.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array_splice.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-9542.php'); diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index 245ba3d554..a415b681bd 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -241,6 +241,26 @@ public function dataCondition(): iterable ['$foo' => 'Foo', 'get_class($foo)' => '\'Foo\''], ['get_class($foo)' => '~\'Foo\''], ], + [ + new Equal( + new FuncCall(new Name('get_debug_type'), [ + new Arg(new Variable('foo')), + ]), + new String_('Foo'), + ), + ['$foo' => 'Foo', 'get_debug_type($foo)' => '\'Foo\''], + ['get_debug_type($foo)' => '~\'Foo\''], + ], + [ + new Equal( + new String_('Foo'), + new FuncCall(new Name('get_debug_type'), [ + new Arg(new Variable('foo')), + ]), + ), + ['$foo' => 'Foo', 'get_debug_type($foo)' => '\'Foo\''], + ['get_debug_type($foo)' => '~\'Foo\''], + ], [ new BooleanNot( new Expr\Instanceof_( diff --git a/tests/PHPStan/Analyser/data/get-debug-type.php b/tests/PHPStan/Analyser/data/get-debug-type.php new file mode 100644 index 0000000000..975ea62613 --- /dev/null +++ b/tests/PHPStan/Analyser/data/get-debug-type.php @@ -0,0 +1,51 @@ + assertType(FinalFoo::class, $class), FinalBar::class => assertType(FinalBar::class, $class), }; + + match (get_debug_type($class)) { + FinalFoo::class => assertType(FinalFoo::class, $class), + FinalBar::class => assertType(FinalBar::class, $class), + }; } } diff --git a/tests/PHPStan/Rules/Comparison/data/match-expr.php b/tests/PHPStan/Rules/Comparison/data/match-expr.php index 37d5e01c1e..5970804bb4 100644 --- a/tests/PHPStan/Rules/Comparison/data/match-expr.php +++ b/tests/PHPStan/Rules/Comparison/data/match-expr.php @@ -203,3 +203,15 @@ public function doMatch(FinalFoo|FinalBar $class): void } } +class TestGetDebugType +{ + + public function doMatch(FinalFoo|FinalBar $class): void + { + match (get_debug_type($class)) { + FinalFoo::class => 1, + FinalBar::class => 2, + }; + } + +}