Skip to content

Commit e0c4844

Browse files
Improve implode signature
1 parent e8b3111 commit e0c4844

File tree

9 files changed

+182
-4
lines changed

9 files changed

+182
-4
lines changed

resources/functionMap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5156,6 +5156,7 @@
51565156
'jobqueue_license_info' => ['array'],
51575157
'join' => ['string', 'glue'=>'string', 'pieces'=>'array'],
51585158
'join\'1' => ['string', 'pieces'=>'array'],
5159+
'join\'2' => ['string', 'pieces'=>'array', 'glue'=>'string'],
51595160
'jpeg2wbmp' => ['bool', 'jpegname'=>'string', 'wbmpname'=>'string', 'dest_height'=>'int', 'dest_width'=>'int', 'threshold'=>'int'],
51605161
'json_decode' => ['mixed', 'json'=>'string', 'assoc='=>'bool|null', 'depth='=>'positive-int', 'options='=>'int'],
51615162
'json_encode' => ['non-empty-string|false', 'data'=>'mixed', 'options='=>'int', 'depth='=>'positive-int'],

resources/functionMap_php74delta.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,6 @@
6060
],
6161
'old' => [
6262
'implode\'2' => ['string', 'pieces'=>'array', 'glue'=>'string'],
63+
'join\'2' => ['string', 'pieces'=>'array', 'glue'=>'string'],
6364
],
6465
];

src/Parser/ImplodeArgVisitor.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Parser;
4+
5+
use Override;
6+
use PhpParser\Node;
7+
use PhpParser\NodeVisitorAbstract;
8+
use PHPStan\DependencyInjection\AutowiredService;
9+
use function in_array;
10+
11+
#[AutowiredService]
12+
final class ImplodeArgVisitor extends NodeVisitorAbstract
13+
{
14+
15+
public const ATTRIBUTE_NAME = 'isImplodeArg';
16+
17+
#[Override]
18+
public function enterNode(Node $node): ?Node
19+
{
20+
if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) {
21+
$functionName = $node->name->toLowerString();
22+
if (in_array($functionName, ['implode', 'join'], true)) {
23+
$args = $node->getRawArgs();
24+
if (isset($args[0])) {
25+
$args[0]->setAttribute(self::ATTRIBUTE_NAME, true);
26+
}
27+
}
28+
}
29+
return null;
30+
}
31+
32+
}

src/Reflection/ParametersAcceptorSelector.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use PHPStan\Parser\ClosureBindArgVisitor;
1616
use PHPStan\Parser\ClosureBindToVarVisitor;
1717
use PHPStan\Parser\CurlSetOptArgVisitor;
18+
use PHPStan\Parser\ImplodeArgVisitor;
1819
use PHPStan\Reflection\Callables\CallableParametersAcceptor;
1920
use PHPStan\Reflection\Native\NativeParameterReflection;
2021
use PHPStan\Reflection\Php\DummyParameter;
@@ -203,6 +204,32 @@ public static function selectFromArgs(
203204
];
204205
}
205206

207+
if (count($args) <= 2 && (bool) $args[0]->getAttribute(ImplodeArgVisitor::ATTRIBUTE_NAME)) {
208+
$acceptor = $namedArgumentsVariants[0] ?? $parametersAcceptors[0];
209+
$parameters = $acceptor->getParameters();
210+
if (isset($args[1]) || ($args[0]->name !== null && $args[0]->name->name === 'array')) {
211+
$parameters = [
212+
new NativeParameterReflection($parameters[0]->getName(), false, new StringType(), PassedByReference::createNo(), false, null),
213+
new NativeParameterReflection($parameters[1]->getName(), false, new ArrayType(new MixedType(), new MixedType()), PassedByReference::createNo(), false, null),
214+
];
215+
} else {
216+
$parameters = [
217+
new NativeParameterReflection($parameters[0]->getName(), false, new ArrayType(new MixedType(), new MixedType()), PassedByReference::createNo(), false, null),
218+
];
219+
}
220+
221+
$parametersAcceptors = [
222+
new FunctionVariant(
223+
$acceptor->getTemplateTypeMap(),
224+
$acceptor->getResolvedTemplateTypeMap(),
225+
$parameters,
226+
$acceptor->isVariadic(),
227+
$acceptor->getReturnType(),
228+
$acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(),
229+
),
230+
];
231+
}
232+
206233
if ((bool) $args[0]->getAttribute(ArrayWalkArgVisitor::ATTRIBUTE_NAME)) {
207234
$arrayWalkParameters = [
208235
new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createReadsArgument(), false, null),

tests/PHPStan/Levels/data/acceptTypes-5.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@
185185
"ignorable": true
186186
},
187187
{
188-
"message": "Parameter #2 $array of function implode expects array|null, int given.",
188+
"message": "Parameter #2 $array of function implode expects array, int given.",
189189
"line": 763,
190190
"ignorable": true
191191
}

