Skip to content

Commit 22c83ec

Browse files
authored
Ignore double assignment by reference if second is inside condition (sirbrillig#235)
* Add test for overwriting assignment by reference inside conditional * Allow overwriting reference assignments within conditions * Make sure unused reference reassignments still trigger in conditions * Include new test case in other tests * Compare position of IF token and last assignment * Fix return type of new function
1 parent 72cb4f9 commit 22c83ec

File tree

4 files changed

+50
-2
lines changed

4 files changed

+50
-2
lines changed

Tests/VariableAnalysisSniff/VariableAnalysisTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,7 @@ public function testAssignmentByReference() {
10401040
34,
10411041
35,
10421042
43,
1043+
70,
10431044
];
10441045
$this->assertEquals($expectedWarnings, $lines);
10451046
}
@@ -1058,6 +1059,7 @@ public function testAssignmentByReferenceWithIgnoreUnusedMatch() {
10581059
26,
10591060
34,
10601061
35,
1062+
70,
10611063
];
10621064
$this->assertEquals($expectedWarnings, $lines);
10631065
}

Tests/VariableAnalysisSniff/fixtures/AssignmentByReferenceFixture.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,21 @@ function doubleUsedThenUsedAssignmentByReference() {
5454
$var = &$bee;
5555
return $var;
5656
}
57+
58+
function somefunc($choice, &$arr1, &$arr_default) {
59+
$var = &$arr_default;
60+
61+
if ($choice) {
62+
$var = &$arr1;
63+
}
64+
65+
echo $var;
66+
}
67+
68+
function somefunc($choice, &$arr1, &$arr_default) {
69+
if ($choice) {
70+
$var = &$arr_default; // unused variable $var
71+
$var = &$arr1;
72+
echo $var;
73+
}
74+
}

VariableAnalysis/Lib/Helpers.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,25 @@ public static function areConditionsWithinFunctionBeforeClass(array $conditions)
102102
return false;
103103
}
104104

105+
/**
106+
* @param (int|string)[] $conditions
107+
*
108+
* @return int|string|null
109+
*/
110+
public static function getClosestIfPositionIfBeforeOtherConditions(array $conditions) {
111+
// Return true if the token conditions are within an if block before
112+
// they are within a class or function.
113+
$conditionsInsideOut = array_reverse($conditions, true);
114+
if (empty($conditions)) {
115+
return null;
116+
}
117+
$scopeCode = reset($conditionsInsideOut);
118+
if ($scopeCode === T_IF) {
119+
return key($conditionsInsideOut);
120+
}
121+
return null;
122+
}
123+
105124
/**
106125
* @param File $phpcsFile
107126
* @param int $stackPtr

VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -937,9 +937,18 @@ protected function processVariableAsAssignment(File $phpcsFile, $stackPtr, $varN
937937
Helpers::debug('processVariableAsAssignment: found reference variable');
938938
$varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
939939
// If the variable was already declared, but was not yet read, it is
940-
// unused because we're about to change the binding.
940+
// unused because we're about to change the binding; that is, unless we
941+
// are inside a conditional block because in that case the condition may
942+
// never activate.
941943
$scopeInfo = $this->getOrCreateScopeInfo($currScope);
942-
$this->processScopeCloseForVariable($phpcsFile, $varInfo, $scopeInfo);
944+
$ifPtr = Helpers::getClosestIfPositionIfBeforeOtherConditions($tokens[$referencePtr]['conditions']);
945+
$lastAssignmentPtr = $varInfo->firstDeclared;
946+
if (! $ifPtr && $lastAssignmentPtr) {
947+
$this->processScopeCloseForVariable($phpcsFile, $varInfo, $scopeInfo);
948+
}
949+
if ($ifPtr && $lastAssignmentPtr && $ifPtr <= $lastAssignmentPtr) {
950+
$this->processScopeCloseForVariable($phpcsFile, $varInfo, $scopeInfo);
951+
}
943952
// The referenced variable may have a different name, but we don't
944953
// actually need to mark it as used in this case because the act of this
945954
// assignment will mark it used on the next token.

0 commit comments

Comments
 (0)