Skip to content

Commit e79ec00

Browse files
committed
add JsonDecodeDynamicReturnTypeExtension
1 parent 4c64f24 commit e79ec00

File tree

5 files changed

+216
-0
lines changed

5 files changed

+216
-0
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Nette;
4+
5+
use Nette\Utils\Json;
6+
use PhpParser\Node\Arg;
7+
use PhpParser\Node\Expr\ClassConstFetch;
8+
use PhpParser\Node\Expr\StaticCall;
9+
use PhpParser\Node\Name;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Reflection\MethodReflection;
12+
use PHPStan\Type\ArrayType;
13+
use PHPStan\Type\BooleanType;
14+
use PHPStan\Type\Constant\ConstantStringType;
15+
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
16+
use PHPStan\Type\FloatType;
17+
use PHPStan\Type\IntegerType;
18+
use PHPStan\Type\MixedType;
19+
use PHPStan\Type\ObjectType;
20+
use PHPStan\Type\StringType;
21+
use PHPStan\Type\Type;
22+
use PHPStan\Type\UnionType;
23+
use stdClass;
24+
25+
final class JsonDecodeDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
26+
{
27+
public function getClass(): string
28+
{
29+
return 'Nette\Utils\Json';
30+
}
31+
32+
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
33+
{
34+
return $methodReflection->getName() === 'decode';
35+
}
36+
37+
public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type
38+
{
39+
$args = $methodCall->getArgs();
40+
41+
$isForceArray = $this->isForceArray($args);
42+
43+
$firstArgValue = $args[0]->value;
44+
$firstValueType = $scope->getType($firstArgValue);
45+
46+
if ($firstValueType instanceof ConstantStringType) {
47+
$resolvedType = $this->resolveConstantStringType($firstValueType, $isForceArray);
48+
} else {
49+
$resolvedType = new MixedType();
50+
}
51+
52+
if (! $resolvedType instanceof MixedType) {
53+
return $resolvedType;
54+
}
55+
56+
// fallback type
57+
if ($isForceArray) {
58+
return new UnionType([
59+
new ArrayType(new MixedType(), new MixedType()),
60+
new StringType(),
61+
new FloatType(),
62+
new IntegerType(),
63+
new BooleanType(),
64+
]);
65+
}
66+
67+
// scalar types with stdClass
68+
return new UnionType([
69+
new ObjectType(stdClass::class),
70+
new StringType(),
71+
new FloatType(),
72+
new IntegerType(),
73+
new BooleanType(),
74+
]);
75+
}
76+
77+
/**
78+
* @param Arg[] $args
79+
*/
80+
private function isForceArray(array $args): bool
81+
{
82+
if (!isset($args[1])) {
83+
return false;
84+
}
85+
86+
$secondArg = $args[1];
87+
88+
// is second arg force array?
89+
if ($secondArg->value instanceof ClassConstFetch) {
90+
$classConstFetch = $secondArg->value;
91+
92+
if ($classConstFetch->class instanceof Name) {
93+
if ($classConstFetch->class->toString() === 'Nette\Utils\Json' && $classConstFetch->name->toString() === 'FORCE_ARRAY') {
94+
return true;
95+
}
96+
}
97+
}
98+
99+
return false;
100+
}
101+
102+
private function resolveConstantStringType(ConstantStringType $constantStringType, bool $isForceArray): Type
103+
{
104+
if ($isForceArray) {
105+
$decodedValue = Json::decode($constantStringType->getValue(), Json::FORCE_ARRAY);
106+
} else {
107+
$decodedValue = Json::decode($constantStringType->getValue());
108+
}
109+
110+
if (is_bool($decodedValue)) {
111+
return new BooleanType();
112+
}
113+
114+
if (is_array($decodedValue)) {
115+
return new ArrayType(new MixedType(), new MixedType());
116+
}
117+
118+
if (is_object($decodedValue) && get_class($decodedValue) === stdClass::class) {
119+
return new ObjectType(stdClass::class);
120+
}
121+
122+
if (is_int($decodedValue)) {
123+
return new IntegerType();
124+
}
125+
126+
if (is_float($decodedValue)) {
127+
return new FloatType();
128+
}
129+
130+
return new MixedType();
131+
}
132+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Tests\Type\Nette;
4+
5+
use PHPStan\Testing\TypeInferenceTestCase;
6+
7+
final class JsonDecodeDynamicReturnTypeExtensionTest extends TypeInferenceTestCase
8+
{
9+
public function dataAsserts(): iterable
10+
{
11+
yield from $this->gatherAssertTypes(__DIR__ . '/data/json_decode.php');
12+
yield from $this->gatherAssertTypes(__DIR__ . '/data/json_decode_force_array.php');
13+
}
14+
15+
/**
16+
* @dataProvider dataAsserts()
17+
* @param mixed ...$args
18+
*/
19+
public function testAsserts(string $assertType, string $file, ...$args): void
20+
{
21+
$this->assertFileAsserts($assertType, $file, ...$args);
22+
}
23+
24+
/**
25+
* @return string[]
26+
*/
27+
public static function getAdditionalConfigFiles(): array
28+
{
29+
return [__DIR__ . '/config/json_decode_extension.neon'];
30+
}
31+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
services:
2+
-
3+
class: PHPStan\Type\Nette\JsonDecodeDynamicReturnTypeExtension
4+
tags:
5+
- phpstan.broker.dynamicStaticMethodReturnTypeExtension

tests/Type/Nette/data/json_decode.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
use Nette\Utils\Json;
4+
use function PHPStan\Testing\assertType;
5+
6+
$value = Json::decode('true');
7+
assertType('bool', $value);
8+
9+
$value = Json::decode('1');
10+
assertType('int', $value);
11+
12+
$value = Json::decode('1.5');
13+
assertType('float', $value);
14+
15+
$value = Json::decode('false');
16+
assertType('bool', $value);
17+
18+
function unknownType($mixed) {
19+
$value = Json::decode($mixed);
20+
assertType('bool|float|int|stdClass|string', $value);
21+
}
22+
23+
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
use Nette\Utils\Json;
4+
use function PHPStan\Testing\assertType;
5+
6+
$value = Json::decode('true', Json::FORCE_ARRAY);
7+
assertType('bool', $value);
8+
9+
$value = Json::decode('1', Json::FORCE_ARRAY);
10+
assertType('int', $value);
11+
12+
$value = Json::decode('1.5', Json::FORCE_ARRAY);
13+
assertType('float', $value);
14+
15+
$value = Json::decode('false', Json::FORCE_ARRAY);
16+
assertType('bool', $value);
17+
18+
$value = Json::decode('{}', Json::FORCE_ARRAY);
19+
assertType('array', $value);
20+
21+
22+
function unknownType($mixed) {
23+
$value = Json::decode($mixed, Json::FORCE_ARRAY);
24+
assertType('array|bool|float|int|string', $value);
25+
}

0 commit comments

Comments
 (0)