Skip to content

Commit 6535f0e

Browse files
committed
add JsonDecodeDynamicReturnTypeExtension
1 parent 3f248d8 commit 6535f0e

7 files changed

+235
-3
lines changed

Diff for: composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
},
5353
"autoload-dev": {
5454
"psr-4": {
55-
"PHPStan\\Tests\\": "tests/"
55+
"PHPStan\\": "tests/"
5656
},
5757
"classmap": [
5858
"tests/"
+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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+
28+
public function getClass(): string
29+
{
30+
return 'Nette\Utils\Json';
31+
}
32+
33+
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
34+
{
35+
return $methodReflection->getName() === 'decode';
36+
}
37+
38+
public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): Type
39+
{
40+
$args = $methodCall->getArgs();
41+
42+
$isForceArray = $this->isForceArray($args);
43+
44+
$firstArgValue = $args[0]->value;
45+
$firstValueType = $scope->getType($firstArgValue);
46+
47+
if ($firstValueType instanceof ConstantStringType) {
48+
$resolvedType = $this->resolveConstantStringType($firstValueType, $isForceArray);
49+
} else {
50+
$resolvedType = new MixedType();
51+
}
52+
53+
if (! $resolvedType instanceof MixedType) {
54+
return $resolvedType;
55+
}
56+
57+
// fallback type
58+
if ($isForceArray) {
59+
return new UnionType([
60+
new ArrayType(new MixedType(), new MixedType()),
61+
new StringType(),
62+
new FloatType(),
63+
new IntegerType(),
64+
new BooleanType(),
65+
]);
66+
}
67+
68+
// scalar types with stdClass
69+
return new UnionType([
70+
new ObjectType(stdClass::class),
71+
new StringType(),
72+
new FloatType(),
73+
new IntegerType(),
74+
new BooleanType(),
75+
]);
76+
}
77+
78+
/**
79+
* @param Arg[] $args
80+
*/
81+
private function isForceArray(array $args): bool
82+
{
83+
if (!isset($args[1])) {
84+
return false;
85+
}
86+
87+
$secondArg = $args[1];
88+
89+
// is second arg force array?
90+
if ($secondArg->value instanceof ClassConstFetch) {
91+
$classConstFetch = $secondArg->value;
92+
93+
if ($classConstFetch->class instanceof Name) {
94+
if (! $classConstFetch->name instanceof \PhpParser\Node\Identifier) {
95+
return false;
96+
}
97+
98+
if ($classConstFetch->class->toString() !== 'Nette\Utils\Json') {
99+
return false;
100+
}
101+
102+
if ($classConstFetch->name->toString() === 'FORCE_ARRAY') {
103+
return true;
104+
}
105+
}
106+
}
107+
108+
return false;
109+
}
110+
111+
private function resolveConstantStringType(ConstantStringType $constantStringType, bool $isForceArray): Type
112+
{
113+
if ($isForceArray) {
114+
$decodedValue = Json::decode($constantStringType->getValue(), Json::FORCE_ARRAY);
115+
} else {
116+
$decodedValue = Json::decode($constantStringType->getValue());
117+
}
118+
119+
if (is_bool($decodedValue)) {
120+
return new BooleanType();
121+
}
122+
123+
if (is_array($decodedValue)) {
124+
return new ArrayType(new MixedType(), new MixedType());
125+
}
126+
127+
if (is_object($decodedValue) && get_class($decodedValue) === stdClass::class) {
128+
return new ObjectType(stdClass::class);
129+
}
130+
131+
if (is_int($decodedValue)) {
132+
return new IntegerType();
133+
}
134+
135+
if (is_float($decodedValue)) {
136+
return new FloatType();
137+
}
138+
139+
return new MixedType();
140+
}
141+
142+
}

Diff for: tests/Type/Nette/FormContainerValuesDynamicReturnTypeExtensionTest.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php declare(strict_types = 1);
22

3-
namespace PHPStan\Tests\Type\Nette;
3+
namespace PHPStan\Type\Nette;
44

55
use PhpParser\Node\Arg;
66
use PhpParser\Node\Expr;
@@ -13,7 +13,6 @@
1313
use PHPStan\Type\Generic\TemplateTypeMap;
1414
use PHPStan\Type\IterableType;
1515
use PHPStan\Type\MixedType;
16-
use PHPStan\Type\Nette\FormContainerValuesDynamicReturnTypeExtension;
1716
use PHPStan\Type\ObjectType;
1817
use PHPStan\Type\UnionType;
1918
use PHPStan\Type\VerbosityLevel;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Nette;
4+
5+
use PHPStan\Testing\TypeInferenceTestCase;
6+
7+
final class JsonDecodeDynamicReturnTypeExtensionTest extends TypeInferenceTestCase
8+
{
9+
10+
/**
11+
* @return iterable<mixed>
12+
*/
13+
public function dataAsserts(): iterable
14+
{
15+
yield from $this->gatherAssertTypes(__DIR__ . '/data/json_decode.php');
16+
yield from $this->gatherAssertTypes(__DIR__ . '/data/json_decode_force_array.php');
17+
}
18+
19+
/**
20+
* @dataProvider dataAsserts()
21+
* @param string $assertType
22+
* @param string $file
23+
* @param mixed ...$args
24+
*/
25+
public function testAsserts(string $assertType, string $file, ...$args): void
26+
{
27+
$this->assertFileAsserts($assertType, $file, ...$args);
28+
}
29+
30+
/**
31+
* @return string[]
32+
*/
33+
public static function getAdditionalConfigFiles(): array
34+
{
35+
return [__DIR__ . '/config/json_decode_extension.neon'];
36+
}
37+
38+
}

Diff for: tests/Type/Nette/config/json_decode_extension.neon

+5
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

Diff for: tests/Type/Nette/data/json_decode.php

+23
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+

Diff for: tests/Type/Nette/data/json_decode_force_array.php

+25
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)