diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 79d4be017bf..6f0dd162e17 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -88,6 +88,7 @@ use PHPStan\Type\NeverType; use PHPStan\Type\NullType; use PHPStan\Type\ObjectType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StaticType; use PHPStan\Type\StaticTypeFactory; use PHPStan\Type\StringType; @@ -1911,6 +1912,13 @@ public function restoreThis(self $restoreThisScope): self $nativeExpressionTypes[$exprString] = $expressionTypeHolder; } + } elseif (isset($restoreThisScope->expressionTypes['$this'])) { + $expressionTypes['$this'] = $restoreThisScope->expressionTypes['$this']; + if (isset($restoreThisScope->nativeExpressionTypes['$this'])) { + $nativeExpressionTypes['$this'] = $restoreThisScope->nativeExpressionTypes['$this']; + } else { + unset($nativeExpressionTypes['$this']); + } } else { unset($expressionTypes['$this']); unset($nativeExpressionTypes['$this']); @@ -2107,6 +2115,10 @@ public function enterAnonymousFunctionWithoutReflection( $expressionTypes[$exprString] = $typeHolder; } } + } elseif (!$closure->static && !$this->isInClosureBind()) { + $node = new Variable('this'); + $expressionTypes['$this'] = ExpressionTypeHolder::createYes($node, new ObjectWithoutClassType()); + $nativeTypes['$this'] = ExpressionTypeHolder::createYes($node, new ObjectWithoutClassType()); } $filteredConditionalExpressions = []; @@ -2251,6 +2263,8 @@ public function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFun if ($arrowFunction->static) { $arrowFunctionScope = $arrowFunctionScope->invalidateExpression(new Variable('this')); + } elseif (!$this->hasVariableType('this')->yes() && !$this->isInClosureBind()) { + $arrowFunctionScope = $arrowFunctionScope->assignVariable('this', new ObjectWithoutClassType(), new ObjectWithoutClassType(), TrinaryLogic::createYes()); } return $this->scopeFactory->create( diff --git a/tests/PHPStan/Analyser/nsrt/bug-1348.php b/tests/PHPStan/Analyser/nsrt/bug-1348.php new file mode 100644 index 00000000000..4a0192c8a6d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-1348.php @@ -0,0 +1,31 @@ + assertType('object', $this); + +class Foo +{ + public function doFoo(): void + { + $closure = function () { + assertType('$this(Bug1348Types\Foo)', $this); + }; + + $arrow = fn() => assertType('$this(Bug1348Types\Foo)', $this); + } +} + +$bound = \Closure::bind( + function () { + assertType('stdClass', $this); + }, + new \stdClass(), + \stdClass::class +); diff --git a/tests/PHPStan/Analyser/nsrt/param-closure-this.php b/tests/PHPStan/Analyser/nsrt/param-closure-this.php index 96f9fb8f159..befa419ce59 100644 --- a/tests/PHPStan/Analyser/nsrt/param-closure-this.php +++ b/tests/PHPStan/Analyser/nsrt/param-closure-this.php @@ -153,36 +153,36 @@ public function interplayWithProcessImmediatelyCalledCallable2(): void } function (Foo $f): void { - assertType('*ERROR*', $this); + assertType('object', $this); $f->paramClosureClass(function () { assertType(Some::class, $this); }); - assertType('*ERROR*', $this); + assertType('object', $this); $f->paramClosureClass(static function () { assertType('*ERROR*', $this); }); - assertType('*ERROR*', $this); + assertType('object', $this); }; function (Foo $f): void { $a = 1; - assertType('*ERROR*', $this); + assertType('object', $this); $f->paramClosureClass(function () use (&$a) { assertType(Some::class, $this); }); - assertType('*ERROR*', $this); + assertType('object', $this); $f->paramClosureClass(static function () use (&$a) { assertType('*ERROR*', $this); }); - assertType('*ERROR*', $this); + assertType('object', $this); }; function (Foo $f): void { - assertType('*ERROR*', $this); + assertType('object', $this); $f->paramClosureClass(fn () => assertType(Some::class, $this)); - assertType('*ERROR*', $this); + assertType('object', $this); $f->paramClosureClass(static fn () => assertType('*ERROR*', $this)); - assertType('*ERROR*', $this); + assertType('object', $this); }; class Bar extends Foo diff --git a/tests/PHPStan/Rules/Debug/DebugScopeRuleTest.php b/tests/PHPStan/Rules/Debug/DebugScopeRuleTest.php index f1bcd81285e..478763c1785 100644 --- a/tests/PHPStan/Rules/Debug/DebugScopeRuleTest.php +++ b/tests/PHPStan/Rules/Debug/DebugScopeRuleTest.php @@ -29,9 +29,11 @@ public function testRuleInPhpStanNamespace(): void '$a (Yes): int', '$b (Yes): int', '$debug (Yes): bool', + '$this (Yes): object', 'native $a (Yes): int', 'native $b (Yes): int', 'native $debug (Yes): bool', + 'native $this (Yes): object', ]), 10, ], @@ -40,10 +42,12 @@ public function testRuleInPhpStanNamespace(): void '$a (Yes): int', '$b (Yes): int', '$debug (Yes): bool', + '$this (Yes): object', '$c (Maybe): 1', 'native $a (Yes): int', 'native $b (Yes): int', 'native $debug (Yes): bool', + 'native $this (Yes): object', 'native $c (Maybe): 1', 'condition about $c #1: if $debug=false then $c is *ERROR* (No)', 'condition about $c #2: if $debug=true then $c is 1 (Yes)', @@ -58,7 +62,9 @@ public function testPr4663(): void $this->analyse([__DIR__ . '/data/pr-4663.php'], [ [ implode("\n", [ + '$this (Yes): object', "\$result (Yes): 'no matches!'", + 'native $this (Yes): object', "native \$result (Yes): 'no matches!'", ]), 11, diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index daf1eec4f64..8494dc765c5 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -703,10 +703,6 @@ public function testFormerThisVariableRule(): void 'Undefined variable: $this', 20, ], - [ - 'Undefined variable: $this', - 26, - ], [ 'Undefined variable: $this', 38, @@ -1562,4 +1558,22 @@ public function testBug10729(): void ]); } + public function testBug1348(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->analyse([__DIR__ . '/data/bug-1348.php'], [ + [ + 'Undefined variable: $this', + 25, + ], + [ + 'Undefined variable: $this', + 28, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-1348.php b/tests/PHPStan/Rules/Variables/data/bug-1348.php new file mode 100644 index 00000000000..e43e5564e7b --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-1348.php @@ -0,0 +1,28 @@ +foo = 'bar'; +}; + +$object = new \stdClass(); + +\Closure::bind($closure, $object, $object)(); +\Closure::bind( + function () { + $this->foo = 'bar'; + }, + $object, + $object +)(); + +// arrow function case +$arrow = fn() => $this; + +// static closures should still report $this as undefined +static function () { + $this->foo = 'bar'; +}; + +static fn() => $this;