Repository navigation
ParametersAcceptorSelector: keep the acceptor-level variadic flag separate from the per-parameter one - #6460
Merged
ondrejmirtes merged 1 commit intoSep 17, 2026
Conversation
…eparate from the per-parameter one * `combineAcceptors()` used a single `$isVariadic` variable both for the variadic flag of the combined variant and for the variadic flag of the parameter currently being merged. Merging a non-variadic acceptor reset the accumulated flag, so the combined variant was returned as non-variadic even though its parameter list still ended in a variadic parameter. The per-parameter flag now lives in its own `$isParameterVariadic` variable. * `combineAcceptors()` also dropped the types of the parameters behind the merged variadic parameter when truncating the parameter list. Their types (plus native and PHPDoc types) are now merged into the variadic parameter, which makes the combined signature independent of the order of the union members. * `IntersectionTypeMethodReflection::getMethodWithMostParameters()` ranked variants by parameter count only, so `IA&IB` and `IB&IA` produced different signatures when one of the methods was variadic. A variadic variant now always wins over a non-variadic one. * Probed and found already correct: named arguments and argument unpacking on union method calls, `Closure::fromCallable()` on unions, `array_map()` with a union first-class callable, and intersections of `callable` PHPDoc types (those collapse in `TypeCombinator`).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Calling a method on a union type where one member is variadic and the other is not (
A::foo(int $a, int ...$rest)vs.B::foo(int $a)) reported a self-contradictoryMethod A::foo() invoked with 3 parameters, 1-2 required.— the combined signature still ended inint ...$rest, but the combined variant itself was marked non-variadic.The same combined acceptor is used for every call form that goes through
ParametersAcceptorSelector::combineAcceptors(), so the false positive showed up for method calls, static calls, first-class callables, callable arrays,call_user_func()and__invoke()alike.Changes
src/Reflection/ParametersAcceptorSelector.phpcombineAcceptors(): the per-parameter variadic flag moved from the shared$isVariadicvariable into a new$isParameterVariadic, so merging a non-variadic acceptor no longer clears the accumulated acceptor-level flag.combineAcceptors(): when the merged parameter at positioniis variadic, the parameter list is truncated after it — the types of the dropped parameters (both the ones already collected and the remaining ones of the acceptor being merged) are now unioned into the variadic parameter's type, native type and PHPDoc type.src/Reflection/Type/IntersectionTypeMethodReflection.phpgetMethodWithMostParameters()now prefers a variadic variant over a non-variadic one, and only falls back to the parameter count when both have the same variadic-ness.Call forms verified to be fixed by the
combineAcceptors()change (each has a regression test that fails without it):$ab->foo(...))[$ab, 'foo']call_user_func([$ab, 'foo'], …)__invoke()on an intersection of object types (IntersectionType::getCallableParametersAcceptors())int &...$rest) on a unionProbed and found already correct, so no test was kept for them: named arguments and argument unpacking on union method calls,
Closure::fromCallable()on a union,array_map()with a union first-class callable, and intersections ofcallablePHPDoc types (TypeCombinatorcollapses those before they reachcombineAcceptors()).Root cause
combineAcceptors()builds oneExtendedFunctionVariantout of severalParametersAcceptors. Two different pieces of state were stored in the same$isVariadicvariable:$isVariadic = $isVariadic || $acceptor->isVariadic();accumulated the variadic flag of the combined variant, which is what ends up in the returnedExtendedFunctionVariant/ExtendedCallableFunctionVariant;$isVariadic = $parameters[$i]->isVariadic() || $parameter->isVariadic();described only the parameter at positioni.For
A|Bthe first assignment set the flag totrueforA::foo(int $a, int ...$rest), and mergingB::foo(int $a)immediately overwrote it withfalseat position0....$reststayed in the parameter list while the variant claimed not to be variadic, which is whyFunctionCallParametersCheckproduced the contradictory1-2 requiredmessage. The order of the union members did not matter;C::foo(int ...$rest)happened to work only because its variadic parameter sits at position0, where the overwrite producestrueby accident.The second defect in the same function is a different symptom of the same "combining a variadic with a non-variadic acceptor" pattern: once a merged parameter is variadic, everything behind it is thrown away with
array_slice(), and the discarded parameter types were lost. Combiningfoo(int ...$rest)withfoo(int $a, string $b, string $c)producedfoo(int ...)and reportedParameter #2 …$rest|a … expects int, string given, while the reverse union order produced a different — also wrong — result.The intersection counterpart lives in
IntersectionTypeMethodReflection, which picks a single representative method instead of combining them. Ranking purely by parameter count madeIA&IBandIB&IAbehave differently when one signature was variadic. A class that implements bothIA::foo(int ...$rest)andIB::foo(int $a)has to accept any number of arguments, so the variadic signature is the correct representative.Test
tests/PHPStan/Rules/Methods/data/bug-15251.phpholds the reproducer from the issue plus one function per analogous call form (static call, first-class callable, callable array,call_user_func(),__invoke()on a union and on an intersection, by-reference variadic, and the dropped-parameter-types case). It is analysed byCallMethodsRuleTest::testBug15251(),CallStaticMethodsRuleTest::testBug15251(),CallCallablesRuleTest::testBug15251()andCallUserFuncRuleTest::testBug15251(); without the fix these report 14arguments.count/argument.typeerrors between them, including the contradictoryinvoked with 0 parameters, 1-2 required.tests/PHPStan/Analyser/nsrt/bug-15251.phppins the combined signature of$ab->foo(...)withassertType(), including thatC|DandD|Cinfer the sameClosure(int|string ...): void.Fixes phpstan/phpstan#15251