Repository navigation
Resolve an inferred template argument to its bound when a lower bound refers back to a site still being resolved - #6815
Open
phpstan-bot wants to merge 1 commit into
Conversation
… refers back to a site still being resolved - TemplateArgumentSolver: count when substituting a marker reaches a site that is still being resolved (a cycle), and when that happens while substituting a lower bound, resolve the site to the template's bound. `T :> Foo<T>` has no finite solution other than the bound; previously the cycle was broken with the delegate, inferring e.g. `WeakAnalysingMap<WeakAnalysingMap<object>>` for `$map = new WeakAnalysingMap(); $map->set($map);` and then reporting the call. - The counter is restored after each site's resolution, so a cycle settled inside another site (indirect cycle `$a->set($b); $b->set($a);`) does not widen the outer site too. - Also covers the analogous shapes: indirect cycles across two objects, the object wrapped in a list/array argument, a cycle after another lower bound, a constructor-inferred initial type, and contravariant templates. - TemplateArgumentSolver is not shadowed by turbo (only instantiated from C++), so no native mirror change is needed.
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
Since 2.3.0, template arguments of
new Foo()are inferred from how the object is used later in the function body. When the object is passed into its own template argument ($map = new WeakAnalysingMap(); $map->set($map);with@param TOwner $owner), the solver producedWeakAnalysingMap<WeakAnalysingMap<object>>, and the call was then reported as not accepting the argument (invariant template). The site now resolves to the template's bound (WeakAnalysingMap<object>), as it did before 2.3.0.Changes
src/Analyser/Generics/TemplateArgumentSolver.php:resolveKey()counts every time a substitution reaches a site that is still being resolved, and restores the count when a site's resolution finishes. That way a cycle that another site already handled does not count for the outer site.resolveObservation()checks whether substituting a lower bound reached such a cycle. If it did, the site resolves to$template->getBound().$a->set($b); $b->set($a);)$b->add(1); $b->add($b);)list<T>parameter ($b->addAll([$b])) or in an array literal ($b->add([$b]))new Box(1); $b->add($b))$s->put($s))$b->add($c); $c->add(1);) still infersBox<Box<1>>.TemplateArgumentSolveris only referenced (instantiated) from turbo-ext, not shadowed, so no C++ change is needed.Root cause
A lower bound such as
T :> WeakAnalysingMap<T>has no finite least solution. The solver's cycle guard inresolveKey()broke the recursion by returning the marker's delegate (object). That made the result one level of nesting too deep,WeakAnalysingMap<object>, so the variable's type becameWeakAnalysingMap<WeakAnalysingMap<object>>, which the invariant parameter rejects. The only type that satisfies such a cyclic lower bound is the template's bound, so the solver now uses the bound when the cycle comes through a lower bound.Test
tests/PHPStan/Rules/Methods/data/bug-15434.php+CallMethodsRuleTest::testBug15434: the reproducer from the issue plus the analogous shapes listed above; no errors are expected.tests/PHPStan/Analyser/nsrt/bug-15434.php:assertType()checks for the reproducer, the indirect cycle, a cycle after another lower bound, the contravariant template, and a non-cyclic chain that must stay precise.Both tests fail without the fix.
Fixes phpstan/phpstan#15434