Skip to content

fix(static-calls): self::, static:: and parent:: return the static type of the calling class - #12643

Merged
orklah merged 5 commits into
vimeo:masterfrom
jirkace:fix/7797-parent-call-returns-called-static
Nov 8, 2026
Merged

orklah merged 5 commits into
vimeo:masterfrom
jirkace:fix/7797-parent-call-returns-called-static

Conversation

@jirkace

@jirkace jirkace commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

parent::method() of a method returning static was typed as the parent class, not as static of the calling class.

Before:

class Child extends Core {
    public function parentInstanceReturn(): static {
        return parent::instance(); // LessSpecificReturnStatement: The type 'Core&static' is more general than the declared return type 'Child&static'
    }
}

After: no issue, parent::instance() is Child&static.

Fixes #7797

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ktu9y737NuDDE88xCYTSix

…pe of the calling class vimeo#7797

`parent::make()` and `parent::instance()` of a method returning `static` were
`Core&static` in a non-final child, so `return parent::instance();` from a method
declared `static` was LessSpecificReturnStatement. The calling class is now passed
to the type expander as an object type, like a method call on $this does.

Fixes vimeo#7797

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ktu9y737NuDDE88xCYTSix
@jirkace

jirkace commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

Behaviour change: in a non-final class, parent::m(), self::m() and static::m() of a method declared to return static (or $this) are now Child&static instead of the declaring class (Core&static), so return parent::m(); from a method declared static no longer raises LessSpecificReturnStatement/MoreSpecificReturnType. In a final class the result is still the class itself, and calls through another class name (Other::make()) are unchanged. The related #11139 is not fixed by this: there static is inherited from a docblock @return $this/static of an interface and is already resolved to the implementing class. No existing test expectation was changed.

…l-returns-called-static

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

✅ Performance: unchanged · Issues: decreased

Project Memory (MB) Wall (s) Issues Weighted
✅ laravel 1 821 87.4 $\color{gray}{\textsf{+0.1\%}}$ 25 915 $\color{red}{\textsf{+2}}$ $\color{green}{\textsf{-4}}$ 51 153 $\color{green}{\textsf{-6}}$
✅ symfony 4 002 139.4 $\color{gray}{\textsf{+3.5\%}}$ 51 065 $\color{gray}{\textsf{+0}}$ $\color{green}{\textsf{-16}}$ 101 056 $\color{green}{\textsf{-48}}$
✅ doctrine-orm 428 14.2 $\color{gray}{\textsf{-3.8\%}}$ 3 505 $\color{gray}{\textsf{+0}}$ $\color{green}{\textsf{-2}}$ 5 574 $\color{green}{\textsf{-6}}$
✅ psl 651 17.7 $\color{gray}{\textsf{-1.9\%}}$ 1 424 5 514
✅ phpunit 506 16.2 $\color{gray}{\textsf{+0.6\%}}$ 1 627 1 265
✅ php-parser 245 15.3 $\color{gray}{\textsf{+0.6\%}}$ 17 169 20 624
✅ tempest 922 30.7 $\color{gray}{\textsf{-0.7\%}}$ 5 016 9 616
✅ amphp-amp 60 1.7 $\color{gray}{\textsf{-1.2\%}}$ 32 79
✅ webmozarts-assert 272 3.7 $\color{gray}{\textsf{-1.3\%}}$ 577 2 714
✅ guzzle-promises 55 1.7 $\color{gray}{\textsf{+3.6\%}}$ 163 407
✅ monolog 161 4.1 $\color{gray}{\textsf{-0.5\%}}$ 1 004 2 722
✅ drupal 5 196 192.4 $\color{gray}{\textsf{-0.6\%}}$ 98 830 161 675
Total 14 319 524.6 $\color{gray}{\textsf{+0.5\%}}$ 206 327 $\color{red}{\textsf{+2}}$ $\color{green}{\textsf{-22}}$ 362 399 $\color{green}{\textsf{-60}}$
How to read this table
  • Memory: Psalm's own peak figure, threads=1. More than +5 % fails, unless the PR changes composer.json.
  • Wall: base and head run on the same runner; ±10 % is noise. Above +20 % warns, never fails. re-run: a reversed second run was taken and the faster kept.
  • Issues: head count, then $\color{red}{\textsf{+added}}$ $\color{green}{\textsf{-removed}}$ against base.
  • Weighted: issues weighted by error level (levels 1–8 as is, always-errors 10, feature-specific and purity suggestions 0). The projects' own code is pinned and their dependencies resolve identically for base and head, so a change means Psalm reports more, fewer or differently serious issues. A rise is not necessarily a regression.
  • Total: sum over the projects compared on both sides. Thresholds apply per project, not to the total.
  • Colors: $\color{red}{\textsf{red}}$ past a fail or warn threshold (or more issues), $\color{green}{\textsf{green}}$ an improvement past noise (or fewer issues), $\color{gray}{\textsf{gray}}$ noise.
  • Performance: regressed if a head run crashed or timed out or a memory or wall delta is red, else improved if one is green, else unchanged. Its icon is the check result: ❌ fail · ⚠️ warn · ✅ pass; the icon next to each project is that project's result, 💥 = not comparable: base crashed or timed out (counts as ⚠️).
  • Issues (heading): the total weighted change, increased · decreased · unchanged. Informational, never fails the check.
