Skip to content

Fix: static returned by a call on a non-static receiver stays late static - #13455

Open
alies-dev wants to merge 9 commits into
vimeo:masterfrom
alies-dev:alies-dev/resolve-static-of-non-static-receivers
Open

alies-dev wants to merge 9 commits into
vimeo:masterfrom
alies-dev:alies-dev/resolve-static-of-non-static-receivers

Conversation

@alies-dev

@alies-dev alies-dev commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

static returned by a method called on another object stays late static

Before

<?php
/** @template T */
class Bag {
    /** @return static<int> */
    public function map(): static { throw new RuntimeException(); }
}

/** @var Bag<string> $bag */
$bag = new Bag();
$mapped = $bag->map();  // Bag<int>&static
$a = box($bag->map());  // Box<Bag<int>&static|null>
<?php
class Bag {
    public function fluent(): static { return $this; }

    public function copyOf(self $other): static {
        return $other->fluent(); // no issue
    }
}
$map = $bag->map(...);  // Closure[impure]():Bag<int>&static

After

$mapped = $bag->map();  // Bag<int>
$a = box($bag->map());  // Box<Bag<int>|null>
    public function copyOf(self $other): static { // MoreSpecificReturnType
        return $other->fluent(); // LessSpecificReturnStatement
$map = $bag->map(...);  // Closure[impure]():Bag<int>

$this instanceof Child drops late static

Before

<?php
/** @template T */
class Bag {
    public function toChild(): static { // MoreSpecificReturnType
        if ($this instanceof StringBag) {
            return $this; // LessSpecificReturnStatement
        }
        return $this;
    }
}

/** @extends Bag<string> */
class StringBag extends Bag {}

After

    public function toChild(): static { // no issue
        if ($this instanceof StringBag) {
            return $this; // no issue

a&static bound to $this is bound again by the next call

Before

<?php
interface a {
    /** @return iter<int, a&static> */
    public function ret(): iter;
}

abstract class b implements a {
    public function test(): void {
        $a = $this->ret();
        $a = $a->stabilize(); // iter<int, a&b&static&iter<int, a&b&static>>
    }
}

After

        $a = $a->stabilize(); // iter<int, a&b&static>

The caller's own static is bound to the receiver of the call it is passed to

Before

<?php
final class Container {
    /**
     * @template T of object
     * @param class-string<T> $abstract
     * @return T
     */
    public function make(string $abstract): object { throw new RuntimeException(); }
}

class Component {
    public function make(Container $container): static {
        $made = $container->make(static::class); // Component&Container
        return $made;
    }
}

After

        $made = $container->make(static::class); // Component&static

$this in a template argument is bound to every later receiver, nesting the type

Before

<?php
class Foo {
    public function test(): void {
        $a = box($this)->same()->same()->same(); // Box<Foo&Box<Foo&Box<Foo&Box<Foo&Box<Foo&Box<Foo&Box<Foo&Box<Foo&static>>>>>>>>
    }
}

/** @template T */
final class Box {
    /** @return self<T> */
    public function same(): self { return $this; }
}

/**
 * @template T
 * @param T $value
 * @return Box<T>
 */
function box(mixed $value): Box { return new Box(); }

After

        $a = box($this)->same()->same()->same(); // Box<Foo&static>

The same on other call paths: the CallMap and an inherited @return static

Before

<?php
interface WithY {
    /** @return static */
    public function withY(int $y);
}

class Message implements WithY {
    public function withY(int $y): self { return clone $this; }
}

function test(Message $message, DateTime $date): void {
    $y = $message->withY(1);      // Message&static
    $time = $date->setTime(1, 2); // DateTime&static
}

After

    $y = $message->withY(1);      // Message
    $time = $date->setTime(1, 2); // DateTime

static becomes the receiver's class only where the type is covariant: a callable's parameters and invariant template arguments stay late static. Code returning another instance's static as its own static is now reported.

Follow-up to #13387.

Fixes #13441

…3387

`$bag->map()` returned `Bag<int>&static` though `$bag` isn't late
static, so `return $other->fluent()` passed as `static`.

- `$this instanceof Child` dropped late static; with the change that
  became a false LessSpecificReturnStatement on `$this->fluent()`
- an `a&static` part bound to `$this` was bound again by the next call

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 764ba67. Configure here.

@github-actions

github-actions Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

✅ Performance: unchanged · Issues: decreased

Project Memory (MB) Wall (s) Issues Weighted
✅ laravel 1 821 65.6 $\color{gray}{\textsf{-6.2\%}}$ 25 905 $\color{red}{\textsf{+71}}$ $\color{green}{\textsf{-78}}$ 51 117 $\color{green}{\textsf{-27}}$
✅ symfony 4 002 132.1 $\color{gray}{\textsf{-3.0\%}}$ 51 063 $\color{red}{\textsf{+11}}$ $\color{green}{\textsf{-11}}$ 101 050
✅ doctrine-orm 428 17.8 $\color{gray}{\textsf{+0.4\%}}$ 3 501 $\color{red}{\textsf{+1}}$ $\color{green}{\textsf{-5}}$ 5 562 $\color{green}{\textsf{-12}}$
✅ psl 651 22.2 $\color{gray}{\textsf{+0.3\%}}$ 1 424 5 514
✅ phpunit 506 12.8 $\color{gray}{\textsf{+0.2\%}}$ 1 627 1 265
✅ php-parser 245 11.4 $\color{gray}{\textsf{-0.2\%}}$ 17 169 20 624
✅ tempest 922 24.8 $\color{gray}{\textsf{+0.1\%}}$ 5 014 $\color{gray}{\textsf{+0}}$ $\color{green}{\textsf{-2}}$ 9 610 $\color{green}{\textsf{-6}}$
✅ amphp-amp 60 2.1 $\color{gray}{\textsf{-0.9\%}}$ 32 79
✅ webmozarts-assert 272 4.9 $\color{gray}{\textsf{+0.8\%}}$ 577 2 714
✅ guzzle-promises 55 2.1 $\color{gray}{\textsf{+0.5\%}}$ 163 407
✅ monolog 161 5.5 $\color{gray}{\textsf{+0.9\%}}$ 1 004 2 722
✅ drupal 5 196 119.4 $\color{gray}{\textsf{+0.2\%}}$ 98 821 $\color{red}{\textsf{+84}}$ $\color{green}{\textsf{-88}}$ 161 656 $\color{green}{\textsf{-12}}$
Total 14 319 420.8 $\color{gray}{\textsf{-1.8\%}}$ 206 300 $\color{red}{\textsf{+167}}$ $\color{green}{\textsf{-184}}$ 362 320 $\color{green}{\textsf{-57}}$
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 +71 −78

Memory 1 821 → 1 821 MB (<0.1 %) · Wall 69.9 → 65.6 s (−6.2 %)

Weighted 51 144 → 51 117 (−27, −0.05 %): L3 −5 · L6 −2

Issue type added removed
ArgumentTypeCoercion 0 6
ImplicitToStringCast 4 4
InvalidArgument 22 22
InvalidReturnStatement 6 7
InvalidReturnType 7 8
LessSpecificReturnStatement 1 0
MissingClosureReturnType 1 1
MissingReturnType 3 3
MixedArgument 5 5
MixedArgumentTypeCoercion 5 5
MixedPropertyTypeCoercion 1 1
MixedReturnTypeCoercion 10 10
MoreSpecificReturnType 1 1
PossiblyInvalidArgument 4 4
PossiblyInvalidPropertyAssignmentValue 1 1

Added (71)

  • src/Illuminate/Auth/EloquentUserProvider.php:134 MixedArgumentTypeCoercion (error): Argument 1 of Illuminate\Database\Eloquent\Builder::where expects Closure[impure](Illuminate\Database\Eloquent\Builder<Illuminate\Database\Eloquent\Model>&static):mixed|Illuminate\Contracts\Database\Query\Expression…
  • src/Illuminate/Collections/Collection.php:1567 InvalidArgument (error): Argument 1 of Illuminate\Support\Collection::newInstance expects (Illuminate\Contracts\Support\Arrayable<TKey:Illuminate\Support\Collection as array-key, TValue:Illuminate\Support\Collection as mixed>)|(iterable<TKey:Illuminate…
  • src/Illuminate/Collections/LazyCollection.php:1037 MoreSpecificReturnType (error): The declared return type '(Illuminate\Support\LazyCollection<int, TValue:Illuminate\Support\LazyCollection as mixed>&static)|(TValue:Illuminate\Support\LazyCollection as mixed)' for Illuminate\Support\LazyCollection:…
  • src/Illuminate/Collections/LazyCollection.php:1045 LessSpecificReturnStatement (error): The type '(Illuminate\Support\Collection<int, TValue:Illuminate\Support\LazyCollection as mixed>)|(Illuminate\Support\LazyCollection<'_0:int, '_1:TValue:Illuminate\Support\LazyCollection as mixed>&static)|(TValu…
  • src/Illuminate/Collections/LazyCollection.php:1045 MixedArgumentTypeCoercion (error): Argument 1 of Illuminate\Support\LazyCollection::__construct expects Closure[impure]():Generator<array-key, mixed, mixed, void>|Illuminate\Contracts\Support\Arrayable<array-key, mixed>|Illuminate\Support\LazyColle…
  • src/Illuminate/Database/Concerns/BuildsQueries.php:503 MixedArgument (error): Argument 1 of Illuminate\Database\Eloquent\Builder::where cannot be Illuminate\Database\Query\Expression<mixed>|mixed|string, expecting Closure[impure](Illuminate\Database\Eloquent\Builder<Illuminate\Database\Eloquent\Mod…
  • src/Illuminate/Database/Concerns/BuildsQueries.php:517 InvalidArgument (error): Argument 1 of Illuminate\Database\Eloquent\Builder::where expects Closure[impure](Illuminate\Database\Eloquent\Builder<Illuminate\Database\Eloquent\Model>&static):mixed|Illuminate\Contracts\Database\Query\Expression|arr…
  • src/Illuminate/Database/Concerns/BuildsQueries.php:529 InvalidArgument (error): Argument 1 of Illuminate\Database\Eloquent\Builder::orWhere expects Closure[impure](Illuminate\Database\Eloquent\Builder<Illuminate\Database\Eloquent\Model>&static):mixed|Illuminate\Contracts\Database\Query\Expression|a…
  • src/Illuminate/Database/DatabaseTransactionsManager.php:65 InvalidReturnType (error): The declared return type 'array<array-key, mixed>' for Illuminate\Database\DatabaseTransactionsManager::commit is incorrect, got 'Illuminate\Support\Collection<int, Illuminate\Database\DatabaseTransactionRecord>|a…
  • src/Illuminate/Database/DatabaseTransactionsManager.php:96 InvalidReturnStatement (error): The inferred type 'Illuminate\Support\Collection<int, Illuminate\Database\DatabaseTransactionRecord>' does not match the declared return type 'array<array-key, mixed>' for Illuminate\Database\DatabaseTransact…
  • … and 61 more

Removed (78)

  • src/Illuminate/Auth/EloquentUserProvider.php:134 MixedArgumentTypeCoercion (error): Argument 1 of Illuminate\Database\Eloquent\Builder::where expects Closure[impure](Illuminate\Database\Eloquent\Builder<Illuminate\Database\Eloquent\Model>):mixed|Illuminate\Contracts\Database\Query\Expression|array<…
  • src/Illuminate/Collections/Collection.php:1567 InvalidArgument (error): Argument 1 of Illuminate\Support\Collection::newInstance expects (Illuminate\Contracts\Support\Arrayable<TKey:Illuminate\Support\Collection as array-key, TValue:Illuminate\Support\Collection as mixed>)|(iterable<TKey:Illuminate…
  • src/Illuminate/Collections/LazyCollection.php:1037 InvalidReturnType (error): The declared return type '(Illuminate\Support\LazyCollection<int, TValue:Illuminate\Support\LazyCollection as mixed>&static)|(TValue:Illuminate\Support\LazyCollection as mixed)' for Illuminate\Support\LazyCollection::rand…
  • src/Illuminate/Collections/LazyCollection.php:1045 InvalidReturnStatement (error): The inferred type '(Illuminate\Support\Collection<int, TValue:Illuminate\Support\LazyCollection as mixed>&static)|(Illuminate\Support\LazyCollection<'_0:int, '_1:TValue:Illuminate\Support\LazyCollection as mixed>&sta…
  • src/Illuminate/Collections/LazyCollection.php:1045 MixedArgumentTypeCoercion (error): Argument 1 of Illuminate\Support\LazyCollection::__construct expects Closure[impure]():Generator<array-key, mixed, mixed, void>|Illuminate\Contracts\Support\Arrayable<array-key, mixed>|Illuminate\Support\LazyColle…
  • src/Illuminate/Database/Concerns/BuildsQueries.php:503 MixedArgument (error): Argument 1 of Illuminate\Database\Eloquent\Builder::where cannot be Illuminate\Database\Query\Expression<mixed>|mixed|string, expecting Closure[impure](Illuminate\Database\Eloquent\Builder<Illuminate\Database\Eloquent\Mod…
  • src/Illuminate/Database/Concerns/BuildsQueries.php:517 InvalidArgument (error): Argument 1 of Illuminate\Database\Eloquent\Builder::where expects Closure[impure](Illuminate\Database\Eloquent\Builder<Illuminate\Database\Eloquent\Model>):mixed|Illuminate\Contracts\Database\Query\Expression|array<arra…
  • src/Illuminate/Database/Concerns/BuildsQueries.php:529 InvalidArgument (error): Argument 1 of Illuminate\Database\Eloquent\Builder::orWhere expects Closure[impure](Illuminate\Database\Eloquent\Builder<Illuminate\Database\Eloquent\Model>):mixed|Illuminate\Contracts\Database\Query\Expression|array<ar…
  • src/Illuminate/Database/DatabaseTransactionsManager.php:65 InvalidReturnType (error): The declared return type 'array<array-key, mixed>' for Illuminate\Database\DatabaseTransactionsManager::commit is incorrect, got 'Illuminate\Support\Collection<int, Illuminate\Database\DatabaseTransactionRecord>&s…
  • src/Illuminate/Database/DatabaseTransactionsManager.php:96 InvalidReturnStatement (error): The inferred type 'Illuminate\Support\Collection<int, Illuminate\Database\DatabaseTransactionRecord>&static' does not match the declared return type 'array<array-key, mixed>' for Illuminate\Database\DatabaseT…
  • … and 68 more
✅ symfony: issues +11 −11

Memory 4 002 → 4 002 MB (<0.1 %) · Wall 136.1 → 132.1 s (−3.0 %)

Issue type added removed
ImplicitToStringCast 2 2
InvalidPropertyAssignmentValue 2 2
InvalidReturnStatement 2 2
InvalidReturnType 2 2
MixedArgument 1 1
MixedArgumentTypeCoercion 2 2

Added (11)

  • src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php:262 MixedArgumentTypeCoercion (error): Argument 1 of Symfony\Component\WebLink\GenericLinkProvider::__construct expects array<array-key, Psr\Link\LinkInterface>, but parent type array<array-key, Symfony\Component\WebLink\Link|mixed> …
  • src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php:272 MixedArgument (error): Argument 1 of Psr\Link\EvolvableLinkProviderInterface::withLink cannot be Symfony\Component\WebLink\Link|mixed, expecting Psr\Link\LinkInterface
  • src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php:158 InvalidReturnType (error): The declared return type 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition<Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition&static>|Symfony\Component\Config\Definitio…
  • src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php:172 InvalidReturnStatement (error): The inferred type 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition<Symfony\Component\Config\Definition\Builder\NodeBuilder<Symfony\Component\Config\Definition\Builder\ArrayNod…
  • src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php:172 InvalidPropertyAssignmentValue (error): $this->prototype with declared type 'Symfony\Component\Config\Definition\Builder\NodeDefinition<Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition<TParent:Symfony\Compone…
  • src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php:493 InvalidReturnType (error): The declared return type 'Symfony\Component\Config\Definition\Builder\NodeBuilder<Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition&static>' for Symfony\Component\Config\Definition\B…
  • src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php:499 InvalidReturnStatement (error): The inferred type 'Symfony\Component\Config\Definition\Builder\NodeBuilder<Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition<Symfony\Component\Config\Definition\Builder\NodePare…
  • src/Symfony/Component/Config/Definition/Builder/TreeBuilder.php:42 InvalidPropertyAssignmentValue (error): $this->root with declared type '(Symfony\Component\Config\Definition\Builder\NodeDefinition<Symfony\Component\Config\Definition\Builder\TreeBuilder<T:Symfony\Component\Config\Definition\Builde…
  • src/Symfony/Component/Console/Attribute/Argument.php:114 ImplicitToStringCast (error): Argument 2 of Symfony\Component\Console\Attribute\AskChoice::tryFrom expects string, but Symfony\Component\String\UnicodeString|non-falsy-string provided with a __toString method
  • src/Symfony/Component/Console/Attribute/Argument.php:114 ImplicitToStringCast (error): Argument 2 of Symfony\Component\Console\Attribute\Ask::tryFrom expects string, but Symfony\Component\String\UnicodeString|non-falsy-string provided with a __toString method
  • … and 1 more

Removed (11)

  • src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php:262 MixedArgumentTypeCoercion (error): Argument 1 of Symfony\Component\WebLink\GenericLinkProvider::__construct expects array<array-key, Psr\Link\LinkInterface>, but parent type array<array-key, Symfony\Component\WebLink\Link&static|…
  • src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php:272 MixedArgument (error): Argument 1 of Psr\Link\EvolvableLinkProviderInterface::withLink cannot be Symfony\Component\WebLink\Link&static|mixed, expecting Psr\Link\LinkInterface
  • src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php:158 InvalidReturnType (error): The declared return type 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition<Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition&static>|Symfony\Component\Config\Definitio…
  • src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php:172 InvalidReturnStatement (error): The inferred type 'Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition<Symfony\Component\Config\Definition\Builder\NodeBuilder<Symfony\Component\Config\Definition\Builder\ArrayNod…
  • src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php:172 InvalidPropertyAssignmentValue (error): $this->prototype with declared type 'Symfony\Component\Config\Definition\Builder\NodeDefinition<Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition<TParent:Symfony\Compone…
  • src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php:493 InvalidReturnType (error): The declared return type 'Symfony\Component\Config\Definition\Builder\NodeBuilder<Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition&static>' for Symfony\Component\Config\Definition\B…
  • src/Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition.php:499 InvalidReturnStatement (error): The inferred type 'Symfony\Component\Config\Definition\Builder\NodeBuilder<Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition<Symfony\Component\Config\Definition\Builder\NodePare…
  • src/Symfony/Component/Config/Definition/Builder/TreeBuilder.php:42 InvalidPropertyAssignmentValue (error): $this->root with declared type '(Symfony\Component\Config\Definition\Builder\NodeDefinition<Symfony\Component\Config\Definition\Builder\TreeBuilder<T:Symfony\Component\Config\Definition\Builde…
  • src/Symfony/Component/Console/Attribute/Argument.php:114 ImplicitToStringCast (error): Argument 2 of Symfony\Component\Console\Attribute\AskChoice::tryFrom expects string, but Symfony\Component\String\UnicodeString&static|non-falsy-string provided with a __toString method
  • src/Symfony/Component/Console/Attribute/Argument.php:114 ImplicitToStringCast (error): Argument 2 of Symfony\Component\Console\Attribute\Ask::tryFrom expects string, but Symfony\Component\String\UnicodeString&static|non-falsy-string provided with a __toString method
  • … and 1 more
✅ doctrine-orm: issues +1 −5

Memory 428 → 428 MB (<0.1 %) · Wall 17.8 → 17.8 s (+0.4 %)

Weighted 5 574 → 5 562 (−12, −0.22 %): L3 −4

Issue type added removed
LessSpecificReturnStatement 1 3
MoreSpecificReturnType 0 2

Added (1)

  • src/Mapping/AssociationMapping.php:158 LessSpecificReturnStatement (error): The type 'Doctrine\ORM\Mapping\AssociationMapping&Doctrine\ORM\Mapping\ManyToManyAssociationMapping|Doctrine\ORM\Mapping\AssociationMapping&static|Doctrine\ORM\Mapping\ManyToManyAssociationMapping&Doctrine\ORM\Mapping\Assoc…

Removed (5)

  • src/Mapping/AssociationMapping.php:158 LessSpecificReturnStatement (error): The type 'Doctrine\ORM\Mapping\AssociationMapping&Doctrine\ORM\Mapping\ManyToManyAssociationMapping|Doctrine\ORM\Mapping\AssociationMapping&static|Doctrine\ORM\Mapping\ManyToManyAssociationMapping&Doctrine\ORM\Mapping\Assoc…
  • src/Mapping/ToOneOwningSideMapping.php:105 MoreSpecificReturnType (error): The declared return type 'Doctrine\ORM\Mapping\ToOneOwningSideMapping&static' for Doctrine\ORM\Mapping\ToOneOwningSideMapping::fromMappingArrayAndName is more specific than the inferred return type 'Doctrine\ORM\Mapping\OneT…
  • src/Mapping/ToOneOwningSideMapping.php:192 LessSpecificReturnStatement (error): The type 'Doctrine\ORM\Mapping\OneToOneOwningSideMapping' is more general than the declared return type 'Doctrine\ORM\Mapping\ToOneOwningSideMapping&static' for Doctrine\ORM\Mapping\ToOneOwningSideMapping::fromMappingAr…
  • src/Mapping/ToOneOwningSideMapping.php:52 MoreSpecificReturnType (error): The declared return type 'Doctrine\ORM\Mapping\ToOneOwningSideMapping&static' for Doctrine\ORM\Mapping\ToOneOwningSideMapping::fromMappingArray is more specific than the inferred return type 'Doctrine\ORM\Mapping\OneToOneOwni…
  • src/Mapping/ToOneOwningSideMapping.php:72 LessSpecificReturnStatement (error): The type 'Doctrine\ORM\Mapping\OneToOneOwningSideMapping' is more general than the declared return type 'Doctrine\ORM\Mapping\ToOneOwningSideMapping&static' for Doctrine\ORM\Mapping\ToOneOwningSideMapping::fromMappingArr…
✅ tempest: issues +0 −2

Memory 922 → 922 MB (<0.1 %) · Wall 24.8 → 24.8 s (+0.1 %)

Weighted 9 616 → 9 610 (−6, −0.06 %): L3 −2

Issue type added removed
ArgumentTypeCoercion 0 2

Removed (2)

  • packages/support/src/Arr/ManipulatesArray.php:859 ArgumentTypeCoercion (error): Argument 2 of Tempest\Support\tap expects callable[impure](Tempest\Support\Arr\ImmutableArray<array-key, mixed>):void, but parent type Closure[impure](Tempest\Support\Arr\ImmutableArray&static):void provided
  • packages/support/src/Arr/ManipulatesArray.php:859 ArgumentTypeCoercion (error): Argument 2 of Tempest\Support\tap expects callable[impure](Tempest\Support\Arr\MutableArray<array-key, mixed>):void, but parent type Closure[impure](Tempest\Support\Arr\MutableArray&static):void provided
✅ drupal: issues +84 −88

Memory 5 196 → 5 196 MB (<0.1 %) · Wall 119.2 → 119.4 s (+0.2 %)

Weighted 161 668 → 161 656 (−12, −0.01 %): L3 −4

Issue type added removed
InvalidReturnStatement 3 3
LessSpecificReturnStatement 16 17
MissingReturnType 2 2
MixedReturnTypeCoercion 45 45
MoreSpecificReturnType 16 19
PossiblyInvalidPropertyAssignmentValue 1 1
TypeDoesNotContainType 1 1

Added (84)

  • core/lib/Drupal/Core/Config/ManagedStorage.php:115 MoreSpecificReturnType (error): The declared return type 'Drupal\Core\Config\ManagedStorage' for Drupal\Core\Config\ManagedStorage::createCollection is more specific than the inferred return type 'Drupal\Core\Config\StorageInterface'
  • core/lib/Drupal/Core/Config/ManagedStorage.php:120 LessSpecificReturnStatement (error): The type 'Drupal\Core\Config\StorageInterface' is more general than the declared return type 'Drupal\Core\Config\ManagedStorage' for Drupal\Core\Config\ManagedStorage::createCollection
  • core/lib/Drupal/Core/Entity/RevisionLogEntityTrait.php:22 MixedReturnTypeCoercion (error): The declared return type 'array<array-key, Drupal\Core\Field\FieldDefinitionInterface>' for Drupal\Core\Entity\RevisionLogEntityTrait::revisionLogBaseFieldDefinitions is more specific than the inferred return…
  • core/lib/Drupal/Core/Field/Plugin/Field/FieldType/BooleanItem.php:40 MixedReturnTypeCoercion (error): The declared return type 'array<array-key, Drupal\Core\TypedData\DataDefinitionInterface>' for Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem::propertyDefinitions is more specific than the in…
  • core/lib/Drupal/Core/Field/Plugin/Field/FieldType/BooleanItem.php:45 MixedReturnTypeCoercion (error): The type 'array{value: Drupal\Core\TypedData\DataDefinition, ...<array-key, mixed>}' is more general than the declared return type 'array<array-key, Drupal\Core\TypedData\DataDefinitionInterface>' …
  • core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php:43 MixedReturnTypeCoercion (error): The declared return type 'array<array-key, Drupal\Core\TypedData\DataDefinitionInterface>' for Drupal\Core\Field\Plugin\Field\FieldType\DecimalItem::propertyDefinitions is more specific than the in…
  • core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php:48 MixedReturnTypeCoercion (error): The type 'array{value: Drupal\Core\TypedData\DataDefinition, ...<array-key, mixed>}' is more general than the declared return type 'array<array-key, Drupal\Core\TypedData\DataDefinitionInterface>' …
  • core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EmailItem.php:29 MixedReturnTypeCoercion (error): The declared return type 'array<array-key, Drupal\Core\TypedData\DataDefinitionInterface>' for Drupal\Core\Field\Plugin\Field\FieldType\EmailItem::propertyDefinitions is more specific than the inferr…
  • core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EmailItem.php:34 MixedReturnTypeCoercion (error): The type 'array{value: Drupal\Core\TypedData\DataDefinition, ...<array-key, mixed>}' is more general than the declared return type 'array<array-key, Drupal\Core\TypedData\DataDefinitionInterface>' fo…
  • core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php:106 MixedReturnTypeCoercion (error): The type 'array{entity: Drupal\Core\TypedData\DataReferenceDefinition, target_id: Drupal\Core\TypedData\DataReferenceTargetDefinition, ...<array-key, mixed>}' is more general than the decl…
  • … and 74 more

Removed (88)

  • core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php:388 MoreSpecificReturnType (error): The declared return type 'Drupal\Core\Config\Entity\ConfigEntityBase&static' for Drupal\Core\Config\Entity\ConfigEntityBase::calculateDependencies is more specific than the inferred return type 'Drupal\Core\…
  • core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php:408 LessSpecificReturnStatement (error): The type 'Drupal\Core\Config\Entity\ConfigEntityBase' is more general than the declared return type 'Drupal\Core\Config\Entity\ConfigEntityBase&static' for Drupal\Core\Config\Entity\ConfigEntityBase::ca…
  • core/lib/Drupal/Core/Config/ManagedStorage.php:115 MoreSpecificReturnType (error): The declared return type 'Drupal\Core\Config\ManagedStorage' for Drupal\Core\Config\ManagedStorage::createCollection is more specific than the inferred return type 'Drupal\Core\Config\StorageInterface&static'
  • core/lib/Drupal/Core/Config/ManagedStorage.php:120 LessSpecificReturnStatement (error): The type 'Drupal\Core\Config\StorageInterface&static' is more general than the declared return type 'Drupal\Core\Config\ManagedStorage' for Drupal\Core\Config\ManagedStorage::createCollection
  • core/lib/Drupal/Core/Entity/RevisionLogEntityTrait.php:22 MixedReturnTypeCoercion (error): The declared return type 'array<array-key, Drupal\Core\Field\FieldDefinitionInterface>' for Drupal\Core\Entity\RevisionLogEntityTrait::revisionLogBaseFieldDefinitions is more specific than the inferred return…
  • core/lib/Drupal/Core/Field/Plugin/Field/FieldType/BooleanItem.php:40 MixedReturnTypeCoercion (error): The declared return type 'array<array-key, Drupal\Core\TypedData\DataDefinitionInterface>' for Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem::propertyDefinitions is more specific than the in…
  • core/lib/Drupal/Core/Field/Plugin/Field/FieldType/BooleanItem.php:45 MixedReturnTypeCoercion (error): The type 'array{value: Drupal\Core\TypedData\DataDefinition&static, ...<array-key, mixed>}' is more general than the declared return type 'array<array-key, Drupal\Core\TypedData\DataDefinitionInter…
  • core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php:43 MixedReturnTypeCoercion (error): The declared return type 'array<array-key, Drupal\Core\TypedData\DataDefinitionInterface>' for Drupal\Core\Field\Plugin\Field\FieldType\DecimalItem::propertyDefinitions is more specific than the in…
  • core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php:48 MixedReturnTypeCoercion (error): The type 'array{value: Drupal\Core\TypedData\DataDefinition&static, ...<array-key, mixed>}' is more general than the declared return type 'array<array-key, Drupal\Core\TypedData\DataDefinitionInter…
  • core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EmailItem.php:29 MixedReturnTypeCoercion (error): The declared return type 'array<array-key, Drupal\Core\TypedData\DataDefinitionInterface>' for Drupal\Core\Field\Plugin\Field\FieldType\EmailItem::propertyDefinitions is more specific than the inferr…
  • … and 78 more

head 4411c95 · base 8a88460 (merge-base) · workflow run

`parent::create()` forwarded to `__call` on a plain parent-typed
receiver, so its `static` lost late static binding.
…#13387

The receiver bound the late static of the caller's own types too, once
they came in through the templates: `make(static::class)` on a
Container gave `Component&Container`. That was masked while every
non-final receiver kept the result late static.

- first-class callables and @method pseudo-methods bind `static` the
  same way; a parameter's `static` stays late static as in a call
- templates of the receiver are the caller's: the call no longer binds
  them to never
- the TypeExpander resolved-flag workaround is no longer needed
…o#13387

Binding the signature's static before the template replacement lost
static that comes from the callee's template arguments
(`@use HasCollection<Collection<array-key, static>>`) and turned
`static::new()->count()` into `Factory<Model>` (Laravel perf corpus).

The template bounds now carry the caller's late static marked as
bound, so the original order binds only the callee's own static. A
bound static is no longer given default type arguments, which dropped
its static flag.
Marking the template bounds also marked `static & self` from the
callee's own `@use HasCollection<...>`, so Eloquent `hydrate()` and
relations lost the binding to the receiver (Laravel perf corpus).

Only `X&static` parsed from a signature needs binding; a late static
built in analysis (`$this`, `new static`, `static::class`) is the
caller's. TNamedObject now starts such a type as bound, which drops
the bound-static visitor.
Merges vimeo#13461 into this PR: a called object binds no static inside a
late static that is already bound (`$this` with `@template T of static`
otherwise hung), and narrowing keeps the bound flag of the child.

Its tests come along, with `(new Post())->find()`,
`box($this)->stat()` and `$this->keys()->same()` no longer late static:
their receivers aren't.
…o#13387

Review follow-ups:
- binding `static` to the class of a receiver that isn't late static
  is sound only in covariant positions: a callable's parameters and
  invariant template arguments keep it late static
- constructors bind static as on master: `new Child($this)` rebound
  the caller's static to Child
- narrowing a late static to a child checks the child's type
  arguments (`static<int>` vs `@extends Bag<string>` contradicts)
- `@psalm-if-this-is` keeps `$this` late static
- the CallMap path, a `@return static` inherited by a native `self`
  and `$other::create()` bind static like an instance call
- a generic expansion of a bound static keeps its type arguments
- setValueIsStatic() sets the resolved flag it is given
…-static-of-non-static-receivers

# Conflicts:
#	src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php
#	tests/MethodCallTest.php

@alies-dev alies-dev left a comment

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.

Self-review: notes on the parts that are not obvious from the diff.

}

$this->value = $value;
$this->is_static_resolved = $is_static;

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.

The core idea of the PR. Only X&static parsed from a signature (including a native : static, which TypeHintResolver parses as Foo&static) is set through $first_type->is_static = true and stays unbound until a call binds it to its receiver. Everything constructed with $is_static during analysis ($this, new static, static::class, instanceof static) is the late static of the calling code, so it starts bound: a later call must never bind it to its own receiver. That rebinding was the cause of #13441 (box($this)->same() nesting) and of make(static::class) on a container giving Component&Container. Doing it in the constructor covers every creation site at once instead of patching each one.

$cloned->value = $value;
$cloned->is_static = $is_static;
$cloned->is_static_resolved = $is_static;
$cloned->is_static_resolved = $is_static_resolved;

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.

Pre-existing bug: this stored $is_static and ignored the $is_static_resolved argument it compares two lines above. Harmless while nothing relied on the flag; now the barrier in TypeExpander::expandAtomic() does.

Comment on lines +911 to +913
public static function bindsStaticToReceiverClass(Codebase $codebase, ?Atomic $static_type): bool
{
return $static_type instanceof TNamedObject

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.

The receiver rule for #13387. static in a called method's signature is the runtime class of the receiver. For $this (or any value that is itself late static) that is still unknown, so the result stays X&static. For a plain $bag: Bag it is "some subclass of Bag", which Bag already means, so the result is Bag. Before, every non-final receiver produced &static, which also let return $other->fluent() pass as the caller's own static (now LessSpecificReturnStatement).

This only feeds the $final flag of TypeExpander; where that flag may actually be applied is restricted to covariant positions, see isFinalWhereNotCovariant().

true,
false,
false,
self::bindsStaticToReceiverClass($codebase, $static_type),

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.

Sibling path: CallMap methods (DateTime::setTime()) passed a hard-coded false, so $date->setTime(1, 2) stayed DateTime&static.

Comment on lines +202 to +207
if ($return_type instanceof TNamedObject
&& $return_type->is_static_resolved
&& !is_string($static_class_type)
) {
$static_class_type = null;
}

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.

Barrier for #13441: a bound late static belongs to the caller, so the receiver of this call must not bind any static inside it, including inside its type arguments (Foo<T of static>, which otherwise recursed). Exempt: a string static class. That is a call by class name (Child::make()), where the called class is exact and still has to replace a static bound to a parent (Methods::getMethodReturnType() pre-binds to the declaring class).

Comment on lines +804 to +806
$tmp_context->vars_in_scope['$__tmp_parent_var__'] = new Union([
$lhs_type_part instanceof TNamedObject ? $lhs_type_part->setIsStatic(true, true) : $lhs_type_part,
]);

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.

parent::create() with no such method forwards to __call through a temporary variable typed as the parent class. That variable stands for $this, so it must be late static, otherwise the receiver rule above would answer the plain parent class (parentMagicMethodCall broke without this).

Comment on lines +2816 to 2826
// still the late static `$this`, narrowed
$this_types = [];
foreach ($this->storage->if_this_is_type->getAtomicTypes() as $this_type) {
$this_types[] = $this_type instanceof TNamedObject
? $this_type->setIsStatic(!$storage->final, true)
: $this_type;
}

$context->vars_in_scope['$this'] = $this->storage->if_this_is_type
->setTypes($this_types)
->setProperties($props);

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.

@psalm-if-this-is replaced $this with the declared type, which is not late static, so return $this->fluent() in such a method became a false LessSpecificReturnStatement under the new receiver rule. $this is still the late static object, only narrowed.

$defining_class_storage->parent_class,
true,
false,
MethodCallReturnTypeFetcher::bindsStaticToReceiverClass($codebase, $lhs_type_part),

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.

@method static fluent() pseudo-methods: same receiver rule as real methods (this path used the default final = false, i.e. always late static, even for a final class).

Comment thread tests/ClassTest.php

/** @template T */
interface Bag {
abstract class Bag {

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.

The preventDoubleStaticResolution* cases (from #13442 and older) now call through $this inside the class. With a plain receiver there is no late static left to re-bind, so the old global-scope versions would no longer exercise the double-binding guard; I checked that each of them fails when the guard (is_static_resolved / the #13442 combiner change) is removed. The global-scope variants are kept as preventDoubleStaticResolutionOnAPlainReceiver*.

Comment thread tests/MethodCallTest.php
'$z===' => 'C&static',
'$y===' => 'C&static',
'$chained===' => 'C&static',
'$z===' => 'C',

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.

Changed expectations in existing tests are all X&static → X for a call on a receiver that isn't late static ((new C())->returnThis() is exactly a C). Where the type is invariant the old &static stays (e.g. $posts in staticDeclaredByTheReceiverClassIsStillBoundToTheReceiver).

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.

$this left in a template argument expands to the receiver on each call, growing the type exponentially

1 participant