Skip to content

Commit 38ca032

Browse files
TomasVotrubaondrejmirtes
authored andcommitted
Extend JsonThrowOnErrorDynamicReturnTypeExtension to detect knonw type from contssant string value
1 parent c4e495a commit 38ca032

File tree

1 file changed

+109
-9
lines changed

1 file changed

+109
-9
lines changed

src/Type/Php/JsonThrowOnErrorDynamicReturnTypeExtension.php

Lines changed: 109 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,41 @@
22

33
namespace PHPStan\Type\Php;
44

5+
<<<<<<< HEAD
6+
=======
7+
use PhpParser\Node\Arg;
8+
use PhpParser\Node\Expr;
9+
use PhpParser\Node\Expr\BinaryOp\BitwiseOr;
10+
use PhpParser\Node\Expr\ConstFetch;
11+
>>>>>>> Extend JsonThrowOnErrorDynamicReturnTypeExtension to detect knonw type from contssant string value
512
use PhpParser\Node\Expr\FuncCall;
613
use PhpParser\Node\Name\FullyQualified;
714
use PHPStan\Analyser\Scope;
815
use PHPStan\Reflection\FunctionReflection;
916
use PHPStan\Reflection\ParametersAcceptorSelector;
1017
use PHPStan\Reflection\ReflectionProvider;
18+
<<<<<<< HEAD
1119
use PHPStan\Type\BitwiseFlagHelper;
1220
use PHPStan\Type\Constant\ConstantBooleanType;
21+
=======
22+
use PHPStan\Type\ArrayType;
23+
use PHPStan\Type\BooleanType;
24+
use PHPStan\Type\Constant\ConstantBooleanType;
25+
use PHPStan\Type\Constant\ConstantIntegerType;
26+
use PHPStan\Type\Constant\ConstantStringType;
27+
use PHPStan\Type\ConstantTypeHelper;
28+
>>>>>>> Extend JsonThrowOnErrorDynamicReturnTypeExtension to detect knonw type from contssant string value
1329
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
30+
use PHPStan\Type\FloatType;
31+
use PHPStan\Type\IntegerType;
32+
use PHPStan\Type\MixedType;
33+
use PHPStan\Type\ObjectType;
34+
use PHPStan\Type\StringType;
1435
use PHPStan\Type\Type;
1536
use PHPStan\Type\TypeCombinator;
16-
use function in_array;
37+
use PHPStan\Type\UnionType;
38+
use stdClass;
39+
use function json_decode;
1740

1841
class JsonThrowOnErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
1942
{
@@ -35,14 +58,11 @@ public function isFunctionSupported(
3558
FunctionReflection $functionReflection,
3659
): bool
3760
{
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-
);
61+
if ($functionReflection->getName() === 'json_decode') {
62+
return true;
63+
}
64+
65+
return $this->reflectionProvider->hasConstant(new FullyQualified('JSON_THROW_ON_ERROR'), null) && $functionReflection->getName() === 'json_encode';
4666
}
4767

4868
public function getTypeFromFunctionCall(
@@ -51,8 +71,19 @@ public function getTypeFromFunctionCall(
5171
Scope $scope,
5272
): Type
5373
{
74+
// update type based on JSON_THROW_ON_ERROR
5475
$argumentPosition = $this->argumentPositions[$functionReflection->getName()];
5576
$defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
77+
78+
// narrow type for json_decode()
79+
if ($functionReflection->getName() === 'json_decode') {
80+
$jsonDecodeNarrowedType = $this->narrowTypeForJsonDecode($functionCall, $scope);
81+
// improve type
82+
if (! $jsonDecodeNarrowedType instanceof MixedType) {
83+
$defaultReturnType = $jsonDecodeNarrowedType;
84+
}
85+
}
86+
5687
if (!isset($functionCall->getArgs()[$argumentPosition])) {
5788
return $defaultReturnType;
5889
}
@@ -65,4 +96,73 @@ public function getTypeFromFunctionCall(
6596
return $defaultReturnType;
6697
}
6798

99+
private function narrowTypeForJsonDecode(FuncCall $funcCall, Scope $scope): Type
100+
{
101+
$args = $funcCall->getArgs();
102+
$isForceArray = $this->isForceArray($funcCall);
103+
104+
$firstArgValue = $args[0]->value;
105+
$firstValueType = $scope->getType($firstArgValue);
106+
107+
if ($firstValueType instanceof ConstantStringType) {
108+
$resolvedType = $this->resolveConstantStringType($firstValueType, $isForceArray);
109+
} else {
110+
$resolvedType = new MixedType();
111+
}
112+
113+
// prefer specific type
114+
if (! $resolvedType instanceof MixedType) {
115+
return $resolvedType;
116+
}
117+
118+
// fallback type
119+
if ($isForceArray) {
120+
return new UnionType([
121+
new ArrayType(new MixedType(), new MixedType()),
122+
new StringType(),
123+
new FloatType(),
124+
new IntegerType(),
125+
new BooleanType(),
126+
]);
127+
}
128+
129+
// scalar types with stdClass
130+
return new UnionType([
131+
new ObjectType(stdClass::class),
132+
new StringType(),
133+
new FloatType(),
134+
new IntegerType(),
135+
new BooleanType(),
136+
]);
137+
}
138+
139+
/**
140+
* Is "json_decode(..., true)"?
141+
* @param Arg[] $args
142+
*/
143+
private function isForceArray(FuncCall $funcCall): bool
144+
{
145+
$args = $funcCall->getArgs();
146+
147+
if (!isset($args[1])) {
148+
return false;
149+
}
150+
151+
$secondArgValue = $args[1]->value;
152+
if ($secondArgValue instanceof ConstFetch) {
153+
if ($secondArgValue->name->toLowerString() === 'true') {
154+
return true;
155+
}
156+
}
157+
158+
return false;
159+
}
160+
161+
private function resolveConstantStringType(ConstantStringType $constantStringType, bool $isForceArray): Type
162+
{
163+
$decodedValue = json_decode($constantStringType->getValue(), $isForceArray);
164+
165+
return ConstantTypeHelper::getTypeFromValue($decodedValue);
166+
}
167+
68168
}

0 commit comments

Comments
 (0)