From b1df559964cc38332a46080f1b33ea2f1f0830db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=A9grier?= Date: Thu, 7 Mar 2019 14:15:52 +0100 Subject: [PATCH 1/2] Adding Safe\preg_replace support --- phpstan-safe-rule.neon | 4 + ...afeFunctionsDynamicReturnTypeExtension.php | 83 +++++++++++++++++++ tests/Rules/CallMethodRuleTest.php | 42 ++++++++++ tests/Rules/UseSafeFunctionsRuleTest.php | 1 + tests/Rules/data/safe_pregreplace.php | 7 ++ 5 files changed, 137 insertions(+) create mode 100644 src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php create mode 100644 tests/Rules/CallMethodRuleTest.php create mode 100644 tests/Rules/data/safe_pregreplace.php diff --git a/phpstan-safe-rule.neon b/phpstan-safe-rule.neon index f86b4ff..34b0346 100644 --- a/phpstan-safe-rule.neon +++ b/phpstan-safe-rule.neon @@ -3,3 +3,7 @@ services: class: TheCodingMachine\Safe\PHPStan\Rules\UseSafeFunctionsRule tags: - phpstan.rules.rule + - + class: TheCodingMachine\Safe\PHPStan\Type\Php\ReplaceSafeFunctionsDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension diff --git a/src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php new file mode 100644 index 0000000..dfe1044 --- /dev/null +++ b/src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php @@ -0,0 +1,83 @@ + */ + private $functions = [ + 'Safe\preg_replace' => 2, + ]; + + public function isFunctionSupported(FunctionReflection $functionReflection): bool + { + return array_key_exists($functionReflection->getName(), $this->functions); + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope + ): Type + { + $type = $this->getPreliminarilyResolvedTypeFromFunctionCall($functionReflection, $functionCall, $scope); + + $possibleTypes = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + + if (TypeCombinator::containsNull($possibleTypes)) { + $type = TypeCombinator::addNull($type); + } + + return $type; + } + + private function getPreliminarilyResolvedTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope + ): Type + { + $argumentPosition = $this->functions[$functionReflection->getName()]; + if (count($functionCall->args) <= $argumentPosition) { + return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + } + + $subjectArgumentType = $scope->getType($functionCall->args[$argumentPosition]->value); + $defaultReturnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + if ($subjectArgumentType instanceof MixedType) { + return TypeUtils::toBenevolentUnion($defaultReturnType); + } + $stringType = new StringType(); + $arrayType = new ArrayType(new MixedType(), new MixedType()); + + $isStringSuperType = $stringType->isSuperTypeOf($subjectArgumentType); + $isArraySuperType = $arrayType->isSuperTypeOf($subjectArgumentType); + $compareSuperTypes = $isStringSuperType->compareTo($isArraySuperType); + if ($compareSuperTypes === $isStringSuperType) { + return $stringType; + } elseif ($compareSuperTypes === $isArraySuperType) { + if ($subjectArgumentType instanceof ArrayType) { + return $subjectArgumentType->generalizeValues(); + } + return $subjectArgumentType; + } + + return $defaultReturnType; + } + +} diff --git a/tests/Rules/CallMethodRuleTest.php b/tests/Rules/CallMethodRuleTest.php new file mode 100644 index 0000000..5ec8644 --- /dev/null +++ b/tests/Rules/CallMethodRuleTest.php @@ -0,0 +1,42 @@ +createBroker(); + $ruleLevelHelper = new RuleLevelHelper($broker, true, true, true); + return new CallMethodsRule( + $broker, + new FunctionCallParametersCheck($ruleLevelHelper, true, true), + $ruleLevelHelper, + true, + true + ); + } + + public function testSafePregReplace() + { + // FIXME: this rule actually runs code but will always return no error because the rule executed is not the correct one. + // This provides code coverage but assert is not ok. + $this->analyse([__DIR__ . '/data/safe_pregreplace.php'], []); + } + + + /** + * @return \PHPStan\Type\DynamicFunctionReturnTypeExtension[] + */ + public function getDynamicFunctionReturnTypeExtensions(): array + { + return [new ReplaceSafeFunctionsDynamicReturnTypeExtension()]; + } +} diff --git a/tests/Rules/UseSafeFunctionsRuleTest.php b/tests/Rules/UseSafeFunctionsRuleTest.php index 177872b..4353dfa 100644 --- a/tests/Rules/UseSafeFunctionsRuleTest.php +++ b/tests/Rules/UseSafeFunctionsRuleTest.php @@ -3,6 +3,7 @@ namespace TheCodingMachine\Safe\PHPStan\Rules; use PHPStan\Testing\RuleTestCase; +use TheCodingMachine\Safe\PHPStan\Type\Php\ReplaceSafeFunctionsDynamicReturnTypeExtension; class UseSafeFunctionsRuleTest extends RuleTestCase { diff --git a/tests/Rules/data/safe_pregreplace.php b/tests/Rules/data/safe_pregreplace.php new file mode 100644 index 0000000..e441e62 --- /dev/null +++ b/tests/Rules/data/safe_pregreplace.php @@ -0,0 +1,7 @@ + Date: Thu, 7 Mar 2019 14:45:31 +0100 Subject: [PATCH 2/2] CS --- .../ReplaceSafeFunctionsDynamicReturnTypeExtension.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php index dfe1044..529b157 100644 --- a/src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/ReplaceSafeFunctionsDynamicReturnTypeExtension.php @@ -15,7 +15,6 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; - class ReplaceSafeFunctionsDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -33,8 +32,7 @@ public function getTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope - ): Type - { + ): Type { $type = $this->getPreliminarilyResolvedTypeFromFunctionCall($functionReflection, $functionCall, $scope); $possibleTypes = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); @@ -50,8 +48,7 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope - ): Type - { + ): Type { $argumentPosition = $this->functions[$functionReflection->getName()]; if (count($functionCall->args) <= $argumentPosition) { return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); @@ -79,5 +76,4 @@ private function getPreliminarilyResolvedTypeFromFunctionCall( return $defaultReturnType; } - }