✅ laravel: issues +2 −4

Memory 1 821 → 1 821 MB (<0.1 %) · Wall 87.3 → 87.4 s (+0.1 %)

Weighted 51 159 → 51 153 (−6, −0.01 %): L3 −2

Issue type added removed
LessSpecificReturnStatement 0 1
MixedReturnTypeCoercion 2 2
MoreSpecificReturnType 0 1

Added (2)

  • src/Illuminate/Collections/LazyCollection.php:1400 MixedReturnTypeCoercion (error): The declared return type 'Illuminate\Support\LazyCollection<int, Illuminate\Support\LazyCollection&static>&static|(Illuminate\Support\LazyCollection<int, Illuminate\Support\LazyCollection<int, TValue:Illuminate\Supp…
  • src/Illuminate/Collections/LazyCollection.php:1405 MixedReturnTypeCoercion (error): The type 'Illuminate\Support\LazyCollection<TKey:Illuminate\Support\LazyCollection as array-key, TValue:Illuminate\Support\LazyCollection as mixed>&static' is more general than the declared return type 'Illuminate\S…

Removed (4)

  • src/Illuminate/Collections/LazyCollection.php:1400 MixedReturnTypeCoercion (error): The declared return type 'Illuminate\Support\LazyCollection<int, Illuminate\Support\LazyCollection&static>&static|(Illuminate\Support\LazyCollection<int, Illuminate\Support\LazyCollection<int, TValue:Illuminate\Supp…
  • src/Illuminate/Collections/LazyCollection.php:1405 MixedReturnTypeCoercion (error): The type 'Illuminate\Support\LazyCollection&static' is more general than the declared return type 'Illuminate\Support\LazyCollection<int, Illuminate\Support\LazyCollection&static>&static|(Illuminate\Support\LazyColl…
  • src/Illuminate/Http/Request.php:565 MoreSpecificReturnType (error): The declared return type 'Illuminate\Http\Request&static' for Illuminate\Http\Request::duplicate is more specific than the inferred return type 'Symfony\Component\HttpFoundation\Request&static'
  • src/Illuminate/Http/Request.php:570 LessSpecificReturnStatement (error): The type 'Symfony\Component\HttpFoundation\Request&static' is more general than the declared return type 'Illuminate\Http\Request&static' for Illuminate\Http\Request::duplicate
✅ symfony: issues +0 −16

Memory 4 002 → 4 002 MB (<0.1 %) · Wall 134.7 → 139.4 s (+3.5 %)

Weighted 101 104 → 101 056 (−48, −0.05 %): L3 −16

Issue type added removed
LessSpecificReturnStatement 0 8
MoreSpecificReturnType 0 8

Removed (16)

  • src/Symfony/Component/String/AbstractUnicodeString.php:485 MoreSpecificReturnType (error): The declared return type 'Symfony\Component\String\AbstractUnicodeString&static' for Symfony\Component\String\AbstractUnicodeString::trimPrefix is more specific than the inferred return type 'Symfony\Componen…
  • src/Symfony/Component/String/AbstractUnicodeString.php:488 LessSpecificReturnStatement (error): The type 'Symfony\Component\String\AbstractString&static' is more general than the declared return type 'Symfony\Component\String\AbstractUnicodeString&static' for Symfony\Component\String\AbstractUnicod…
  • src/Symfony/Component/String/AbstractUnicodeString.php:524 MoreSpecificReturnType (error): The declared return type 'Symfony\Component\String\AbstractUnicodeString&static' for Symfony\Component\String\AbstractUnicodeString::trimSuffix is more specific than the inferred return type 'Symfony\Componen…
  • src/Symfony/Component/String/AbstractUnicodeString.php:527 LessSpecificReturnStatement (error): The type 'Symfony\Component\String\AbstractString&static' is more general than the declared return type 'Symfony\Component\String\AbstractUnicodeString&static' for Symfony\Component\String\AbstractUnicod…
  • src/Symfony/Component/String/UnicodeString.php:204 MoreSpecificReturnType (error): The declared return type 'Symfony\Component\String\UnicodeString&static' for Symfony\Component\String\UnicodeString::join is more specific than the inferred return type 'Symfony\Component\String\AbstractUnicodeString…
  • src/Symfony/Component/String/UnicodeString.php:209 LessSpecificReturnStatement (error): The type 'Symfony\Component\String\AbstractUnicodeString&static' is more general than the declared return type 'Symfony\Component\String\UnicodeString&static' for Symfony\Component\String\UnicodeString::join
  • src/Symfony/Component/String/UnicodeString.php:288 MoreSpecificReturnType (error): The declared return type 'Symfony\Component\String\UnicodeString&static' for Symfony\Component\String\UnicodeString::replaceMatches is more specific than the inferred return type 'Symfony\Component\String\AbstractUni…
  • src/Symfony/Component/String/UnicodeString.php:293 LessSpecificReturnStatement (error): The type 'Symfony\Component\String\AbstractUnicodeString&static' is more general than the declared return type 'Symfony\Component\String\UnicodeString&static' for Symfony\Component\String\UnicodeString::replaceM…
  • src/Symfony/Component/String/UnicodeString.php:402 MoreSpecificReturnType (error): The declared return type 'Symfony\Component\String\UnicodeString&static' for Symfony\Component\String\UnicodeString::trimPrefix is more specific than the inferred return type 'Symfony\Component\String\AbstractUnicode…
  • src/Symfony/Component/String/UnicodeString.php:428 LessSpecificReturnStatement (error): The type 'Symfony\Component\String\AbstractUnicodeString&static|Symfony\Component\String\UnicodeString&static' is more general than the declared return type 'Symfony\Component\String\UnicodeString&static' for Sy…
  • … and 6 more
✅ doctrine-orm: issues +0 −2

Memory 428 → 428 MB (<0.1 %) · Wall 14.8 → 14.2 s (−3.8 %)

Weighted 5 580 → 5 574 (−6, −0.11 %): L3 −2

Issue type added removed
LessSpecificReturnStatement 0 1
MoreSpecificReturnType 0 1

Removed (2)

  • src/Mapping/ToOneInverseSideMapping.php:35 MoreSpecificReturnType (error): The declared return type 'Doctrine\ORM\Mapping\ToOneInverseSideMapping&static' for Doctrine\ORM\Mapping\ToOneInverseSideMapping::fromMappingArrayAndName is more specific than the inferred return type 'Doctrine\ORM\Mapping\As…
  • src/Mapping/ToOneInverseSideMapping.php:50 LessSpecificReturnStatement (error): The type 'Doctrine\ORM\Mapping\AssociationMapping&static' is more general than the declared return type 'Doctrine\ORM\Mapping\ToOneInverseSideMapping&static' for Doctrine\ORM\Mapping\ToOneInverseSideMapping::fromMapping…

head a4c737d · base b0cb649 (merge-base) · workflow run

…calling class vimeo#7797

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

jirkace commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Behaviour change (addition)

  • CI after merging master failed on master's ClosureTest::FirstClassCallable:StaticMethod:ClassTemplatesAndStatic: for static::make(...) and self::make(...) inside class Ints extends Collection it expected Closure():(Collection&static). With this PR the static of a self::, static:: or parent:: call is the calling class, so the closure is Closure():(Ints&static); the two expectations are updated in the new commit.
  • Verified: that case passes. Not run: the full test suite (CI), Infection.

@orklah

orklah commented Nov 8, 2026

Copy link
Copy Markdown
Collaborator

The fix looks correct for the original issue, but it introduces a generic type regression.

Using new TNamedObject($context->self) causes TypeExpander::expandNamedObject() to discard generic parameters when resolving static<TKey, TValue>.

This explains the 8 new Laravel diagnostics in sliding(), map(), mapWithKeys() and partition(), which appear to be false positives.

Could you preserve the generic type arguments and add an exact-type regression test?

…nts of a static<...> return type vimeo#7797

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

jirkace commented Nov 8, 2026

Copy link
Copy Markdown
Contributor Author

@orklah you were right: new TNamedObject($context->self) as the static type made TypeExpander::expandNamedObject() replace a static<TKey, TValue> return type by the bare class. Pushed b0e471e: getStaticClassType() now builds the type of $this for the calling class with ClassAnalyzer::getThisObjectType() (made public; it already existed), so a generic class keeps its own template arguments and static<TMakeKey, TMakeValue> keeps its type arguments.

Behaviour change

  • self::make(["a" => 1]), static::make(...) and parent::make(...) of a method returning static<TMakeKey, TMakeValue> give Collection<'a', 1>&static (SubCollection<'a', 1>&static for parent::); on the previous head they gave Collection&static. New exact-type case: selfStaticAndParentCallsKeepTheTypeArgumentsOfAStaticReturnType (fails on the previous head with Collection&static, passes now).
  • Laravel (perf corpus pin, perf config, --threads=1), this branch vs the PR's base 07336077059b: the 8 added diagnostics from the perf report (sliding(), map(), mapWithKeys(), partition(), Request::duplicate) are gone. The only difference left is the two MixedReturnTypeCoercion in LazyCollection.php, which master reports too; their text now shows LazyCollection<TKey, TValue>&static instead of LazyCollection&static.
  • Also run as single cases: the PR's two earlier MethodCallTest cases and the ClosureTest case it edits pass; phpcs rc 0; full self-analysis "No errors found".
  • Not run: the other perf projects (only laravel was named in the report), and the rest of the test files (left to CI).

# Conflicts:
#	tests/MethodCallTest.php
@orklah orklah added the release:fix The PR will be included in 'Fixes' section of the release notes label Nov 8, 2026
@orklah
orklah merged commit 8a88460 into vimeo:master Nov 8, 2026
73 of 74 checks passed
@orklah

orklah commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

Thanks! I closed the two other as duplicate but I may have skipped some value, please reopen one if needed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:fix The PR will be included in 'Fixes' section of the release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Why showing warnings MoreSpecificReturnType and LessSpecificReturnStatement

2 participants