diff --git a/phpstan-safe-rule.neon b/phpstan-safe-rule.neon index 5573fe9..d922803 100644 --- a/phpstan-safe-rule.neon +++ b/phpstan-safe-rule.neon @@ -1,4 +1,8 @@ services: + - + class: TheCodingMachine\Safe\PHPStan\Rules\UseSafeCallablesRule + tags: + - phpstan.rules.rule - class: TheCodingMachine\Safe\PHPStan\Rules\UseSafeFunctionsRule tags: diff --git a/phpstan.neon b/phpstan.neon index f4ecada..692e10a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,7 +4,9 @@ parameters: - src - tests excludePaths: - - tests/Rules/data + - tests/Rules/UseSafeCallablesRule + - tests/Rules/UseSafeClassesRule + - tests/Rules/UseSafeFunctionsRule ignoreErrors: - message: '#^Implementing PHPStan\\Rules\\IdentifierRuleError is not covered by backward compatibility promise\. The interface might change in a minor PHPStan version\.$#' diff --git a/src/Rules/UseSafeCallablesRule.php b/src/Rules/UseSafeCallablesRule.php new file mode 100644 index 0000000..a6388fe --- /dev/null +++ b/src/Rules/UseSafeCallablesRule.php @@ -0,0 +1,44 @@ + + */ +class UseSafeCallablesRule implements Rule +{ + /** + * @see JSON_THROW_ON_ERROR + */ + const JSON_THROW_ON_ERROR = 4194304; + + public function getNodeType(): string + { + return FunctionCallableNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $nodeName = $node->getName(); + if (!$nodeName instanceof Node\Name) { + return []; + } + $functionName = $nodeName->toString(); + $unsafeFunctions = FunctionListLoader::getFunctionList(); + + if (isset($unsafeFunctions[$functionName])) { + return [new SafeFunctionRuleError($nodeName, $node->getStartLine())]; + } + + return []; + } +} diff --git a/src/Rules/UseSafeFunctionsRule.php b/src/Rules/UseSafeFunctionsRule.php index aaba401..0d77b12 100644 --- a/src/Rules/UseSafeFunctionsRule.php +++ b/src/Rules/UseSafeFunctionsRule.php @@ -1,6 +1,5 @@ name instanceof Node\Name) { + $nodeName = $node->name; + if (!$nodeName instanceof Node\Name) { return []; } - $functionName = $node->name->toString(); + $functionName = $nodeName->toString(); $unsafeFunctions = FunctionListLoader::getFunctionList(); if (isset($unsafeFunctions[$functionName])) { @@ -58,7 +63,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - return [new SafeFunctionRuleError($node->name, $node->getStartLine())]; + return [new SafeFunctionRuleError($nodeName, $node->getStartLine())]; } return []; @@ -87,11 +92,16 @@ private function argValueIncludeJSONTHROWONERROR(?Arg $arg): bool 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); - })), true); + $intOptions = array_filter($options, function (mixed $option): bool { + return is_int($option); + }); + + foreach ($intOptions as $option) { + if (($option & self::JSON_THROW_ON_ERROR) === self::JSON_THROW_ON_ERROR) { + return true; + } + } + + return false; } } diff --git a/src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php index ca7ba4f..443d1bb 100644 --- a/src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php @@ -56,22 +56,17 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( Scope $scope ): Type { $argumentPosition = $this->functions[$functionReflection->getName()]; - $defaultReturnType = ParametersAcceptorSelector::selectFromArgs( - $scope, - $functionCall->getArgs(), - $functionReflection->getVariants() - ) + + $args = $functionCall->getArgs(); + $variants = $functionReflection->getVariants(); + $defaultReturnType = ParametersAcceptorSelector::selectFromArgs($scope, $args, $variants) ->getReturnType(); - - if (count($functionCall->args) <= $argumentPosition) { - return $defaultReturnType; - } - $subjectArgument = $functionCall->args[$argumentPosition]; - if (!$subjectArgument instanceof Arg) { + if (count($args) <= $argumentPosition) { return $defaultReturnType; } - + + $subjectArgument = $args[$argumentPosition]; $subjectArgumentType = $scope->getType($subjectArgument->value); $mixedType = new MixedType(); if ($subjectArgumentType->isSuperTypeOf($mixedType)->yes()) { diff --git a/tests/Rules/UseSafeCallablesRule/expr.php b/tests/Rules/UseSafeCallablesRule/expr.php new file mode 100644 index 0000000..0646c60 --- /dev/null +++ b/tests/Rules/UseSafeCallablesRule/expr.php @@ -0,0 +1,4 @@ + + */ +class UseSafeCallablesRuleTest extends RuleTestCase +{ + protected function getRule(): Rule + { + return new UseSafeCallablesRule(); + } + + public function testUnsafe(): void + { + $this->analyse([__DIR__ . '/UseSafeCallablesRule/unsafe.php'], [ + [ + "Function json_encode is unsafe to use. It can return FALSE instead of throwing an exception. Please add 'use function Safe\\json_encode;' at the beginning of the file to use the variant provided by the 'thecodingmachine/safe' library.", + 3, + ], + ]); + } + + public function testUseSafe(): void + { + $this->analyse([__DIR__ . '/UseSafeCallablesRule/use_safe.php'], []); + } + + public function testNativeSafe(): void + { + $this->analyse([__DIR__ . '/UseSafeCallablesRule/native_safe.php'], []); + } + + public function testExpr(): void + { + $this->analyse([__DIR__ . '/UseSafeCallablesRule/expr.php'], []); + } +} diff --git a/tests/Rules/data/datetime.php b/tests/Rules/UseSafeClassesRule/datetime.php similarity index 100% rename from tests/Rules/data/datetime.php rename to tests/Rules/UseSafeClassesRule/datetime.php diff --git a/tests/Rules/UseSafeClassesRuleTest.php b/tests/Rules/UseSafeClassesRuleTest.php index bfa8aaf..d6ca6b1 100644 --- a/tests/Rules/UseSafeClassesRuleTest.php +++ b/tests/Rules/UseSafeClassesRuleTest.php @@ -17,7 +17,7 @@ protected function getRule(): Rule public function testDateTime(): void { - $this->analyse([__DIR__ . '/data/datetime.php'], [ + $this->analyse([__DIR__ . '/UseSafeClassesRule/datetime.php'], [ [ "Class DateTime is unsafe to use. Its methods can return FALSE instead of throwing an exception. Please add 'use Safe\DateTime;' at the beginning of the file to use the variant provided by the 'thecodingmachine/safe' library.", 3, diff --git a/tests/Rules/UseSafeFunctionsRule/expr.php b/tests/Rules/UseSafeFunctionsRule/expr.php new file mode 100644 index 0000000..41480b6 --- /dev/null +++ b/tests/Rules/UseSafeFunctionsRule/expr.php @@ -0,0 +1,4 @@ +analyse([__DIR__ . '/data/fopen.php'], [ + $this->analyse([__DIR__ . '/UseSafeFunctionsRule/unsafe.php'], [ [ "Function fopen is unsafe to use. It can return FALSE instead of throwing an exception. Please add 'use function Safe\\fopen;' at the beginning of the file to use the variant provided by the 'thecodingmachine/safe' library.", + 3, + ], + [ + "Function json_decode is unsafe to use. It can return FALSE instead of throwing an exception. Please add 'use function Safe\\json_decode;' at the beginning of the file to use the variant provided by the 'thecodingmachine/safe' library.", 4, ], + [ + "Function json_encode is unsafe to use. It can return FALSE instead of throwing an exception. Please add 'use function Safe\\json_encode;' at the beginning of the file to use the variant provided by the 'thecodingmachine/safe' library.", + 5, + ], ]); } - public function testNoCatchSafe(): void - { - $this->analyse([__DIR__ . '/data/safe_fopen.php'], []); - } - - public function testExprCall(): void + public function testUseSafe(): void { - $this->analyse([__DIR__ . '/data/undirect_call.php'], []); + $this->analyse([__DIR__ . '/UseSafeFunctionsRule/use_safe.php'], []); } - public function testJSONDecodeNoCatchSafe(): void + public function testNativeSafe(): void { - $this->analyse([__DIR__ . '/data/safe_json_decode.php'], []); + $this->analyse([__DIR__ . '/UseSafeFunctionsRule/native_safe.php'], []); } - public function testJSONEncodeNoCatchSafe(): void + public function testExpr(): void { - $this->analyse([__DIR__ . '/data/safe_json_encode.php'], []); + $this->analyse([__DIR__ . '/UseSafeFunctionsRule/expr.php'], []); } } diff --git a/tests/Rules/data/fopen.php b/tests/Rules/data/fopen.php deleted file mode 100644 index fcf5e8f..0000000 --- a/tests/Rules/data/fopen.php +++ /dev/null @@ -1,5 +0,0 @@ -