Skip to content

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

Merged
staabm merged 3 commits into
phpstan:2.3.xfrom
pawell67:fix-static-generic-concrete-arguments
Oct 23, 2026
Merged

staabm merged 3 commits into
phpstan:2.3.xfrom
pawell67:fix-static-generic-concrete-arguments

Conversation

@pawell67

@pawell67 pawell67 commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

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.

The turbo-ext mirror of CalledOnTypeUnresolvedMethodPrototypeReflection is ported in the same change, and the new nsrt files pass with and without the extension loaded.
Closes phpstan/phpstan#12680

🤖 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
Contributor 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
Contributor 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
Contributor 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 3 times, most recently from b5f2604 to d58614e Compare October 15, 2026 16:09
@pawell67
pawell67 force-pushed the fix-static-generic-concrete-arguments branch 2 times, most recently from b1ede1d to 9e52a3d Compare October 20, 2026 07:00
Comment on lines -1370 to -1375
# A reflection error, here a class that extends itself, still keeps the result cache from
# being saved.
cp loop/Loop.php src/Loop.php
OUTPUT=$(../bashunit assert exit_code "1" "../../bin/phpstan analyse -vvv --error-format raw")
echo "$OUTPUT"
../bashunit assert contains 'Result cache was not saved because of non-ignorable exception: Reflection error: Circular reference' "$OUTPUT"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to adjust this tests expectations?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not part of the fix: it comes from the last commit, a cherry-pick of #6729 (author kept), because the result-cache-parse-error E2E step fails on 2.3.x since the BetterReflection update, independently of this PR. It drops out of the diff once #6729 is merged and this branch is rebased, so please review it there.

Comment thread tests/PHPStan/Analyser/nsrt/generic-static-concrete-arguments.php
Comment thread e2e/result-cache-parse-error/loop/Loop.php
@pawell67

Copy link
Copy Markdown
Contributor Author

@staabm fixed. Removed not related changes

@staabm

staabm commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

@pawell67 please fix

1) PHPStan\Build\RequiredPhpVersionCommentTest::testFixtureHasRequiredLintComment@/home/runner/work/phpstan-src/phpstan-src/tests/PHPStan/Build/../Rules/Functions/data/bug-11507.php with data ('/home/runner/work/phpstan-src...07.php')
Fixture uses promoted properties on line 14 which requires PHP 8.0. Add a `<?php // lint >= 8.0` comment on the first line so the fixture is skipped on older PHP versions in CI.
Failed asserting that 0 is equal to 80000 or is greater than 80000.

//cc @SanderMuller please review

@staabm
staabm requested a balanced review from Copilot October 23, 2026 10:32

This comment was marked as outdated.

@SanderMuller SanderMuller left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked 2846823 against 2.3.x (5fb9346).

  • The example from the description gives array<int, string> with this PR, and the base gives array<TKey of (int|string), TValue>. I also probed a subclass that binds the templates, a generic subclass, a union receiver and new Box(). Two more probes used a nested static<int, Box<int, TValue>> and a template method that returns static<TKey|int, TValue|TOther>. Every result that changes is now the resolved type, and nothing changes for the worse. For GSub<float>, $g->concrete() is GSub<string> on both sides. But ->all() on it gave array<int, float> on the base, and it now gives array<int, string>.
  • Both new nsrt files fail with the change in CalledOnTypeUnresolvedMethodPrototypeReflection.php reverted. bug-12680.php matches Ondřej's playground snippet, and the reporter's snippet in phpstan/phpstan#12680 now gives mixed for both lines instead of TValue.
  • Turbo: I built the extension from this head (macOS arm64, PHP 8.5). smoke.php and signature-parity.php pass, and the two nsrt files pass with the extension loaded. With the extension active and the PHP change reverted, my probe still gives the fixed types, so the C++ port carries the fix by itself. I did not build it for other platforms or PHP versions.
  • The full paratest suite passes with and without the extension (22615 tests). make phpstan is clean, and so is phpcs on the changed source file.
  • Performance: cold self-analysis without the extension, 3 interleaved rounds. The median user CPU is 120.9 s on the base and 121.2 s with this PR. The runs took 119.7 to 128.9 s and 119.0 to 130.2 s, so there is no measurable difference.
  • CI: the red jobs also fail on #6782, #6783 and #6785. The Turbo compile jobs fail in zend_globals_macros.h ('ZEND_API' does not name a type), and d78c82f on 2.3.x targets that. Its CI was still running when I checked. result-cache-parse-error fails on every current 2.3.x PR. 87bbf77 on 2.3.x adds the same lint comment to bug-11507.php, so after a rebase that line drops out of the diff.

Two small things in the description. It still says the C++ mirror is not ported and the suite did not run with the extension, but c6d2009 ports it. And phpstan/phpstan#12680 is still open, so a Closes line could go in if this is the fix for it.

pawell67 and others added 3 commits October 23, 2026 13:14
…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>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@pawell67
pawell67 force-pushed the fix-static-generic-concrete-arguments branch from 2846823 to 2c8d328 Compare October 23, 2026 11:15
@staabm
staabm merged commit 1cb6abf into phpstan:2.3.x Oct 23, 2026
989 of 991 checks passed
@staabm

staabm commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

thank you!

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.

Generic type resolution issue with method chaining when static return type breaks global generic parameter

4 participants