diff --git a/conf/config.level5.neon b/conf/config.level5.neon index d4cdc74875..184cee83b8 100644 --- a/conf/config.level5.neon +++ b/conf/config.level5.neon @@ -12,7 +12,11 @@ conditionalTags: phpstan.rules.rule: %featureToggles.arrayValues% PHPStan\Rules\Functions\CallUserFuncRule: phpstan.rules.rule: %featureToggles.callUserFunc% - PHPStan\Rules\Functions\ParameterCastableToStringFunctionRule: + PHPStan\Rules\Functions\ParameterCastableToStringRule: + phpstan.rules.rule: %featureToggles.checkParameterCastableToStringFunctions% + PHPStan\Rules\Functions\ImplodeParameterCastableToStringRule: + phpstan.rules.rule: %featureToggles.checkParameterCastableToStringFunctions% + PHPStan\Rules\Functions\SortParameterCastableToStringRule: phpstan.rules.rule: %featureToggles.checkParameterCastableToStringFunctions% rules: @@ -45,4 +49,8 @@ services: tags: - phpstan.rules.rule - - class: PHPStan\Rules\Functions\ParameterCastableToStringFunctionRule + class: PHPStan\Rules\Functions\ParameterCastableToStringRule + - + class: PHPStan\Rules\Functions\ImplodeParameterCastableToStringRule + - + class: PHPStan\Rules\Functions\SortParameterCastableToStringRule diff --git a/conf/config.neon b/conf/config.neon index 4a97f4b5ea..c4ea3933bf 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -948,6 +948,8 @@ services: - class: PHPStan\Rules\FunctionReturnTypeCheck + - + class: PHPStan\Rules\ParameterCastableToStringCheck - class: PHPStan\Rules\Generics\CrossCheckInterfacesHelper diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1f74a8d838..c664a131e7 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1823,7 +1823,7 @@ parameters: - message: """ #^Instantiation of deprecated class PHPStan\\\\Rules\\\\Functions\\\\ImplodeFunctionRule\\: - Replaced by PHPStan\\\\Rules\\\\Functions\\\\ParameterCastableToStringFunctionRule$# + Replaced by PHPStan\\\\Rules\\\\Functions\\\\ImplodeParameterCastableToStringRuleTest$# """ count: 1 path: tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php @@ -1831,7 +1831,7 @@ parameters: - message: """ #^Return type of method PHPStan\\\\Rules\\\\Functions\\\\ImplodeFunctionRuleTest\\:\\:getRule\\(\\) has typehint with deprecated class PHPStan\\\\Rules\\\\Functions\\\\ImplodeFunctionRule\\: - Replaced by PHPStan\\\\Rules\\\\Functions\\\\ParameterCastableToStringFunctionRule$# + Replaced by PHPStan\\\\Rules\\\\Functions\\\\ImplodeParameterCastableToStringRuleTest$# """ count: 1 path: tests/PHPStan/Rules/Functions/ImplodeFunctionRuleTest.php diff --git a/src/Rules/Functions/ImplodeFunctionRule.php b/src/Rules/Functions/ImplodeFunctionRule.php index ef94a288b2..b890a8ff66 100644 --- a/src/Rules/Functions/ImplodeFunctionRule.php +++ b/src/Rules/Functions/ImplodeFunctionRule.php @@ -17,7 +17,7 @@ use function sprintf; /** - * @deprecated Replaced by PHPStan\Rules\Functions\ParameterCastableToStringFunctionRule + * @deprecated Replaced by PHPStan\Rules\Functions\ImplodeParameterCastableToStringRuleTest * @implements Rule */ class ImplodeFunctionRule implements Rule diff --git a/src/Rules/Functions/ImplodeParameterCastableToStringRule.php b/src/Rules/Functions/ImplodeParameterCastableToStringRule.php new file mode 100644 index 0000000000..df5136a808 --- /dev/null +++ b/src/Rules/Functions/ImplodeParameterCastableToStringRule.php @@ -0,0 +1,117 @@ + + */ +class ImplodeParameterCastableToStringRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private ParameterCastableToStringCheck $parameterCastableToStringCheck, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + $functionName = $functionReflection->getName(); + if (!in_array($functionName, ['implode', 'join'], true)) { + return []; + } + + $origArgs = $node->getArgs(); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $origArgs, + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + + if ($normalizedFuncCall === null) { + return []; + } + + $normalizedArgs = $normalizedFuncCall->getArgs(); + $errorMessage = 'Parameter %s of function %s expects array, %s given.'; + if (count($normalizedArgs) === 1) { + $argsToCheck = [0 => $normalizedArgs[0]]; + } elseif (count($normalizedArgs) === 2) { + $argsToCheck = [1 => $normalizedArgs[1]]; + } else { + return []; + } + + $origNamedArgs = []; + foreach ($origArgs as $arg) { + if ($arg->unpack || $arg->name === null) { + continue; + } + + $origNamedArgs[$arg->name->toString()] = $arg; + } + + $errors = []; + + foreach ($argsToCheck as $argIdx => $arg) { + // implode has weird variants, so $array has to be fixed. It's especially weird with named arguments. + if (array_key_exists('array', $origNamedArgs)) { + $argName = '$array'; + } elseif (array_key_exists('separator', $origNamedArgs) && count($origArgs) === 1) { + $argName = '$separator'; + } else { + $argName = sprintf('#%d $array', $argIdx + 1); + } + + $error = $this->parameterCastableToStringCheck->checkParameter( + $arg, + $scope, + $errorMessage, + static fn (Type $t) => $t->toString(), + $functionName, + $argName, + ); + + if ($error === null) { + continue; + } + + $errors[] = $error; + } + + return $errors; + } + +} diff --git a/src/Rules/Functions/ParameterCastableToStringFunctionRule.php b/src/Rules/Functions/ParameterCastableToStringFunctionRule.php deleted file mode 100644 index 8717633425..0000000000 --- a/src/Rules/Functions/ParameterCastableToStringFunctionRule.php +++ /dev/null @@ -1,170 +0,0 @@ - - */ -class ParameterCastableToStringFunctionRule implements Rule -{ - - public function __construct( - private ReflectionProvider $reflectionProvider, - private RuleLevelHelper $ruleLevelHelper, - ) - { - } - - public function getNodeType(): string - { - return FuncCall::class; - } - - public function processNode(Node $node, Scope $scope): array - { - if (!($node->name instanceof Node\Name)) { - return []; - } - - if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { - return []; - } - - $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); - $functionName = $functionReflection->getName(); - $implodeFunctions = ['implode', 'join']; - $checkAllArgsFunctions = ['array_intersect', 'array_intersect_assoc', 'array_diff', 'array_diff_assoc']; - $checkFirstArgFunctions = [ - 'array_unique', - 'array_combine', - 'sort', - 'rsort', - 'asort', - 'arsort', - 'natcasesort', - 'natsort', - 'array_count_values', - 'array_fill_keys', - ]; - - if ( - !in_array($functionName, $checkAllArgsFunctions, true) - && !in_array($functionName, $checkFirstArgFunctions, true) - && !in_array($functionName, $implodeFunctions, true) - ) { - return []; - } - - $origArgs = $node->getArgs(); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $origArgs, - $functionReflection->getVariants(), - $functionReflection->getNamedArgumentsVariants(), - ); - - $errorMessage = 'Parameter %s of function %s expects an array of values castable to string, %s given.'; - $getNormalizedArgs = static function () use ($parametersAcceptor, $node): array { - $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); - - if ($normalizedFuncCall === null) { - return []; - } - - return $normalizedFuncCall->getArgs(); - }; - if (in_array($functionName, $implodeFunctions, true)) { - $normalizedArgs = $getNormalizedArgs(); - $errorMessage = 'Parameter %s of function %s expects array, %s given.'; - if (count($normalizedArgs) === 1) { - $argsToCheck = [0 => $normalizedArgs[0]]; - } elseif (count($normalizedArgs) === 2) { - $argsToCheck = [1 => $normalizedArgs[1]]; - } else { - return []; - } - } elseif (in_array($functionName, $checkAllArgsFunctions, true)) { - $argsToCheck = $origArgs; - } elseif (in_array($functionName, $checkFirstArgFunctions, true)) { - $normalizedArgs = $getNormalizedArgs(); - if ($normalizedArgs === []) { - return []; - } - $argsToCheck = [0 => $normalizedArgs[0]]; - } else { - return []; - } - - $origNamedArgs = []; - foreach ($origArgs as $arg) { - if ($arg->unpack || $arg->name === null) { - continue; - } - - $origNamedArgs[$arg->name->toString()] = $arg; - } - - $errors = []; - $functionParameters = $parametersAcceptor->getParameters(); - - foreach ($argsToCheck as $argIdx => $arg) { - if ($arg->unpack) { - continue; - } - - $typeResult = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $arg->value, - '', - static fn (Type $type): bool => !$type->getIterableValueType()->toString() instanceof ErrorType, - ); - - if ($typeResult->getType() instanceof ErrorType - || !$typeResult->getType()->getIterableValueType()->toString() instanceof ErrorType) { - continue; - } - - if (in_array($functionName, $implodeFunctions, true)) { - // implode has weird variants, so $array has to be fixed. It's especially weird with named arguments. - if (array_key_exists('array', $origNamedArgs)) { - $argName = '$array'; - } elseif (array_key_exists('separator', $origNamedArgs) && count($origArgs) === 1) { - $argName = '$separator'; - } else { - $argName = sprintf('#%d $array', $argIdx + 1); - } - } elseif (array_key_exists($argIdx, $functionParameters)) { - $paramName = $functionParameters[$argIdx]->getName(); - $argName = array_key_exists($paramName, $origNamedArgs) - ? sprintf('$%s', $paramName) - : sprintf('#%d $%s', $argIdx + 1, $paramName); - } else { - $argName = sprintf('#%d', $argIdx + 1); - } - - $errors[] = RuleErrorBuilder::message( - sprintf($errorMessage, $argName, $functionName, $typeResult->getType()->describe(VerbosityLevel::typeOnly())), - )->identifier('argument.type')->build(); - } - - return $errors; - } - -} diff --git a/src/Rules/Functions/ParameterCastableToStringRule.php b/src/Rules/Functions/ParameterCastableToStringRule.php new file mode 100644 index 0000000000..b6b26da76e --- /dev/null +++ b/src/Rules/Functions/ParameterCastableToStringRule.php @@ -0,0 +1,118 @@ + + */ +class ParameterCastableToStringRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private ParameterCastableToStringCheck $parameterCastableToStringCheck, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + $functionName = $functionReflection->getName(); + $checkAllArgsFunctions = ['array_intersect', 'array_intersect_assoc', 'array_diff', 'array_diff_assoc']; + $checkFirstArgFunctions = [ + 'array_combine', + 'natcasesort', + 'natsort', + 'array_count_values', + 'array_fill_keys', + ]; + + if ( + !in_array($functionName, $checkAllArgsFunctions, true) + && !in_array($functionName, $checkFirstArgFunctions, true) + ) { + return []; + } + + $origArgs = $node->getArgs(); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $origArgs, + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $errorMessage = 'Parameter %s of function %s expects an array of values castable to string, %s given.'; + $functionParameters = $parametersAcceptor->getParameters(); + + if (in_array($functionName, $checkAllArgsFunctions, true)) { + $argsToCheck = $origArgs; + } elseif (in_array($functionName, $checkFirstArgFunctions, true)) { + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + + if ($normalizedFuncCall === null) { + return []; + } + + $normalizedArgs = $normalizedFuncCall->getArgs(); + if (!array_key_exists(0, $normalizedArgs)) { + return []; + } + $argsToCheck = [0 => $normalizedArgs[0]]; + } else { + return []; + } + + $errors = []; + + foreach ($argsToCheck as $argIdx => $arg) { + $error = $this->parameterCastableToStringCheck->checkParameter( + $arg, + $scope, + $errorMessage, + static fn (Type $t) => $t->toString(), + $functionName, + $this->parameterCastableToStringCheck->getParameterName( + $arg, + $argIdx, + $functionParameters[$argIdx] ?? null, + ), + ); + + if ($error === null) { + continue; + } + + $errors[] = $error; + } + + return $errors; + } + +} diff --git a/src/Rules/Functions/SortParameterCastableToStringRule.php b/src/Rules/Functions/SortParameterCastableToStringRule.php new file mode 100644 index 0000000000..dc1d4b63cf --- /dev/null +++ b/src/Rules/Functions/SortParameterCastableToStringRule.php @@ -0,0 +1,150 @@ + + */ +class SortParameterCastableToStringRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private ParameterCastableToStringCheck $parameterCastableToStringCheck, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + $functionName = $functionReflection->getName(); + + if (!in_array($functionName, ['array_unique', 'sort', 'rsort', 'asort', 'arsort'], true)) { + return []; + } + + $origArgs = $node->getArgs(); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $origArgs, + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $functionParameters = $parametersAcceptor->getParameters(); + + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + + if ($normalizedFuncCall === null) { + return []; + } + + $normalizedArgs = $normalizedFuncCall->getArgs(); + if (!array_key_exists(0, $normalizedArgs)) { + return []; + } + + $argsToCheck = [0 => $normalizedArgs[0]]; + $flags = null; + if (array_key_exists(1, $normalizedArgs)) { + $flags = $scope->getType($normalizedArgs[1]->value); + } elseif (array_key_exists(1, $functionParameters)) { + $flags = $functionParameters[1]->getDefaultValue(); + } + + if ($flags === null || $flags->equals(new ConstantIntegerType(SORT_REGULAR))) { + return []; + } + + $constantIntFlags = TypeUtils::getConstantIntegers($flags); + $mustBeCastableToString = $mustBeCastableToFloat = $constantIntFlags === []; + + foreach ($constantIntFlags as $flag) { + if ($flag->getValue() === SORT_NUMERIC) { + $mustBeCastableToFloat = true; + } elseif (in_array($flag->getValue() & (~SORT_FLAG_CASE), [SORT_STRING, SORT_LOCALE_STRING, SORT_NATURAL], true)) { + $mustBeCastableToString = true; + } + } + + if ($mustBeCastableToString && !$mustBeCastableToFloat) { + $errorMessage = 'Parameter %s of function %s expects an array of values castable to string, %s given.'; + $castFn = static fn (Type $t) => $t->toString(); + } elseif ($mustBeCastableToString) { + $errorMessage = 'Parameter %s of function %s expects an array of values castable to string and float, %s given.'; + $castFn = static function (Type $t): Type { + $float = $t->toFloat(); + + return $float instanceof ErrorType + ? $float + : $t->toString(); + }; + } elseif ($mustBeCastableToFloat) { + $errorMessage = 'Parameter %s of function %s expects an array of values castable to float, %s given.'; + $castFn = static fn (Type $t) => $t->toFloat(); + } else { + return []; + } + + $errors = []; + + foreach ($argsToCheck as $argIdx => $arg) { + $error = $this->parameterCastableToStringCheck->checkParameter( + $arg, + $scope, + $errorMessage, + $castFn, + $functionName, + $this->parameterCastableToStringCheck->getParameterName( + $arg, + $argIdx, + $functionParameters[$argIdx] ?? null, + ), + ); + + if ($error === null) { + continue; + } + + $errors[] = $error; + } + + return $errors; + } + +} diff --git a/src/Rules/ParameterCastableToStringCheck.php b/src/Rules/ParameterCastableToStringCheck.php new file mode 100644 index 0000000000..e34f6fba22 --- /dev/null +++ b/src/Rules/ParameterCastableToStringCheck.php @@ -0,0 +1,70 @@ +unpack) { + return null; + } + + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $parameter->value, + '', + static fn (Type $type): bool => !$castFn($type->getIterableValueType()) instanceof ErrorType, + ); + + if ($typeResult->getType() instanceof ErrorType + || !$castFn($typeResult->getType()->getIterableValueType()) instanceof ErrorType) { + return null; + } + + return RuleErrorBuilder::message( + sprintf($errorMessageTemplate, $parameterName, $functionName, $typeResult->getType()->describe(VerbosityLevel::typeOnly())), + )->identifier('argument.type')->build(); + } + + public function getParameterName(Arg $parameter, int $parameterIdx, ?ParameterReflection $parameterReflection): string + { + if ($parameterReflection === null) { + return sprintf('#%d', $parameterIdx + 1); + } + + $paramName = $parameterReflection->getName(); + $origParameter = $parameter->getAttributes()[ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE] ?? null; + + if (!$origParameter instanceof Arg) { + $origParameter = $parameter; + } + + return $origParameter->name !== null + ? sprintf('$%s', $paramName) + : sprintf('#%d $%s', $parameterIdx + 1, $paramName); + } + +} diff --git a/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php b/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php new file mode 100644 index 0000000000..25f0facf27 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ImplodeParameterCastableToStringRuleTest.php @@ -0,0 +1,103 @@ + + */ +class ImplodeParameterCastableToStringRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $broker = $this->createReflectionProvider(); + return new ImplodeParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, false, false, true, false))); + } + + public function testNamedArguments(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/implode-param-castable-to-string-functions-named-args.php'], [ + [ + 'Parameter $array of function implode expects array, array> given.', + 8, + ], + [ + 'Parameter $separator of function implode expects array, array> given.', + 9, + ], + [ + 'Parameter $array of function implode expects array, array> given.', + 10, + ], + [ + 'Parameter $array of function implode expects array, array> given.', + 11, + ], + ]); + } + + public function testEnum(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/implode-param-castable-to-string-functions-enum.php'], [ + [ + 'Parameter #2 $array of function implode expects array, array given.', + 12, + ], + ]); + } + + public function testImplode(): 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, + ], + ]); + } + + public function testBug6000(): void + { + $this->analyse([__DIR__ . '/../Arrays/data/bug-6000.php'], []); + } + + public function testBug8467a(): void + { + $this->analyse([__DIR__ . '/../Arrays/data/bug-8467a.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Functions/ParameterCastableToStringFunctionRuleTest.php b/tests/PHPStan/Rules/Functions/ParameterCastableToStringRuleTest.php similarity index 60% rename from tests/PHPStan/Rules/Functions/ParameterCastableToStringFunctionRuleTest.php rename to tests/PHPStan/Rules/Functions/ParameterCastableToStringRuleTest.php index 8574786b3e..8619497c4f 100644 --- a/tests/PHPStan/Rules/Functions/ParameterCastableToStringFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ParameterCastableToStringRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Functions; +use PHPStan\Rules\ParameterCastableToStringCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -10,15 +11,15 @@ use const PHP_VERSION_ID; /** - * @extends RuleTestCase + * @extends RuleTestCase */ -class ParameterCastableToStringFunctionRuleTest extends RuleTestCase +class ParameterCastableToStringRuleTest extends RuleTestCase { protected function getRule(): Rule { $broker = $this->createReflectionProvider(); - return new ParameterCastableToStringFunctionRule($broker, new RuleLevelHelper($broker, true, false, true, false, false, true, false)); + return new ParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, false, false, true, false))); } public function testRule(): void @@ -44,49 +45,25 @@ public function testRule(): void 'Parameter #2 $arrays of function array_diff_assoc expects an array of values castable to string, array given.', 20, ], - [ - 'Parameter #1 $array of function array_unique expects an array of values castable to string, array> given.', - 22, - ], [ 'Parameter #1 $keys of function array_combine expects an array of values castable to string, array> given.', - 23, - ], - [ - 'Parameter #1 $array of function sort expects an array of values castable to string, array> given.', - 26, - ], - [ - 'Parameter #1 $array of function sort expects an array of values castable to string, array given.', - 27, - ], - [ - 'Parameter #1 $array of function rsort expects an array of values castable to string, array> given.', - 28, - ], - [ - 'Parameter #1 $array of function asort expects an array of values castable to string, array> given.', - 29, - ], - [ - 'Parameter #1 $array of function arsort expects an array of values castable to string, array> given.', - 30, + 22, ], [ 'Parameter #1 $array of function natsort expects an array of values castable to string, array> given.', - 31, + 24, ], [ 'Parameter #1 $array of function natcasesort expects an array of values castable to string, array> given.', - 32, + 25, ], [ 'Parameter #1 $array of function array_count_values expects an array of values castable to string, array> given.', - 33, + 26, ], [ 'Parameter #1 $keys of function array_fill_keys expects an array of values castable to string, array> given.', - 34, + 27, ], ])); } @@ -98,49 +75,13 @@ public function testNamedArguments(): void } $this->analyse([__DIR__ . '/data/param-castable-to-string-functions-named-args.php'], [ - [ - 'Parameter $array of function array_unique expects an array of values castable to string, array> given.', - 16, - ], [ 'Parameter $keys of function array_combine expects an array of values castable to string, array> given.', - 17, - ], - [ - 'Parameter $array of function sort expects an array of values castable to string, array> given.', - 19, - ], - [ - 'Parameter $array of function rsort expects an array of values castable to string, array> given.', - 20, - ], - [ - 'Parameter $array of function asort expects an array of values castable to string, array> given.', - 21, - ], - [ - 'Parameter $array of function arsort expects an array of values castable to string, array> given.', - 22, + 7, ], [ 'Parameter $keys of function array_fill_keys expects an array of values castable to string, array> given.', - 23, - ], - [ - 'Parameter $array of function implode expects array, array> given.', - 25, - ], - [ - 'Parameter $separator of function implode expects array, array> given.', - 26, - ], - [ - 'Parameter $array of function implode expects array, array> given.', - 27, - ], - [ - 'Parameter $array of function implode expects array, array> given.', - 28, + 9, ], ]); } @@ -173,92 +114,28 @@ public function testEnum(): void 16, ], [ - 'Parameter #1 $array of function array_unique expects an array of values castable to string, array given.', + 'Parameter #1 $keys of function array_combine expects an array of values castable to string, array given.', 18, ], [ - 'Parameter #1 $keys of function array_combine expects an array of values castable to string, array given.', - 19, + 'Parameter #1 $array of function natsort expects an array of values castable to string, array given.', + 20, ], [ - 'Parameter #1 $array of function sort expects an array of values castable to string, array given.', + 'Parameter #1 $array of function natcasesort expects an array of values castable to string, array given.', 21, ], [ - 'Parameter #1 $array of function rsort expects an array of values castable to string, array given.', + 'Parameter #1 $array of function array_count_values expects an array of values castable to string, array given.', 22, ], [ - 'Parameter #1 $array of function asort expects an array of values castable to string, array given.', + 'Parameter #1 $keys of function array_fill_keys expects an array of values castable to string, array given.', 23, ], - [ - 'Parameter #1 $array of function arsort expects an array of values castable to string, array given.', - 24, - ], - [ - 'Parameter #1 $array of function natsort expects an array of values castable to string, array given.', - 25, - ], - [ - 'Parameter #1 $array of function natcasesort expects an array of values castable to string, array given.', - 26, - ], - [ - 'Parameter #1 $array of function array_count_values expects an array of values castable to string, array given.', - 27, - ], - [ - 'Parameter #1 $keys of function array_fill_keys expects an array of values castable to string, array given.', - 28, - ], - [ - 'Parameter #2 $array of function implode expects array, array given.', - 31, - ], ]); } - public function testImplode(): void - { - $this->analyse([__DIR__ . '/data/implode.php'], $this->hackParameterNames([ - [ - '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, - ], - ])); - } - - public function testBug6000(): void - { - $this->analyse([__DIR__ . '/../Arrays/data/bug-6000.php'], []); - } - - public function testBug8467a(): void - { - $this->analyse([__DIR__ . '/../Arrays/data/bug-8467a.php'], []); - } - public function testBug5848(): void { $this->analyse([__DIR__ . '/data/bug-5848.php'], $this->hackParameterNames([ @@ -338,10 +215,6 @@ private function hackParameterNames(array $errors): array '$arrays of function array_intersect', '$arrays of function array_diff', '$arrays of function array_diff_assoc', - '$array of function sort', - '$array of function rsort', - '$array of function asort', - '$array of function arsort', '$array of function natsort', '$array of function natcasesort', '$array of function array_count_values', @@ -354,10 +227,6 @@ private function hackParameterNames(array $errors): array '$arr2 of function array_intersect', '$arr2 of function array_diff', '$arr2 of function array_diff_assoc', - '$array_arg of function sort', - '$array_arg of function rsort', - '$array_arg of function asort', - '$array_arg of function arsort', '$array_arg of function natsort', '$array_arg of function natcasesort', '$input of function array_count_values', diff --git a/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php b/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php new file mode 100644 index 0000000000..98076839d1 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/SortParameterCastableToStringRuleTest.php @@ -0,0 +1,179 @@ + + */ +class SortParameterCastableToStringRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + $broker = $this->createReflectionProvider(); + return new SortParameterCastableToStringRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, false, false, true, false))); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/sort-param-castable-to-string-functions.php'], $this->hackParameterNames([ + [ + 'Parameter #1 $array of function array_unique expects an array of values castable to string, array> given.', + 16, + ], + [ + 'Parameter #1 $array of function sort expects an array of values castable to string, array> given.', + 19, + ], + [ + 'Parameter #1 $array of function sort expects an array of values castable to string, array given.', + 20, + ], + [ + 'Parameter #1 $array of function rsort expects an array of values castable to string, array> given.', + 21, + ], + [ + 'Parameter #1 $array of function asort expects an array of values castable to string, array> given.', + 22, + ], + [ + 'Parameter #1 $array of function arsort expects an array of values castable to string, array> given.', + 23, + ], + [ + 'Parameter #1 $array of function sort expects an array of values castable to string, array> given.', + 25, + ], + [ + 'Parameter #1 $array of function rsort expects an array of values castable to string, array> given.', + 26, + ], + [ + 'Parameter #1 $array of function asort expects an array of values castable to string, array> given.', + 27, + ], + [ + 'Parameter #1 $array of function arsort expects an array of values castable to float, array given.', + 31, + ], + [ + 'Parameter #1 $array of function arsort expects an array of values castable to string and float, array given.', + 32, + ], + [ + 'Parameter #1 $array of function sort expects an array of values castable to string, array> given.', + 33, + ], + [ + 'Parameter #1 $array of function sort expects an array of values castable to string and float, array> given.', + 34, + ], + ])); + } + + public function testNamedArguments(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/sort-param-castable-to-string-functions-named-args.php'], [ + [ + 'Parameter $array of function array_unique expects an array of values castable to string, array> given.', + 7, + ], + [ + 'Parameter $array of function sort expects an array of values castable to string, array> given.', + 9, + ], + [ + 'Parameter $array of function rsort expects an array of values castable to string, array> given.', + 10, + ], + [ + 'Parameter $array of function asort expects an array of values castable to string, array> given.', + 11, + ], + [ + 'Parameter $array of function arsort expects an array of values castable to string, array> given.', + 12, + ], + ]); + } + + public function testEnum(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/sort-param-castable-to-string-functions-enum.php'], [ + [ + 'Parameter #1 $array of function array_unique expects an array of values castable to string, array given.', + 12, + ], + [ + 'Parameter #1 $array of function sort expects an array of values castable to string, array given.', + 14, + ], + [ + 'Parameter #1 $array of function rsort expects an array of values castable to string, array given.', + 15, + ], + [ + 'Parameter #1 $array of function asort expects an array of values castable to string, array given.', + 16, + ], + [ + 'Parameter #1 $array of function arsort expects an array of values castable to string, array given.', + 17, + ], + ]); + } + + public function testBug11167(): void + { + $this->analyse([__DIR__ . '/data/bug-11167.php'], []); + } + + /** + * @param list $errors + * @return list + */ + private function hackParameterNames(array $errors): array + { + if (PHP_VERSION_ID >= 80000) { + return $errors; + } + + return array_map(static function (array $error): array { + $error[0] = str_replace( + [ + '$array of function sort', + '$array of function rsort', + '$array of function asort', + '$array of function arsort', + ], + [ + '$array_arg of function sort', + '$array_arg of function rsort', + '$array_arg of function asort', + '$array_arg of function arsort', + ], + $error[0], + ); + + return $error; + }, $errors); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/bug-11167.php b/tests/PHPStan/Rules/Functions/data/bug-11167.php new file mode 100644 index 0000000000..8afdd21029 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11167.php @@ -0,0 +1,5 @@ += 8.1 + +namespace ImplodeParamCastableToStringFunctionsEnum; + +enum FooEnum +{ + case A; +} + +function invalidUsages() +{ + implode(',', [FooEnum::A]); +} diff --git a/tests/PHPStan/Rules/Functions/data/implode-param-castable-to-string-functions-named-args.php b/tests/PHPStan/Rules/Functions/data/implode-param-castable-to-string-functions-named-args.php new file mode 100644 index 0000000000..362f02ea8b --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/implode-param-castable-to-string-functions-named-args.php @@ -0,0 +1,18 @@ += 8.0 + +namespace ImplodeParamCastableToStringFunctionsNamedArgs; + +function invalidUsages() +{ + // implode weirdness + implode(array: [['a']], separator: ','); + implode(separator: [['a']]); + implode(',', array: [['a']]); + implode(separator: ',', array: [['']]); +} + +function wrongNumberOfArguments(): void +{ + implode(array: ','); + join(array: ','); +} diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-enum.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-enum.php index ee0cca82ea..bbf189cf96 100644 --- a/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-enum.php +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-enum.php @@ -15,18 +15,10 @@ function invalidUsages() array_diff(['a'], [FooEnum::A]); array_diff_assoc(['a'], [FooEnum::A]); - array_unique(['a', FooEnum::A]); array_combine([FooEnum::A], [['b']]); $arr1 = [FooEnum::A]; - sort($arr1); - rsort($arr1); - asort($arr1); - arsort($arr1); natsort($arr1); natcasesort($arr1); array_count_values($arr1); array_fill_keys($arr1, 5); - - - implode(',', [FooEnum::A]); } diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-named-args.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-named-args.php index d36d76d194..b8790a475d 100644 --- a/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-named-args.php +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions-named-args.php @@ -2,40 +2,22 @@ namespace ParamCastableToStringFunctionsNamedArgs; -class ClassWithoutToString {} -class ClassWithToString -{ - public function __toString(): string - { - return 'foo'; - } -} - function invalidUsages() { - array_unique(flags: SORT_STRING, array: [['a'], ['b']]); array_combine(values: [['b']], keys: [['a']]); $arr1 = [['a']]; - sort(flags: SORT_REGULAR, array: $arr1); - rsort(flags: SORT_REGULAR, array: $arr1); - asort(flags: SORT_REGULAR, array: $arr1); - arsort(flags: SORT_REGULAR, array: $arr1); array_fill_keys(value: 5, keys: $arr1); - // implode weirdness - implode(array: [['a']], separator: ','); - implode(separator: [['a']]); - implode(',', array: [['a']]); - implode(separator: ',', array: [['']]); +} + +function wrongNumberOfArguments(): void +{ + array_combine(values: [[5]]); + array_fill_keys(value: [5]); } function validUsages() { - array_unique(flags: SORT_STRING, array: ['a', 'b']); array_combine(values: [['b']], keys: ['a']); $arr1 = ['a']; - sort(flags: SORT_REGULAR, array: $arr1); - rsort(flags: SORT_REGULAR, array: $arr1); - asort(flags: SORT_REGULAR, array: $arr1); - arsort(flags: SORT_REGULAR, array: $arr1); array_fill_keys(value: 5, keys: $arr1); } diff --git a/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions.php b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions.php index aef61a2f53..008c2d0142 100644 --- a/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions.php +++ b/tests/PHPStan/Rules/Functions/data/param-castable-to-string-functions.php @@ -19,36 +19,21 @@ function invalidUsages(): void array_diff(['a'], [new ClassWithoutToString()]); array_diff_assoc(['a'], [new ClassWithoutToString()]); - array_unique([['a'], ['b']]); array_combine([['a']], [['b']]); $arr1 = [['a']]; - $arr2 = [new ClassWithoutToString()]; - sort($arr1); - sort($arr2); - rsort($arr1); - asort($arr1); - arsort($arr1); natsort($arr1); natcasesort($arr1); array_count_values($arr1); array_fill_keys($arr1, 5); - } function wrongNumberOfArguments(): void { - implode(); - join(); array_intersect(); array_intersect_assoc(); array_diff(); array_diff_assoc(); - array_unique(); array_combine(); - sort(); - rsort(); - asort(); - arsort(); natcasesort(); natsort(); array_count_values(); @@ -63,13 +48,8 @@ function validUsages(): void array_diff(['a'], [new ClassWithToString()]); array_diff_assoc(['a'], [new ClassWithToString()]); - array_unique(['a', 'b']); array_combine(['a'], [['b']]); $arr1 = ['a', new ClassWithToString()]; - sort($arr1); - rsort($arr1); - asort($arr1); - arsort($arr1); natsort($arr1); natcasesort($arr1); array_count_values($arr1); diff --git a/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions-enum.php b/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions-enum.php new file mode 100644 index 0000000000..e3911d590f --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions-enum.php @@ -0,0 +1,26 @@ += 8.1 + +namespace SortParamCastableToStringFunctionsEnum; + +enum FooEnum +{ + case A; +} + +function invalidUsages():void +{ + array_unique(['a', FooEnum::A]); + $arr1 = [FooEnum::A]; + sort($arr1, SORT_STRING); + rsort($arr1, SORT_LOCALE_STRING); + asort($arr1, SORT_STRING | SORT_FLAG_CASE); + arsort($arr1, SORT_LOCALE_STRING | SORT_FLAG_CASE); +} + +function validUsages(): void +{ + $arr = [FooEnum::A, 1]; + array_unique($arr, SORT_REGULAR); + sort($arr, SORT_REGULAR); + rsort($arr, 128); +} diff --git a/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions-named-args.php b/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions-named-args.php new file mode 100644 index 0000000000..2a69d861ce --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions-named-args.php @@ -0,0 +1,32 @@ += 8.0 + +namespace SortParamCastableToStringFunctionsNamedArgs; + +function invalidUsages() +{ + array_unique(flags: SORT_STRING, array: [['a'], ['b']]); + $arr1 = [['a']]; + sort(flags: SORT_STRING, array: $arr1); + rsort(flags: SORT_STRING, array: $arr1); + asort(flags: SORT_STRING, array: $arr1); + arsort(flags: SORT_STRING, array: $arr1); +} + +function wrongNumberOfArguments(): void +{ + array_unique(flags: SORT_STRING); + sort(flags: SORT_STRING); + rsort(flags: SORT_STRING); + asort(flags: SORT_STRING); + arsort(flags: SORT_STRING); +} + +function validUsages() +{ + array_unique(flags: SORT_STRING, array: ['a', 'b']); + $arr1 = ['a']; + sort(flags: SORT_STRING, array: $arr1); + rsort(flags: SORT_STRING, array: $arr1); + asort(flags: SORT_STRING, array: $arr1); + arsort(flags: SORT_STRING, array: $arr1); +} diff --git a/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions.php b/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions.php new file mode 100644 index 0000000000..e1e0ff0dca --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/sort-param-castable-to-string-functions.php @@ -0,0 +1,71 @@ +