Skip to content

Commit 41ff9e0

Browse files
phpstan-botVincentLangletclaude
authored
Fix phpstan/phpstan#11978: Ordering of Intersections matters for method calls, first matching wins (#5408)
Co-authored-by: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Vincent Langlet <vincentlanglet@hotmail.fr>
1 parent 85b4f24 commit 41ff9e0

File tree

3 files changed

+84
-4
lines changed

3 files changed

+84
-4
lines changed

src/Reflection/Type/IntersectionTypeMethodReflection.php

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
final class IntersectionTypeMethodReflection implements ExtendedMethodReflection
2323
{
2424

25+
private ?ExtendedMethodReflection $methodWithMostParameters = null;
26+
2527
/**
2628
* @param ExtendedMethodReflection[] $methods
2729
*/
@@ -31,7 +33,7 @@ public function __construct(private string $methodName, private array $methods)
3133

3234
public function getDeclaringClass(): ClassReflection
3335
{
34-
return $this->methods[0]->getDeclaringClass();
36+
return $this->getMethodWithMostParameters()->getDeclaringClass();
3537
}
3638

3739
public function isStatic(): bool
@@ -104,7 +106,7 @@ public function getVariants(): array
104106
$phpDocReturnType,
105107
$nativeReturnType,
106108
$acceptor->getCallSiteVarianceMap(),
107-
), $this->methods[0]->getVariants());
109+
), $this->getMethodWithMostParameters()->getVariants());
108110
}
109111

110112
public function getOnlyVariant(): ExtendedParametersAcceptor
@@ -237,7 +239,7 @@ public function isAbstract(): TrinaryLogic
237239

238240
public function getAttributes(): array
239241
{
240-
return $this->methods[0]->getAttributes();
242+
return $this->getMethodWithMostParameters()->getAttributes();
241243
}
242244

243245
public function mustUseReturnValue(): TrinaryLogic
@@ -247,7 +249,36 @@ public function mustUseReturnValue(): TrinaryLogic
247249

248250
public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock
249251
{
250-
return $this->methods[0]->getResolvedPhpDoc();
252+
return $this->getMethodWithMostParameters()->getResolvedPhpDoc();
253+
}
254+
255+
/**
256+
* Since every intersected method should be compatible,
257+
* selects the method whose variant has the widest parameter list,
258+
* so intersection ordering does not affect call validation.
259+
*/
260+
private function getMethodWithMostParameters(): ExtendedMethodReflection
261+
{
262+
if ($this->methodWithMostParameters !== null) {
263+
return $this->methodWithMostParameters;
264+
}
265+
266+
$methodWithMostParameters = $this->methods[0];
267+
$maxParameters = 0;
268+
foreach ($this->methods as $method) {
269+
foreach ($method->getVariants() as $variant) {
270+
if (count($variant->getParameters()) <= $maxParameters) {
271+
continue;
272+
}
273+
274+
$maxParameters = count($variant->getParameters());
275+
$methodWithMostParameters = $method;
276+
}
277+
}
278+
279+
$this->methodWithMostParameters = $methodWithMostParameters;
280+
281+
return $methodWithMostParameters;
251282
}
252283

253284
}

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3975,4 +3975,21 @@ public function testBug4608(): void
39753975
]);
39763976
}
39773977

3978+
public function testBug11978(): void
3979+
{
3980+
$this->checkThisOnly = false;
3981+
$this->checkNullables = true;
3982+
$this->checkUnionTypes = true;
3983+
$this->analyse([__DIR__ . '/data/bug-11978.php'], [
3984+
[
3985+
'Method Bug11978\ViewB::render() invoked with 2 parameters, 0-1 required.',
3986+
25,
3987+
],
3988+
[
3989+
'Method Bug11978\ViewB::render() invoked with 2 parameters, 0-1 required.',
3990+
26,
3991+
],
3992+
]);
3993+
}
3994+
39783995
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug11978;
6+
7+
interface ViewA {
8+
public function render(): string;
9+
}
10+
interface ViewB {
11+
public function render(string $foo = ''): string;
12+
}
13+
14+
class Foo
15+
{
16+
public function __construct(
17+
private readonly ViewA&ViewB $view1,
18+
private readonly ViewB&ViewA $view2,
19+
) {}
20+
21+
public function renderFoo(string $foo): string
22+
{
23+
$a = $this->view1->render($foo);
24+
$b = $this->view2->render($foo);
25+
$c = $this->view1->render($foo, $foo);
26+
$d = $this->view2->render($foo, $foo);
27+
$e = $this->view1->render();
28+
$f = $this->view2->render();
29+
30+
return $a . $b;
31+
}
32+
}

0 commit comments

Comments
 (0)