diff --git a/src/Rules/UseSafeFunctionsRule.php b/src/Rules/UseSafeFunctionsRule.php index 00317e0..36799fe 100644 --- a/src/Rules/UseSafeFunctionsRule.php +++ b/src/Rules/UseSafeFunctionsRule.php @@ -10,6 +10,9 @@ use PHPStan\Rules\Rule; use PHPStan\ShouldNotHappenException; use TheCodingMachine\Safe\PHPStan\Utils\FunctionListLoader; +use PhpParser\Node\Arg; +use PhpParser\Node\Expr; +use PhpParser\Node\Scalar; /** * This rule checks that no superglobals are used in code. @@ -35,9 +38,53 @@ public function processNode(Node $node, Scope $scope): array $unsafeFunctions = FunctionListLoader::getFunctionList(); if (isset($unsafeFunctions[$functionName])) { + if (version_compare(PHP_VERSION, '7.3.0', '>=')) { + if ($functionName === "json_decode") { + if (count($node->args) == 4) { + if ($this->argValueIncludeJSONTHROWONERROR($node->args[3])) { + return []; + } + } + } + if ($functionName === "json_encode") { + if (count($node->args) >= 2) { + if ($this->argValueIncludeJSONTHROWONERROR($node->args[1])) { + return []; + } + } + } + } + return ["Function $functionName is unsafe to use. It can return FALSE instead of throwing an exception. Please add 'use function Safe\\$functionName;' at the beginning of the file to use the variant provided by the 'thecodingmachine/safe' library."]; } return []; } + + private function argValueIncludeJSONTHROWONERROR(Arg $arg): bool + { + $parseValue = function ($expr, array $options) use (&$parseValue): array { + if ($expr instanceof Expr\BinaryOp\BitwiseOr) { + return array_merge($parseValue($expr->left, $options), $parseValue($expr->right, $options)); + } elseif ($expr instanceof Expr\ConstFetch) { + return array_merge($options, $expr->name->parts); + } elseif ($expr instanceof Scalar\LNumber) { + return array_merge($options, [$expr->value]); + } else { + return $options; + } + }; + $options = $parseValue($arg->value, []); + + if (in_array("JSON_THROW_ON_ERROR", $options)) { + return true; + } + + return in_array(true, array_map(function ($element) { + // JSON_THROW_ON_ERROR == 4194304 + return ($element & 4194304) == 4194304; + }, array_filter($options, function ($element) { + return is_int($element); + }))); + } } diff --git a/tests/Rules/UseSafeFunctionsRuleTest.php b/tests/Rules/UseSafeFunctionsRuleTest.php index 4353dfa..4014828 100644 --- a/tests/Rules/UseSafeFunctionsRuleTest.php +++ b/tests/Rules/UseSafeFunctionsRuleTest.php @@ -31,4 +31,22 @@ public function testExprCall() { $this->analyse([__DIR__ . '/data/undirect_call.php'], []); } + + public function testJSONDecodeNoCatchSafe() + { + if (version_compare(PHP_VERSION, '7.3.0', '>=')) { + $this->analyse([__DIR__ . '/data/safe_json_decode_for_7.3.0.php'], []); + } else { + $this->assertTrue(true); + } + } + + public function testJSONEncodeNoCatchSafe() + { + if (version_compare(PHP_VERSION, '7.3.0', '>=')) { + $this->analyse([__DIR__ . '/data/safe_json_encode_for_7.3.0.php'], []); + } else { + $this->assertTrue(true); + } + } } diff --git a/tests/Rules/data/safe_json_decode_for_7.3.0.php b/tests/Rules/data/safe_json_decode_for_7.3.0.php new file mode 100644 index 0000000..c69e7ca --- /dev/null +++ b/tests/Rules/data/safe_json_decode_for_7.3.0.php @@ -0,0 +1,9 @@ +