Skip to content
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

DynamicFunctionReturnTypeExtension for the get_debug_type function. #2910

Open
wants to merge 1 commit into
base: 1.11.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions conf/config.neon
Expand Up @@ -1360,6 +1360,11 @@ services:
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\GetDebugTypeFunctionReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\GetParentClassDynamicFunctionReturnTypeExtension
tags:
Expand Down
14 changes: 2 additions & 12 deletions src/Analyser/TypeSpecifier.php
Expand Up @@ -1812,17 +1812,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()
) {
Expand Down Expand Up @@ -1920,7 +1910,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()) {
Expand Down
93 changes: 93 additions & 0 deletions src/Type/Php/GetDebugTypeFunctionReturnTypeExtension.php
@@ -0,0 +1,93 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use Closure;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use function array_map;
use function count;

class GetDebugTypeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->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();
}

}
4 changes: 4 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -1385,6 +1385,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');
Expand Down
20 changes: 20 additions & 0 deletions tests/PHPStan/Analyser/TypeSpecifierTest.php
Expand Up @@ -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_(
Expand Down
51 changes: 51 additions & 0 deletions tests/PHPStan/Analyser/data/get-debug-type.php
@@ -0,0 +1,51 @@
<?php

namespace GetDebugType;

use function PHPStan\Testing\assertType;

final class A {}

/**
* @param double $d
* @param resource $r
* @param int|string $intOrString
* @param array|A $arrayOrObject
*/
function doFoo(bool $b, int $i, float $f, $d, $r, string $s, array $a, $intOrString, $arrayOrObject) {
$null = null;
$resource = fopen('php://memory', 'r');
$o = new \stdClass();
$A = new A();
$anonymous = new class {};

assertType("'bool'", get_debug_type($b));
assertType("'bool'", get_debug_type(true));
assertType("'bool'", get_debug_type(false));
assertType("'int'", get_debug_type($i));
assertType("'float'", get_debug_type($f));
assertType("'float'", get_debug_type($d));
assertType("'string'", get_debug_type($s));
assertType("'array'", get_debug_type($a));
assertType("string", get_debug_type($o));
assertType("'GetDebugType\\\\A'", get_debug_type($A));
assertType("string", get_debug_type($r));
assertType("'bool'|string", get_debug_type($resource));
assertType("'null'", get_debug_type($null));
assertType("'int'|'string'", get_debug_type($intOrString));
assertType("'array'|'GetDebugType\\\\A'", get_debug_type($arrayOrObject));
assertType("'class@anonymous'", get_debug_type($anonymous));
}

/**
* @param non-empty-string $nonEmptyString
* @param non-falsy-string $falsyString
* @param numeric-string $numericString
* @param class-string $classString
*/
function strings($nonEmptyString, $falsyString, $numericString, $classString) {
assertType("'string'", get_debug_type($nonEmptyString));
assertType("'string'", get_debug_type($falsyString));
assertType("'string'", get_debug_type($numericString));
assertType("'string'", get_debug_type($classString));
}
5 changes: 5 additions & 0 deletions tests/PHPStan/Analyser/data/match-expr.php
Expand Up @@ -109,6 +109,11 @@ public function doMatch(FinalFoo|FinalBar $class): void
FinalFoo::class => 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),
};
}

}
12 changes: 12 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/match-expr.php
Expand Up @@ -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,
};
}

}