Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -2829,6 +2829,33 @@
return $filteredExpressionTypes;
}

/**
* @param array<string, ExpressionTypeHolder> $expressionTypes
* @return array<string, ExpressionTypeHolder>
*/
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
Expand Down Expand Up @@ -2901,13 +2928,20 @@
$arrowFunctionScope = $arrowFunctionScope->invalidateExpression(new Variable('this'));
}

$expressionTypes = $this->invalidateStaticExpressions($arrowFunctionScope->expressionTypes);
$nativeExpressionTypes = $arrowFunctionScope->nativeExpressionTypes;
if (!$arrowFunction->static && $this->hasVariableType('this')->yes()) {

Check warning on line 2933 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $expressionTypes = $this->invalidateStaticExpressions($arrowFunctionScope->expressionTypes); $nativeExpressionTypes = $arrowFunctionScope->nativeExpressionTypes; - if (!$arrowFunction->static && $this->hasVariableType('this')->yes()) { + if (!$arrowFunction->static && !$this->hasVariableType('this')->no()) { $expressionTypes = $this->invalidateNonReadonlyPropertyFetches($expressionTypes); $nativeExpressionTypes = $this->invalidateNonReadonlyPropertyFetches($nativeExpressionTypes); }

Check warning on line 2933 in src/Analyser/MutatingScope.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $expressionTypes = $this->invalidateStaticExpressions($arrowFunctionScope->expressionTypes); $nativeExpressionTypes = $arrowFunctionScope->nativeExpressionTypes; - if (!$arrowFunction->static && $this->hasVariableType('this')->yes()) { + if (!$arrowFunction->static && !$this->hasVariableType('this')->no()) { $expressionTypes = $this->invalidateNonReadonlyPropertyFetches($expressionTypes); $nativeExpressionTypes = $this->invalidateNonReadonlyPropertyFetches($nativeExpressionTypes); }
$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(),
Expand Down
5 changes: 3 additions & 2 deletions src/Type/Generic/TemplateTypeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
55 changes: 55 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13563.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php declare(strict_types=1);

namespace Bug13563;

use function PHPStan\Testing\assertType;

class Invoker
{
/**
* @var array<string, \Closure>
*/
private array $callbacks = [];

public function willReturnCallback(string $method, callable $callback): void
{
$this->callbacks[$method] = \Closure::fromCallable($callback);
}
}

class MyTest
{
/**
* @var array<int, \DateTime>
*/
private array $dates = [];

/**
* @var array<int, \DateTime>
*/
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<int, DateTime>', $this->dates));

// Closure sees the PHPDoc type - this works correctly
$invoker->willReturnCallback('get2', function (int $id) {
assertType('array<int, DateTime>', $this->dates);
});

// Property not cleared - both should see PHPDoc type
$invoker->willReturnCallback('get3', fn (int $id) => assertType('array<int, DateTime>', $this->propNotCleared));

$invoker->willReturnCallback('get4', function (int $id) {
assertType('array<int, DateTime>', $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);
}
}
Loading