diff --git a/resources/functionMap.php b/resources/functionMap.php index fade07b20c..16b8e6bf6d 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -5695,6 +5695,7 @@ 'jobqueue_license_info' => ['array'], 'join' => ['string', 'glue'=>'string', 'pieces'=>'array'], 'join\'1' => ['string', 'pieces'=>'array'], +'join\'2' => ['string', 'pieces'=>'array', 'glue'=>'string'], 'jpeg2wbmp' => ['bool', 'jpegname'=>'string', 'wbmpname'=>'string', 'dest_height'=>'int', 'dest_width'=>'int', 'threshold'=>'int'], 'json_decode' => ['mixed', 'json'=>'string', 'assoc='=>'bool|null', 'depth='=>'positive-int', 'options='=>'int'], 'json_encode' => ['non-empty-string|false', 'data'=>'mixed', 'options='=>'int', 'depth='=>'positive-int'], diff --git a/resources/functionMap_php74delta.php b/resources/functionMap_php74delta.php index 9b187510c8..86bc99ef80 100644 --- a/resources/functionMap_php74delta.php +++ b/resources/functionMap_php74delta.php @@ -60,5 +60,6 @@ ], 'old' => [ 'implode\'2' => ['string', 'pieces'=>'array', 'glue'=>'string'], + 'join\'2' => ['string', 'pieces'=>'array', 'glue'=>'string'], ], ]; diff --git a/src/Parser/ImplodeArgVisitor.php b/src/Parser/ImplodeArgVisitor.php new file mode 100644 index 0000000000..36e0201b83 --- /dev/null +++ b/src/Parser/ImplodeArgVisitor.php @@ -0,0 +1,32 @@ +name instanceof Node\Name) { + $functionName = $node->name->toLowerString(); + if (in_array($functionName, ['implode', 'join'], true)) { + $args = $node->getRawArgs(); + if (isset($args[0])) { + $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); + } + } + } + return null; + } + +} diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 81bc3a2a99..a2986b2fbc 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -15,6 +15,7 @@ use PHPStan\Parser\ClosureBindArgVisitor; use PHPStan\Parser\ClosureBindToVarVisitor; use PHPStan\Parser\CurlSetOptArgVisitor; +use PHPStan\Parser\ImplodeArgVisitor; use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\Php\DummyParameter; @@ -203,6 +204,32 @@ public static function selectFromArgs( ]; } + if (count($args) <= 2 && (bool) $args[0]->getAttribute(ImplodeArgVisitor::ATTRIBUTE_NAME)) { + $acceptor = $namedArgumentsVariants[0] ?? $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + if (isset($args[1]) || ($args[0]->name !== null && $args[0]->name->name === 'array')) { + $parameters = [ + new NativeParameterReflection($parameters[0]->getName(), false, new StringType(), PassedByReference::createNo(), false, null), + new NativeParameterReflection($parameters[1]->getName(), false, new ArrayType(new MixedType(), new MixedType()), PassedByReference::createNo(), false, null), + ]; + } else { + $parameters = [ + new NativeParameterReflection($parameters[0]->getName(), false, new ArrayType(new MixedType(), new MixedType()), PassedByReference::createNo(), false, null), + ]; + } + + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + $parameters, + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + if (isset($args[0]) && (bool) $args[0]->getAttribute(ArrayWalkArgVisitor::ATTRIBUTE_NAME)) { $arrayWalkParameters = [ new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createReadsArgument(), false, null), diff --git a/tests/PHPStan/Levels/data/acceptTypes-5.json b/tests/PHPStan/Levels/data/acceptTypes-5.json index 76aac5c080..4bb076e055 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-5.json +++ b/tests/PHPStan/Levels/data/acceptTypes-5.json @@ -185,7 +185,7 @@ "ignorable": true }, { - "message": "Parameter #2 $array of function implode expects array|null, int given.", + "message": "Parameter #2 $array of function implode expects array, int given.", "line": 763, "ignorable": true } diff --git a/tests/PHPStan/Levels/data/acceptTypes-7.json b/tests/PHPStan/Levels/data/acceptTypes-7.json index ba67e6a9d6..216fad8987 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-7.json +++ b/tests/PHPStan/Levels/data/acceptTypes-7.json @@ -165,7 +165,7 @@ "ignorable": true }, { - "message": "Parameter #2 $array of function implode expects array|null, array|int|string given.", + "message": "Parameter #2 $array of function implode expects array, array|int|string given.", "line": 756, "ignorable": true } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 385fab83b5..69207e94f3 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -322,7 +322,11 @@ public function testImplodeOnPhp74(): void if (PHP_VERSION_ID >= 80000) { $errors = [ [ - 'Parameter #2 $array of function implode expects array|null, string given.', + 'Parameter #1 $separator of function implode expects string, array given.', + 8, + ], + [ + 'Parameter #2 $array of function implode expects array, string given.', 8, ], ]; @@ -336,7 +340,11 @@ public function testImplodeOnLessThanPhp74(): void if (PHP_VERSION_ID >= 80000) { $errors = [ [ - 'Parameter #2 $array of function implode expects array|null, string given.', + 'Parameter #1 $separator of function implode expects string, array given.', + 8, + ], + [ + 'Parameter #2 $array of function implode expects array, string given.', 8, ], ]; @@ -356,6 +364,21 @@ public function testImplodeOnLessThanPhp74(): void $this->analyse([__DIR__ . '/data/implode-74.php'], $errors); } + #[RequiresPhp('>= 8.0')] + public function testImplodeNamedParameters(): void + { + $this->analyse([__DIR__ . '/data/implode-named-parameters.php'], [ + [ + 'Missing parameter $separator (string) in call to function implode.', + 6, + ], + [ + 'Missing parameter $separator (string) in call to function join.', + 7, + ], + ]); + } + public function testVariableIsNotNullAfterSeriesOfConditions(): void { require_once __DIR__ . '/data/variable-is-not-null-after-conditions.php'; @@ -2192,6 +2215,54 @@ public function testBug13065(): void $this->analyse([__DIR__ . '/data/bug-13065.php'], $errors); } + public function testBug5760(): void + { + if (PHP_VERSION_ID < 80000) { + $param1Name = '$glue'; + $param2Name = '$pieces'; + } else { + $param1Name = '$separator'; + $param2Name = '$array'; + } + + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-5760.php'], [ + [ + sprintf('Parameter #2 %s of function join expects array, list|null given.', $param2Name), + 10, + ], + [ + sprintf('Parameter #1 %s of function join expects array, list|null given.', $param1Name), + 11, + ], + [ + sprintf('Parameter #2 %s of function implode expects array, list|null given.', $param2Name), + 13, + ], + [ + sprintf('Parameter #1 %s of function implode expects array, list|null given.', $param1Name), + 14, + ], + [ + sprintf('Parameter #2 %s of function join expects array, array|string given.', $param2Name), + 22, + ], + [ + sprintf('Parameter #1 %s of function join expects array, array|string given.', $param1Name), + 23, + ], + [ + sprintf('Parameter #2 %s of function implode expects array, array|string given.', $param2Name), + 25, + ], + [ + sprintf('Parameter #1 %s of function implode expects array, array|string given.', $param1Name), + 26, + ], + ]); + } + #[RequiresPhp('>= 8.0')] public function testBug12317(): void { diff --git a/tests/PHPStan/Rules/Functions/data/bug-5760.php b/tests/PHPStan/Rules/Functions/data/bug-5760.php new file mode 100644 index 0000000000..f3ec36535f --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-5760.php @@ -0,0 +1,36 @@ +|null $arrayOrNull + */ +function doImplode(?array $arrayOrNull): void +{ + join(',', $arrayOrNull); + join($arrayOrNull); + + implode(',', $arrayOrNull); + implode($arrayOrNull); +} + +/** + * @param array|string $union + */ +function more(array|string $union): void +{ + join(',', $union); + join($union); + + implode(',', $union); + implode($union); +} + +function success(): void +{ + join(',', ['']); + join(['']); + + implode(',', ['']); + implode(['']); +} diff --git a/tests/PHPStan/Rules/Functions/data/implode-named-parameters.php b/tests/PHPStan/Rules/Functions/data/implode-named-parameters.php new file mode 100644 index 0000000000..8e016b5157 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/implode-named-parameters.php @@ -0,0 +1,10 @@ +