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
92 changes: 92 additions & 0 deletions src/Analyser/ArrayByRefItemSlots.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ArrayDimFetch;
use PHPStan\Node\Expr\TypeExpr;
use PHPStan\Type\IntegerType;
use function count;
use function is_int;

/**
* Resolves the `&` items of an array literal to the array slots they alias.
*
* The slot keeps aliasing the referenced expression for as long as it exists - copying
* the array preserves its reference items - so a write into the slot is a write into
* the referenced expression and the other way around.
*/
final class ArrayByRefItemSlots
{

/**
* Slots aliased by the `&` items of $array, including the items of nested array literals.
* The returned slot expressions are dim fetches rooted at $rootExpr.
*
* @return list<array{Expr, ArrayDimFetch}> pairs of [referenced expression, slot expression]
*/
public static function resolve(Scope $scope, Expr\Array_ $array, Expr $rootExpr): array
{
$slots = [];
self::collect($scope, $array, $rootExpr, $slots);

return $slots;
}

/**
* @param list<array{Expr, ArrayDimFetch}> $slots
* @param-out list<array{Expr, ArrayDimFetch}> $slots
*/
private static function collect(Scope $scope, Expr\Array_ $array, Expr $parentExpr, array &$slots): void
{
$implicitIndex = 0;
foreach ($array->items as $arrayItem) {
if ($arrayItem->unpack) {
// The unpacked array shifts every subsequent implicit index by an
// unknown amount, and its own items cannot be referenced from here.
$implicitIndex = null;
continue;
}

if ($arrayItem->key !== null) {
$keyType = $scope->getType($arrayItem->key)->toArrayKey();

if ($implicitIndex !== null) {
$keyValues = $keyType->getConstantScalarValues();
if (count($keyValues) === 1) {
$keyValue = $keyValues[0];
if (is_int($keyValue) && $keyValue >= $implicitIndex) {
$implicitIndex = $keyValue + 1;
}
} elseif (!$keyType->isInteger()->no()) {

Check warning on line 62 in src/Analyser/ArrayByRefItemSlots.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if (is_int($keyValue) && $keyValue >= $implicitIndex) { $implicitIndex = $keyValue + 1; } - } elseif (!$keyType->isInteger()->no()) { + } elseif ($keyType->isInteger()->yes()) { // Key could be an integer, but we don't know which one, // so subsequent implicit indices are unpredictable $implicitIndex = null;
// Key could be an integer, but we don't know which one,
// so subsequent implicit indices are unpredictable
$implicitIndex = null;
}
}

$dimExpr = $arrayItem->key;
} elseif ($implicitIndex !== null) {
$dimExpr = new Node\Scalar\Int_($implicitIndex);
$implicitIndex++;
} else {
$dimExpr = new TypeExpr(new IntegerType());
}

$dimFetchExpr = new ArrayDimFetch($parentExpr, $dimExpr);

if ($arrayItem->value instanceof Expr\Array_) {
self::collect($scope, $arrayItem->value, $dimFetchExpr, $slots);
continue;
}

if (!$arrayItem->byRef) {
continue;
}

$slots[] = [$arrayItem->value, $dimFetchExpr];
}
}

}
41 changes: 4 additions & 37 deletions src/Analyser/ExprHandler/AssignHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
use PHPStan\Analyser\ArrayByRefItemSlots;
use PHPStan\Analyser\AssignTargetWalkMode;
use PHPStan\Analyser\ConditionalExpressionHolder;
use PHPStan\Analyser\ExpressionContext;
Expand Down Expand Up @@ -68,7 +69,6 @@
use PHPStan\Type\ConstantTypeHelper;
use PHPStan\Type\ErrorType;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\NullType;
Expand All @@ -86,7 +86,6 @@
use function array_slice;
use function count;
use function in_array;
use function is_int;
use function is_string;

