Skip to content

Commit 70b296b

Browse files
authored
Mark concatenation inside another expression as variable read (#186)
* Add test case for concatination on the same line as assignment * Remove array functions from getPassByReferenceFunctions The list of pass-by-reference functions is only currently used to determine if a function's argument might be counted as a define. All arguments passed to the function are counted as a read. Functions like `array_pop()` will throw a PHP warning if used with an undefined variable, so this removes them. There are probably many others in this list that can be removed, but this is a start. * Add variations of concat-and-assign to test fixture * Add function expression to assign-and-concat test * Mark assignments as read if they are part of another expression * Fix test for undefined variable after array assignment in loop * Just check for RHS * Add check for inside function calls * Replace bool casts for indices with is_int Indices can sometimes be 0.
1 parent c4fb5ee commit 70b296b

File tree

5 files changed

+132
-13
lines changed

5 files changed

+132
-13
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
namespace VariableAnalysis\Tests\VariableAnalysisSniff;
3+
4+
use VariableAnalysis\Tests\BaseTestCase;
5+
6+
class WhileLoopTest extends BaseTestCase {
7+
public function testFunctionWithWhileLoopWarnings() {
8+
$fixtureFile = $this->getFixture('FunctionWithWhileLoopFixture.php');
9+
$phpcsFile = $this->prepareLocalFileForSniffs($fixtureFile);
10+
$phpcsFile->process();
11+
$lines = $this->getWarningLineNumbersFromFile($phpcsFile);
12+
$expectedWarnings = [
13+
38,
14+
46,
15+
];
16+
$this->assertEquals($expectedWarnings, $lines);
17+
}
18+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
function loopAndPush($parts) {
4+
$suggestions = [];
5+
while ($part = array_shift($parts)) {
6+
$suggestions[] = $part;
7+
}
8+
return $suggestions;
9+
}
10+
11+
function concatAndAssignAndPush($parts) {
12+
$suggestions = [];
13+
$suggestion = 'block';
14+
while ($part = array_shift($parts)) {
15+
$suggestions[] = $suggestion .= '__' . strtr($part, '-', '_');
16+
}
17+
return $suggestions;
18+
}
19+
20+
function concatAndAssignAndPushWithoutLoop() {
21+
$suggestions = [];
22+
$suggestion = 'block';
23+
$suggestions[] = $suggestion .= '__';
24+
return $suggestions;
25+
}
26+
27+
function concatAndPush($parts) {
28+
$suggestions = [];
29+
$suggestion = 'block';
30+
while ($part = array_shift($parts)) {
31+
$suggestions[] = $suggestion . '__' . strtr($part, '-', '_');
32+
}
33+
return $suggestions;
34+
}
35+
36+
function whileLoopAssignWithUndefinedShift() {
37+
$suggestions = [];
38+
while ($part = array_shift($parts)) { // undefined variable parts
39+
$suggestions[] = $part;
40+
}
41+
return $suggestions;
42+
}
43+
44+
function loopAndPushWithUndefinedArray($parts) {
45+
while ($part = array_shift($parts)) {
46+
$suggestions[] = $part; // undefined variable suggestions
47+
}
48+
return $suggestions;
49+
}
50+
51+
function concatAndAssignAndPush2($parts) {
52+
$suggestions = [];
53+
$suggestion = 'block';
54+
while ($part = array_shift($parts)) {
55+
$suggestions[] = ( $suggestion .= '__' . strtr($part, '-', '_') );
56+
}
57+
return $suggestions;
58+
}
59+
60+
function concatAndAssignAndPush3($parts) {
61+
$suggestions = [];
62+
$suggestion = 'block';
63+
while ($part = array_shift($parts)) {
64+
$suggestions[] = getPrefix() . $suggestion .= '__' . strtr($part, '-', '_');
65+
}
66+
return $suggestions;
67+
}
68+
69+
function concatAndAssignAndPush4($parts) {
70+
$suggestion = 'block';
71+
while ($part = array_shift($parts)) {
72+
addToSuggestion($suggestion .= $part);
73+
}
74+
}

VariableAnalysis/Lib/Constants.php

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ class Constants {
88
* by reference, the arguments are numbered starting from 1 and an elipsis '...'
99
* means all argument numbers after the previous should be considered pass-by-reference.
1010
*
11+
* This does not need to cover all pass-by-reference arguments, only the
12+
* ones which can be passed an undefined variable (eg: `$matches` in
13+
* `preg_match`) and will define that variable.
14+
*
1115
* @return array[]
1216
*/
1317
public static function getPassByReferenceFunctions() {
@@ -28,16 +32,6 @@ public static function getPassByReferenceFunctions() {
2832
'apcu_fetch' => [2],
2933
'apcu_inc' => [3],
3034
'areConfusable' => [3],
31-
'array_multisort' => [1],
32-
'array_pop' => [1],
33-
'array_push' => [1],
34-
'array_replace' => [1],
35-
'array_replace_recursive' => [1, 2, 3, '...'],
36-
'array_shift' => [1],
37-
'array_splice' => [1],
38-
'array_unshift' => [1],
39-
'array_walk' => [1],
40-
'array_walk_recursive' => [1],
4135
'arsort' => [1],
4236
'asort' => [1],
4337
'bindColumn' => [2],

VariableAnalysis/Lib/Helpers.php

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,17 @@ public static function areConditionsWithinFunctionBeforeClass(array $conditions)
9797
* @return bool
9898
*/
9999
public static function isTokenInsideFunctionDefinitionArgumentList(File $phpcsFile, $stackPtr) {
100-
return (bool) self::getFunctionIndexForFunctionArgument($phpcsFile, $stackPtr);
100+
return is_int(self::getFunctionIndexForFunctionArgument($phpcsFile, $stackPtr));
101+
}
102+
103+
/**
104+
* @param File $phpcsFile
105+
* @param int $stackPtr
106+
*
107+
* @return bool
108+
*/
109+
public static function isTokenInsideFunctionCall(File $phpcsFile, $stackPtr) {
110+
return is_int(self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr));
101111
}
102112

103113
/**
@@ -163,7 +173,7 @@ public static function getFunctionIndexForFunctionArgument(File $phpcsFile, $sta
163173
* @return bool
164174
*/
165175
public static function isTokenInsideFunctionUseImport(File $phpcsFile, $stackPtr) {
166-
return (bool) self::getUseIndexForUseImport($phpcsFile, $stackPtr);
176+
return is_int(self::getUseIndexForUseImport($phpcsFile, $stackPtr));
167177
}
168178

169179
/**
@@ -303,7 +313,7 @@ public static function findVariableScope(File $phpcsFile, $stackPtr) {
303313
$token = $tokens[$stackPtr];
304314

305315
$arrowFunctionIndex = self::getContainingArrowFunctionIndex($phpcsFile, $stackPtr);
306-
$isTokenInsideArrowFunctionBody = (bool) $arrowFunctionIndex;
316+
$isTokenInsideArrowFunctionBody = is_int($arrowFunctionIndex);
307317
if ($isTokenInsideArrowFunctionBody) {
308318
// Get the list of variables defined by the arrow function
309319
// If this matches any of them, the scope is the arrow function,
@@ -851,4 +861,22 @@ public static function isVariableInsideUnset(File $phpcsFile, $stackPtr) {
851861
}
852862
return false;
853863
}
864+
865+
/**
866+
* @param File $phpcsFile
867+
* @param int $stackPtr
868+
*
869+
* @return bool
870+
*/
871+
public static function isTokenInsideAssignmentRHS(File $phpcsFile, $stackPtr) {
872+
$previousStatementPtr = $phpcsFile->findPrevious([T_SEMICOLON, T_CLOSE_CURLY_BRACKET, T_OPEN_CURLY_BRACKET, T_COMMA], $stackPtr - 1);
873+
if (! is_int($previousStatementPtr)) {
874+
$previousStatementPtr = 1;
875+
}
876+
$previousTokenPtr = $phpcsFile->findPrevious([T_EQUAL], $stackPtr - 1, $previousStatementPtr);
877+
if (is_int($previousTokenPtr)) {
878+
return true;
879+
}
880+
return false;
881+
}
854882
}

VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,6 +1399,11 @@ protected function processVariable(File $phpcsFile, $stackPtr) {
13991399

14001400
// Is the next non-whitespace an assignment?
14011401
if ($this->processVariableAsAssignment($phpcsFile, $stackPtr, $varName, $currScope)) {
1402+
if (Helpers::isTokenInsideAssignmentRHS($phpcsFile, $stackPtr) || Helpers::isTokenInsideFunctionCall($phpcsFile, $stackPtr)) {
1403+
Helpers::debug("found assignment that's also inside an expression");
1404+
$this->markVariableRead($varName, $stackPtr, $currScope);
1405+
return;
1406+
}
14021407
Helpers::debug('found assignment');
14031408
return;
14041409
}

0 commit comments

Comments
 (0)