Skip to content

Fix phpstan/phpstan#11978: Ordering of Intersections matters for method calls, first matching wins#5408

Merged
VincentLanglet merged 5 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-tgg99t8
Apr 7, 2026
Merged

Fix phpstan/phpstan#11978: Ordering of Intersections matters for method calls, first matching wins#5408
VincentLanglet merged 5 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-tgg99t8

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When calling a method on an intersection type like ViewA&ViewB, PHPStan always used the first type's method signature for parameter validation. This meant that ViewA&ViewB and ViewB&ViewA could produce different errors, violating PHP's guarantee that intersection type ordering is irrelevant.

Changes

  • Modified src/Reflection/Type/IntersectionTypeMethodReflection.php: In getVariants(), instead of always using $this->methods[0] for parameters, find the method with the most parameters across all intersection members and use that
  • Added regression test tests/PHPStan/Rules/Methods/data/bug-11978.php and test method in CallMethodsRuleTest.php

Root cause

IntersectionTypeMethodReflection::getVariants() mapped over $this->methods[0]->getVariants() to build the combined variant, taking the parameters from the first method in the intersection. For an intersection ViewA&ViewB where ViewA::render() has 0 parameters and ViewB::render(string $foo = '') has 1 optional parameter, the first-wins approach used ViewA's 0-parameter signature, causing a false positive when calling render($foo).

The fix selects the method with the most parameters, since any concrete implementation of the intersection must satisfy all interface contracts, meaning the widest parameter list is always valid to call.

Test

Added tests/PHPStan/Rules/Methods/data/bug-11978.php with two intersection types (ViewA&ViewB and ViewB&ViewA) where both call render() with one argument. Neither should produce an error.

Fixes phpstan/phpstan#11978

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is multiple occurence of

$this->methods[0]

in this class.

Extract this in a private method to use it.

Also add a cache on the selected method.

@VincentLanglet VincentLanglet self-assigned this Apr 6, 2026
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Background make tests also completed successfully. All tests pass, static analysis is clean, and the commit has been pushed.

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a SHORT phpdoc to comment getMethodWithMostParameters

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Tests passed as well. All done — the PHPDoc has been added, verified with both make phpstan and make tests, and pushed.

@VincentLanglet VincentLanglet requested a review from staabm April 6, 2026 18:21
public function renderFoo(string $foo): string
{
$a = $this->view1->render($foo);
$b = $this->view2->render($foo);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

append these line to the test:

$c = $this->view1->render($foo, $foo);
$d = $this->view2->render($foo, $foo);
$e = $this->view1->render();
$f = $this->view2->render();

and adjust expected errors in tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Background make tests also completed successfully. All done — the review changes are committed and pushed.

Copy link
Copy Markdown
Contributor

@staabm staabm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs conflict resolution and rebase. otherwise LGTM.

VincentLanglet and others added 5 commits April 7, 2026 10:33
…depend on ordering

- IntersectionTypeMethodReflection::getVariants() now picks the method with the most parameters instead of always using the first method
- Added regression test in tests/PHPStan/Rules/Methods/data/bug-11978.php
- The root cause was that the first method's parameter list was always used, so ViewA&ViewB would use ViewA's 0-param render() while ViewB&ViewA would use ViewB's 1-param render()
Replace all occurrences of $this->methods[0] with a call to the new
getMethodWithMostParameters() private method, which caches its result.
This addresses the review feedback to avoid duplicating the method
selection logic and to add caching.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-tgg99t8 branch from fda2f70 to 61b8eb8 Compare April 7, 2026 08:33
@VincentLanglet VincentLanglet requested a review from staabm April 7, 2026 09:01
@VincentLanglet VincentLanglet merged commit 41ff9e0 into phpstan:2.1.x Apr 7, 2026
653 of 655 checks passed
@VincentLanglet VincentLanglet deleted the create-pull-request/patch-tgg99t8 branch April 7, 2026 09:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants