Skip to content

Commit df1a732

Browse files
github-actions[bot]VincentLanglet
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 5e40360 commit df1a732

3 files changed

Lines changed: 85 additions & 3 deletions

File tree

src/Analyser/MutatingScope.php

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

2250+
$filteredExpressionTypes = $this->invalidateStaticExpressions($arrowFunctionScope->expressionTypes);
2251+
$filteredNativeExpressionTypes = $arrowFunctionScope->nativeExpressionTypes;
2252+
2253+
if (!$arrowFunction->static && $this->hasVariableType('this')->yes()) {
2254+
foreach ($filteredExpressionTypes as $exprString => $typeHolder) {
2255+
$expr = $typeHolder->getExpr();
2256+
if (!$expr instanceof PropertyFetch) {
2257+
continue;
2258+
}
2259+
if ($this->isReadonlyPropertyFetch($expr, true)) {
2260+
continue;
2261+
}
2262+
unset($filteredExpressionTypes[$exprString]);
2263+
}
2264+
foreach ($filteredNativeExpressionTypes as $exprString => $typeHolder) {
2265+
$expr = $typeHolder->getExpr();
2266+
if (!$expr instanceof PropertyFetch) {
2267+
continue;
2268+
}
2269+
if ($this->isReadonlyPropertyFetch($expr, true)) {
2270+
continue;
2271+
}
2272+
unset($filteredNativeExpressionTypes[$exprString]);
2273+
}
2274+
}
2275+
22502276
return $this->scopeFactory->create(
22512277
$arrowFunctionScope->context,
22522278
$this->isDeclareStrictTypes(),
22532279
$arrowFunctionScope->getFunction(),
22542280
$arrowFunctionScope->getNamespace(),
2255-
$this->invalidateStaticExpressions($arrowFunctionScope->expressionTypes),
2256-
$arrowFunctionScope->nativeExpressionTypes,
2281+
$filteredExpressionTypes,
2282+
$filteredNativeExpressionTypes,
22572283
$arrowFunctionScope->conditionalExpressions,
22582284
$arrowFunctionScope->inClosureBindScopeClasses,
22592285
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)