Skip to content

Commit 6394b9b

Browse files
VincentLangletphpstan-bot
authored andcommitted
Make $this available as object in non-static closures and arrow functions 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.
1 parent 7604335 commit 6394b9b

6 files changed

Lines changed: 106 additions & 13 deletions

File tree

src/Analyser/MutatingScope.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
use PHPStan\Type\NeverType;
8989
use PHPStan\Type\NullType;
9090
use PHPStan\Type\ObjectType;
91+
use PHPStan\Type\ObjectWithoutClassType;
9192
use PHPStan\Type\StaticType;
9293
use PHPStan\Type\StaticTypeFactory;
9394
use PHPStan\Type\StringType;
@@ -1911,6 +1912,13 @@ public function restoreThis(self $restoreThisScope): self
19111912

19121913
$nativeExpressionTypes[$exprString] = $expressionTypeHolder;
19131914
}
1915+
} elseif (isset($restoreThisScope->expressionTypes['$this'])) {
1916+
$expressionTypes['$this'] = $restoreThisScope->expressionTypes['$this'];
1917+
if (isset($restoreThisScope->nativeExpressionTypes['$this'])) {
1918+
$nativeExpressionTypes['$this'] = $restoreThisScope->nativeExpressionTypes['$this'];
1919+
} else {
1920+
unset($nativeExpressionTypes['$this']);
1921+
}
19141922
} else {
19151923
unset($expressionTypes['$this']);
19161924
unset($nativeExpressionTypes['$this']);
@@ -2107,6 +2115,10 @@ public function enterAnonymousFunctionWithoutReflection(
21072115
$expressionTypes[$exprString] = $typeHolder;
21082116
}
21092117
}
2118+
} elseif (!$closure->static && !$this->isInClosureBind()) {
2119+
$node = new Variable('this');
2120+
$expressionTypes['$this'] = ExpressionTypeHolder::createYes($node, new ObjectWithoutClassType());
2121+
$nativeTypes['$this'] = ExpressionTypeHolder::createYes($node, new ObjectWithoutClassType());
21102122
}
21112123

21122124
$filteredConditionalExpressions = [];
@@ -2251,6 +2263,8 @@ public function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFun
22512263

22522264
if ($arrowFunction->static) {
22532265
$arrowFunctionScope = $arrowFunctionScope->invalidateExpression(new Variable('this'));
2266+
} elseif (!$this->hasVariableType('this')->yes() && !$this->isInClosureBind()) {
2267+
$arrowFunctionScope = $arrowFunctionScope->assignVariable('this', new ObjectWithoutClassType(), new ObjectWithoutClassType(), TrinaryLogic::createYes());
22542268
}
22552269

22562270
return $this->scopeFactory->create(
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug1348Types;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
$closure = function () {
8+
assertType('object', $this);
9+
};
10+
11+
$arrow = fn() => assertType('object', $this);
12+
13+
class Foo
14+
{
15+
public function doFoo(): void
16+
{
17+
$closure = function () {
18+
assertType('$this(Bug1348Types\Foo)', $this);
19+
};
20+
21+
$arrow = fn() => assertType('$this(Bug1348Types\Foo)', $this);
22+
}
23+
}
24+
25+
$bound = \Closure::bind(
26+
function () {
27+
assertType('stdClass', $this);
28+
},
29+
new \stdClass(),
30+
\stdClass::class
31+
);

tests/PHPStan/Analyser/nsrt/param-closure-this.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -153,36 +153,36 @@ public function interplayWithProcessImmediatelyCalledCallable2(): void
153153
}
154154

155155
function (Foo $f): void {
156-
assertType('*ERROR*', $this);
156+
assertType('object', $this);
157157
$f->paramClosureClass(function () {
158158
assertType(Some::class, $this);
159159
});
160-
assertType('*ERROR*', $this);
160+
assertType('object', $this);
161161
$f->paramClosureClass(static function () {
162162
assertType('*ERROR*', $this);
163163
});
164-
assertType('*ERROR*', $this);
164+
assertType('object', $this);
165165
};
166166

167167
function (Foo $f): void {
168168
$a = 1;
169-
assertType('*ERROR*', $this);
169+
assertType('object', $this);
170170
$f->paramClosureClass(function () use (&$a) {
171171
assertType(Some::class, $this);
172172
});
173-
assertType('*ERROR*', $this);
173+
assertType('object', $this);
174174
$f->paramClosureClass(static function () use (&$a) {
175175
assertType('*ERROR*', $this);
176176
});
177-
assertType('*ERROR*', $this);
177+
assertType('object', $this);
178178
};
179179

180180
function (Foo $f): void {
181-
assertType('*ERROR*', $this);
181+
assertType('object', $this);
182182
$f->paramClosureClass(fn () => assertType(Some::class, $this));
183-
assertType('*ERROR*', $this);
183+
assertType('object', $this);
184184
$f->paramClosureClass(static fn () => assertType('*ERROR*', $this));
185-
assertType('*ERROR*', $this);
185+
assertType('object', $this);
186186
};
187187

188188
class Bar extends Foo

tests/PHPStan/Rules/Debug/DebugScopeRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@ public function testRuleInPhpStanNamespace(): void
2929
'$a (Yes): int',
3030
'$b (Yes): int',
3131
'$debug (Yes): bool',
32+
'$this (Yes): object',
3233
'native $a (Yes): int',
3334
'native $b (Yes): int',
3435
'native $debug (Yes): bool',
36+
'native $this (Yes): object',
3537
]),
3638
10,
3739
],
@@ -40,10 +42,12 @@ public function testRuleInPhpStanNamespace(): void
4042
'$a (Yes): int',
4143
'$b (Yes): int',
4244
'$debug (Yes): bool',
45+
'$this (Yes): object',
4346
'$c (Maybe): 1',
4447
'native $a (Yes): int',
4548
'native $b (Yes): int',
4649
'native $debug (Yes): bool',
50+
'native $this (Yes): object',
4751
'native $c (Maybe): 1',
4852
'condition about $c #1: if $debug=false then $c is *ERROR* (No)',
4953
'condition about $c #2: if $debug=true then $c is 1 (Yes)',
@@ -58,7 +62,9 @@ public function testPr4663(): void
5862
$this->analyse([__DIR__ . '/data/pr-4663.php'], [
5963
[
6064
implode("\n", [
65+
'$this (Yes): object',
6166
"\$result (Yes): 'no matches!'",
67+
'native $this (Yes): object',
6268
"native \$result (Yes): 'no matches!'",
6369
]),
6470
11,

tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -703,10 +703,6 @@ public function testFormerThisVariableRule(): void
703703
'Undefined variable: $this',
704704
20,
705705
],
706-
[
707-
'Undefined variable: $this',
708-
26,
709-
],
710706
[
711707
'Undefined variable: $this',
712708
38,
@@ -1562,4 +1558,22 @@ public function testBug10729(): void
15621558
]);
15631559
}
15641560

1561+
public function testBug1348(): void
1562+
{
1563+
$this->cliArgumentsVariablesRegistered = true;
1564+
$this->polluteScopeWithLoopInitialAssignments = false;
1565+
$this->checkMaybeUndefinedVariables = true;
1566+
$this->polluteScopeWithAlwaysIterableForeach = true;
1567+
$this->analyse([__DIR__ . '/data/bug-1348.php'], [
1568+
[
1569+
'Undefined variable: $this',
1570+
25,
1571+
],
1572+
[
1573+
'Undefined variable: $this',
1574+
28,
1575+
],
1576+
]);
1577+
}
1578+
15651579
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug1348;
4+
5+
$closure = function () {
6+
$this->foo = 'bar';
7+
};
8+
9+
$object = new \stdClass();
10+
11+
\Closure::bind($closure, $object, $object)();
12+
\Closure::bind(
13+
function () {
14+
$this->foo = 'bar';
15+
},
16+
$object,
17+
$object
18+
)();
19+
20+
// arrow function case
21+
$arrow = fn() => $this;
22+
23+
// static closures should still report $this as undefined
24+
static function () {
25+
$this->foo = 'bar';
26+
};
27+
28+
static fn() => $this;

0 commit comments

Comments
 (0)