Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 231990a

Browse files
Narrow type of json_decode
1 parent 8ad35b3 commit 231990a

File tree

7 files changed

+184
-9
lines changed

7 files changed

+184
-9
lines changed

‎build/composer-require-checker.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"Clue\\React\\Block\\await", "Hoa\\File\\Read"
1313
],
1414
"php-core-extensions" : [
15+
"json",
1516
"Core",
1617
"date",
1718
"pcre",

‎src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php‎

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@
1010
use PHPStan\Reflection\ReflectionProvider;
1111
use PHPStan\Type\BitwiseFlagHelper;
1212
use PHPStan\Type\Constant\ConstantBooleanType;
13+
use PHPStan\Type\Constant\ConstantStringType;
14+
use PHPStan\Type\ConstantScalarType;
15+
use PHPStan\Type\ConstantTypeHelper;
1316
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
17+
use PHPStan\Type\ObjectType;
1418
use PHPStan\Type\Type;
1519
use PHPStan\Type\TypeCombinator;
16-
use function in_array;
20+
use stdClass;
21+
use function is_bool;
22+
use function json_decode;
1723

1824
class JsonThrowOnErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
1925
{
@@ -35,14 +41,11 @@ public function isFunctionSupported(
3541
FunctionReflection $functionReflection,
3642
): bool
3743
{
38-
return $this->reflectionProvider->hasConstant(new FullyQualified('JSON_THROW_ON_ERROR'), null) && in_array(
39-
$functionReflection->getName(),
40-
[
41-
'json_encode',
42-
'json_decode',
43-
],
44-
true,
45-
);
44+
if ($functionReflection->getName() === 'json_decode') {
45+
return true;
46+
}
47+
48+
return $this->reflectionProvider->hasConstant(new FullyQualified('JSON_THROW_ON_ERROR'), null) && $functionReflection->getName() === 'json_encode';
4649
}
4750

4851
public function getTypeFromFunctionCall(
@@ -53,6 +56,11 @@ public function getTypeFromFunctionCall(
5356
{
5457
$argumentPosition = $this->argumentPositions[$functionReflection->getName()];
5558
$defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
59+
60+
if ($functionReflection->getName() === 'json_decode') {
61+
$defaultReturnType = $this->narrowTypeForJsonDecode($functionCall, $scope, $defaultReturnType);
62+
}
63+
5664
if (!isset($functionCall->getArgs()[$argumentPosition])) {
5765
return $defaultReturnType;
5866
}
@@ -65,4 +73,53 @@ public function getTypeFromFunctionCall(
6573
return $defaultReturnType;
6674
}
6775

76+
private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope, Type $fallbackType): Type
77+
{
78+
$args = $funcCall->getArgs();
79+
$isArrayWithoutStdClass = $this->isForceArrayWithoutStdClass($funcCall, $scope);
80+
81+
$firstValueType = $scope->getType($args[0]->value);
82+
if ($firstValueType instanceof ConstantStringType) {
83+
return $this->resolveConstantStringType($firstValueType, $isArrayWithoutStdClass);
84+
}
85+
86+
if ($isArrayWithoutStdClass) {
87+
return TypeCombinator::remove($fallbackType, new ObjectType(stdClass::class));
88+
}
89+
90+
return $fallbackType;
91+
}
92+
93+
/**
94+
* Is "json_decode(..., true)"?
95+
*/
96+
private function isForceArrayWithoutStdClass(FuncCall $funcCall, Scope $scope): bool
97+
{
98+
$args = $funcCall->getArgs();
99+
if (!isset($args[1])) {
100+
return false;
101+
}
102+
103+
$secondArgType = $scope->getType($args[1]->value);
104+
$secondArgValue = $secondArgType instanceof ConstantScalarType ? $secondArgType->getValue() : null;
105+
106+
if (is_bool($secondArgValue)) {
107+
return $secondArgValue;
108+
}
109+
110+
if ($secondArgValue !== null || !isset($args[3])) {
111+
return false;
112+
}
113+
114+
// depends on used constants, @see https://www.php.net/manual/en/json.constants.php#constant.json-object-as-array
115+
return $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($args[3]->value, $scope, 'JSON_OBJECT_AS_ARRAY')->yes();
116+
}
117+
118+
private function resolveConstantStringType(ConstantStringType $constantStringType, bool $isForceArray): Type
119+
{
120+
$decodedValue = json_decode($constantStringType->getValue(), $isForceArray);
121+
122+
return ConstantTypeHelper::getTypeFromValue($decodedValue);
123+
}
124+
68125
}

‎tests/PHPStan/Analyser/NodeScopeResolverTest.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ public function dataFileAsserts(): iterable
1717
require_once __DIR__ . '/data/implode.php';
1818
yield from $this->gatherAssertTypes(__DIR__ . '/data/implode.php');
1919

20+
yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/narrow_type.php');
21+
yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/narrow_type_with_force_array.php');
22+
yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/invalid_type.php');
23+
yield from $this->gatherAssertTypes(__DIR__ . '/data/json-decode/json_object_as_array.php');
24+
2025
require_once __DIR__ . '/data/bug2574.php';
2126

2227
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug2574.php');
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Analyser\JsonDecode;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
$value = json_decode('{"key"}');
8+
assertType('null', $value);
9+
10+
$value = json_decode('{"key"}', true);
11+
assertType('null', $value);
12+
13+
$value = json_decode('{"key"}', null);
14+
assertType('null', $value);
15+
16+
$value = json_decode('{"key"}', false);
17+
assertType('null', $value);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Analyser\JsonDecode;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
// @see https://3v4l.org/YFlHF
8+
function ($mixed) {
9+
$value = json_decode($mixed, null, 512, JSON_OBJECT_AS_ARRAY);
10+
assertType('mixed~stdClass', $value);
11+
};
12+
13+
function ($mixed) {
14+
$flagsAsVariable = JSON_OBJECT_AS_ARRAY;
15+
16+
$value = json_decode($mixed, null, 512, $flagsAsVariable);
17+
assertType('mixed~stdClass', $value);
18+
};
19+
20+
function ($mixed) {
21+
$value = json_decode($mixed, null, 512, JSON_OBJECT_AS_ARRAY | JSON_BIGINT_AS_STRING);
22+
assertType('mixed~stdClass', $value);
23+
};
24+
25+
function ($mixed) {
26+
$value = json_decode($mixed, null);
27+
assertType('mixed', $value);
28+
};
29+
30+
function ($mixed, $unknownFlags) {
31+
$value = json_decode($mixed, null, 512, $unknownFlags);
32+
assertType('mixed', $value);
33+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Analyser\JsonDecode;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
$value = json_decode('true');
8+
assertType('true', $value);
9+
10+
$value = json_decode('1');
11+
assertType('1', $value);
12+
13+
$value = json_decode('1.5');
14+
assertType('1.5', $value);
15+
16+
$value = json_decode('false');
17+
assertType('false', $value);
18+
19+
$value = json_decode('{}');
20+
assertType('stdClass', $value);
21+
22+
$value = json_decode('[1, 2, 3]');
23+
assertType('array{1, 2, 3}', $value);
24+
25+
26+
function ($mixed) {
27+
$value = json_decode($mixed);
28+
assertType('mixed', $value);
29+
};
30+
31+
function ($mixed) {
32+
$value = json_decode($mixed, false);
33+
assertType('mixed', $value);
34+
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Analyser\JsonDecode;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
$value = json_decode('true', true);
8+
assertType('true', $value);
9+
10+
$value = json_decode('1', true);
11+
assertType('1', $value);
12+
13+
$value = json_decode('1.5', true);
14+
assertType('1.5', $value);
15+
16+
$value = json_decode('false', true);
17+
assertType('false', $value);
18+
19+
$value = json_decode('{}', true);
20+
assertType('array{}', $value);
21+
22+
$value = json_decode('[1, 2, 3]', true);
23+
assertType('array{1, 2, 3}', $value);
24+
25+
function ($mixed) {
26+
$value = json_decode($mixed, true);
27+
assertType('mixed~stdClass', $value);
28+
};

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /