Make $this available as object in non-static closures and arrow functions outside class context#5543
Closed
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
Closed
Conversation
…unctions outside class context - In `MutatingScope::enterAnonymousFunctionWithoutReflection()`, add `$this` with type `object` when the closure is non-static and the enclosing scope does not have `$this` and is not inside a `Closure::bind()` call. This removes the false positive "Undefined variable: $this" for closures that will later be bound via `Closure::bind()`. - Apply the same fix in `MutatingScope::enterArrowFunctionWithoutReflection()` for non-static arrow functions outside class context. - Fix `MutatingScope::restoreThis()` to preserve `$this` from the restore scope when not in a class but `$this` is defined (e.g. as `object` in a closure outside a class). Previously it unconditionally removed `$this` when `isInClass()` was false, which caused `$this` to disappear after method calls with `@param-closure-this` inside non-class closures. - Update existing tests that asserted the old buggy behavior.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When a closure using
$thiswas created outside a class context and stored in a variable, PHPStan reported "Undefined variable: $this" at the definition site. However, closures passed inline toClosure::bind()worked correctly because PHPStan already handled that case specially viaStaticCallHandler. This fix makes$thisavailable with typeobjectinside all non-static closures and arrow functions, even outside class context, sinceClosure::bind()can bind$thisto any closure at runtime.Changes
src/Analyser/MutatingScope.php:enterAnonymousFunctionWithoutReflection(): Added anelseifbranch that sets$thistoObjectWithoutClassTypewhen the closure is non-static, the enclosing scope lacks$this, and we are not inside aClosure::bind()call (checked viaisInClosureBind())enterArrowFunctionWithoutReflection(): Same fix for arrow functions — add$thisasobjectwhen the arrow function is non-static, the parent scope lacks$this, and we are not in a closure bind scoperestoreThis(): Added anelseifbranch to preserve$thisfrom the restore scope when not in a class but$thisis defined. Previously theelsebranch unconditionally removed$thiswhenisInClass()was false, which caused$thisto disappear after processing closure arguments with@param-closure-thisannotations inside non-class closuresuse PHPStan\Type\ObjectWithoutClassTypeimportUpdated test expectations in:
tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php: Removed expected error for$thisin non-static closure outside class (line 26 ofthis.php)tests/PHPStan/Analyser/nsrt/param-closure-this.php: Changed*ERROR*toobjectfor$thisin non-static closures outside class contexttests/PHPStan/Rules/Debug/DebugScopeRuleTest.php: Added$this (Yes): objectto expected scope output for closures outside classRoot cause
MutatingScope::enterAnonymousFunctionWithoutReflection()only copied$thisinto the closure scope when the enclosing scope had$thisdefined (i.e. inside a class). Non-static closures outside a class never received$this, even though PHP allows$thisin such closures when they are later bound viaClosure::bind(). The inlineClosure::bind()case worked becauseStaticCallHandlercreated a specialclosureBindScopewith$thisalready set before entering the closure.A secondary issue was that
restoreThis()unconditionally removed$thiswhen the restore scope was not in a class, which caused$this(now set toobjectby the fix) to be lost after processing method calls with@param-closure-thisannotations.Analogous cases probed
enterArrowFunctionWithoutReflection()with the same approach$this— no fix needed, verified with testsClosure::bind()with null target: Correctly does NOT add$this— theisInClosureBind()guard prevents the newelseiffrom firing in bind scopes@param-closure-thisinteraction: Fixed viarestoreThis()change —$thisis now properly preserved after processing closure arguments with this annotationTest
tests/PHPStan/Rules/Variables/data/bug-1348.php): Tests that non-static closures and arrow functions outside class context do NOT report "Undefined variable: $this", while static closures/arrow functions still dotests/PHPStan/Analyser/nsrt/bug-1348.php): Verifies$thishas typeobjectin closures outside class,$this(ClassName)inside class closures, and specific bound type inside inlineClosure::bind()Fixes phpstan/phpstan#1348