Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
68e4440
Remove variable writes that are never read
ondrejmirtes Sep 10, 2026
6cae955
Remove unused private-method parameters
ondrejmirtes Sep 10, 2026
fd4a172
Remove assignments of values the variables already have
ondrejmirtes Sep 10, 2026
471b8fd
Use the assigned variables in E2E fixtures
ondrejmirtes Sep 10, 2026
69e0a94
Use the assigned variables in levels test fixtures
ondrejmirtes Sep 10, 2026
9359d69
Normalize paths in NodeScopeResolver::setAnalysedFiles()
ondrejmirtes Sep 10, 2026
b1c223b
Detect unused variables and parameters with immutable handler results
ondrejmirtes Sep 10, 2026
6a8637a
Bump expected turbo version
ondrejmirtes Sep 10, 2026
b1ba6f8
Cover variable reads in nullsafe call arguments
ondrejmirtes Sep 10, 2026
435856a
Add regression tests for #12789, #13472 and #14258
ondrejmirtes Sep 10, 2026
c1bc35d
Add regression test for #12012
ondrejmirtes Sep 10, 2026
fb7a088
Add regression test for #11483
ondrejmirtes Sep 10, 2026
5645d3d
Add regression test for #10202
ondrejmirtes Sep 10, 2026
69b9979
Cover PHP-version-dependent unused catch variables
ondrejmirtes Sep 10, 2026
bcb75f5
Do not report writes to a variable returned by reference
ondrejmirtes Sep 10, 2026
bbed891
Cover unused writes in statically dead branches
ondrejmirtes Sep 10, 2026
3723cc1
Treat comment-only bodies as no-op stubs in the unused-parameter rules
ondrejmirtes Sep 10, 2026
842976e
Cover array-shift loop inference and statically unreachable assignments
ondrejmirtes Sep 10, 2026
8a3b098
Treat func_get_arg() like func_get_args() when observing parameters
ondrejmirtes Sep 10, 2026
8c268f0
Cover variable liveness across evaluation order and control flow
ondrejmirtes Sep 10, 2026
7fdb62a
Cover variable liveness when foreach scope pollution is disabled
ondrejmirtes Sep 10, 2026
163f9a8
Cover variable usage through globals, static initializers and nested …
ondrejmirtes Sep 10, 2026
610b854
Respect short-circuiting of nullsafe dynamic member names
ondrejmirtes Sep 10, 2026
695d68f
Reuse concrete return types across inference contexts
ondrejmirtes Sep 10, 2026
bf91dae
Keep variables read by fallback Throwable catches live
ondrejmirtes Sep 10, 2026
a33871d
Track types written through variable variables
ondrejmirtes Sep 10, 2026
3f12a51
Preserve array writes made while evaluating an offset assignment
ondrejmirtes Sep 10, 2026
e5e374f
Retain variable mentions in skipped catch bodies
ondrejmirtes Sep 10, 2026
b84e05c
Observe writes through arrow function reference parameters
ondrejmirtes Sep 10, 2026
7d2352b
Include implicit throw points when analysing broad catches
ondrejmirtes Sep 10, 2026
d9ad990
Put the arrow reference fixture PHP version constraint on its first line
ondrejmirtes Sep 10, 2026
a83b91f
Mark reflection parameter checks and type stringification as non-thro…
ondrejmirtes Sep 10, 2026
5195842
Preserve reflection parameter query return types on PHP 7.4
ondrejmirtes Sep 10, 2026
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
Track types written through variable variables
  • Loading branch information
