Repository navigation
Let a callable union member take the callable argument it accepts before it can flow into a naked template argument marker - #6809
Open
phpstan-bot wants to merge 1 commit into
Conversation
…ore it can flow into a naked template argument marker - TemplateArgumentObserver::observeLowerBound(): in a union parameter such as `(callable(TValue): bool)|TValue`, a marker-carrying callable member never changed the constraints (callables put markers in a contravariant position), so it never "took" the argument. The argument then became a lower bound of the naked receiver marker, and `$a->merge($b)->reject(isExcluded(...))` widened `TValue` to `Closure(Field): bool|Field`. A callable member now takes an argument member it is definitely a supertype of, like UnionType::inferTemplateTypes() does. - Ported the same check to turbo-ext/src/TemplateArgumentObserver.cpp. - Covered: function, instance-method and static-method first-class callables, closures, arrow functions, `(Closure(TValue): bool)|TValue` parameters, and invokable objects. - Probed and already correct: function-level templates (`@param (callable(T): bool)|T`), fresh `new Coll()` receivers, and merging a closure into the collection, which still correctly widens it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
With bleedingEdge (
unresolvedTemplateArguments), passing a first-class callable to a(callable(TValue): bool)|TValueparameter added the callable to the receiver'sTValue. This happened when the receiver came from a call that left a template argument unresolved, e.g.Coll<TValue|TMergeValue>frommerge(). So$a->merge($b)->reject(isExcluded(...))gaveColl<(Closure(Field): bool)|Field>instead ofColl<Field>. This is the shape of Laravel'sCollection::merge()->reject().Changes
src/Analyser/Generics/TemplateArgumentObserver.php: in the union branch ofobserveLowerBound(), a member that carries markers and is a callable now takes an argument member it is definitely a supertype of. That argument member is then no longer sent as a lower bound to the naked markers next to it.turbo-ext/src/TemplateArgumentObserver.cpp: the same change in the native mirror.(Closure(TValue): bool)|TValueparameters; invokable objects.@template T,@param (callable(T): bool)|T, in either argument order), freshnew Coll()receivers, andmerge([closure]), which still correctly widens the element type.Root cause
observeLowerBound()mirrorsUnionType::inferTemplateTypes(): an argument member that a sibling member takes does not flow into a naked marker. A member "took" an argument only if it was marker-free and a supertype of it, or if recursing into it added constraints. Callable members return early, because markers inside a callable signature are in a contravariant position and are never lower bounds. So the recursion never changed anything, the callable member never took the closure, and the closure became a lower bound of the nakedTValuemarker. The marker's merge site then resolved toField|Closure(Field): bool. Now a callable member takes the arguments it accepts, as the legacyisSuperTypeOf()->yes()absorption does.Test
tests/PHPStan/Analyser/nsrt/bug-15414.phpcontains the issue's reproducer verbatim, plus method, static-method, closure,Closure-typed-parameter and invokable-object variants, and a fresh-receiver case. Without the fix, 5 of the assertions fail (the function, method and static-method first-class callables, theClosure-typed parameter, and the invokable object); with it, all pass, with and without the turbo extension loaded.Fixes phpstan/phpstan#15414
🤖 Generated with Claude Code