Skip to content

Resolve self/parent/static in Closure::bind() scope - #4081

Open
zonuexe wants to merge 9 commits into
phpstan:2.3.xfrom
zonuexe:fix/bind-class-scope
Open

zonuexe wants to merge 9 commits into
phpstan:2.3.xfrom
zonuexe:fix/bind-class-scope

Conversation

@zonuexe

@zonuexe zonuexe commented Jul 4, 2025 •

Copy link
Copy Markdown
Contributor

This PR targets 2.3.x. It makes self, parent and static inside closures bound with Closure::bind() (and Closure::call() / @param-closure-this) resolve against the bound class: in types (class constants, ::class, new, static calls, static properties and, through MutatingScope::getFunctionType(), closure parameter and return type hints) and in the rules that check them (ClassConstantRule, StaticMethodCallCheck, AccessStaticPropertiesCheck, InstantiationRule; the closure type-hint rules follow from the resolved type hints), at the top level and in functions as well as inside classes.

The design is scope-based. The bind factory evaluates newThis/newScope at the call site and stores the bound classes in the scope's existing inClosureBindScopeClasses; entering a class or function resets them, and the arguments of the Closure::bind() call itself keep resolving against the enclosing scope (regression test for phpstan/phpstan#6319). MutatingScope::resolveName(), resolveTypeByName() and getFunctionType() honour the bound class outside a class too, and the rules ask a small SelfClassResolver helper which class self/parent/static are relative to.

When newScope is not exactly one known class (one of several classes, or a class-string/string/object of no known class), the scope stays bound to an unknown class (MutatingScope::UNKNOWN_CLOSURE_BIND_SCOPE_CLASS) instead of looking unbound or picking a candidate: self/static/parent resolve to the closest class all candidates extend (or object / class-string / mixed without one), members are considered accessible when the bound class is unknown (with several known candidates, access granted by any of them counts, as before), and the rules stay silent; 'static' and null keep meaning "no class". Unknown results surface as mixed/object/class-string, so at level 10 they are reported like any other unknown value.

The first three commits are #6679's commits included verbatim (identical patches: named-argument handling in ClosureBindArgVisitor, the bind factory and ParametersAcceptorSelector) and drop out when this is rebased after #6679 lands. #6678 (typing $this from newThis) is not included; this PR does not depend on it to be correct, but self:: calls to instance methods of a bound $this are only as exact as the $this type, which #6678 improves; likewise $this->member accessibility under an unknown scope class becomes exact only together with #6678. Every change to a turbo-shadowed class (MutatingScope, StaticCallHandler, ClassConstFetchHandler, NewHandler, ParametersAcceptorSelector, ClosureBindArgVisitor) carries its phpstan_turbo C++ twin in the same commit, with differential coverage (scope-family, walk-trace fixtures, parser-visitors).

Known limitations: static:: inside a bound closure is the newScope class, not get_class($newThis). A closure passed to a @param-closure-this parameter is assumed to be scoped to that class, so self:: names it even outside a class (as with Closure::call() or bind($c, $o, $o), unlike bindTo($o) of an unscoped closure). A single bound class that does not exist (Unknown::class) is reported as not found; inside a class, constants and ::class then follow the enclosing class while new self() and the rules follow the unknown name. instanceof self/static/parent and catch (self $e) inside a bound closure still resolve against the lexical class only (InstanceofHandler, ExistingClassInInstanceOfRule, CaughtExceptionExistenceRule); that is left for a follow-up. Constants through self/static in a scope bound to one of several classes are not typed as the union of the candidates' constants (no type rather than a wrong one).

Disclosure: The first version resolved self/parent/static inside Closure::bind() closures through a parser-level annotation re-evaluated in the closure body's scope. An adversarial review by Claude Fable showed this disagreed with the scope the call site had already entered (object newScope not recognised, variable newScope picking up reassignments in the body) and that the annotation leaked into classes and functions declared inside the bound closure, producing false positives. The redesign drops the annotation and reads the bound class from the scope's existing inClosureBindScopeClasses, which the bind factory evaluates at the call site and which entering a class-like resets; MutatingScope::resolveName()/resolveTypeByName() now honour it outside a class as well, and the four rules consult it through a small SelfClassResolver helper. A second review led to distinguishing an ambiguous or unknown bound class from an unbound scope, and a third to keeping such a scope ambiguous everywhere (new/::class typing, type hints, accessibility, inside classes, Closure::call() and @param-closure-this with an object of no known class). The full development history, including the earlier designs, is preserved at https://github.com/zonuexe/phpstan-src/commits/backup/bind-class-scope-redesign-history; the PR itself is a clean series with the same final tree.

zonuexe added a commit to zonuexe/phpstan-src that referenced this pull request Jul 8, 2026
Annotate special class-name nodes (self/parent/static) that appear inside a
Closure::bind() call with the bind scope argument via ClosureBindArgVisitor, and
resolve the class constant type against that bound class in ClassConstFetchHandler.

Reconstruction of PR phpstan#4081 (closure-bind-scope) rebased onto 2.2.x: the original
MutatingScope::getType() ClassConstFetch branch no longer exists after expression
handling was split into ExprHandler services.
zonuexe added a commit to zonuexe/phpstan-src that referenced this pull request Jul 8, 2026
… properties and instantiation

Extend the Closure::bind() scope binding beyond class constants to the natural
remaining surface:

- MutatingScope::resolveName()/resolveTypeByName() now honour the bind scope
  annotated on self/parent/static class-name nodes, so static method-call return
  types, static property types and `new self/parent/static` resolve against the
  bound class even outside a class context (the closure body's type is inferred in
  the enclosing scope where the closure-bind scope classes are otherwise absent).
- StaticMethodCallCheck, AccessStaticPropertiesCheck and InstantiationRule no
  longer report "outside of class scope" for self/parent/static inside a bound
  closure, resolving accessibility against the bound class.
- ClosureBindArgVisitor now only rescopes the closure body (1st argument), not the
  $newThis/$newScope arguments, avoiding a self-referential scope annotation on a
  `self::class` scope argument.

Continues the reconstruction of PR phpstan#4081 on 2.2.x.
@zonuexe
zonuexe force-pushed the fix/bind-class-scope branch from b3c42c8 to 1fb897e Compare July 8, 2026 12:46
@zonuexe
zonuexe changed the base branch from 2.1.x to 2.2.x July 8, 2026 12:46
@zonuexe zonuexe changed the title Closure::bind() scope binding improvements Resolve self/parent/static in Closure::bind() scope Jul 8, 2026
@zonuexe
zonuexe marked this pull request as ready for review July 8, 2026 12:49
@phpstan-bot

Copy link
Copy Markdown
Collaborator

This pull request has been marked as ready for review.

@zonuexe
zonuexe marked this pull request as draft July 14, 2026 03:23
zonuexe added a commit to zonuexe/phpstan-src that referenced this pull request Jul 14, 2026
Annotate special class-name nodes (self/parent/static) that appear inside a
Closure::bind() call with the bind scope argument via ClosureBindArgVisitor, and
resolve the class constant type against that bound class in ClassConstFetchHandler.

Reconstruction of PR phpstan#4081 (closure-bind-scope) rebased onto 2.2.x: the original
MutatingScope::getType() ClassConstFetch branch no longer exists after expression
handling was split into ExprHandler services.
zonuexe added a commit to zonuexe/phpstan-src that referenced this pull request Jul 14, 2026
… properties and instantiation

Extend the Closure::bind() scope binding beyond class constants to the natural
remaining surface:

- MutatingScope::resolveName()/resolveTypeByName() now honour the bind scope
  annotated on self/parent/static class-name nodes, so static method-call return
  types, static property types and `new self/parent/static` resolve against the
  bound class even outside a class context (the closure body's type is inferred in
  the enclosing scope where the closure-bind scope classes are otherwise absent).
