Skip to content

fix(static): a static return called through parent:: or self:: is the calling class - #13401

Closed
jirkace wants to merge 2 commits into
vimeo:masterfrom
jirkace:fix/7847-parent-call-static-return
Closed

jirkace wants to merge 2 commits into
vimeo:masterfrom
jirkace:fix/7847-parent-call-static-return

Conversation

@jirkace

@jirkace jirkace commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

A static return type of a method called through parent:: or self:: is the declaring class instead of the class of the calling method, which causes false LessSpecificReturnStatement / MoreSpecificReturnType.

Before

<?php
abstract class BaseModel
{
    public function setId(?string $value): static
    {
        return $this;
    }
}

class ConcreteModel extends BaseModel
{
    public function setId(?string $value): static
    {
        return parent::setId($value); // LessSpecificReturnStatement: 'BaseModel&static'
    }
}

After

        return parent::setId($value); // no issue, ConcreteModel&static

Refs #7847

@jirkace

jirkace commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

Behaviour change

  • TypeExpander::expandNamedObject() resolved a native/docblock static return type (stored as the declaring class with is_static, e.g. BaseModel&static) against the called class only when that class was known as a type object, or when it was final. For parent::method() / self::method() the called class is a plain string (the calling class) and it is not final, so the type stayed BaseModel&static. A new branch sets such a return type to the calling class (ConcreteModel&static) when that class extends the declaring one.
  • Visible effects: no LessSpecificReturnStatement / MoreSpecificReturnType for return parent::setId($value); in a method declared to return static (the issue's second snippet), also for parent::make() / self::make() in static methods. Unchanged (checked on master and here): $this->setId(), ConcreteModel::make() and (new ConcreteModel())->setId() from outside, final classes, and a return type that doesn't extend the declaring class.
  • Verified: the two new valid MethodCallTest cases fail on master (LessSpecificReturnStatement) and pass here, the invalid guard (a wrong return type is still InvalidReturnStatement) passes on both; 20 static-related cases of MethodCallTest/ReturnTypeTest and ten of ClosureTest/ClassTest pass; phpcs and full self-analysis are clean. Not run: the rest of the test suite and the real-project checks.
  • Not changed: the first snippet of the issue (MethodSignatureMismatch for static against an interface) no longer reports anything on master, so nothing to fix there; hence Refs #7847.

@github-actions

github-actions Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

✅ Performance: unchanged · Issues: decreased

Project Memory (MB) Wall (s) Issues Weighted
✅ laravel 1 821 94.4 $\color{gray}{\textsf{-2.1\%}}$ 25 916 $\color{red}{\textsf{+1}}$ $\color{green}{\textsf{-2}}$ 51 156 $\color{green}{\textsf{-3}}$
✅ symfony 4 002 153.2 $\color{gray}{\textsf{+1.4\%}}$ 51 065 $\color{gray}{\textsf{+0}}$ $\color{green}{\textsf{-16}}$ 101 056 $\color{green}{\textsf{-48}}$
✅ doctrine-orm 428 17.8 $\color{gray}{\textsf{+0.6\%}}$ 3 505 $\color{gray}{\textsf{+0}}$ $\color{green}{\textsf{-2}}$ 5 574 $\color{green}{\textsf{-6}}$
✅ psl 651 22.2 $\color{gray}{\textsf{-0.4\%}}$ 1 424 5 514
✅ phpunit 506 10.6 $\color{gray}{\textsf{-1.1\%}}$ 1 627 1 265
✅ php-parser 245 9.6 $\color{gray}{\textsf{-1.3\%}}$ 17 169 20 624
✅ tempest 922 20.6 $\color{gray}{\textsf{-1.2\%}}$ 5 016 9 616
✅ amphp-amp 60 2.1 $\color{gray}{\textsf{-0.5\%}}$ 32 79
✅ webmozarts-assert 272 4.8 $\color{gray}{\textsf{-1.2\%}}$ 577 2 714
✅ guzzle-promises 55 2.1 $\color{gray}{\textsf{+1.0\%}}$ 163 407
✅ monolog 161 5.3 $\color{gray}{\textsf{-0.6\%}}$ 1 004 2 722
✅ drupal 5 196 136.2 $\color{gray}{\textsf{+7.9\%}}$ 98 829 $\color{gray}{\textsf{+0}}$ $\color{green}{\textsf{-1}}$ 161 671 $\color{green}{\textsf{-4}}$
Total 14 319 479.0 $\color{gray}{\textsf{+2.0\%}}$ 206 327 $\color{red}{\textsf{+1}}$ $\color{green}{\textsf{-21}}$ 362 398 $\color{green}{\textsf{-61}}$
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 +1 −2

Memory 1 821 → 1 821 MB (<0.1 %) · Wall 96.4 → 94.4 s (−2.1 %)

Weighted 51 159 → 51 156 (−3, −0.01 %): L3 −1

Issue type added removed
LessSpecificReturnStatement 0 1
MoreSpecificReturnType 1 1

Added (1)

  • src/Illuminate/Database/Eloquent/HasCollection.php:24 MoreSpecificReturnType (error): The declared return type 'Illuminate\Database\Eloquent\Collection<array-key, Illuminate\Notifications\DatabaseNotification&static>' for Illuminate\Database\Eloquent\HasCollection::newCollection is more specific th…

Removed (2)

  • 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 151.0 → 153.2 s (+1.4 %)

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 17.7 → 17.8 s (+0.6 %)

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…
✅ drupal: issues +0 −1

Memory 5 196 → 5 196 MB (<0.1 %) · Wall 126.2 → 136.2 s (+7.9 %)

Weighted 161 675 → 161 671 (−4, <0.01 %): L4 −1

Issue type added removed
ImplementedReturnTypeMismatch 0 1

Removed (1)

  • core/lib/Drupal/Core/Entity/Query/ConditionFundamentals.php:95 ImplementedReturnTypeMismatch (error): The inherited return type 'Drupal\Core\Entity\Query\ConditionInterface&static' for Drupal\Core\Entity\Query\ConditionInterface::setQuery is different to the implemented return type for Drupal\Core\…

head 1900d5d · base b0cb649 (merge-base) · workflow run

@jirkace

jirkace commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

Behaviour change (CI follow-up)

  • CI found one existing test whose expectation encoded the old behaviour, and that I had missed: ClosureTest FirstClassCallable:StaticMethod:ClassTemplatesAndStatic expected static::make(...) and self::make(...) inside Ints extends Collection<int> to be Closure():(Collection&static). With this change they are Closure():(Ints&static), the same rule as for parent::/self:: calls (the calling class), so I updated those two expectations. The rest of that test (Ints::make(...), FinalInts::make(...), parent::one(...)) is unchanged. Only this test failed in CI on all PHP versions.
  • Correction: my previous comment listed ten ClosureTest/ClassTest cases as passing; this one was not among them because the filter matched by name only.

@orklah

orklah commented Nov 8, 2026

Copy link
Copy Markdown
Collaborator

Can you rebase and check interactions with merged #12643

jirkace and others added 2 commits November 8, 2026 14:28
… calling class vimeo#7847

`parent::setId()` (and `self::make()`) in a child returned `BaseModel&static` instead of the class
of the calling method, as its `static` return type was left at the declaring class when the called
class is only known as a name.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…turn the child's static

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@jirkace
jirkace force-pushed the fix/7847-parent-call-static-return branch from 9317095 to 1900d5d Compare November 8, 2026 13:30
@jirkace

jirkace commented Nov 8, 2026

Copy link
Copy Markdown
Contributor Author

@orklah Rebased onto master (new head 1900d5d). The only conflict was in tests/MethodCallTest.php, where master added mysqliGetWarningsMayReturnFalse at the same spot; both entries are kept.

About #12643: it is not merged yet (still open). It fixes the same behaviour as this PR and #13374. Each of the three passes the other two's new test cases on its own, and this branch with #12643 merged on top passes all their cases plus ClosureTest FirstClassCallable:StaticMethod:ClassTemplatesAndStatic, which all three change the same way. So only one of them is needed. This one and #13374 only rewrite a plain TNamedObject return type, so they don't touch static<...> generics, which is where #12643 regressed.

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