44namespace TheCodingMachine \Safe \PHPStan \Type \Php ;
55
66use PhpParser \Node \Expr \FuncCall ;
7+ use PhpParser \Node \Name ;
78use PHPStan \Analyser \Scope ;
89use PHPStan \Reflection \FunctionReflection ;
9- use PHPStan \Reflection \ParametersAcceptorSelector ;
10- use PHPStan \Type \BitwiseFlagHelper ;
11- use PHPStan \Type \Constant \ConstantStringType ;
12- use PHPStan \Type \ConstantScalarType ;
13- use PHPStan \Type \ConstantTypeHelper ;
10+ use PHPStan \Reflection \ReflectionProvider ;
1411use PHPStan \Type \DynamicFunctionReturnTypeExtension ;
1512use PHPStan \Type \NeverType ;
16- use PHPStan \Type \ObjectWithoutClassType ;
13+ use PHPStan \Type \Php \ JsonThrowOnErrorDynamicReturnTypeExtension ;
1714use PHPStan \Type \Type ;
18- use PHPStan \Type \TypeCombinator ;
19- use Safe \Exceptions \JsonException ;
2015
2116/**
2217 * @see \PHPStan\Type\Php\JsonThrowOnErrorDynamicReturnTypeExtension
2318 */
2419final class JsonDecodeDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
2520{
2621 public function __construct (
27- private readonly BitwiseFlagHelper $ bitwiseFlagAnalyser ,
22+ private readonly JsonThrowOnErrorDynamicReturnTypeExtension $ phpstanCheck ,
23+ private readonly ReflectionProvider $ reflectionProvider ,
2824 ) {
2925 }
3026
@@ -35,74 +31,14 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo
3531
3632 public function getTypeFromFunctionCall (FunctionReflection $ functionReflection , FuncCall $ functionCall , Scope $ scope ): Type
3733 {
38- $ defaultReturnType = ParametersAcceptorSelector::selectFromArgs (
39- $ scope ,
40- $ functionCall ->getArgs (),
41- $ functionReflection ->getVariants (),
42- )->getReturnType ();
34+ $ functionReflection = $ this ->reflectionProvider ->getFunction (new Name ('json_decode ' ), null );
35+ $ result = $ this ->phpstanCheck ->getTypeFromFunctionCall ($ functionReflection , $ functionCall , $ scope );
4336
44- return $ this ->narrowTypeForJsonDecode ($ functionCall , $ scope , $ defaultReturnType );
45- }
46-
47- private function narrowTypeForJsonDecode (FuncCall $ funcCall , Scope $ scope , Type $ fallbackType ): Type
48- {
49- $ args = $ funcCall ->getArgs ();
50- $ isForceArray = $ this ->isForceArray ($ funcCall , $ scope );
51- if (!isset ($ args [0 ])) {
52- return $ fallbackType ;
53- }
54-
55- $ firstValueType = $ scope ->getType ($ args [0 ]->value );
56- if ([] !== $ firstValueType ->getConstantStrings ()) {
57- $ types = [];
58-
59- foreach ($ firstValueType ->getConstantStrings () as $ constantString ) {
60- $ types [] = $ this ->resolveConstantStringType ($ constantString , $ isForceArray );
61- }
62-
63- return TypeCombinator::union (...$ types );
64- }
65-
66- if ($ isForceArray ) {
67- return TypeCombinator::remove ($ fallbackType , new ObjectWithoutClassType ());
68- }
69-
70- return $ fallbackType ;
71- }
72-
73- /**
74- * Is "json_decode(..., true)"?
75- */
76- private function isForceArray (FuncCall $ funcCall , Scope $ scope ): bool
77- {
78- $ args = $ funcCall ->getArgs ();
79- if (!isset ($ args [1 ])) {
80- return false ;
81- }
82-
83- $ secondArgType = $ scope ->getType ($ args [1 ]->value );
84- $ secondArgValue = 1 === \count ($ secondArgType ->getConstantScalarValues ()) ? $ secondArgType ->getConstantScalarValues ()[0 ] : null ;
85-
86- if (is_bool ($ secondArgValue )) {
87- return $ secondArgValue ;
88- }
89-
90- if ($ secondArgValue !== null || !isset ($ args [3 ])) {
91- return false ;
92- }
93-
94- // depends on used constants, @see https://www.php.net/manual/en/json.constants.php#constant.json-object-as-array
95- return $ this ->bitwiseFlagAnalyser ->bitwiseOrContainsConstant ($ args [3 ]->value , $ scope , 'JSON_OBJECT_AS_ARRAY ' )->yes ();
96- }
97-
98- private function resolveConstantStringType (ConstantStringType $ constantStringType , bool $ isForceArray ): Type
99- {
100- try {
101- $ decodedValue = \Safe \json_decode ($ constantStringType ->getValue (), $ isForceArray );
102- } catch (JsonException ) {
37+ // if PHPStan reports null and there is a json error, then an invalid constant string was passed
38+ if ($ result ->isNull ()->yes () && JSON_ERROR_NONE !== json_last_error ()) {
10339 return new NeverType ();
10440 }
10541
106- return ConstantTypeHelper:: getTypeFromValue ( $ decodedValue ) ;
42+ return $ result ;
10743 }
10844}
0 commit comments