- StaticMethodCallCheck, AccessStaticPropertiesCheck and InstantiationRule no
  longer report "outside of class scope" for self/parent/static inside a bound
  closure, resolving accessibility against the bound class.
- ClosureBindArgVisitor now only rescopes the closure body (1st argument), not the
  $newThis/$newScope arguments, avoiding a self-referential scope annotation on a
  `self::class` scope argument.

Continues the reconstruction of PR phpstan#4081 on 2.2.x.
@zonuexe
zonuexe force-pushed the fix/bind-class-scope branch from 1fb897e to 8d9f662 Compare July 14, 2026 03:29
zonuexe added a commit to zonuexe/phpstan-src that referenced this pull request Jul 14, 2026
Annotate special class-name nodes (self/parent/static) that appear inside a
Closure::bind() call with the bind scope argument via ClosureBindArgVisitor, and
resolve the class constant type against that bound class in ClassConstFetchHandler.

Reconstruction of PR phpstan#4081 (closure-bind-scope) rebased onto 2.2.x: the original
MutatingScope::getType() ClassConstFetch branch no longer exists after expression
handling was split into ExprHandler services.
zonuexe added a commit to zonuexe/phpstan-src that referenced this pull request Jul 14, 2026
… properties and instantiation

Extend the Closure::bind() scope binding beyond class constants to the natural
remaining surface:

- MutatingScope::resolveName()/resolveTypeByName() now honour the bind scope
  annotated on self/parent/static class-name nodes, so static method-call return
  types, static property types and `new self/parent/static` resolve against the
  bound class even outside a class context (the closure body's type is inferred in
  the enclosing scope where the closure-bind scope classes are otherwise absent).
- StaticMethodCallCheck, AccessStaticPropertiesCheck and InstantiationRule no
  longer report "outside of class scope" for self/parent/static inside a bound
  closure, resolving accessibility against the bound class.
- ClosureBindArgVisitor now only rescopes the closure body (1st argument), not the
  $newThis/$newScope arguments, avoiding a self-referential scope annotation on a
  `self::class` scope argument.

Continues the reconstruction of PR phpstan#4081 on 2.2.x.
@zonuexe
zonuexe force-pushed the fix/bind-class-scope branch from 8d9f662 to 0b15116 Compare July 14, 2026 13:23
zonuexe added a commit to zonuexe/phpstan-src that referenced this pull request Jul 20, 2026
Annotate special class-name nodes (self/parent/static) that appear inside a
Closure::bind() call with the bind scope argument via ClosureBindArgVisitor, and
resolve the class constant type against that bound class in ClassConstFetchHandler.

Reconstruction of PR phpstan#4081 (closure-bind-scope) rebased onto 2.2.x: the original
MutatingScope::getType() ClassConstFetch branch no longer exists after expression
handling was split into ExprHandler services.
zonuexe added a commit to zonuexe/phpstan-src that referenced this pull request Jul 20, 2026
… properties and instantiation

Extend the Closure::bind() scope binding beyond class constants to the natural
remaining surface:

- MutatingScope::resolveName()/resolveTypeByName() now honour the bind scope
  annotated on self/parent/static class-name nodes, so static method-call return
  types, static property types and `new self/parent/static` resolve against the
  bound class even outside a class context (the closure body's type is inferred in
  the enclosing scope where the closure-bind scope classes are otherwise absent).
- StaticMethodCallCheck, AccessStaticPropertiesCheck and InstantiationRule no
  longer report "outside of class scope" for self/parent/static inside a bound
  closure, resolving accessibility against the bound class.
- ClosureBindArgVisitor now only rescopes the closure body (1st argument), not the
  $newThis/$newScope arguments, avoiding a self-referential scope annotation on a
  `self::class` scope argument.

Continues the reconstruction of PR phpstan#4081 on 2.2.x.
@zonuexe
zonuexe force-pushed the fix/bind-class-scope branch from 0b15116 to 58a5dde Compare July 20, 2026 05:15
zonuexe added a commit to zonuexe/phpstan-src that referenced this pull request Jul 20, 2026
… properties and instantiation

Extend the Closure::bind() scope binding beyond class constants to the natural
remaining surface:

- MutatingScope::resolveName()/resolveTypeByName() now honour the bind scope
  annotated on self/parent/static class-name nodes, so static method-call return
  types, static property types and `new self/parent/static` resolve against the
  bound class even outside a class context (the closure body's type is inferred in
  the enclosing scope where the closure-bind scope classes are otherwise absent).
- StaticMethodCallCheck, AccessStaticPropertiesCheck and InstantiationRule no
  longer report "outside of class scope" for self/parent/static inside a bound
  closure, resolving accessibility against the bound class.
- ClosureBindArgVisitor now only rescopes the closure body (1st argument), not the
  $newThis/$newScope arguments, avoiding a self-referential scope annotation on a
  `self::class` scope argument.

Continues the reconstruction of PR phpstan#4081 on 2.2.x.
@zonuexe
zonuexe force-pushed the fix/bind-class-scope branch from 58a5dde to 514ad90 Compare July 20, 2026 05:33
@zonuexe
zonuexe marked this pull request as ready for review July 20, 2026 05:41
@phpstan-bot

Copy link
Copy Markdown
Collaborator

This pull request has been marked as ready for review.

zonuexe added a commit to zonuexe/phpstan-src that referenced this pull request Jul 23, 2026
Annotate special class-name nodes (self/parent/static) that appear inside a
Closure::bind() call with the bind scope argument via ClosureBindArgVisitor, and
resolve the class constant type against that bound class in ClassConstFetchHandler.

Reconstruction of PR phpstan#4081 (closure-bind-scope) rebased onto 2.2.x: the original
MutatingScope::getType() ClassConstFetch branch no longer exists after expression
handling was split into ExprHandler services.
zonuexe added a commit to zonuexe/phpstan-src that referenced this pull request Jul 23, 2026
… properties and instantiation

Extend the Closure::bind() scope binding beyond class constants to the natural
remaining surface:

- MutatingScope::resolveName()/resolveTypeByName() now honour the bind scope
  annotated on self/parent/static class-name nodes, so static method-call return
  types, static property types and `new self/parent/static` resolve against the
  bound class even outside a class context (the closure body's type is inferred in
  the enclosing scope where the closure-bind scope classes are otherwise absent).
- StaticMethodCallCheck, AccessStaticPropertiesCheck and InstantiationRule no
  longer report "outside of class scope" for self/parent/static inside a bound
  closure, resolving accessibility against the bound class.
- ClosureBindArgVisitor now only rescopes the closure body (1st argument), not the
  $newThis/$newScope arguments, avoiding a self-referential scope annotation on a
  `self::class` scope argument.

Continues the reconstruction of PR phpstan#4081 on 2.2.x.
@zonuexe
zonuexe force-pushed the fix/bind-class-scope branch from 514ad90 to 2fc7cd8 Compare July 23, 2026 10:22
zonuexe added a commit to zonuexe/phpstan-src that referenced this pull request Aug 9, 2026
Annotate special class-name nodes (self/parent/static) that appear inside a
Closure::bind() call with the bind scope argument via ClosureBindArgVisitor, and
resolve the class constant type against that bound class in ClassConstFetchHandler.

Reconstruction of PR phpstan#4081 (closure-bind-scope) rebased onto 2.2.x: the original
MutatingScope::getType() ClassConstFetch branch no longer exists after expression
handling was split into ExprHandler services.
zonuexe added a commit to zonuexe/phpstan-src that referenced this pull request Aug 9, 2026
… properties and instantiation

Extend the Closure::bind() scope binding beyond class constants to the natural
remaining surface:

