Skip to content

Commit af67d36

Browse files
github-actions[bot]phpstan-bot
authored andcommitted
Fix arrow functions inheriting property type narrowings from parent scope
- Arrow functions inherited all expression types from the parent scope, including property narrowings from assignments (e.g. $this->prop = []) - Closures correctly reset property types by building a fresh scope, but arrow functions did not - Filter out non-readonly PropertyFetch narrowings in enterArrowFunctionWithoutReflection, matching closure behavior - Fix TemplateTypeTrait to capture $this->default in a local variable before passing to arrow function - New regression test in tests/PHPStan/Analyser/nsrt/bug-13563.php
1 parent 54f3522 commit af67d36

File tree

3 files changed

+85
-3
lines changed

3 files changed

+85
-3
lines changed

src/Analyser/MutatingScope.php

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2321,13 +2321,39 @@ public function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFun
23212321
$arrowFunctionScope = $arrowFunctionScope->invalidateExpression(new Variable('this'));
23222322
}
23232323

2324+
$filteredExpressionTypes = $this->invalidateStaticExpressions($arrowFunctionScope->expressionTypes);
2325+
$filteredNativeExpressionTypes = $arrowFunctionScope->nativeExpressionTypes;
2326+
2327+
if (!$arrowFunction->static && $this->hasVariableType('this')->yes()) {
2328+
foreach ($filteredExpressionTypes as $exprString => $typeHolder) {
2329+
$expr = $typeHolder->getExpr();
2330+
if (!$expr instanceof PropertyFetch) {
2331+
continue;
2332+
}
2333+
if ($this->isReadonlyPropertyFetch($expr, true)) {
2334+
continue;
2335+
}
2336+
unset($filteredExpressionTypes[$exprString]);
2337+
}
2338+
foreach ($filteredNativeExpressionTypes as $exprString => $typeHolder) {
2339+
$expr = $typeHolder->getExpr();
2340+
if (!$expr instanceof PropertyFetch) {
2341+
continue;
2342+
}
2343+
if ($this->isReadonlyPropertyFetch($expr, true)) {
2344+
continue;
2345+
}
2346+
unset($filteredNativeExpressionTypes[$exprString]);
2347+
}
2348+
}
2349+
23242350
return $this->scopeFactory->create(
23252351
$arrowFunctionScope->context,
23262352
$this->isDeclareStrictTypes(),
23272353
$arrowFunctionScope->getFunction(),
23282354
$arrowFunctionScope->getNamespace(),
2329-
$this->invalidateStaticExpressions($arrowFunctionScope->expressionTypes),
2330-
$arrowFunctionScope->nativeExpressionTypes,
2355+
$filteredExpressionTypes,
2356+
$filteredNativeExpressionTypes,
23312357
$arrowFunctionScope->conditionalExpressions,
23322358
$arrowFunctionScope->inClosureBindScopeClasses,
23332359
new ClosureType(),

src/Type/Generic/TemplateTypeTrait.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ public function describe(VerbosityLevel $level): string
7474
}
7575
$defaultDescription = '';
7676
if ($this->default !== null) {
77-
$recursionGuard = RecursionGuard::runOnObjectIdentity($this->default, fn () => $this->default->describe($level));
77+
$default = $this->default;
78+
$recursionGuard = RecursionGuard::runOnObjectIdentity($default, static fn () => $default->describe($level));
7879
if (!$recursionGuard instanceof ErrorType) {
7980
$defaultDescription .= sprintf(' = %s', $recursionGuard);
8081
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug13563;
4+
5+
use DateTime;
6+
use function PHPStan\Testing\assertType;
7+
8+
class Invoker
9+
{
10+
/**
11+
* @var array<string, \Closure>
12+
*/
13+
private array $callbacks = [];
14+
15+
public function willReturnCallback(string $method, callable $callback): void
16+
{
17+
$this->callbacks[$method] = \Closure::fromCallable($callback);
18+
}
19+
}
20+
21+
class MyTest
22+
{
23+
/**
24+
* @var array<int, DateTime>
25+
*/
26+
private array $dates = [];
27+
28+
/**
29+
* @var array<int, DateTime>
30+
*/
31+
private array $propNotCleared = [];
32+
33+
public function setUp(): void
34+
{
35+
$invoker = new Invoker();
36+
$this->dates = [];
37+
38+
assertType('array{}', $this->dates);
39+
40+
// Arrow function should see the declared property type, not the narrowed array{} type
41+
$invoker->willReturnCallback('get', fn (int $id) => assertType('array<int, DateTime>', $this->dates));
42+
43+
// Closure correctly sees the declared property type
44+
$invoker->willReturnCallback('get', function (int $id) {
45+
assertType('array<int, DateTime>', $this->dates);
46+
});
47+
48+
// Property not cleared - both should see the declared type
49+
assertType('array<int, DateTime>', $this->propNotCleared);
50+
$invoker->willReturnCallback('get', fn (int $id) => assertType('array<int, DateTime>', $this->propNotCleared));
51+
$invoker->willReturnCallback('get', function (int $id) {
52+
assertType('array<int, DateTime>', $this->propNotCleared);
53+
});
54+
}
55+
}

0 commit comments

Comments
 (0)