55namespace Rector \Php83 \Rector \BooleanAnd ;
66
77use PhpParser \Node ;
8+ use PhpParser \Node \Arg ;
89use PhpParser \Node \Expr \BinaryOp \BooleanAnd ;
910use PhpParser \Node \Expr \BinaryOp \Identical ;
1011use PhpParser \Node \Expr \BinaryOp \NotIdentical ;
1112use PhpParser \Node \Expr \ConstFetch ;
1213use PhpParser \Node \Expr \FuncCall ;
14+ use PhpParser \Node \Identifier ;
1315use PhpParser \Node \Name ;
16+ use PHPStan \Analyser \Scope ;
17+ use PHPStan \Reflection \Native \NativeFunctionReflection ;
18+ use Rector \NodeAnalyzer \ArgsAnalyzer ;
1419use Rector \NodeManipulator \BinaryOpManipulator ;
20+ use Rector \NodeTypeResolver \Node \AttributeKey ;
21+ use Rector \NodeTypeResolver \PHPStan \ParametersAcceptorSelectorVariantsWrapper ;
1522use Rector \Php71 \ValueObject \TwoNodeMatch ;
23+ use Rector \PhpParser \Node \Value \ValueResolver ;
1624use Rector \Rector \AbstractRector ;
25+ use Rector \Reflection \ReflectionResolver ;
1726use Rector \ValueObject \PhpVersionFeature ;
1827use Rector \ValueObject \PolyfillPackage ;
1928use Rector \VersionBonding \Contract \MinPhpVersionInterface ;
2635 */
2736final class JsonValidateRector extends AbstractRector implements MinPhpVersionInterface, RelatedPolyfillInterface
2837{
38+ protected const ARG_NAMES = ['json ' , 'associative ' , 'depth ' , 'flags ' ];
39+
40+ private const JSON_MAX_DEPTH = 0x7FFFFFFF ;
41+
2942 public function __construct (
30- private readonly BinaryOpManipulator $ binaryOpManipulator
43+ private readonly BinaryOpManipulator $ binaryOpManipulator ,
44+ private readonly ReflectionResolver $ reflectionResolver ,
45+ private readonly ArgsAnalyzer $ argsAnalyzer ,
46+ private ValueResolver $ valueResolver ,
3147 ) {
3248 }
3349
@@ -80,32 +96,29 @@ public function refactor(Node $node): ?Node
8096 return null ;
8197 }
8298
99+ $ scope = $ node ->getAttribute (AttributeKey::SCOPE );
100+ if (! $ scope instanceof Scope) {
101+ return null ;
102+ }
103+
83104 $ args = $ funcCall ->getArgs ();
105+ $ positions = $ this ->argsAnalyzer ->hasNamedArg ($ args )
106+ ? $ this ->resolveNamedPositions ($ args )
107+ : $ this ->resolveOriginalPositions ($ funcCall , $ scope );
84108
85- if (! $ this -> validateFlag ( $ args )) {
109+ if ( $ positions === []) {
86110 return null ;
87111 }
88112
113+ if (! $ this ->validateArgs ($ args , $ positions )) {
114+ return null ;
115+ }
89116 $ funcCall ->name = new Name ('json_validate ' );
90117 $ funcCall ->args = $ args ;
91118
92119 return $ funcCall ;
93120 }
94121
95- protected function validateFlag (array $ args ){
96- if (0 !== $ flags && \defined ('JSON_INVALID_UTF8_IGNORE ' ) && \JSON_INVALID_UTF8_IGNORE !== $ flags ) {
97- throw new \ValueError ('json_validate(): Argument #3 ($flags) must be a valid flag (allowed flags: JSON_INVALID_UTF8_IGNORE) ' );
98- }
99-
100- if ($ depth <= 0 ) {
101- throw new \ValueError ('json_validate(): Argument #2 ($depth) must be greater than 0 ' );
102- }
103-
104- if ($ depth > self ::JSON_MAX_DEPTH ) {
105- throw new \ValueError (sprintf ('json_validate(): Argument #2 ($depth) must be less than %d ' , self ::JSON_MAX_DEPTH ));
106- }
107- }
108-
109122 public function providePolyfillPackage (): string
110123 {
111124 return PolyfillPackage::PHP_83 ;
@@ -114,29 +127,29 @@ public function providePolyfillPackage(): string
114127 public function matchJsonValidateArg (BooleanAnd $ booleanAnd ): ?FuncCall
115128 {
116129 // match: json_decode(...) !== null OR null !== json_decode(...)
117- if (!($ booleanAnd ->left instanceof NotIdentical)) {
130+ if (! ($ booleanAnd ->left instanceof NotIdentical)) {
118131 return null ;
119132 }
120133
121134 $ decodeMatch = $ this ->binaryOpManipulator ->matchFirstAndSecondConditionNode (
122135 $ booleanAnd ->left ,
123- fn ($ node ) => $ node instanceof FuncCall && $ this ->isName ($ node ->name , 'json_decode ' ),
124- fn ($ node ) => $ node instanceof ConstFetch && $ this ->isName ($ node ->name , 'null ' )
136+ fn ($ node ) => $ node instanceof FuncCall && $ this ->isName ($ node ->name , 'json_decode ' ),
137+ fn ($ node ) => $ node instanceof ConstFetch && $ this ->isName ($ node ->name , 'null ' )
125138 );
126139
127140 if (! $ decodeMatch instanceof TwoNodeMatch) {
128141 return null ;
129142 }
130143
131144 // match: json_last_error() === JSON_ERROR_NONE OR JSON_ERROR_NONE === json_last_error()
132- if (!($ booleanAnd ->right instanceof Identical)) {
145+ if (! ($ booleanAnd ->right instanceof Identical)) {
133146 return null ;
134147 }
135148
136149 $ errorMatch = $ this ->binaryOpManipulator ->matchFirstAndSecondConditionNode (
137150 $ booleanAnd ->right ,
138- fn ($ node ) => $ node instanceof FuncCall && $ this ->isName ($ node ->name , 'json_last_error ' ),
139- fn ($ node ) => $ node instanceof ConstFetch && $ this ->isName ($ node ->name , 'JSON_ERROR_NONE ' )
151+ fn ($ node ) => $ node instanceof FuncCall && $ this ->isName ($ node ->name , 'json_last_error ' ),
152+ fn ($ node ) => $ node instanceof ConstFetch && $ this ->isName ($ node ->name , 'JSON_ERROR_NONE ' )
140153 );
141154
142155 if (! $ errorMatch instanceof TwoNodeMatch) {
@@ -145,10 +158,87 @@ public function matchJsonValidateArg(BooleanAnd $booleanAnd): ?FuncCall
145158
146159 // always return the json_decode(...) call
147160 $ funcCall = $ decodeMatch ->getFirstExpr ();
148- if (! $ funcCall instanceof FuncCall){
161+ if (! $ funcCall instanceof FuncCall) {
149162 return null ;
150163 }
151164 return $ funcCall ;
152165 }
153166
154- }
167+ /**
168+ * @param Arg[] $args
169+ * @param int[]|string[] $positions
170+ */
171+ protected function validateArgs (array $ args , array $ positions ): bool
172+ {
173+ foreach ($ positions as $ position ) {
174+ $ arg = $ args [$ position ] ?? '' ;
175+ if ($ arg instanceof Arg && $ arg ->name instanceof Identifier && $ arg ->name ->toString () === 'flags ' ) {
176+ $ flags = $ this ->valueResolver ->getValue ($ arg );
177+ if ($ flags !== JSON_INVALID_UTF8_IGNORE ) {
178+ return false ;
179+ }
180+ }
181+ if ($ arg instanceof Arg && $ arg ->name instanceof Identifier && $ arg ->name ->toString () === 'depth ' ) {
182+ $ depth = $ this ->valueResolver ->getValue ($ arg );
183+ if ($ depth <= 0 ) {
184+ return false ;
185+ }
186+ if ($ depth > static ::JSON_MAX_DEPTH ) {
187+ return false ;
188+ }
189+ }
190+ }
191+
192+ return true ;
193+ }
194+
195+ /**
196+ * @param Arg[] $args
197+ * @return int[]|string[]
198+ */
199+ private function resolveNamedPositions (array $ args ): array
200+ {
201+ $ positions = [];
202+
203+ foreach ($ args as $ position => $ arg ) {
204+ if (! $ arg ->name instanceof Identifier) {
205+ continue ;
206+ }
207+
208+ if (! $ this ->isNames ($ arg ->name , static ::ARG_NAMES )) {
209+ continue ;
210+ }
211+
212+ $ positions [] = $ position ;
213+ }
214+
215+ return $ positions ;
216+ }
217+
218+ /**
219+ * @return int[]|string[]
220+ */
221+ private function resolveOriginalPositions (FuncCall $ funcCall , Scope $ scope ): array
222+ {
223+ $ functionReflection = $ this ->reflectionResolver ->resolveFunctionLikeReflectionFromCall ($ funcCall );
224+ if (! $ functionReflection instanceof NativeFunctionReflection) {
225+ return [];
226+ }
227+
228+ $ parametersAcceptor = ParametersAcceptorSelectorVariantsWrapper::select (
229+ $ functionReflection ,
230+ $ funcCall ,
231+ $ scope
232+ );
233+
234+ $ positions = [];
235+
236+ foreach ($ parametersAcceptor ->getParameters () as $ position => $ parameterReflection ) {
237+ if (in_array ($ parameterReflection ->getName (), static ::ARG_NAMES , true )) {
238+ $ positions [] = $ position ;
239+ }
240+ }
241+
242+ return $ positions ;
243+ }
244+ }
0 commit comments