Skip to content

Commit

Permalink
DynamicFunctionReturnTypeExtension for the get_debug_type function.
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickkusebauch committed Feb 10, 2024
1 parent 6320e1d commit 1e360f4
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 0 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
89 changes: 89 additions & 0 deletions src/Type/Php/GetDebugTypeFunctionReturnTypeExtension.php
@@ -0,0 +1,89 @@
<?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
{
$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
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));
}

0 comments on commit 1e360f4

Please sign in to comment.