Skip to content

Commit

Permalink
DynamicFunctionReturnTypeExtension for the get_debug_type function.
Browse files Browse the repository at this point in the history
Conditional type specifier for `get_debug_type` function.
  • Loading branch information
patrickkusebauch committed Feb 16, 2024
1 parent 6320e1d commit d5dc25f
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 12 deletions.
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,
};
}

}

0 comments on commit d5dc25f

Please sign in to comment.