Skip to content

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
phpstan:2.3.xfrom
phpstan-bot:create-pull-request/patch-eytidcw
Open

phpstan-bot wants to merge 1 commit into
phpstan:2.3.xfrom
phpstan-bot:create-pull-request/patch-eytidcw

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

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 produced WeakAnalysingMap<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().
  • Analogous cases probed; all were broken before and work now (covered by tests):
    • indirect cycle across two objects ($a->set($b); $b->set($a);)
    • self-send after another lower bound ($b->add(1); $b->add($b);)
    • object wrapped in a list<T> parameter ($b->addAll([$b])) or in an array literal ($b->add([$b]))
    • initial type inferred from the constructor (new Box(1); $b->add($b))
    • contravariant template ($s->put($s))
  • A non-cyclic chain ($b->add($c); $c->add(1);) still infers Box<Box<1>>.
  • TemplateArgumentSolver is 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 in resolveKey() 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 became WeakAnalysingMap<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

… 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.
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.

object template does not accept exact object type since v2.3.0

1 participant