Skip to content

Commit f9ad159

Browse files
ArshidArshid
authored andcommitted
[php 8.3] Add json_validate rule
1 parent bfc6cc1 commit f9ad159

3 files changed

Lines changed: 130 additions & 24 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php83\Rector\BooleanAnd\JsonValidateRector\Fixture;
4+
5+
if (json_decode($json, true, -2) !== null && json_last_error() === JSON_ERROR_NONE){
6+
echo 1;
7+
}
8+
?>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php83\Rector\BooleanAnd\JsonValidateRector\Fixture;
4+
5+
if (json_decode(json: $json, associative: true, depth:512, flags: 1) !== null && json_last_error() === JSON_ERROR_NONE){
6+
echo 1;
7+
}
8+
?>

rules/Php83/Rector/BooleanAnd/JsonValidateRector.php

Lines changed: 114 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,24 @@
55
namespace Rector\Php83\Rector\BooleanAnd;
66

77
use PhpParser\Node;
8+
use PhpParser\Node\Arg;
89
use PhpParser\Node\Expr\BinaryOp\BooleanAnd;
910
use PhpParser\Node\Expr\BinaryOp\Identical;
1011
use PhpParser\Node\Expr\BinaryOp\NotIdentical;
1112
use PhpParser\Node\Expr\ConstFetch;
1213
use PhpParser\Node\Expr\FuncCall;
14+
use PhpParser\Node\Identifier;
1315
use PhpParser\Node\Name;
16+
use PHPStan\Analyser\Scope;
17+
use PHPStan\Reflection\Native\NativeFunctionReflection;
18+
use Rector\NodeAnalyzer\ArgsAnalyzer;
1419
use Rector\NodeManipulator\BinaryOpManipulator;
20+
use Rector\NodeTypeResolver\Node\AttributeKey;
21+
use Rector\NodeTypeResolver\PHPStan\ParametersAcceptorSelectorVariantsWrapper;
1522
use Rector\Php71\ValueObject\TwoNodeMatch;
23+
use Rector\PhpParser\Node\Value\ValueResolver;
1624
use Rector\Rector\AbstractRector;
25+
use Rector\Reflection\ReflectionResolver;
1726
use Rector\ValueObject\PhpVersionFeature;
1827
use Rector\ValueObject\PolyfillPackage;
1928
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
@@ -26,8 +35,15 @@
2635
*/
2736
final 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

Comments
 (0)