diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 6be5088a39..a2d281b4f4 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2829,6 +2829,33 @@ private function invalidateStaticExpressions(array $expressionTypes): array return $filteredExpressionTypes; } + /** + * @param array $expressionTypes + * @return array + */ + private function invalidateNonReadonlyPropertyFetches(array $expressionTypes): array + { + $filteredExpressionTypes = []; + $nodeFinder = new NodeFinder(); + foreach ($expressionTypes as $exprString => $expressionType) { + $expr = $expressionType->getExpr(); + + $propertyFetch = $nodeFinder->findFirst( + [$expr], + fn ($node) => $node instanceof PropertyFetch + && $node->var instanceof Variable + && $node->var->name === 'this' + && !$this->isReadonlyPropertyFetch($node, true), + ); + if ($propertyFetch !== null) { + continue; + } + + $filteredExpressionTypes[$exprString] = $expressionType; + } + return $filteredExpressionTypes; + } + /** * @api * @param ParameterReflection[]|null $callableParameters @@ -2901,13 +2928,20 @@ private function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFu $arrowFunctionScope = $arrowFunctionScope->invalidateExpression(new Variable('this')); } + $expressionTypes = $this->invalidateStaticExpressions($arrowFunctionScope->expressionTypes); + $nativeExpressionTypes = $arrowFunctionScope->nativeExpressionTypes; + if (!$arrowFunction->static && $this->hasVariableType('this')->yes()) { + $expressionTypes = $this->invalidateNonReadonlyPropertyFetches($expressionTypes); + $nativeExpressionTypes = $this->invalidateNonReadonlyPropertyFetches($nativeExpressionTypes); + } + return $this->scopeFactory->create( $arrowFunctionScope->context, $this->isDeclareStrictTypes(), $arrowFunctionScope->getFunction(), $arrowFunctionScope->getNamespace(), - $this->invalidateStaticExpressions($arrowFunctionScope->expressionTypes), - $arrowFunctionScope->nativeExpressionTypes, + $expressionTypes, + $nativeExpressionTypes, $arrowFunctionScope->conditionalExpressions, $arrowFunctionScope->inClosureBindScopeClasses, new ClosureType(), diff --git a/src/Type/Generic/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index a1f381e6f5..3b60dd3eaf 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -72,8 +72,9 @@ public function describe(VerbosityLevel $level): string $boundDescription = sprintf(' of %s', $this->bound->describe($level)); } $defaultDescription = ''; - if ($this->default !== null) { - $recursionGuard = RecursionGuard::runOnObjectIdentity($this->default, fn () => $this->default->describe($level)); + $default = $this->default; + if ($default !== null) { + $recursionGuard = RecursionGuard::runOnObjectIdentity($default, fn () => $default->describe($level)); if (!$recursionGuard instanceof ErrorType) { $defaultDescription .= sprintf(' = %s', $recursionGuard); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-13563.php b/tests/PHPStan/Analyser/nsrt/bug-13563.php new file mode 100644 index 0000000000..0d782eb644 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13563.php @@ -0,0 +1,55 @@ + + */ + private array $callbacks = []; + + public function willReturnCallback(string $method, callable $callback): void + { + $this->callbacks[$method] = \Closure::fromCallable($callback); + } +} + +class MyTest +{ + /** + * @var array + */ + private array $dates = []; + + /** + * @var array + */ + private array $propNotCleared = []; + + public function setUp(): void + { + $invoker = new Invoker(); + $this->dates = []; + + // Arrow function should see the PHPDoc type, not the narrowed array{} from parent scope + $invoker->willReturnCallback('get1', fn (int $id) => assertType('array', $this->dates)); + + // Closure sees the PHPDoc type - this works correctly + $invoker->willReturnCallback('get2', function (int $id) { + assertType('array', $this->dates); + }); + + // Property not cleared - both should see PHPDoc type + $invoker->willReturnCallback('get3', fn (int $id) => assertType('array', $this->propNotCleared)); + + $invoker->willReturnCallback('get4', function (int $id) { + assertType('array', $this->propNotCleared); + }); + + // Arrow function accessing property via array dim fetch - should also use PHPDoc type + $invoker->willReturnCallback('get5', fn (int $id): ?\DateTime => $this->dates[$id] ?? null); + } +}