|
| 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 | +} |
0 commit comments