Skip to content

fix: resolve static/self/parent relative to @param-closure-this bound class#6061

Open
calebdw wants to merge 1 commit into
phpstan:2.2.xfrom
calebdw:calebdw/push-qwswwvrkussm
Open

fix: resolve static/self/parent relative to @param-closure-this bound class#6061
calebdw wants to merge 1 commit into
phpstan:2.2.xfrom
calebdw:calebdw/push-qwswwvrkussm

Conversation

@calebdw

@calebdw calebdw commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Bug

When a closure or arrow function has its $this type overridden via @param-closure-this, the static/self/parent keywords in return type declarations resolve to the enclosing class instead of the bound class.

Reproducer

final class CollectionMacros
{
    public static function boot(): void
    {
        Collection::macro(
            'asValueLabel',
            // PHPStan errors with:
            // Anonymous function should return static(\...\CollectionMacros)
            // but returns Illuminate\Support\Collection<int, array<string, mixed>>
            fn (): static => $this->map(fn ($l, $v) => ['value' => $v, 'label' => $l])->values(),
        );
    }
}

Where Collection::macro() has @param-closure-this static $macro, PHPStan correctly resolves $this inside the closure to Collection, but the static return type still resolves to CollectionMacros (the enclosing class) instead of Collection.

Root Cause

Two layers:

  1. inClosureBindScopeClasses was never set when @param-closure-this was applied. NodeScopeResolver only called assignVariable('this', ...) to change the $this type, but did not set inClosureBindScopeClasses — which is what resolveTypeByName checks when resolving static/self/parent keywords.

  2. getFunctionType used the enclosing class to resolve static in return type declarations. The closure's return type static went through InitializerExprTypeResolver::getFunctionTypeParserNodeTypeToPHPStanType::resolve, which used InitializerExprContext::fromScope($this) — always returning the enclosing class via $scope->getClassReflection().

Fix

src/Analyser/NodeScopeResolver.php

After assignVariable('this', ...), chain ->withClosureBindScopeClasses(...) so static/self resolve to the closure-this class. Applied to both closure and arrow function paths.

src/Analyser/MutatingScope.php

  • withClosureBindScopeClasses() (new method): Creates a new scope with inClosureBindScopeClasses set, preserving all other state.
  • restoreThis(): Restore inClosureBindScopeClasses from the saved scope (was using $this->, now uses $restoreThisScope->).
  • getFunctionType(): When inClosureBindScopeClasses is set and the type is static/self/parent, pass a context with the bound class to InitializerExprTypeResolver so the return type resolves correctly.

… class

When a closure or arrow function has its $this type overridden via
@param-closure-this, the static/self/parent keywords in return type
declarations were still resolving to the enclosing class instead of
the bound class.

Two fixes were needed:

1. Set inClosureBindScopeClasses when applying @param-closure-this so
   that resolveTypeByName and resolveName use the bound class for
   static/self resolution in expressions like static method calls.

2. Override the InitializerExprContext in getFunctionType when
   inClosureBindScopeClasses is set, so that static/self/parent in
   return type declarations (processed via ParserNodeTypeToPHPStanType)
   also resolve to the bound class instead of the enclosing class.
@calebdw
calebdw force-pushed the calebdw/push-qwswwvrkussm branch from 69afe68 to ea2dddf Compare July 18, 2026 03:41
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.

1 participant