Skip to content

Resolve template types of method calls on a concrete generic static return type - #6695

Open
pawell67 wants to merge 4 commits into
phpstan:2.3.xfrom
pawell67:fix-static-generic-concrete-arguments
Open

pawell67 wants to merge 4 commits into
phpstan:2.3.xfrom
pawell67:fix-static-generic-concrete-arguments

Conversation

@pawell67

@pawell67 pawell67 commented Oct 7, 2026

Copy link
Copy Markdown

A static<...> return type whose arguments reference no template type is not resolved on a generic receiver:

/**
 * @template TKey of array-key
 * @template TValue
 */
class Box
{
    /** @return static<int, string> */
    public function concrete() { return new static(); }

    /** @return static<int, TValue> */
    public function withTemplate() { return new static(); }

    /** @return array<TKey, TValue> */
    public function all(): array { return []; }
}

/** @param Box<string, bool> $box */
function test(Box $box): void {
    \PHPStan\dumpType($box->concrete()->all());     // array<TKey of (int|string), TValue>   (expected array<int, string>)
    \PHPStan\dumpType($box->withTemplate()->all()); // array<int, bool>
}

CalledOnTypeUnresolvedMethodPrototypeReflection::transformStaticType() turns the GenericStaticType into getStaticObjectType(), which hands the declaring class' ClassReflection (without the type arguments) to the GenericObjectType. When an argument contains a template, traverse() recreates the object and the reflection is resolved lazily, which is why only the fully concrete case breaks. I reproduced this on 2.2.14 and 2.3.0, so it is not a regression.

It is visible in Laravel's Collection (@return static<int, mixed> on flatten(), collapse(), values(), …): $collection->flatten()->merge($x) leaks TKey/TValue, which Larastan has to baseline in its monica E2E.

The fix builds the object without the stale reflection in transformStaticType() only. I first changed GenericStaticType::getStaticObjectType(), but that also changes new static(...) (NewHandler) and drops the expected error of ReturnTypeRuleTest::testBug4590, so I narrowed it to the method return type resolution.

Caveats:

  • CalledOnTypeUnresolvedMethodPrototypeReflection is shadowed by turbo-ext. I have not ported the change to the .cpp mirror or run the suite with the extension loaded, since I can't build and verify it properly here. Happy to follow up if you prefer that in this PR.
  • Full test suite (paratest, 22505 tests) passes without the extension.

🤖 Generated with Claude Code

@pawell67
pawell67 force-pushed the fix-static-generic-concrete-arguments branch from 7ac51a6 to c062fdd Compare October 7, 2026 13:29
@pawell67
pawell67 force-pushed the fix-static-generic-concrete-arguments branch from 619e135 to babb3d5 Compare October 7, 2026 13:42
@pawell67

pawell67 commented Oct 7, 2026

Copy link
Copy Markdown
Author

The last commit is a cherry-pick of #6697 (author kept): ResultCacheInfoCommandTest::testWarmAnalysisDoesNotRewriteUnchangedResultCache fails in the ubuntu "Integration tests" job regardless of this change, so it is included here only to get a green run. It drops out of the diff once #6697 is merged and this branch is rebased.

@pawell67
pawell67 force-pushed the fix-static-generic-concrete-arguments branch from 35cedda to fd0daee Compare October 12, 2026 09:33
@pawell67

Copy link
Copy Markdown
Author

Rebased on the current 2.3.x (the #6697 cherry-pick is gone, it landed upstream) and bumped the expected turbo version for the ported commit.

The last commit is a cherry-pick of #6729 (author kept): the result-cache-parse-error E2E step fails on 2.3.x since the BetterReflection update, independently of this change. It is here only to get a green run and drops out once #6729 is merged and this branch is rebased.

@pawell67

Copy link
Copy Markdown
Author

@staabm would you mind taking a look? With phpstan/phpstan#15468 merged, CI here is fully green now. The fix is in CalledOnTypeUnresolvedMethodPrototypeReflection::transformStaticType() plus its turbo-ext mirror: a fully concrete static<int, string> return type kept the declaring class reflection, so calls on the result saw unresolved TKey/TValue. The last commit is the cherry-pick of #6729 and drops out once that is merged.

@pawell67
pawell67 force-pushed the fix-static-generic-concrete-arguments branch from eedada2 to bbbe74c Compare October 14, 2026 12:27
pawell67 and others added 4 commits October 14, 2026 20:31
…eturn type

A return type like `static<int, string>` (no template type in its arguments) was
turned into a GenericObjectType that kept the ClassReflection of the declaring class,
so methods called on the result saw unresolved TKey/TValue (`array<TKey, TValue>`).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Mirrors CalledOnTypeUnresolvedMethodPrototypeReflection::transformStaticType()
from the previous commit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rule

Since the BetterReflection update in 613b1b1, a class that extends
itself is no longer a reflection error, so the last part of the
result-cache-parse-error e2e test failed. A rule that throws
ClassNotFoundException now tests the same branch of the save, in the
result-cache-internal-error fixture.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@pawell67
pawell67 force-pushed the fix-static-generic-concrete-arguments branch from bbbe74c to b5f2604 Compare October 14, 2026 18:32
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.

2 participants