Skip to content

Commit ef64849

Browse files
authored
[DowngradePhp83] Add DowngradeJsonValidateRector (#313)
* [DowngradePhp83] Add DowngradeJsonValidateRector * [DowngradePhp83] skip different function * register
1 parent 73e0dbb commit ef64849

File tree

7 files changed

+281
-0
lines changed

7 files changed

+281
-0
lines changed

config/set/downgrade-php83.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
use Rector\ValueObject\PhpVersion;
88
use Rector\DowngradePhp83\Rector\ClassConst\DowngradeTypedClassConstRector;
99
use Rector\DowngradePhp83\Rector\ClassConstFetch\DowngradeDynamicClassConstFetchRector;
10+
use Rector\DowngradePhp83\Rector\FuncCall\DowngradeJsonValidateRector;
1011

1112
return static function (RectorConfig $rectorConfig): void {
1213
$rectorConfig->phpVersion(PhpVersion::PHP_82);
1314
$rectorConfig->rules([
1415
DowngradeTypedClassConstRector::class,
1516
DowngradeReadonlyAnonymousClassRector::class,
1617
DowngradeDynamicClassConstFetchRector::class,
18+
DowngradeJsonValidateRector::class,
1719
]);
1820
};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\DowngradePhp83\Rector\FuncCall\DowngradeJsonValidateRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class DowngradeJsonValidateRectorTest extends AbstractRectorTestCase
12+
{
13+
#[DataProvider('provideData')]
14+
public function test(string $filePath): void
15+
{
16+
$this->doTestFile($filePath);
17+
}
18+
19+
public static function provideData(): Iterator
20+
{
21+
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
22+
}
23+
24+
public function provideConfigFilePath(): string
25+
{
26+
return __DIR__ . '/config/configured_rule.php';
27+
}
28+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace Rector\Tests\DowngradePhp83\Rector\FuncCall\DowngradeJsonValidateRector\Fixture;
4+
5+
class InEcho
6+
{
7+
public function run($json)
8+
{
9+
echo (int) json_validate($json);
10+
}
11+
}
12+
13+
?>
14+
-----
15+
<?php
16+
17+
namespace Rector\Tests\DowngradePhp83\Rector\FuncCall\DowngradeJsonValidateRector\Fixture;
18+
19+
class InEcho
20+
{
21+
public function run($json)
22+
{
23+
$jsonValidate = function (string $json, int $depth = 512, int $flags = 0) {
24+
if (function_exists('json_validate')) {
25+
return json_validate($json, $depth, $flags);
26+
}
27+
$maxDepth = 0x7fffffff;
28+
if (0 !== $flags && \defined('JSON_INVALID_UTF8_IGNORE') && \JSON_INVALID_UTF8_IGNORE !== $flags) {
29+
throw new \ValueError('json_validate(): Argument #3 ($flags) must be a valid flag (allowed flags: JSON_INVALID_UTF8_IGNORE)');
30+
}
31+
if ($depth <= 0) {
32+
throw new \ValueError('json_validate(): Argument #2 ($depth) must be greater than 0');
33+
}
34+
if ($depth > $maxDepth) {
35+
throw new \ValueError(sprintf('json_validate(): Argument #2 ($depth) must be less than %d', $maxDepth));
36+
}
37+
json_decode($json, true, $depth, $flags);
38+
return \JSON_ERROR_NONE === json_last_error();
39+
};
40+
echo (int) $jsonValidate($json);
41+
}
42+
}
43+
44+
?>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Rector\Tests\DowngradePhp83\Rector\FuncCall\DowngradeJsonValidateRector\Fixture;
4+
5+
class SkipDifferentFunction
6+
{
7+
public function run($json)
8+
{
9+
echo strlen($json);
10+
}
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use Rector\DowngradePhp83\Rector\FuncCall\DowngradeJsonValidateRector;
7+
8+
return static function (RectorConfig $rectorConfig): void {
9+
$rectorConfig->rule(DowngradeJsonValidateRector::class);
10+
};
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\DowngradePhp83\Rector\FuncCall;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\Assign;
9+
use PhpParser\Node\Expr\CallLike;
10+
use PhpParser\Node\Expr\Closure;
11+
use PhpParser\Node\Expr\FuncCall;
12+
use PhpParser\Node\Expr\Variable;
13+
use PhpParser\Node\Stmt\Echo_;
14+
use PhpParser\Node\Stmt\Expression;
15+
use PhpParser\Node\Stmt\Return_;
16+
use PhpParser\Node\Stmt\Switch_;
17+
use PHPStan\Analyser\Scope;
18+
use Rector\Contract\PhpParser\Node\StmtsAwareInterface;
19+
use Rector\Exception\ShouldNotHappenException;
20+
use Rector\NodeAnalyzer\ExprInTopStmtMatcher;
21+
use Rector\NodeTypeResolver\Node\AttributeKey;
22+
use Rector\PhpParser\Parser\InlineCodeParser;
23+
use Rector\Rector\AbstractRector;
24+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
25+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
26+
27+
/**
28+
* @changelog https://wiki.php.net/rfc/json_validate
29+
*
30+
* @see \Rector\Tests\DowngradePhp83\Rector\FuncCall\DowngradeJsonValidateRector\DowngradeJsonValidateRectorTest
31+
*/
32+
final class DowngradeJsonValidateRector extends AbstractRector
33+
{
34+
private ?Closure $cachedClosure = null;
35+
36+
public function __construct(
37+
private readonly InlineCodeParser $inlineCodeParser,
38+
private readonly ExprInTopStmtMatcher $exprInTopStmtMatcher
39+
) {
40+
}
41+
42+
public function getRuleDefinition(): RuleDefinition
43+
{
44+
return new RuleDefinition('Replace json_validate() function', [
45+
new CodeSample(
46+
<<<'CODE_SAMPLE'
47+
json_validate('{"foo": "bar"}');
48+
CODE_SAMPLE
49+
,
50+
<<<'CODE_SAMPLE'
51+
$jsonValidate = function (string $json, int $depth = 512, int $flags = 0) {
52+
if (function_exists('json_validate')) {
53+
return json_validate($json, $depth, $flags);
54+
}
55+
56+
$maxDepth = 0x7FFFFFFF;
57+
58+
if (0 !== $flags && \defined('JSON_INVALID_UTF8_IGNORE') && \JSON_INVALID_UTF8_IGNORE !== $flags) {
59+
throw new \ValueError('json_validate(): Argument #3 ($flags) must be a valid flag (allowed flags: JSON_INVALID_UTF8_IGNORE)');
60+
}
61+
62+
if ($depth <= 0) {
63+
throw new \ValueError('json_validate(): Argument #2 ($depth) must be greater than 0');
64+
}
65+
66+
if ($depth > $maxDepth) {
67+
throw new \ValueError(sprintf('json_validate(): Argument #2 ($depth) must be less than %d', $maxDepth));
68+
}
69+
70+
json_decode($json, true, $depth, $flags);
71+
return \JSON_ERROR_NONE === json_last_error();
72+
};
73+
$jsonValidate('{"foo": "bar"}');
74+
CODE_SAMPLE
75+
),
76+
]);
77+
}
78+
79+
/**
80+
* @return array<class-string<Node>>
81+
*/
82+
public function getNodeTypes(): array
83+
{
84+
return [StmtsAwareInterface::class, Switch_::class, Return_::class, Expression::class, Echo_::class];
85+
}
86+
87+
/**
88+
* @param StmtsAwareInterface|Switch_|Return_|Expression|Echo_ $node
89+
* @return Node[]|null
90+
*/
91+
public function refactor(Node $node): ?array
92+
{
93+
$expr = $this->exprInTopStmtMatcher->match(
94+
$node,
95+
function (Node $subNode): bool {
96+
if (! $subNode instanceof FuncCall) {
97+
return false;
98+
}
99+
100+
// need pull Scope from target traversed sub Node
101+
return ! $this->shouldSkip($subNode);
102+
}
103+
);
104+
105+
if (! $expr instanceof FuncCall) {
106+
return null;
107+
}
108+
109+
$variable = new Variable('jsonValidate');
110+
111+
$function = $this->createClosure();
112+
$expression = new Expression(new Assign($variable, $function));
113+
114+
$expr->name = $variable;
115+
116+
return [$expression, $node];
117+
}
118+
119+
private function createClosure(): Closure
120+
{
121+
if ($this->cachedClosure instanceof Closure) {
122+
return clone $this->cachedClosure;
123+
}
124+
125+
$stmts = $this->inlineCodeParser->parseFile(__DIR__ . '/../../snippet/json_validate_closure.php.inc');
126+
127+
/** @var Expression $expression */
128+
$expression = $stmts[0];
129+
130+
$expr = $expression->expr;
131+
if (! $expr instanceof Closure) {
132+
throw new ShouldNotHappenException();
133+
}
134+
135+
$this->cachedClosure = $expr;
136+
137+
return $expr;
138+
}
139+
140+
private function shouldSkip(CallLike $callLike): bool
141+
{
142+
if (! $callLike instanceof FuncCall) {
143+
return false;
144+
}
145+
146+
if (! $this->isName($callLike, 'json_validate')) {
147+
return true;
148+
}
149+
150+
$scope = $callLike->getAttribute(AttributeKey::SCOPE);
151+
if ($scope instanceof Scope && $scope->isInFunctionExists('json_validate')) {
152+
return true;
153+
}
154+
155+
if ($callLike->isFirstClassCallable()) {
156+
return true;
157+
}
158+
159+
$args = $callLike->getArgs();
160+
return count($args) < 1;
161+
}
162+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
function (string $json, int $depth = 512, int $flags = 0) {
4+
if (function_exists('json_validate')) {
5+
return json_validate($json, $depth, $flags);
6+
}
7+
8+
$maxDepth = 0x7FFFFFFF;
9+
10+
if (0 !== $flags && \defined('JSON_INVALID_UTF8_IGNORE') && \JSON_INVALID_UTF8_IGNORE !== $flags) {
11+
throw new \ValueError('json_validate(): Argument #3 ($flags) must be a valid flag (allowed flags: JSON_INVALID_UTF8_IGNORE)');
12+
}
13+
14+
if ($depth <= 0) {
15+
throw new \ValueError('json_validate(): Argument #2 ($depth) must be greater than 0');
16+
}
17+
18+
if ($depth > $maxDepth) {
19+
throw new \ValueError(sprintf('json_validate(): Argument #2 ($depth) must be less than %d', $maxDepth));
20+
}
21+
22+
json_decode($json, true, $depth, $flags);
23+
return \JSON_ERROR_NONE === json_last_error();
24+
};

0 commit comments

Comments
 (0)