Skip to content
Open
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
7 changes: 7 additions & 0 deletions src/Analyser/ExprHandler/AssignHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -1275,6 +1275,13 @@ public function applyWrite(
$this->readAssignedValueType($nodeScopeResolver, $storedAssignedExprResult, $assignedExpr, $scopeBeforeAssignEval->doNotTreatPhpDocTypesAsCertain()),
);
}

if (!is_string($var->name)) {
// a dynamic $$name write can target any variable, including the
// foreach value/key/iteratee - drop the value aliases so a later
// narrowing is not projected onto a dim fetch it may have desynced
$scope = $scope->invalidateForeachValueAliases();
}
} elseif ($kind === PreparedAssignTarget::KIND_ARRAY_DIM_FETCH) {
if (!$var instanceof ArrayDimFetch) {
throw new ShouldNotHappenException();
Expand Down
181 changes: 163 additions & 18 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use PHPStan\Node\EmitCollectedDataNode;
use PHPStan\Node\Expr\AlwaysRememberedExpr;
use PHPStan\Node\Expr\CloneReinitializationExpr;
use PHPStan\Node\Expr\ForeachValueAliasExpr;
use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr;
use PHPStan\Node\Expr\NativeTypeExpr;
use PHPStan\Node\Expr\OriginalForeachKeyExpr;
Expand Down Expand Up @@ -505,25 +506,30 @@

public function afterExtractCall(): self
{
return $this->scopeFactory->create(
$this->context,
$this->isDeclareStrictTypes(),
$this->getFunction(),
$this->getNamespace(),
$this->expressionTypes,
$this->nativeExpressionTypes,
// extract() may (re)define any variable, including the foreach value/key/
// iteratee - drop the value aliases so a later narrowing is not projected
// onto a dim fetch the extracted values may have desynced.
$scope = $this->invalidateForeachValueAliases();

return $scope->scopeFactory->create(
$scope->context,
$scope->isDeclareStrictTypes(),
$scope->getFunction(),
$scope->getNamespace(),
$scope->expressionTypes,
$scope->nativeExpressionTypes,
[],
$this->inClosureBindScopeClasses,
$this->anonymousFunctionReflection,
$this->isInFirstLevelStatement(),
$this->currentlyAssignedExpressions,
$this->currentlyAllowedUndefinedExpressions,
$this->inFunctionCallsStack,
$scope->inClosureBindScopeClasses,
$scope->anonymousFunctionReflection,
$scope->isInFirstLevelStatement(),
$scope->currentlyAssignedExpressions,
$scope->currentlyAllowedUndefinedExpressions,
$scope->inFunctionCallsStack,
true,
$this->parentScope,
$this->nativeTypesPromoted,
$this->templateArgumentFrame,
$this->templateArgumentConstraints,
$scope->parentScope,
$scope->nativeTypesPromoted,
$scope->templateArgumentFrame,
$scope->templateArgumentConstraints,
);
}

Expand Down Expand Up @@ -3008,7 +3014,7 @@
return $this->assignExpression($condExpr, $type, $nativeType);
}

public function enterForeach(self $originalScope, Expr $iteratee, Type $iterateeType, Type $nativeIterateeType, string $valueName, ?string $keyName, bool $valueByRef): self
public function enterForeach(self $originalScope, Expr $iteratee, Type $iterateeType, Type $nativeIterateeType, string $valueName, ?string $keyName, bool $valueByRef, bool $recordValueAlias = true): self
{
$valueType = $originalScope->getIterableValueType($iterateeType);
$nativeValueType = $originalScope->getIterableValueType($nativeIterateeType);
Expand Down Expand Up @@ -3044,6 +3050,23 @@
if ($keyName !== null) {
$scope = $scope->enterForeachKey($originalScope, $iteratee, $iterateeType, $nativeIterateeType, $keyName);

if ($recordValueAlias && $iterateeType->isArray()->yes()) {

Check warning on line 3053 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": @@ @@ if ($keyName !== null) { $scope = $scope->enterForeachKey($originalScope, $iteratee, $iterateeType, $nativeIterateeType, $keyName); - if ($recordValueAlias && $iterateeType->isArray()->yes()) { + if ($recordValueAlias && !$iterateeType->isArray()->no()) { // for the current iteration the value variable and the iteratee dim // fetch alias one runtime value - narrowings landed on the value // variable are projected onto the tracked dim fetch through this
// for the current iteration the value variable and the iteratee dim
// fetch alias one runtime value - narrowings landed on the value
// variable are projected onto the tracked dim fetch through this
// link (applySpecifiedTypes()); a write to any of the three
// participating expressions invalidates it through containment.
// The alias is only recorded when the loop body does not mutate the
// iteratee at a foreign key or reassign the key ($recordValueAlias,
// decided in ForeachHandler): a cross-iteration write would desync
// $array[$key] from the snapshot value variable.
$scope = $scope->assignExpression(
new ForeachValueAliasExpr($valueName, new Expr\ArrayDimFetch($iteratee, new Variable($keyName))),
$valueType,
$nativeValueType,
);
}

if ($valueByRef && $iterateeType->isArray()->yes() && $iterateeType->isConstantArray()->no()) {
$scope = $scope->assignExpression(
new IntertwinedVariableByReferenceWithExpr($valueName, new Expr\ArrayDimFetch($iteratee, new Variable($keyName)), new Variable($valueName)),
Expand Down Expand Up @@ -3842,6 +3865,57 @@
);
}

/**
* Drops every foreach value-variable alias (ForeachValueAliasExpr). The alias
* is a precision optimization whose soundness depends on the value variable
* still holding the iteratee element - code paths that write a variable
* without going through the assignment-time containment invalidation (a by-ref
* closure use, extract(), a dynamic $$name write) must sever it here, because
* they may have desynced the value variable from $array[$key]. Losing the alias
* conservatively is always sound; the element type falls back to the iterable
* value type.
*/
public function invalidateForeachValueAliases(): self
{
$changed = false;
$expressionTypes = $this->expressionTypes;
$nativeExpressionTypes = $this->nativeExpressionTypes;
foreach ([$this->expressionTypes, $this->nativeExpressionTypes] as $types) {
foreach (array_keys($types) as $exprString) {
if (!str_starts_with($exprString, ForeachValueAliasExpr::KEY_PREFIX)) {
continue;
}

unset($expressionTypes[$exprString]);
unset($nativeExpressionTypes[$exprString]);
$changed = true;
}
}

if (!$changed) {
return $this;
}

return $this->scopeFactory->create(
$this->context,
$this->isDeclareStrictTypes(),
$this->getFunction(),
$this->getNamespace(),
$expressionTypes,
$nativeExpressionTypes,
$this->conditionalExpressions,
$this->inClosureBindScopeClasses,
$this->anonymousFunctionReflection,
$this->isInFirstLevelStatement(),
$this->currentlyAssignedExpressions,
$this->currentlyAllowedUndefinedExpressions,
$this->inFunctionCallsStack,
$this->afterExtractCall,
$this->parentScope,
$this->nativeTypesPromoted,
);
}

/** @internal called by ScopeOps */
public function isPrivatePropertyOfDifferentClass(Expr $expr, ClassReflection $invalidatingClass): bool
{
Expand Down Expand Up @@ -4233,6 +4307,61 @@
$specifiedExpressions[$exprString] = ExpressionTypeHolder::createYes($expr, $holderType);
}

// while a foreach value variable still aliases the iteratee dim fetch
// (ForeachValueAliasExpr link intact - none of the participating
// expressions were written), a narrowing landed on the value variable
// also narrows the tracked dim fetch: they hold the same runtime value
foreach ($specifiedExpressions as $specifiedHolder) {
$specifiedExpr = $specifiedHolder->getExpr();
if (!$specifiedExpr instanceof Variable || !is_string($specifiedExpr->name)) {
continue;
}
$aliasHolder = $scope->expressionTypes[ForeachValueAliasExpr::key($specifiedExpr->name)] ?? null;
if ($aliasHolder === null || !$aliasHolder->getCertainty()->yes()) {
continue;
}
$aliasExpr = $aliasHolder->getExpr();
if (!$aliasExpr instanceof ForeachValueAliasExpr) {
continue;
}
$valueExprString = '$' . $specifiedExpr->name;
$valueHolder = $scope->expressionTypes[$valueExprString] ?? null;
$valueNativeHolder = $scope->nativeExpressionTypes[$valueExprString] ?? null;
if (
$valueHolder === null || !$valueHolder->getCertainty()->yes()

Check warning on line 4331 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": @@ @@ $valueHolder = $scope->expressionTypes[$valueExprString] ?? null; $valueNativeHolder = $scope->nativeExpressionTypes[$valueExprString] ?? null; if ( - $valueHolder === null || !$valueHolder->getCertainty()->yes() + $valueHolder === null || $valueHolder->getCertainty()->no() || $valueNativeHolder === null || !$valueNativeHolder->getCertainty()->yes() ) { continue;
|| $valueNativeHolder === null || !$valueNativeHolder->getCertainty()->yes()

Check warning on line 4332 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": @@ @@ $valueNativeHolder = $scope->nativeExpressionTypes[$valueExprString] ?? null; if ( $valueHolder === null || !$valueHolder->getCertainty()->yes() - || $valueNativeHolder === null || !$valueNativeHolder->getCertainty()->yes() + || $valueNativeHolder === null || $valueNativeHolder->getCertainty()->no() ) { continue; }
) {
continue;
}
$dimFetchExpr = $aliasExpr->getDimFetch();
$dimFetchString = $scope->getNodeKey($dimFetchExpr);
$dimFetchHolder = $scope->expressionTypes[$dimFetchString] ?? null;
$dimFetchNativeHolder = $scope->nativeExpressionTypes[$dimFetchString] ?? null;
if (
$dimFetchHolder === null || !$dimFetchHolder->getCertainty()->yes()

Check warning on line 4341 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": @@ @@ $dimFetchHolder = $scope->expressionTypes[$dimFetchString] ?? null; $dimFetchNativeHolder = $scope->nativeExpressionTypes[$dimFetchString] ?? null; if ( - $dimFetchHolder === null || !$dimFetchHolder->getCertainty()->yes() + $dimFetchHolder === null || $dimFetchHolder->getCertainty()->no() || $dimFetchNativeHolder === null || !$dimFetchNativeHolder->getCertainty()->yes() ) { continue;
|| $dimFetchNativeHolder === null || !$dimFetchNativeHolder->getCertainty()->yes()

Check warning on line 4342 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": @@ @@ $dimFetchNativeHolder = $scope->nativeExpressionTypes[$dimFetchString] ?? null; if ( $dimFetchHolder === null || !$dimFetchHolder->getCertainty()->yes() - || $dimFetchNativeHolder === null || !$dimFetchNativeHolder->getCertainty()->yes() + || $dimFetchNativeHolder === null || $dimFetchNativeHolder->getCertainty()->no() ) { continue; }
) {
continue;
}
if ($scope->isComplexUnionType($dimFetchHolder->getType())) {
continue;
}

$newDimFetchType = TypeCombinator::intersect($dimFetchHolder->getType(), $valueHolder->getType());
$newDimFetchNativeType = TypeCombinator::intersect($dimFetchNativeHolder->getType(), $valueNativeHolder->getType());
if (
$newDimFetchType->equals($dimFetchHolder->getType())
&& $newDimFetchNativeType->equals($dimFetchNativeHolder->getType())
) {
continue;
}
if (!$scopeIsWorkingCopy) {
$scope = $scope->openSpecificationScope();
$scopeIsWorkingCopy = true;
}
$scope->specifyExpressionTypeInPlace($dimFetchExpr, $newDimFetchType, $newDimFetchNativeType, TrinaryLogic::createYes());
}

$scope = $scope->processConditionalExpressionsAfterSpecifying($specifiedExpressions);

$newConditionalExpressionHolders = $specifiedTypes->getNewConditionalExpressionHolders();
Expand Down Expand Up @@ -4812,6 +4941,22 @@
$nativeExpressionTypes[$variableExprString] = $holder;
}

// The by-ref uses above write variable holders directly, bypassing the
// assignment-time containment invalidation. A by-ref use may write the
// foreach value/key/iteratee variable, desyncing the value variable from
// $array[$key] - drop the value aliases so a later narrowing is not
// projected onto the tracked dim fetch.
foreach ([$expressionTypes, $nativeExpressionTypes] as $types) {
foreach (array_keys($types) as $exprString) {
if (!str_starts_with($exprString, ForeachValueAliasExpr::KEY_PREFIX)) {
continue;
}

unset($expressionTypes[$exprString]);
unset($nativeExpressionTypes[$exprString]);
}
}

return $this->scopeFactory->create(
$this->context,
$this->isDeclareStrictTypes(),
Expand Down
Loading
Loading