10
10
use PHPStan \Reflection \ReflectionProvider ;
11
11
use PHPStan \Type \BitwiseFlagHelper ;
12
12
use PHPStan \Type \Constant \ConstantBooleanType ;
13
+ use PHPStan \Type \Constant \ConstantStringType ;
14
+ use PHPStan \Type \ConstantScalarType ;
15
+ use PHPStan \Type \ConstantTypeHelper ;
13
16
use PHPStan \Type \DynamicFunctionReturnTypeExtension ;
17
+ use PHPStan \Type \ObjectType ;
14
18
use PHPStan \Type \Type ;
15
19
use PHPStan \Type \TypeCombinator ;
16
- use function in_array ;
20
+ use stdClass ;
21
+ use function is_bool ;
22
+ use function json_decode ;
17
23
18
24
class JsonThrowOnErrorDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
19
25
{
@@ -35,14 +41,11 @@ public function isFunctionSupported(
35
41
FunctionReflection $ functionReflection ,
36
42
): bool
37
43
{
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 ' ;
46
49
}
47
50
48
51
public function getTypeFromFunctionCall (
@@ -53,6 +56,11 @@ public function getTypeFromFunctionCall(
53
56
{
54
57
$ argumentPosition = $ this ->argumentPositions [$ functionReflection ->getName ()];
55
58
$ defaultReturnType = ParametersAcceptorSelector::selectSingle ($ functionReflection ->getVariants ())->getReturnType ();
59
+
60
+ if ($ functionReflection ->getName () === 'json_decode ' ) {
61
+ $ defaultReturnType = $ this ->narrowTypeForJsonDecode ($ functionCall , $ scope , $ defaultReturnType );
62
+ }
63
+
56
64
if (!isset ($ functionCall ->getArgs ()[$ argumentPosition ])) {
57
65
return $ defaultReturnType ;
58
66
}
@@ -65,4 +73,53 @@ public function getTypeFromFunctionCall(
65
73
return $ defaultReturnType ;
66
74
}
67
75
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
+
68
125
}
0 commit comments