From 2ea78136f33fbcc0015d0b602cf58ce72dfd4329 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 9 Aug 2026 17:47:45 +0200 Subject: [PATCH] [deprecation] Deprecate JsonThrowOnErrorRector as it can cause BC breaks Adding JSON_THROW_ON_ERROR turns silent false/null results into a thrown JsonException. Every call site has to be reviewed by hand, so the change needs manual care instead. Use a wrapper tool like nette/utils Json for clear error reporting on any PHP version. --- .../Fixture/fixture.php.inc | 35 --- .../json_last_error_next_after_return.php.inc | 33 --- ...json_last_error_next_after_return2.php.inc | 31 --- .../process_content_from_param.php.inc | 31 --- .../Fixture/process_mixed_content.php.inc | 53 ----- .../Fixture/skip_exact_value.php.inc | 28 --- .../Fixture/skip_json_last_error_next.php.inc | 11 - .../skip_json_last_error_next2.php.inc | 12 - .../Fixture/skip_named_argument_flags.php.inc | 8 - .../skip_named_argument_value_or_json.php.inc | 8 - .../Fixture/skip_with_constant.php.inc | 15 -- .../JsonThrowOnErrorRectorTest.php | 28 --- .../config/configured_rule.php | 9 - .../FuncCall/JsonThrowOnErrorRector.php | 206 +----------------- .../fixture.php.inc | 57 ----- ...xact_value_after_unreachable_stmts.php.inc | 45 ---- ...act_value_after_unreachable_stmts2.php.inc | 51 ----- ...ter_unreachable_stmts_on_top_level.php.inc | 33 --- .../JsonThrowCaseSensitiveConstFetchTest.php | 28 --- .../json_throw_case_sensitive_const_fetch.php | 10 - 20 files changed, 8 insertions(+), 724 deletions(-) delete mode 100644 rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/fixture.php.inc delete mode 100644 rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/json_last_error_next_after_return.php.inc delete mode 100644 rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/json_last_error_next_after_return2.php.inc delete mode 100644 rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/process_content_from_param.php.inc delete mode 100644 rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/process_mixed_content.php.inc delete mode 100644 rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/skip_exact_value.php.inc delete mode 100644 rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/skip_json_last_error_next.php.inc delete mode 100644 rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/skip_json_last_error_next2.php.inc delete mode 100644 rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/skip_named_argument_flags.php.inc delete mode 100644 rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/skip_named_argument_value_or_json.php.inc delete mode 100644 rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/skip_with_constant.php.inc delete mode 100644 rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/JsonThrowOnErrorRectorTest.php delete mode 100644 rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/config/configured_rule.php delete mode 100644 tests/Issues/ScopeNotAvailable/FixtureJsonThrowCaseSensitiveConstFetch/fixture.php.inc delete mode 100644 tests/Issues/ScopeNotAvailable/FixtureJsonThrowCaseSensitiveConstFetch/non_exact_value_after_unreachable_stmts.php.inc delete mode 100644 tests/Issues/ScopeNotAvailable/FixtureJsonThrowCaseSensitiveConstFetch/non_exact_value_after_unreachable_stmts2.php.inc delete mode 100644 tests/Issues/ScopeNotAvailable/FixtureJsonThrowCaseSensitiveConstFetch/non_exact_value_after_unreachable_stmts_on_top_level.php.inc delete mode 100644 tests/Issues/ScopeNotAvailable/JsonThrowCaseSensitiveConstFetchTest.php delete mode 100644 tests/Issues/ScopeNotAvailable/config/json_throw_case_sensitive_const_fetch.php diff --git a/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/fixture.php.inc b/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/fixture.php.inc deleted file mode 100644 index 4447810e978..00000000000 --- a/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/fixture.php.inc +++ /dev/null @@ -1,35 +0,0 @@ - ------ - diff --git a/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/json_last_error_next_after_return.php.inc b/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/json_last_error_next_after_return.php.inc deleted file mode 100644 index 7f8e807748c..00000000000 --- a/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/json_last_error_next_after_return.php.inc +++ /dev/null @@ -1,33 +0,0 @@ - ------ - diff --git a/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/json_last_error_next_after_return2.php.inc b/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/json_last_error_next_after_return2.php.inc deleted file mode 100644 index 8be2214843c..00000000000 --- a/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/json_last_error_next_after_return2.php.inc +++ /dev/null @@ -1,31 +0,0 @@ - ------ - diff --git a/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/process_content_from_param.php.inc b/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/process_content_from_param.php.inc deleted file mode 100644 index 6dcfc79aac6..00000000000 --- a/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/process_content_from_param.php.inc +++ /dev/null @@ -1,31 +0,0 @@ - ------ - diff --git a/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/process_mixed_content.php.inc b/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/process_mixed_content.php.inc deleted file mode 100644 index 3792dcd129c..00000000000 --- a/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/process_mixed_content.php.inc +++ /dev/null @@ -1,53 +0,0 @@ - $message, - 'type' => 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html', - 'title' => 'Unprocessable Entity', - 'status' => $code, - 'detail' => $detail, - ]; - - $response = json_encode($jsonData); - assert(is_string($response)); -} - -function processMixedContent2($data){ - $json = "{$data}"; - - $response = json_decode($json); - assert(is_array($response)); -} - -?> ------ - $message, - 'type' => 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html', - 'title' => 'Unprocessable Entity', - 'status' => $code, - 'detail' => $detail, - ]; - - $response = json_encode($jsonData, JSON_THROW_ON_ERROR); - assert(is_string($response)); -} - -function processMixedContent2($data){ - $json = "{$data}"; - - $response = json_decode($json, null, 512, JSON_THROW_ON_ERROR); - assert(is_array($response)); -} - -?> diff --git a/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/skip_exact_value.php.inc b/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/skip_exact_value.php.inc deleted file mode 100644 index a66833e5a7a..00000000000 --- a/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/skip_exact_value.php.inc +++ /dev/null @@ -1,28 +0,0 @@ - - [ - 'foo' => [ - 'regexNotMatch' => 'The input does not match against pattern \'/^[a-zA-Z0-9 .\-]+$/\'' - ] - ], - 'type' => 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html', - 'title' => 'Unprocessable Entity', - 'status' => 422, - 'detail' => 'Failed Validation' - ]; - - $response = json_encode($jsonData); - assert(is_string($response)); -} - -function skipExactValue2(){ - $json = '{}'; - - $response = json_decode($json); - assert(is_array($response)); -} diff --git a/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/skip_json_last_error_next.php.inc b/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/skip_json_last_error_next.php.inc deleted file mode 100644 index ac779207917..00000000000 --- a/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/skip_json_last_error_next.php.inc +++ /dev/null @@ -1,11 +0,0 @@ - diff --git a/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/skip_json_last_error_next2.php.inc b/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/skip_json_last_error_next2.php.inc deleted file mode 100644 index 394a2b3e8ab..00000000000 --- a/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/skip_json_last_error_next2.php.inc +++ /dev/null @@ -1,12 +0,0 @@ - diff --git a/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/skip_named_argument_flags.php.inc b/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/skip_named_argument_flags.php.inc deleted file mode 100644 index 24b0df4e808..00000000000 --- a/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/Fixture/skip_named_argument_flags.php.inc +++ /dev/null @@ -1,8 +0,0 @@ -doTestFile($filePath); - } - - public static function provideData(): Iterator - { - return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); - } - - public function provideConfigFilePath(): string - { - return __DIR__ . '/config/configured_rule.php'; - } -} diff --git a/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/config/configured_rule.php b/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/config/configured_rule.php deleted file mode 100644 index d5ae69666d9..00000000000 --- a/rules-tests/Php73/Rector/FuncCall/JsonThrowOnErrorRector/config/configured_rule.php +++ /dev/null @@ -1,9 +0,0 @@ -withRules([JsonThrowOnErrorRector::class]); diff --git a/rules/Php73/Rector/FuncCall/JsonThrowOnErrorRector.php b/rules/Php73/Rector/FuncCall/JsonThrowOnErrorRector.php index 32eafaacdc7..0d9e77c2f5e 100644 --- a/rules/Php73/Rector/FuncCall/JsonThrowOnErrorRector.php +++ b/rules/Php73/Rector/FuncCall/JsonThrowOnErrorRector.php @@ -5,17 +5,9 @@ namespace Rector\Php73\Rector\FuncCall; use PhpParser\Node; -use PhpParser\Node\Arg; -use PhpParser\Node\Expr; -use PhpParser\Node\Expr\BinaryOp\BitwiseOr; -use PhpParser\Node\Expr\ConstFetch; -use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Identifier; -use PhpParser\Node\Name; -use PhpParser\Node\Scalar\Int_; +use Rector\Configuration\Deprecation\Contract\DeprecatedInterface; +use Rector\Exception\ShouldNotHappenException; use Rector\PhpParser\Enum\NodeGroup; -use Rector\PhpParser\Node\BetterNodeFinder; -use Rector\PhpParser\Node\Value\ValueResolver; use Rector\Rector\AbstractRector; use Rector\ValueObject\PhpVersionFeature; use Rector\VersionBonding\Contract\MinPhpVersionInterface; @@ -23,18 +15,10 @@ use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; /** - * @see \Rector\Tests\Php73\Rector\FuncCall\JsonThrowOnErrorRector\JsonThrowOnErrorRectorTest + * @deprecated This rule is deprecated, as it can cause BC breaks. Adding JSON_THROW_ON_ERROR turns silent false/null results into a thrown JsonException, so every call site has to be reviewed manually. Use a wrapper tool like nette/utils Json instead, to get clear error reporting on any PHP version. */ -final class JsonThrowOnErrorRector extends AbstractRector implements MinPhpVersionInterface +final class JsonThrowOnErrorRector extends AbstractRector implements MinPhpVersionInterface, DeprecatedInterface { - private const array FLAGS = ['JSON_THROW_ON_ERROR']; - - public function __construct( - private readonly ValueResolver $valueResolver, - private readonly BetterNodeFinder $betterNodeFinder - ) { - } - public function getRuleDefinition(): RuleDefinition { return new RuleDefinition( @@ -65,188 +49,14 @@ public function getNodeTypes(): array public function refactor(Node $node): ?Node { - // if found, skip it :) - $hasJsonErrorFuncCall = (bool) $this->betterNodeFinder->findFirst( - $node, - fn (Node $node): bool => $this->isNames($node, ['json_last_error', 'json_last_error_msg']) - ); - - if ($hasJsonErrorFuncCall) { - return null; - } - - $hasChanged = false; - - $this->traverseNodesWithCallable($node, function (Node $currentNode) use (&$hasChanged): ?FuncCall { - if (! $currentNode instanceof FuncCall) { - return null; - } - - if ($this->shouldSkipFuncCall($currentNode)) { - return null; - } - - if ($this->isName($currentNode, 'json_encode')) { - return $this->processJsonEncode($currentNode, $hasChanged); - } - - if ($this->isName($currentNode, 'json_decode')) { - return $this->processJsonDecode($currentNode, $hasChanged); - } - - return null; - }); - - if ($hasChanged) { - return $node; - } - - return null; + throw new ShouldNotHappenException(sprintf( + '"%s" rule is deprecated, as it can cause BC breaks. The thrown JsonException has to be handled manually on every call site', + self::class + )); } public function provideMinPhpVersion(): int { return PhpVersionFeature::JSON_EXCEPTION; } - - private function shouldSkipFuncCall(FuncCall $funcCall): bool - { - if ($funcCall->isFirstClassCallable()) { - return true; - } - - if ($funcCall->args === []) { - return true; - } - - foreach ($funcCall->args as $arg) { - if (! $arg instanceof Arg) { - continue; - } - - if ($arg->name instanceof Identifier) { - return true; - } - } - - return $this->isFirstValueStringOrArray($funcCall); - } - - private function processJsonEncode(FuncCall $funcCall, bool &$hasChanged): FuncCall - { - $flags = []; - if (isset($funcCall->args[1])) { - /** @var Arg $arg */ - $arg = $funcCall->args[1]; - $flags = $this->getFlags($arg); - } - - $newArg = $this->getArgWithFlags($flags); - if ($newArg instanceof Arg) { - $hasChanged = true; - $funcCall->args[1] = $newArg; - } - - return $funcCall; - } - - private function processJsonDecode(FuncCall $funcCall, bool &$hasChanged): FuncCall - { - $flags = []; - if (isset($funcCall->args[3])) { - /** @var Arg $arg */ - $arg = $funcCall->args[3]; - $flags = $this->getFlags($arg); - } - - // set default to inter-args - if (! isset($funcCall->args[1])) { - $funcCall->args[1] = new Arg($this->nodeFactory->createNull()); - } - - if (! isset($funcCall->args[2])) { - $funcCall->args[2] = new Arg(new Int_(512)); - } - - $newArg = $this->getArgWithFlags($flags); - if ($newArg instanceof Arg) { - $hasChanged = true; - $funcCall->args[3] = $newArg; - } - - return $funcCall; - } - - private function createConstFetch(string $name): ConstFetch - { - return new ConstFetch(new Name($name)); - } - - private function isFirstValueStringOrArray(FuncCall $funcCall): bool - { - if (! isset($funcCall->getArgs()[0])) { - return false; - } - - $firstArg = $funcCall->getArgs()[0]; - - $value = $this->valueResolver->getValue($firstArg->value); - if (is_string($value)) { - return true; - } - - return is_array($value); - } - - /** - * @param string[] $flags - * @return string[] - */ - private function getFlags(Expr|Arg $arg, array $flags = []): array - { - // Unwrap Arg - if ($arg instanceof Arg) { - $arg = $arg->value; - } - - // Single flag: SOME_CONST - if ($arg instanceof ConstFetch) { - $flags[] = $arg->name->getFirst(); - return $flags; - } - - // Multiple flags: FLAG_A | FLAG_B | FLAG_C - if ($arg instanceof BitwiseOr) { - $flags = $this->getFlags($arg->left, $flags); - $flags = $this->getFlags($arg->right, $flags); - } - - return array_values(array_unique($flags)); // array_unique in case the same flag is written multiple times - } - - /** - * @param string[] $flags - */ - private function getArgWithFlags(array $flags): ?Arg - { - $originalCount = count($flags); - $flags = array_values(array_unique(array_merge($flags, self::FLAGS))); - if ($originalCount === count($flags)) { - return null; - } - - // Single flag - if (count($flags) === 1) { - return new Arg($this->createConstFetch($flags[0])); - } - - // Build FLAG_A | FLAG_B | FLAG_C - $expr = $this->createConstFetch(array_shift($flags)); - - foreach ($flags as $flag) { - $expr = new BitwiseOr($expr, $this->createConstFetch($flag)); - } - - return new Arg($expr); - } } diff --git a/tests/Issues/ScopeNotAvailable/FixtureJsonThrowCaseSensitiveConstFetch/fixture.php.inc b/tests/Issues/ScopeNotAvailable/FixtureJsonThrowCaseSensitiveConstFetch/fixture.php.inc deleted file mode 100644 index 69fc0964e6d..00000000000 --- a/tests/Issues/ScopeNotAvailable/FixtureJsonThrowCaseSensitiveConstFetch/fixture.php.inc +++ /dev/null @@ -1,57 +0,0 @@ - ------ - diff --git a/tests/Issues/ScopeNotAvailable/FixtureJsonThrowCaseSensitiveConstFetch/non_exact_value_after_unreachable_stmts.php.inc b/tests/Issues/ScopeNotAvailable/FixtureJsonThrowCaseSensitiveConstFetch/non_exact_value_after_unreachable_stmts.php.inc deleted file mode 100644 index f3ccd3a5aae..00000000000 --- a/tests/Issues/ScopeNotAvailable/FixtureJsonThrowCaseSensitiveConstFetch/non_exact_value_after_unreachable_stmts.php.inc +++ /dev/null @@ -1,45 +0,0 @@ - ------ - diff --git a/tests/Issues/ScopeNotAvailable/FixtureJsonThrowCaseSensitiveConstFetch/non_exact_value_after_unreachable_stmts2.php.inc b/tests/Issues/ScopeNotAvailable/FixtureJsonThrowCaseSensitiveConstFetch/non_exact_value_after_unreachable_stmts2.php.inc deleted file mode 100644 index 1ead7870b06..00000000000 --- a/tests/Issues/ScopeNotAvailable/FixtureJsonThrowCaseSensitiveConstFetch/non_exact_value_after_unreachable_stmts2.php.inc +++ /dev/null @@ -1,51 +0,0 @@ - ------ - diff --git a/tests/Issues/ScopeNotAvailable/FixtureJsonThrowCaseSensitiveConstFetch/non_exact_value_after_unreachable_stmts_on_top_level.php.inc b/tests/Issues/ScopeNotAvailable/FixtureJsonThrowCaseSensitiveConstFetch/non_exact_value_after_unreachable_stmts_on_top_level.php.inc deleted file mode 100644 index 400ae26a00e..00000000000 --- a/tests/Issues/ScopeNotAvailable/FixtureJsonThrowCaseSensitiveConstFetch/non_exact_value_after_unreachable_stmts_on_top_level.php.inc +++ /dev/null @@ -1,33 +0,0 @@ - ------ - diff --git a/tests/Issues/ScopeNotAvailable/JsonThrowCaseSensitiveConstFetchTest.php b/tests/Issues/ScopeNotAvailable/JsonThrowCaseSensitiveConstFetchTest.php deleted file mode 100644 index b0440ad0685..00000000000 --- a/tests/Issues/ScopeNotAvailable/JsonThrowCaseSensitiveConstFetchTest.php +++ /dev/null @@ -1,28 +0,0 @@ -doTestFile($filePath); - } - - public static function provideData(): Iterator - { - return self::yieldFilesFromDirectory(__DIR__ . '/FixtureJsonThrowCaseSensitiveConstFetch'); - } - - public function provideConfigFilePath(): string - { - return __DIR__ . '/config/json_throw_case_sensitive_const_fetch.php'; - } -} diff --git a/tests/Issues/ScopeNotAvailable/config/json_throw_case_sensitive_const_fetch.php b/tests/Issues/ScopeNotAvailable/config/json_throw_case_sensitive_const_fetch.php deleted file mode 100644 index 681b3c03f57..00000000000 --- a/tests/Issues/ScopeNotAvailable/config/json_throw_case_sensitive_const_fetch.php +++ /dev/null @@ -1,10 +0,0 @@ -withRules([JsonThrowOnErrorRector::class, SensitiveConstantNameRector::class]);