Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Report constant-offset assignments that are redundant
`$a = [1, 2]; $a[0] = 1;` assigns a value the offset already holds, the
same finding the rule already reports for whole variables. The check
walks the constant dimensions of the target down the array type in both
type flavours; append, dynamic keys, by-reference items and PhpDoc-only
certainty are left alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CZ79ZBiMt4KWgU3uy4Y4j
  • Loading branch information
ondrejmirtes and claude committed Sep 11, 2026
commit 66ff8bb4f6b959c09a8bce519327631ec7de3744
44 changes: 34 additions & 10 deletions src/Analyser/ExprHandler/AssignHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
}
}

$redundantType = $expr instanceof Assign && $expr->var instanceof Variable && is_string($expr->var->name)
? self::redundant($assignedExprResult, $expr->var->name)
$redundantType = $expr instanceof Assign
? self::redundant($assignedExprResult, $expr->var, $storage)
: null;
$variableFlow = VariableFlow::sequence(
VariableFlowBuilder::targetRead($expr->var, $storage, false),
Expand Down Expand Up @@ -2500,21 +2500,45 @@ private static function hasArrayReference(Expr\Array_ $array): bool
return false;
}

private static function redundant(ExpressionResult $rhs, string $name): ?Type
private static function redundant(ExpressionResult $rhs, Expr $target, ExpressionResultStorage $storage): ?Type
{
$scope = $rhs->getScope();
if (!$scope->hasVariableType($name)->yes()) {
return null;
$dimensions = [];
while ($target instanceof ArrayDimFetch) {
if ($target->dim === null) {
return null;
}
$dimResult = $storage->findExpressionResult($target->dim);
if ($dimResult === null || count($dimResult->getType()->toArrayKey()->getConstantScalarValues()) !== 1) {
return null;
}
$dimensions[] = $dimResult;
$target = $target->var;
}
$values = $scope->getVariableType($name)->getFiniteTypes();
if (count($values) !== 1 || !$values[0]->equals($rhs->getType())) {
if (!$target instanceof Variable || !is_string($target->name)) {
return null;
}
$name = $target->name;
$scope = $rhs->getScope();
$nativeScope = $scope->doNotTreatPhpDocTypesAsCertain();
if (!$nativeScope->hasVariableType($name)->yes()) {
if (!$scope->hasVariableType($name)->yes() || !$nativeScope->hasVariableType($name)->yes()) {
return null;
}
$type = $scope->getVariableType($name);
$nativeType = $nativeScope->getVariableType($name);
foreach (array_reverse($dimensions) as $dimension) {
$offset = $dimension->getType()->toArrayKey();
$nativeOffset = $dimension->getNativeType()->toArrayKey();
if (!$type->isArray()->yes() || !$nativeType->isArray()->yes() || !$type->hasOffsetValueType($offset)->yes() || !$nativeType->hasOffsetValueType($nativeOffset)->yes()) {
return null;
}
$type = $type->getOffsetValueType($offset);
$nativeType = $nativeType->getOffsetValueType($nativeOffset);
}
$values = $type->getFiniteTypes();
if (count($values) !== 1 || !$values[0]->equals($rhs->getType())) {
return null;
}
$nativeValues = $nativeScope->getVariableType($name)->getFiniteTypes();
$nativeValues = $nativeType->getFiniteTypes();
return count($nativeValues) === 1 && $nativeValues[0]->equals($rhs->getNativeType()) ? $rhs->getType() : null;
}

Expand Down
13 changes: 9 additions & 4 deletions src/Rules/DeadCode/UnusedVariableRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,19 @@ public function processNode(Node $node, Scope $scope): array
}

$redundantType = $node->getRedundantType($write);
if ($redundantType !== null && $node->isUsed($write)) {
if ($redundantType !== null && ($write->isOffsetWrite() || $node->isUsed($write))) {
$target = $write->getNode();
if (!$target instanceof Node\Expr) {
throw new ShouldNotHappenException();
}
$description = $write->isOffsetWrite() ? 'Offset ' . $this->exprPrinter->printExpr($target) : 'Variable $' . $name;
$errors[] = RuleErrorBuilder::message(sprintf(
'Variable $%s is assigned value %s but it already has that value.',
$name,
'%s is assigned value %s but it already has that value.',
$description,
$redundantType->describe(VerbosityLevel::value()),
))
->identifier('assign.redundant')
->line($write->getNode()->getStartLine())
->line($target->getStartLine())
->build();
continue;
}
Expand Down
17 changes: 17 additions & 0 deletions tests/PHPStan/Rules/DeadCode/UnusedVariableRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,23 @@ public function testOffsets(): void
]);
}

public function testRedundantOffsets(): void
{
$redundant = [];
foreach ($this->gatherAnalyserErrors([__DIR__ . '/data/unused-variable-redundant-offsets.php']) as $error) {
if ($error->getIdentifier() !== 'assign.redundant') {
continue;
}
$redundant[] = [$error->getMessage(), $error->getLine()];
}
$this->assertSame([
['Offset $a[0] is assigned value 1 but it already has that value.', 8],
['Offset $a[0] is assigned value 1 but it already has that value.', 14],
['Offset $a[\'x\'][\'y\'] is assigned value 1 but it already has that value.', 21],
['Offset $a[\'0\'] is assigned value 1 but it already has that value.', 28],
], $redundant);
}

public function testDynamicOffsetOverwritten(): void
{
$errors = $this->gatherAnalyserErrors([__DIR__ . '/data/unused-variable-offset-overwrite.php']);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php declare(strict_types = 1);

namespace UnusedVariableRedundantOffsets;

function unused(): void
{
$a = [1, 2];
$a[0] = 1;
}

function used(): array
{
$a = [1, 2];
$a[0] = 1;
return $a;
}

function nested(): array
{
$a = ['x' => ['y' => 1]];
$a['x']['y'] = 1;
return $a;
}

function coercedKey(): array
{
$a = [1];
$a['0'] = 1;
return $a;
}

function differentValue(): array
{
$a = [1, 2];
$a[0] = 3;
return $a;
}

function newOffset(): array
{
$a = [1];
$a[1] = 1;
return $a;
}

/** @param array{0?: 1} $a */
function optionalOffset(array $a): array
{
$a[0] = 1;
return $a;
}

/** @param array{0: 1} $a */
function phpDocOnly(array $a): array
{
$a[0] = 1;
return $a;
}

function append(): array
{
$a = [1];
$a[] = 1;
return $a;
}

function dynamicKey(int $i): array
{
$a = [1, 2];
$a[$i] = 1;
return $a;
}

function reference(): array
{
$x = 1;
$a = [&$x];
$a[0] = 1;
return $a;
}

function stringOffset(): string
{
$a = '12';
$a[0] = '1';
return $a;
}