ondrejmirtes committed Sep 10, 2026
commit a33871df872475df112afcecbf4dc4e95215d184
66 changes: 56 additions & 10 deletions src/Analyser/ExprHandler/AssignHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -1253,16 +1253,24 @@ public function applyWrite(
if ($assignedExpr instanceof Expr\Array_) {
$scope = $this->processArrayByRefItems($nodeScopeResolver, $scope, $storage, $var->name, $assignedExpr, new Variable($var->name));
}
} elseif ($target->getVariableNameResult() === null) {
// a plain assignment does not read the target, so the dynamic name
// is walked here; read-modify-write targets walked it in
// prepareTarget() and already carry its state
$nameExprResult = $nodeScopeResolver->processExprNode($stmt, $var->name, $scope, $storage, $nodeCallback, $context);
$hasYield = $hasYield || $nameExprResult->hasYield();
$throwPoints = array_merge($throwPoints, $nameExprResult->getThrowPoints());
$impurePoints = array_merge($impurePoints, $nameExprResult->getImpurePoints());
$isAlwaysTerminating = $isAlwaysTerminating || $nameExprResult->isAlwaysTerminating();
$scope = $nameExprResult->getScope();
} else {
$nameExprResult = $target->getVariableNameResult();
if ($nameExprResult === null) {
// Read-modify-write targets already evaluated the dynamic name in prepareTarget().
$nameExprResult = $nodeScopeResolver->processExprNode($stmt, $var->name, $scope, $storage, $nodeCallback, $context);
$hasYield = $hasYield || $nameExprResult->hasYield();
$throwPoints = array_merge($throwPoints, $nameExprResult->getThrowPoints());
$impurePoints = array_merge($impurePoints, $nameExprResult->getImpurePoints());
$isAlwaysTerminating = $isAlwaysTerminating || $nameExprResult->isAlwaysTerminating();
$scope = $nameExprResult->getScope();
}
$storedAssignedExprResult = $assignedValueResult ?? $storage->findExpressionResult($assignedExpr);
$scope = $this->assignDynamicVariable(
$scope,
$nameExprResult,
$this->readAssignedValueType($nodeScopeResolver, $storedAssignedExprResult, $assignedExpr, $scopeBeforeAssignEval),
$this->readAssignedValueType($nodeScopeResolver, $storedAssignedExprResult, $assignedExpr, $scopeBeforeAssignEval->doNotTreatPhpDocTypesAsCertain()),
);
}
} elseif ($kind === PreparedAssignTarget::KIND_ARRAY_DIM_FETCH) {
if (!$var instanceof ArrayDimFetch) {
Expand Down Expand Up @@ -1816,6 +1824,44 @@ private function resolveContainerTypesAfterAssignedExprEval(
return [$varResult->getType(), $varResult->getNativeType()];
}

private function assignDynamicVariable(MutatingScope $scope, ExpressionResult $nameResult, Type $valueType, Type $nativeValueType): MutatingScope
{
$nameType = $nameResult->getType()->toString();
$nativeNameType = $nameResult->getNativeType()->toString();
$names = [];
foreach ($nameType->getConstantStrings() as $name) {
$names[$name->getValue()] = $name;
}
foreach (array_merge($scope->getDefinedVariables(), $scope->getMaybeDefinedVariables()) as $name) {
$nameStringType = new ConstantStringType($name);
if ($nameType->isSuperTypeOf($nameStringType)->no()) {
continue;
}
$names[$name] = $nameStringType;
}

$beforeScope = $scope;
foreach ($names as $nameStringType) {
$name = $nameStringType->getValue();
if ($name === 'this') {
continue;
}
$certainty = $beforeScope->hasVariableType($name);
$type = $valueType;
$nativeType = $nativeValueType;
if (!$certainty->no()) {
if (!$nameType->equals($nameStringType)) {
$type = TypeCombinator::union($beforeScope->getVariableType($name), $type);
}
if (!$nativeNameType->equals($nameStringType)) {
$nativeType = TypeCombinator::union($beforeScope->doNotTreatPhpDocTypesAsCertain()->getVariableType($name), $nativeType);
}
}
$scope = $scope->assignVariable($name, $type, $nativeType, $nameType->equals($nameStringType) ? TrinaryLogic::createYes() : $certainty->or(TrinaryLogic::createMaybe()));
}
return $scope;
}

private function unwrapAssign(Expr $expr): Expr
{
if ($expr instanceof Assign) {
Expand Down
6 changes: 3 additions & 3 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -975,9 +975,9 @@ public function testBug12767(): void
$errors = $this->runAnalyse(__DIR__ . '/data/bug-12767.php');
$this->assertCount(3, $errors);

$this->assertSame('Expected type int, actual: *ERROR*', $errors[0]->getMessage());
$this->assertSame('Undefined variable: $field1', $errors[1]->getMessage());
$this->assertSame('Undefined variable: $field2', $errors[2]->getMessage());
$this->assertSame('Expected type int, actual: int<1, max>', $errors[0]->getMessage());
$this->assertSame('Variable $field1 might not be defined.', $errors[1]->getMessage());
$this->assertSame('Variable $field2 might not be defined.', $errors[2]->getMessage());
}

public function testBug7554(): void
Expand Down
51 changes: 51 additions & 0 deletions tests/PHPStan/Analyser/nsrt/variable-variable-assign.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace VariableVariableAssign;

use function PHPStan\Testing\assertType;
use function PHPStan\Testing\assertNativeType;

function knownName(): void
{
$name = 'value';
$$name = 1;
assertType('1', $value);
assertNativeType('1', $value);
}

function possibleNames(bool $condition): void
{
$first = [];
$second = [];
$name = $condition ? 'first' : 'second';
$$name = ['value'];
assertType("array{}|array{'value'}", $first);
assertType("array{}|array{'value'}", $second);
assertNativeType("array{}|array{'value'}", $first);
}

function unknownName(string $name): void
{
$value = [];
$$name = ['value'];
assertType("array{}|array{'value'}", $value);
assertNativeType("array{}|array{'value'}", $value);
}

function updateKnownName(): void
{
$name = 'counter';
$counter = 1;
$$name += 2;
assertType('3', $counter);
assertNativeType('3', $counter);
}

/** @param 'value' $name */
function documentedName(string $name): void
{
$value = 1;
$$name = 2;
assertType('2', $value);
assertNativeType('1|2', $value);
}
5 changes: 5 additions & 0 deletions tests/PHPStan/Rules/Functions/UnusedClosureUsesRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ protected function getRule(): Rule
return new UnusedClosureUsesRule(true);
}

public function testCapturesAssignedThroughVariableVariables(): void
{
$this->analyse([__DIR__ . '/data/unused-closure-uses-variable-variables.php'], []);
}

public function testUnusedClosureUses(): void
{
$this->analyse([__DIR__ . '/data/unused-closure-uses.php'], [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace UnusedClosureUsesVariableVariables;

function translate(string $value): \Closure
{
$fromTranslations = [];
$toTranslations = [];
foreach (['from', 'to'] as $key) {
$translationKey = $key . 'Translations';
$$translationKey = [$value];
}

return function (string $chunk) use ($fromTranslations, $toTranslations): string {
foreach ($fromTranslations as $index => $word) {
if (preg_match("/^$word\$/iu", $chunk)) {
return $toTranslations[$index] ?? '';
}
}
return $chunk;
};
}