/**
Expand Down Expand Up @@ -1695,44 +1694,12 @@ private function isImplicitArrayCreation(array $dimFetchStack, Scope $scope): Tr

private function processArrayByRefItems(MutatingScope $scope, string $rootVarName, Expr\Array_ $arrayExpr, Expr $parentExpr): MutatingScope
{
$implicitIndex = 0;
foreach ($arrayExpr->items as $arrayItem) {
if ($arrayItem->key !== null) {
$keyType = $scope->getType($arrayItem->key)->toArrayKey();

if ($implicitIndex !== null) {
$keyValues = $keyType->getConstantScalarValues();
if (count($keyValues) === 1) {
$keyValue = $keyValues[0];
if (is_int($keyValue) && $keyValue >= $implicitIndex) {
$implicitIndex = $keyValue + 1;
}
} elseif (!$keyType->isInteger()->no()) {
// Key could be an integer, but we don't know which one,
// so subsequent implicit indices are unpredictable
$implicitIndex = null;
}
}

$dimExpr = $arrayItem->key;
} elseif ($implicitIndex !== null) {
$dimExpr = new Node\Scalar\Int_($implicitIndex);
$implicitIndex++;
} else {
$dimExpr = new TypeExpr(new IntegerType());
}

if ($arrayItem->value instanceof Expr\Array_) {
$dimFetchExpr = new ArrayDimFetch($parentExpr, $dimExpr);
$scope = $this->processArrayByRefItems($scope, $rootVarName, $arrayItem->value, $dimFetchExpr);
}

if (!$arrayItem->byRef || !$arrayItem->value instanceof Variable || !is_string($arrayItem->value->name)) {
foreach (ArrayByRefItemSlots::resolve($scope, $arrayExpr, $parentExpr) as [$referencedExpr, $dimFetchExpr]) {
if (!$referencedExpr instanceof Variable || !is_string($referencedExpr->name)) {
continue;
}

$refVarName = $arrayItem->value->name;
$dimFetchExpr = new ArrayDimFetch($parentExpr, $dimExpr);
$refVarName = $referencedExpr->name;
$refType = $scope->getType(new Variable($refVarName));
$refNativeType = $scope->getNativeType(new Variable($refVarName));

Expand Down
39 changes: 39 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -2880,6 +2880,45 @@
return array_key_exists($exprString, $this->currentlyAllowedUndefinedExpressions);
}

/**
* Types of the expressions aliased by `&` array items (`$array = [&$v]`) of $variableName,
* resolved from $arrayType as if that was the array's new value. The slot keeps aliasing
* the referenced expression even after the array is copied, so a write into the slot is
* a write into the referenced expression.
*
* @return list<array{Expr, Type}> pairs of [referenced expression, slot type]
*/
public function resolveByRefArrayItemTypes(string $variableName, Type $arrayType): array
{
$itemTypes = [];
foreach ($this->expressionTypes as $expressionType) {
$expr = $expressionType->getExpr();
if (!$expr instanceof IntertwinedVariableByReferenceWithExpr) {
continue;
}
if (!$expressionType->getCertainty()->yes()) {

Check warning on line 2899 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 (!$expr instanceof IntertwinedVariableByReferenceWithExpr) { continue; } - if (!$expressionType->getCertainty()->yes()) { + if ($expressionType->getCertainty()->no()) { continue; } if ($expr->getVariableName() !== $variableName) {
continue;
}
if ($expr->getVariableName() !== $variableName) {
continue;
}
$assignedExpr = $expr->getAssignedExpr();
if (
!$assignedExpr instanceof Expr\ArrayDimFetch
|| ScopeOps::getIntertwinedRefRootVariableName($assignedExpr) !== $variableName
) {
continue;
}

$itemTypes[] = [
$expr->getExpr(),
$this->resolveIntertwinedAssignedType($this, $arrayType, $assignedExpr, $variableName, false),
];
}

return $itemTypes;
}

/**
* @param list<string> $intertwinedPropagatedFrom
*/
Expand Down
113 changes: 113 additions & 0 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -2326,6 +2326,37 @@ public function processArgs(
}
}

foreach ($args as $i => $arg) {
if ($arg->unpack) {
// spread elements land on parameters this loop cannot map, and PHP
// does not carry the reference through the unpacking anyway
continue;
}

$currentParameter = null;
if ($writebackParameters !== null) {
if (isset($writebackParameters[$i])) {
$currentParameter = $writebackParameters[$i];
} elseif (count($writebackParameters) > 0 && $writebackAcceptor->isVariadic()) {
$currentParameter = array_last($writebackParameters);
}
}

if ($currentParameter !== null && $currentParameter->passedByReference()->createsNewVariable()) {
// the by-reference writeback above already propagates through the slots
continue;
}

$scope = $this->processByRefArrayItemsPassedByValue(
$scope,
$storage,
$stmt,
$arg->value,
$currentParameter !== null ? $currentParameter->getType() : new MixedType(),
$nodeCallback,
);
}

// not storing this, it's scope after processing all args
return new ArgsResult(
$this->expressionResultFactory->create($scope, $scope, $callLike, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints),
Expand Down Expand Up @@ -2612,6 +2643,88 @@ private function getParameterOutExtensionsType(CallLike $callLike, $calleeReflec
return null;
}

/**
* A `&$v` item in an array literal keeps aliasing $v after the array is copied into
* the callee, so anything the callee writes into that slot lands in $v. The same holds
* for an array variable built with by-reference items and only then passed to the call.
*
* @param callable(Node $node, Scope $scope): void $nodeCallback
*/
private function processByRefArrayItemsPassedByValue(
MutatingScope $scope,
ExpressionResultStorage $storage,
Node\Stmt $stmt,
Expr $argValue,
Type $parameterType,
callable $nodeCallback,
): MutatingScope
{
foreach ($this->resolveByRefArrayItemTypes($scope, $argValue, $parameterType) as [$referencedExpr, $slotType]) {
if ($referencedExpr instanceof Variable && $referencedExpr->name === 'this') {
continue;
}

if ($scope->hasExpressionType($referencedExpr)->yes()) {
// the callee does not have to write into the slot at all
$slotType = TypeCombinator::union($scope->getType($referencedExpr), $slotType);
}

$scope = $this->processVirtualAssign(
$scope,
$storage,
$stmt,
$referencedExpr,
new TypeExpr($slotType),
$nodeCallback,
)->getScope();
}

return $scope;
}

/**
* By-reference array item slots of $argValue, each with the type the callee can
* write through the reference - the slot read off $parameterType.
*
* @return list<array{Expr, Type}> pairs of [referenced expression, slot type]
*/
private function resolveByRefArrayItemTypes(MutatingScope $scope, Expr $argValue, Type $parameterType): array
{
if ($argValue instanceof Expr\Array_) {
// rooting the slot expressions at the parameter type resolves the offsets
// through the usual dim fetch reading
$slots = [];
foreach (ArrayByRefItemSlots::resolve($scope, $argValue, new TypeExpr($parameterType)) as [$referencedExpr, $slotExpr]) {
if (!$this->isByRefArrayItemWritable($referencedExpr)) {
continue;
}

$slots[] = [$referencedExpr, $scope->getType($slotExpr)];
}

return $slots;
}

if ($argValue instanceof Variable && is_string($argValue->name)) {
// the array was built with by-reference items earlier - the slots are
// already recorded in the scope
return $scope->resolveByRefArrayItemTypes($argValue->name, $parameterType);
}

return [];
}

private function isByRefArrayItemWritable(Expr $expr): bool
{
if ($expr instanceof Variable) {
return is_string($expr->name);
}

return $expr instanceof PropertyFetch
|| $expr instanceof StaticPropertyFetch
|| $expr instanceof ArrayDimFetch;
}

/**
* @param callable(Node $node, Scope $scope): void $nodeCallback
*/
Expand Down
Loading
Loading