- MutatingScope::resolveName()/resolveTypeByName() now honour the bind scope
  annotated on self/parent/static class-name nodes, so static method-call return
  types, static property types and `new self/parent/static` resolve against the
  bound class even outside a class context (the closure body's type is inferred in
  the enclosing scope where the closure-bind scope classes are otherwise absent).
- StaticMethodCallCheck, AccessStaticPropertiesCheck and InstantiationRule no
  longer report "outside of class scope" for self/parent/static inside a bound
  closure, resolving accessibility against the bound class.
- ClosureBindArgVisitor now only rescopes the closure body (1st argument), not the
  $newThis/$newScope arguments, avoiding a self-referential scope annotation on a
  `self::class` scope argument.

Continues the reconstruction of PR phpstan#4081 on 2.2.x.
@zonuexe
zonuexe force-pushed the fix/bind-class-scope branch from 2fc7cd8 to 5c4cac0 Compare August 9, 2026 07:26
@staabm

staabm commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

@SanderMuller please review :)

@zonuexe
zonuexe force-pushed the fix/bind-class-scope branch from c469c18 to aa40aab Compare August 11, 2026 13:14
zonuexe added a commit to zonuexe/phpstan-src that referenced this pull request Aug 26, 2026
Annotate special class-name nodes (self/parent/static) that appear inside a
Closure::bind() call with the bind scope argument via ClosureBindArgVisitor, and
resolve the class constant type against that bound class in ClassConstFetchHandler.

Reconstruction of PR phpstan#4081 (closure-bind-scope) rebased onto 2.2.x: the original
MutatingScope::getType() ClassConstFetch branch no longer exists after expression
handling was split into ExprHandler services.
zonuexe added a commit to zonuexe/phpstan-src that referenced this pull request Aug 26, 2026
… properties and instantiation

Extend the Closure::bind() scope binding beyond class constants to the natural
remaining surface:

- MutatingScope::resolveName()/resolveTypeByName() now honour the bind scope
  annotated on self/parent/static class-name nodes, so static method-call return
  types, static property types and `new self/parent/static` resolve against the
  bound class even outside a class context (the closure body's type is inferred in
  the enclosing scope where the closure-bind scope classes are otherwise absent).
- StaticMethodCallCheck, AccessStaticPropertiesCheck and InstantiationRule no
  longer report "outside of class scope" for self/parent/static inside a bound
  closure, resolving accessibility against the bound class.
- ClosureBindArgVisitor now only rescopes the closure body (1st argument), not the
  $newThis/$newScope arguments, avoiding a self-referential scope annotation on a
  `self::class` scope argument.

Continues the reconstruction of PR phpstan#4081 on 2.2.x.
@zonuexe
zonuexe force-pushed the fix/bind-class-scope branch from aa40aab to f443381 Compare August 26, 2026 04:41
zonuexe added a commit to zonuexe/phpstan-src that referenced this pull request Sep 22, 2026
Annotate special class-name nodes (self/parent/static) that appear inside a
Closure::bind() call with the bind scope argument via ClosureBindArgVisitor, and
resolve the class constant type against that bound class in ClassConstFetchHandler.

Reconstruction of PR phpstan#4081 (closure-bind-scope) rebased onto 2.2.x: the original
MutatingScope::getType() ClassConstFetch branch no longer exists after expression
handling was split into ExprHandler services.
zonuexe added a commit to zonuexe/phpstan-src that referenced this pull request Sep 22, 2026
… properties and instantiation

Extend the Closure::bind() scope binding beyond class constants to the natural
remaining surface:

- MutatingScope::resolveName()/resolveTypeByName() now honour the bind scope
  annotated on self/parent/static class-name nodes, so static method-call return
  types, static property types and `new self/parent/static` resolve against the
  bound class even outside a class context (the closure body's type is inferred in
  the enclosing scope where the closure-bind scope classes are otherwise absent).
