Skip to content

Commit 57b6434

Browse files
patrickkusebauchondrejmirtes
authored andcommitted
DynamicFunctionReturnTypeExtension for the get_debug_type function.
Conditional type specifier for `get_debug_type` function.
1 parent 1ebcae0 commit 57b6434

File tree

8 files changed

+192
-12
lines changed

8 files changed

+192
-12
lines changed

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1420,6 +1420,11 @@ services:
14201420
tags:
14211421
- phpstan.broker.dynamicFunctionReturnTypeExtension
14221422

1423+
-
1424+
class: PHPStan\Type\Php\GetDebugTypeFunctionReturnTypeExtension
1425+
tags:
1426+
- phpstan.broker.dynamicFunctionReturnTypeExtension
1427+
14231428
-
14241429
class: PHPStan\Type\Php\GetParentClassDynamicFunctionReturnTypeExtension
14251430
tags:

src/Analyser/TypeSpecifier.php

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1844,17 +1844,7 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif
18441844
if (
18451845
$exprNode instanceof FuncCall
18461846
&& $exprNode->name instanceof Name
1847-
&& strtolower($exprNode->name->toString()) === 'gettype'
1848-
&& isset($exprNode->getArgs()[0])
1849-
&& $constantType->isString()->yes()
1850-
) {
1851-
return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context, $rootExpr);
1852-
}
1853-
1854-
if (
1855-
$exprNode instanceof FuncCall
1856-
&& $exprNode->name instanceof Name
1857-
&& strtolower($exprNode->name->toString()) === 'get_class'
1847+
&& in_array(strtolower($exprNode->name->toString()), ['gettype', 'get_class', 'get_debug_type'], true)
18581848
&& isset($exprNode->getArgs()[0])
18591849
&& $constantType->isString()->yes()
18601850
) {
@@ -1952,7 +1942,7 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
19521942
$context->true()
19531943
&& $unwrappedLeftExpr instanceof FuncCall
19541944
&& $unwrappedLeftExpr->name instanceof Name
1955-
&& strtolower($unwrappedLeftExpr->name->toString()) === 'get_class'
1945+
&& in_array(strtolower($unwrappedLeftExpr->name->toString()), ['get_class', 'get_debug_type'], true)
19561946
&& isset($unwrappedLeftExpr->getArgs()[0])
19571947
) {
19581948
if ($rightType->getClassStringObjectType()->isObject()->yes()) {
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use Closure;
6+
use PhpParser\Node\Expr\FuncCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Reflection\FunctionReflection;
9+
use PHPStan\Type\Constant\ConstantStringType;
10+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
11+
use PHPStan\Type\StringType;
12+
use PHPStan\Type\Type;
13+
use PHPStan\Type\UnionType;
14+
use function array_map;
15+
use function count;
16+
17+
class GetDebugTypeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
18+
{
19+
20+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
21+
{
22+
return $functionReflection->getName() === 'get_debug_type';
23+
}
24+
25+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type
26+
{
27+
if (count($functionCall->getArgs()) < 1) {
28+
return null;
29+
}
30+
31+
$argType = $scope->getType($functionCall->getArgs()[0]->value);
32+
if ($argType instanceof UnionType) {
33+
return new UnionType(array_map(Closure::fromCallable([self::class, 'resolveOneType']), $argType->getTypes()));
34+
}
35+
return self::resolveOneType($argType);
36+
}
37+
38+
/**
39+
* @see https://www.php.net/manual/en/function.get-debug-type.php#refsect1-function.get-debug-type-returnvalues
40+
*/
41+
private static function resolveOneType(Type $type): Type
42+
{
43+
if ($type->isNull()->yes()) {
44+
return new ConstantStringType('null');
45+
}
46+
if ($type->isBoolean()->yes()) {
47+
return new ConstantStringType('bool');
48+
}
49+
if ($type->isInteger()->yes()) {
50+
return new ConstantStringType('int');
51+
}
52+
if ($type->isFloat()->yes()) {
53+
return new ConstantStringType('float');
54+
}
55+
if ($type->isString()->yes()) {
56+
return new ConstantStringType('string');
57+
}
58+
if ($type->isArray()->yes()) {
59+
return new ConstantStringType('array');
60+
}
61+
62+
// "resources" type+state is skipped since we cannot infer the state
63+
64+
if ($type->isObject()->yes()) {
65+
$classNames = $type->getObjectClassNames();
66+
$reflections = $type->getObjectClassReflections();
67+
68+
$types = [];
69+
foreach ($classNames as $index => $className) {
70+
// if the class is not final, the actual returned string might be of a child class
71+
if ($reflections[$index]->isFinal() && !$reflections[$index]->isAnonymous()) {
72+
$types[] = new ConstantStringType($className);
73+
}
74+
75+
if ($reflections[$index]->isAnonymous()) { // phpcs:ignore
76+
$types[] = new ConstantStringType('class@anonymous');
77+
}
78+
}
79+
80+
switch (count($types)) {
81+
case 0:
82+
return new StringType();
83+
case 1:
84+
return $types[0];
85+
default:
86+
return new UnionType($types);
87+
}
88+
}
89+
90+
return new StringType();
91+
}
92+
93+
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1424,6 +1424,10 @@ public function dataFileAsserts(): iterable
14241424
yield from $this->gatherAssertTypes(__DIR__ . '/data/call-user-func-php7.php');
14251425
}
14261426

1427+
if (PHP_VERSION_ID >= 80000) {
1428+
yield from $this->gatherAssertTypes(__DIR__ . '/data/get-debug-type.php');
1429+
}
1430+
14271431
yield from $this->gatherAssertTypes(__DIR__ . '/data/gettype.php');
14281432
yield from $this->gatherAssertTypes(__DIR__ . '/data/array_splice.php');
14291433
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-9542.php');

tests/PHPStan/Analyser/TypeSpecifierTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,26 @@ public function dataCondition(): iterable
241241
['$foo' => 'Foo', 'get_class($foo)' => '\'Foo\''],
242242
['get_class($foo)' => '~\'Foo\''],
243243
],
244+
[
245+
new Equal(
246+
new FuncCall(new Name('get_debug_type'), [
247+
new Arg(new Variable('foo')),
248+
]),
249+
new String_('Foo'),
250+
),
251+
['$foo' => 'Foo', 'get_debug_type($foo)' => '\'Foo\''],
252+
['get_debug_type($foo)' => '~\'Foo\''],
253+
],
254+
[
255+
new Equal(
256+
new String_('Foo'),
257+
new FuncCall(new Name('get_debug_type'), [
258+
new Arg(new Variable('foo')),
259+
]),
260+
),
261+
['$foo' => 'Foo', 'get_debug_type($foo)' => '\'Foo\''],
262+
['get_debug_type($foo)' => '~\'Foo\''],
263+
],
244264
[
245265
new BooleanNot(
246266
new Expr\Instanceof_(
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace GetDebugType;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
final class A {}
8+
9+
/**
10+
* @param double $d
11+
* @param resource $r
12+
* @param int|string $intOrString
13+
* @param array|A $arrayOrObject
14+
*/
15+
function doFoo(bool $b, int $i, float $f, $d, $r, string $s, array $a, $intOrString, $arrayOrObject) {
16+
$null = null;
17+
$resource = fopen('php://memory', 'r');
18+
$o = new \stdClass();
19+
$A = new A();
20+
$anonymous = new class {};
21+
22+
assertType("'bool'", get_debug_type($b));
23+
assertType("'bool'", get_debug_type(true));
24+
assertType("'bool'", get_debug_type(false));
25+
assertType("'int'", get_debug_type($i));
26+
assertType("'float'", get_debug_type($f));
27+
assertType("'float'", get_debug_type($d));
28+
assertType("'string'", get_debug_type($s));
29+
assertType("'array'", get_debug_type($a));
30+
assertType("string", get_debug_type($o));
31+
assertType("'GetDebugType\\\\A'", get_debug_type($A));
32+
assertType("string", get_debug_type($r));
33+
assertType("'bool'|string", get_debug_type($resource));
34+
assertType("'null'", get_debug_type($null));
35+
assertType("'int'|'string'", get_debug_type($intOrString));
36+
assertType("'array'|'GetDebugType\\\\A'", get_debug_type($arrayOrObject));
37+
assertType("'class@anonymous'", get_debug_type($anonymous));
38+
}
39+
40+
/**
41+
* @param non-empty-string $nonEmptyString
42+
* @param non-falsy-string $falsyString
43+
* @param numeric-string $numericString
44+
* @param class-string $classString
45+
*/
46+
function strings($nonEmptyString, $falsyString, $numericString, $classString) {
47+
assertType("'string'", get_debug_type($nonEmptyString));
48+
assertType("'string'", get_debug_type($falsyString));
49+
assertType("'string'", get_debug_type($numericString));
50+
assertType("'string'", get_debug_type($classString));
51+
}

tests/PHPStan/Analyser/data/match-expr.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ public function doMatch(FinalFoo|FinalBar $class): void
109109
FinalFoo::class => assertType(FinalFoo::class, $class),
110110
FinalBar::class => assertType(FinalBar::class, $class),
111111
};
112+
113+
match (get_debug_type($class)) {
114+
FinalFoo::class => assertType(FinalFoo::class, $class),
115+
FinalBar::class => assertType(FinalBar::class, $class),
116+
};
112117
}
113118

114119
}

tests/PHPStan/Rules/Comparison/data/match-expr.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,15 @@ public function doMatch(FinalFoo|FinalBar $class): void
203203
}
204204

205205
}
206+
class TestGetDebugType
207+
{
208+
209+
public function doMatch(FinalFoo|FinalBar $class): void
210+
{
211+
match (get_debug_type($class)) {
212+
FinalFoo::class => 1,
213+
FinalBar::class => 2,
214+
};
215+
}
216+
217+
}

0 commit comments

Comments
 (0)