From be79bcef9a3a2dd1c2e81a7472f886158e9fd959 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 22 Sep 2021 15:20:47 +0200 Subject: [PATCH] Level 5 - ImplodeFunctionRule --- conf/config.level5.neon | 2 + src/Rules/Functions/ImplodeFunctionRule.php | 82 +++++++++++++++++++ tests/PHPStan/Levels/data/acceptTypes-5.json | 5 ++ tests/PHPStan/Levels/data/acceptTypes-6.json | 15 ++++ tests/PHPStan/Levels/data/acceptTypes-7.json | 5 ++ tests/PHPStan/Levels/data/acceptTypes.php | 16 ++++ .../Functions/ImplodeFunctionRuleTest.php | 49 +++++++++++ .../PHPStan/Rules/Functions/data/implode.php | 25 ++++++ 8 files changed, 199 insertions(+) create mode 100644 src/Rules/Functions/ImplodeFunctionRule.php create mode 100644 tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php create mode 100644 tests/PHPStan/Rules/Functions/data/implode.php diff --git a/conf/config.level5.neon b/conf/config.level5.neon index 57dc11d4a0..cde1e6e556 100644 --- a/conf/config.level5.neon +++ b/conf/config.level5.neon @@ -11,6 +11,8 @@ parameters: checkFunctionArgumentTypes: true checkArgumentsPassedByReference: true +rules: + - PHPStan\Rules\Functions\ImplodeFunctionRule services: - diff --git a/src/Rules/Functions/ImplodeFunctionRule.php b/src/Rules/Functions/ImplodeFunctionRule.php new file mode 100644 index 0000000000..0bcbb6ad73 --- /dev/null +++ b/src/Rules/Functions/ImplodeFunctionRule.php @@ -0,0 +1,82 @@ + + */ +class ImplodeFunctionRule implements \PHPStan\Rules\Rule +{ + + private RuleLevelHelper $ruleLevelHelper; + + private ReflectionProvider $reflectionProvider; + + public function __construct( + ReflectionProvider $reflectionProvider, + RuleLevelHelper $ruleLevelHelper + ) + { + $this->reflectionProvider = $reflectionProvider; + $this->ruleLevelHelper = $ruleLevelHelper; + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof \PhpParser\Node\Name)) { + return []; + } + + $functionName = $this->reflectionProvider->resolveFunctionName($node->name, $scope); + if (!in_array($functionName, ['implode', 'join'], true)) { + return []; + } + + $args = $node->getArgs(); + if (count($args) === 1) { + $arrayArg = $args[0]->value; + $paramNo = 1; + } elseif (count($args) === 2) { + $arrayArg = $args[1]->value; + $paramNo = 2; + } else { + return []; + } + + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $arrayArg, + '', + static function (Type $type): bool { + return !$type->getIterableValueType()->toString() instanceof ErrorType; + } + ); + + if ($typeResult->getType() instanceof ErrorType + || !$typeResult->getType()->getIterableValueType()->toString() instanceof ErrorType) { + return []; + } + + return [ + RuleErrorBuilder::message( + sprintf('Parameter #%d $array of function %s expects array, %s given.', $paramNo, $functionName, $typeResult->getType()->getIterableValueType()->describe(VerbosityLevel::typeOnly())) + )->build(), + ]; + } + +} diff --git a/tests/PHPStan/Levels/data/acceptTypes-5.json b/tests/PHPStan/Levels/data/acceptTypes-5.json index 5e4de19fb5..78f19787d7 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-5.json +++ b/tests/PHPStan/Levels/data/acceptTypes-5.json @@ -198,5 +198,10 @@ "message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects array&nonEmpty, array given.", "line": 735, "ignorable": true + }, + { + "message": "Parameter #2 $pieces of function implode expects array, int given.", + "line": 763, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes-6.json b/tests/PHPStan/Levels/data/acceptTypes-6.json index e4fa1ffa2c..35bd142bb9 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-6.json +++ b/tests/PHPStan/Levels/data/acceptTypes-6.json @@ -148,5 +148,20 @@ "message": "Method Levels\\AcceptTypes\\ArrayShapes::doBar() has no return type specified.", "line": 603, "ignorable": true + }, + { + "message": "Method Levels\\AcceptTypes\\Implode::partlySupportedUnion() has no return type specified.", + "line": 755, + "ignorable": true + }, + { + "message": "Method Levels\\AcceptTypes\\Implode::partlySupportedUnion() has parameter $union with no value type specified in iterable type array.", + "line": 755, + "ignorable": true + }, + { + "message": "Method Levels\\AcceptTypes\\Implode::invalidType() has no return type specified.", + "line": 762, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes-7.json b/tests/PHPStan/Levels/data/acceptTypes-7.json index 76c6e1399e..4dd170abf9 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-7.json +++ b/tests/PHPStan/Levels/data/acceptTypes-7.json @@ -153,5 +153,10 @@ "message": "Parameter #1 $min (int<-1, 1>) of function random_int expects lower number than parameter #2 $max (int<-1, 1>).", "line": 692, "ignorable": true + }, + { + "message": "Parameter #2 $pieces of function implode expects array, array|int|string given.", + "line": 756, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes.php b/tests/PHPStan/Levels/data/acceptTypes.php index bc99693645..d16184f062 100644 --- a/tests/PHPStan/Levels/data/acceptTypes.php +++ b/tests/PHPStan/Levels/data/acceptTypes.php @@ -747,3 +747,19 @@ public function doBar( } } + +class Implode { + /** + * @param string|int|array $union + */ + public function partlySupportedUnion($union) { + $imploded = implode('abc', $union); + } + + /** + * @param int $invalid + */ + public function invalidType($invalid) { + $imploded = implode('abc', $invalid); + } +} diff --git a/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php b/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php new file mode 100644 index 0000000000..9baec6e5e6 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php @@ -0,0 +1,49 @@ + + */ +class ImplodeFunctionRuleTest extends \PHPStan\Testing\RuleTestCase +{ + + protected function getRule(): \PHPStan\Rules\Rule + { + $broker = $this->createReflectionProvider(); + return new ImplodeFunctionRule($broker, new RuleLevelHelper($broker, true, false, true, false)); + } + + public function testFile(): void + { + $this->analyse([__DIR__ . '/data/implode.php'], [ + [ + 'Parameter #2 $array of function implode expects array, array|string given.', + 9, + ], + [ + 'Parameter #1 $array of function implode expects array, array given.', + 11, + ], + [ + 'Parameter #1 $array of function implode expects array, array given.', + 12, + ], + [ + 'Parameter #1 $array of function implode expects array, array given.', + 13, + ], + [ + 'Parameter #2 $array of function implode expects array, array given.', + 15, + ], + [ + 'Parameter #2 $array of function join expects array, array given.', + 16, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/implode.php b/tests/PHPStan/Rules/Functions/data/implode.php new file mode 100644 index 0000000000..d034500d28 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/implode.php @@ -0,0 +1,25 @@ +