- StaticMethodCallCheck, AccessStaticPropertiesCheck and InstantiationRule no
  longer report "outside of class scope" for self/parent/static inside a bound
  closure, resolving accessibility against the bound class.
- ClosureBindArgVisitor now only rescopes the closure body (1st argument), not the
  $newThis/$newScope arguments, avoiding a self-referential scope annotation on a
  `self::class` scope argument.

Continues the reconstruction of PR phpstan#4081 on 2.2.x.
@zonuexe
zonuexe force-pushed the fix/bind-class-scope branch from f443381 to eb16ce0 Compare September 22, 2026 21:36
zonuexe added a commit to zonuexe/phpstan-src that referenced this pull request Sep 25, 2026
Annotate special class-name nodes (self/parent/static) that appear inside a
Closure::bind() call with the bind scope argument via ClosureBindArgVisitor, and
resolve the class constant type against that bound class in ClassConstFetchHandler.

Reconstruction of PR phpstan#4081 (closure-bind-scope) rebased onto 2.2.x: the original
MutatingScope::getType() ClassConstFetch branch no longer exists after expression
handling was split into ExprHandler services.
zonuexe added a commit to zonuexe/phpstan-src that referenced this pull request Sep 25, 2026
… properties and instantiation

Extend the Closure::bind() scope binding beyond class constants to the natural
remaining surface:

- MutatingScope::resolveName()/resolveTypeByName() now honour the bind scope
  annotated on self/parent/static class-name nodes, so static method-call return
  types, static property types and `new self/parent/static` resolve against the
  bound class even outside a class context (the closure body's type is inferred in
  the enclosing scope where the closure-bind scope classes are otherwise absent).
- StaticMethodCallCheck, AccessStaticPropertiesCheck and InstantiationRule no
  longer report "outside of class scope" for self/parent/static inside a bound
  closure, resolving accessibility against the bound class.
- ClosureBindArgVisitor now only rescopes the closure body (1st argument), not the
  $newThis/$newScope arguments, avoiding a self-referential scope annotation on a
  `self::class` scope argument.

Continues the reconstruction of PR phpstan#4081 on 2.2.x.
@zonuexe
zonuexe force-pushed the fix/bind-class-scope branch from eb16ce0 to 73c257b Compare September 25, 2026 00:22
@zonuexe
zonuexe marked this pull request as draft October 5, 2026 12:10
@zonuexe
zonuexe changed the base branch from 2.2.x to 2.3.x October 5, 2026 12:10
@zonuexe
zonuexe force-pushed the fix/bind-class-scope branch 2 times, most recently from ae94ccd to af4dbee Compare October 5, 2026 19:34
zonuexe and others added 9 commits October 7, 2026 02:07
The bind-scope factory captured the call as written and read $newThis and
$newScope from getArgs()[1] and [2]. processArgs() already walks the
normalized call, so the closure gets the factory in any argument order, but
with named arguments the factory picked the wrong arguments: $this became
mixed, object or a class-name string, and the class scope was lost, so
accessing the scope class's private and protected members was reported.

Normalize the call inside the factory as well. The reordered Args keep the
same value expressions, so their stored results are still found.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ClosureBindArgVisitor marked the first argument of any Closure::bind() call
with more than one argument as the bound closure. With named arguments the
first argument can be $newThis - Closure::bind(newThis: $c, closure: ...) -
and when it was a variable with a @param-closure-this type,
ParametersAcceptorSelector narrowed the $newThis parameter to that type and
reported the variable passed for it.

Find the closure and $newThis arguments by position or by name, and mark the
closure argument only when both are passed. A duplicated named argument does
not replace the one already found, like in ArgumentsNormalizer::reorderArgs().

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…amed