tests/PHPStan/Levels/data/acceptTypes-7.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@
165165
"ignorable": true
166166
},
167167
{
168-
"message": "Parameter #2 $array of function implode expects array|null, array|int|string given.",
168+
"message": "Parameter #2 $array of function implode expects array, array|int|string given.",
169169
"line": 756,
170170
"ignorable": true
171171
}

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,11 @@ public function testImplodeOnPhp74(): void
322322
if (PHP_VERSION_ID >= 80000) {
323323
$errors = [
324324
[
325-
'Parameter #2 $array of function implode expects array|null, string given.',
325+
'Parameter #1 $separator of function implode expects string, array given.',
326+
8,
327+
],
328+
[
329+
'Parameter #2 $array of function implode expects array, string given.',
326330
8,
327331
],
328332
];
@@ -336,7 +340,11 @@ public function testImplodeOnLessThanPhp74(): void
336340
if (PHP_VERSION_ID >= 80000) {
337341
$errors = [
338342
[
339-
'Parameter #2 $array of function implode expects array|null, string given.',
343+
'Parameter #1 $separator of function implode expects string, array given.',
344+
8,
345+
],
346+
[
347+
'Parameter #2 $array of function implode expects array, string given.',
340348
8,
341349
],
342350
];
@@ -356,6 +364,21 @@ public function testImplodeOnLessThanPhp74(): void
356364
$this->analyse([__DIR__ . '/data/implode-74.php'], $errors);
357365
}
358366

367+
#[RequiresPhp('>= 8.0')]
368+
public function testImplodeNamedParameters(): void
369+
{
370+
$this->analyse([__DIR__ . '/data/implode-named-parameters.php'], [
371+
[
372+
'Missing parameter $separator (string) in call to function implode.',
373+
6,
374+
],
375+
[
376+
'Missing parameter $separator (string) in call to function join.',
377+
7,
378+
],
379+
]);
380+
}
381+
359382
public function testVariableIsNotNullAfterSeriesOfConditions(): void
360383
{
361384
require_once __DIR__ . '/data/variable-is-not-null-after-conditions.php';
@@ -2224,6 +2247,54 @@ public function testBug3506(): void
22242247
$this->analyse([__DIR__ . '/data/bug-3506.php'], []);
22252248
}
22262249

2250+
public function testBug5760(): void
2251+
{
2252+
if (PHP_VERSION_ID < 80000) {
2253+
$param1Name = '$glue';
2254+
$param2Name = '$pieces';
2255+
} else {
2256+
$param1Name = '$separator';
2257+
$param2Name = '$array';
2258+
}
2259+
2260+
$this->checkExplicitMixed = true;
2261+
$this->checkImplicitMixed = true;
2262+
$this->analyse([__DIR__ . '/data/bug-5760.php'], [
2263+
[
2264+
sprintf('Parameter #2 %s of function join expects array, list<int>|null given.', $param2Name),
2265+
10,
2266+
],
2267+
[
2268+
sprintf('Parameter #1 %s of function join expects array, list<int>|null given.', $param1Name),
2269+
11,
2270+
],
2271+
[
2272+
sprintf('Parameter #2 %s of function implode expects array, list<int>|null given.', $param2Name),
2273+
13,
2274+
],
2275+
[
2276+
sprintf('Parameter #1 %s of function implode expects array, list<int>|null given.', $param1Name),
2277+
14,
2278+
],
2279+
[
2280+
sprintf('Parameter #2 %s of function join expects array, array<string>|string given.', $param2Name),
2281+
22,
2282+
],
2283+
[
2284+
sprintf('Parameter #1 %s of function join expects array, array<string>|string given.', $param1Name),
2285+
23,
2286+
],
2287+
[
2288+
sprintf('Parameter #2 %s of function implode expects array, array<string>|string given.', $param2Name),
2289+
25,
2290+
],
2291+
[
2292+
sprintf('Parameter #1 %s of function implode expects array, array<string>|string given.', $param1Name),
2293+
26,
2294+
],
2295+
]);
2296+
}
2297+
22272298
#[RequiresPhp('>= 8.0')]
22282299
public function testBug12317(): void
22292300
{
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bug5760;
4+
5+
/**
6+
* @param list<int>|null $arrayOrNull
7+
*/
8+
function doImplode(?array $arrayOrNull): void
9+
{
10+
join(',', $arrayOrNull);
11+
join($arrayOrNull);
12+
13+
implode(',', $arrayOrNull);
14+
implode($arrayOrNull);
15+
}
16+
17+
/**
18+
* @param array<string>|string $union
19+
*/
20+
function more(array|string $union): void
21+
{
22+
join(',', $union);
23+
join($union);
24+
25+
implode(',', $union);
26+
implode($union);
27+
}
28+
29+
function success(): void
30+
{
31+
join(',', ['']);
32+
join(['']);
33+
34+
implode(',', ['']);
35+
implode(['']);
36+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ImplodeNamedParameters;
4+
5+
function (): void {
6+
implode(array: ['']); // error
7+
join(array: ['']); // error
8+
implode(separator: '', array: ['']);
9+
join(separator: '', array: ['']);
10+
};

0 commit comments

Comments
 (0)