diff --git a/phpstan-safe-rule.neon b/phpstan-safe-rule.neon index d922803..5c615ca 100644 --- a/phpstan-safe-rule.neon +++ b/phpstan-safe-rule.neon @@ -23,3 +23,7 @@ services: class: TheCodingMachine\Safe\PHPStan\Type\Php\PregMatchTypeSpecifyingExtension tags: - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - + class: TheCodingMachine\Safe\PHPStan\Type\Php\JsonDecodeDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension diff --git a/phpstan.neon b/phpstan.neon index 692e10a..faf698f 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -25,5 +25,10 @@ parameters: identifier: phpstanApi.interface count: 1 path: src/Rules/Error/SafeRuleError.php + - + message: '#^Calling PHPStan\\Type\\Php\\JsonThrowOnErrorDynamicReturnTypeExtension\:\:getTypeFromFunctionCall\(\) is not covered by backward compatibility promise\. The method might change in a minor PHPStan version\.$#' + identifier: phpstanApi.method + count: 1 + path: src/Type/Php/JsonDecodeDynamicReturnTypeExtension.php includes: - phpstan-safe-rule.neon diff --git a/src/Type/Php/JsonDecodeDynamicReturnTypeExtension.php b/src/Type/Php/JsonDecodeDynamicReturnTypeExtension.php new file mode 100644 index 0000000..8ffbe62 --- /dev/null +++ b/src/Type/Php/JsonDecodeDynamicReturnTypeExtension.php @@ -0,0 +1,46 @@ +nativeJsonDecodeReflection = $reflectionProvider->getFunction(new Name('json_decode'), null); + } + + public function isFunctionSupported(FunctionReflection $functionReflection): bool + { + return strtolower($functionReflection->getName()) === 'safe\json_decode'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + $result = $this->phpstanCheck->getTypeFromFunctionCall($this->nativeJsonDecodeReflection, $functionCall, $scope); + + // if PHPStan reports null and there is a json error, then an invalid constant string was passed + if ($result->isNull()->yes() && JSON_ERROR_NONE !== json_last_error()) { + return new NeverType(); + } + + return $result; + } +} diff --git a/tests/Type/Php/TypeAssertionsTest.php b/tests/Type/Php/TypeAssertionsTest.php index 120b85a..75c1c7b 100644 --- a/tests/Type/Php/TypeAssertionsTest.php +++ b/tests/Type/Php/TypeAssertionsTest.php @@ -14,6 +14,7 @@ public static function dataFileAsserts(): iterable yield from self::gatherAssertTypes(__DIR__ . '/data/preg_match_unchecked.php'); yield from self::gatherAssertTypes(__DIR__ . '/data/preg_match_checked.php'); yield from self::gatherAssertTypes(__DIR__ . '/data/preg_replace_return.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/json_decode_return.php'); } /** diff --git a/tests/Type/Php/data/json_decode_return.php b/tests/Type/Php/data/json_decode_return.php new file mode 100644 index 0000000..f6c681a --- /dev/null +++ b/tests/Type/Php/data/json_decode_return.php @@ -0,0 +1,35 @@ +