ParametersAcceptorSelector narrowed the $newThis parameter of Closure::bind()
to a closure variable's @param-closure-this type only when the closure was
the first argument. With named arguments the closure can be anywhere -
Closure::bind(newThis: $o, closure: $c) - and ClosureBindArgVisitor marks it
wherever it is, so look the marked argument up when the first one is named.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
MutatingScope already recorded the classes a Closure::bind() / Closure::call()
scope is bound to (inClosureBindScopeClasses: the bind factory evaluates
newScope at the call site, and entering a class or function resets them),
but honoured them for self/parent/static only inside a class. Now:

- resolveName(), resolveTypeByName() and getFunctionType() resolve
  self/static to the bound class and parent to its parent, outside a class
  as well as inside one; outside a class only these names follow the bound
  class. getClosureBindScopeClassReflection() exposes it.
- A newScope whose class is not exactly one known class keeps the scope bound
  without naming a class: a class-string, string or object of no known class
  records UNKNOWN_CLOSURE_BIND_SCOPE_CLASS ('static' and null name none), and
  several classes stay several. isClosureBindScopeClassAmbiguous() reports
  it; self/static/parent then never fall back to the enclosing class or one
  of the candidates, but to the closest class all candidates extend
  (getClosureBindScopeCommonAncestor()), and type hints to mixed without one.
- A member is accessible from a scope bound to an unknown class, which may
  be the class declaring it.

The native MutatingScope and StaticCallHandler mirror it, and the scope
differential walks closures bound to an unknown class, several classes,
'static' and null.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…pe class

ClassConstFetchHandler took the class self/parent/static::CONST resolve
against from the enclosing class only, and NewHandler / ::class built a type
literally named "self" outside a class. Read the bound class off the scope
instead: inside a closure bound to one class it is that class (outside a
class too); bound to a class that is not exactly one known class, no class
for constants, and for new / ::class the closest class all candidates extend
- an object / class-string of some class without one.

Both handlers' C++ twins mirror it; the walk-trace fixtures cover a bound
class, an ambiguous one and named arguments.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…lass

ClassConstantRule, StaticMethodCallCheck, AccessStaticPropertiesCheck and
InstantiationRule reported "outside of class scope" for self/parent/static
in a closure bound to a class outside any class, and checked them against the
enclosing class inside one. A small SelfClassResolver helper now answers which
class they are relative to - the bound class (Scope::resolveName()), else the
enclosing class - and the rules check against it: "outside of class scope"
only without one, "does not extend any class" for a bound class without a
parent, members through Scope::resolveTypeByName() (static:: described as
static(Foo)). Bound to a class that is not exactly one known class, the rules
stay silent for self/parent/static.

The tests cover the bound class's protected members, an object newScope,
private members from a subclass scope, parent-less bound classes, undefined
members, unbound and 'static'/null scopes, binds inside another class,
ambiguous and unknown scopes (including member access from them), named
arguments, and closure type hints resolved against the bound class.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A closure bound with Closure::bind($closure, $object, Foo::class) may call
Foo's instance methods through self::/static::, also at the top level and in
functions: PHP forwards the bound $this. StaticMethodCallCheck only accepted
that inside a non-static method of the class. Inside a closure-bind scope
with a defined $this, decide by whether $this is the method's class or a
subclass of it instead.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…sure-this

- A class or function declared inside a bound closure has its own
  self/parent/static (rule data and a walk-trace fixture).
- The bind scope applies to the closure body only: the arguments of the call
  keep resolving against the enclosing class (regression test for
  phpstan/phpstan#6319, reproducer from the issue's
  playground sample).
- A closure passed to a @param-closure-this parameter is analysed as scoped
  to that class, so self:: names it even outside any class.
- More parser-visitor snippets with self/parent/static names inside
  Closure::bind() calls, which the visitor leaves alone.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@zonuexe
zonuexe marked this pull request as ready for review October 6, 2026 17:13
@zonuexe
zonuexe force-pushed the fix/bind-class-scope branch from af4dbee to c5366df Compare October 6, 2026 17:13
@phpstan-bot

Copy link
Copy Markdown
Collaborator

This pull request has been marked as ready for review.

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.

4 participants