Skip to content

Commit 4debf53

Browse files
authored
Accept namespaces in sitePassByRefFunctions (#351)
* Add test for using a namespace in sitePassByRefFunctions * Add namespace to function names when looking for pass-by-reference * Make sure cache is not used before it is ready * Make sure we don't try to use null in getPassByReferenceFunction * Init passByRefFunctionsCache to null explicitly
1 parent 5e2f18e commit 4debf53

File tree

4 files changed

+102
-2
lines changed

4 files changed

+102
-2
lines changed

Tests/VariableAnalysisSniff/VariableAnalysisTest.php

+32
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ public function testFunctionWithReferenceWarnings()
298298
77,
299299
81,
300300
98,
301+
106,
301302
];
302303
$this->assertSame($expectedWarnings, $lines);
303304
}
@@ -331,6 +332,36 @@ public function testFunctionWithReferenceWarningsAllowsCustomFunctions()
331332
$this->assertSame($expectedWarnings, $lines);
332333
}
333334

335+
public function testFunctionWithReferenceWarningsAllowsCustomFunctionsNamespaced()
336+
{
337+
$fixtureFile = $this->getFixture('FunctionWithReferenceFixture.php');
338+
$phpcsFile = $this->prepareLocalFileForSniffs($fixtureFile);
339+
$this->setSniffProperty($phpcsFile, 'sitePassByRefFunctions', '\My\Functions\my_reference_function:2,3 another_reference_function:2,...');
340+
$phpcsFile->process();
341+
$lines = $this->getWarningLineNumbersFromFile($phpcsFile);
342+
$expectedWarnings = [
343+
10,
344+
11,
345+
12,
346+
13,
347+
14,
348+
16,
349+
29,
350+
41,
351+
42,
352+
43,
353+
46,
354+
52,
355+
56,
356+
57,
357+
63,
358+
76,
359+
81,
360+
98,
361+
];
362+
$this->assertSame($expectedWarnings, $lines);
363+
}
364+
334365
public function testFunctionWithReferenceWarningsAllowsWordPressFunctionsIfSet()
335366
{
336367
$fixtureFile = $this->getFixture('FunctionWithReferenceFixture.php');
@@ -357,6 +388,7 @@ public function testFunctionWithReferenceWarningsAllowsWordPressFunctionsIfSet()
357388
76,
358389
77,
359390
98,
391+
106,
360392
];
361393
$this->assertSame($expectedWarnings, $lines);
362394
}

Tests/VariableAnalysisSniff/fixtures/FunctionWithReferenceFixture.php

+5
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,8 @@ function function_with_foreach_with_reference($derivatives, $base_plugin_definit
100100
}
101101
return $derivatives;
102102
}
103+
104+
function function_with_ignored_reference_call_with_namespace() {
105+
$foo = 'bar';
106+
\My\Functions\my_reference_function($foo, $baz, $bip); // Undefined variable $bar, Undefined variable $bip
107+
}

VariableAnalysis/Lib/Helpers.php

+35
Original file line numberDiff line numberDiff line change
@@ -1658,6 +1658,41 @@ public static function isConstructorPromotion(File $phpcsFile, $stackPtr)
16581658
return false;
16591659
}
16601660

1661+
/**
1662+
* If looking at a function call token, return a string for the full function
1663+
* name including any inline namespace.
1664+
*
1665+
* So for example, if the call looks like `\My\Namespace\doSomething($bar)`
1666+
* and `$stackPtr` refers to `doSomething`, this will return
1667+
* `\My\Namespace\doSomething`.
1668+
*
1669+
* @param File $phpcsFile
1670+
* @param int $stackPtr
1671+
*
1672+
* @return string|null
1673+
*/
1674+
public static function getFunctionNameWithNamespace(File $phpcsFile, $stackPtr)
1675+
{
1676+
$tokens = $phpcsFile->getTokens();
1677+
1678+
if (! isset($tokens[$stackPtr])) {
1679+
return null;
1680+
}
1681+
$startOfScope = self::findVariableScope($phpcsFile, $stackPtr);
1682+
$functionName = $tokens[$stackPtr]['content'];
1683+
1684+
// Move backwards from the token, collecting namespace separators and
1685+
// strings, until we encounter whitespace or something else.
1686+
$partOfNamespace = [T_NS_SEPARATOR, T_STRING];
1687+
for ($i = $stackPtr - 1; $i > $startOfScope; $i--) {
1688+
if (! in_array($tokens[$i]['code'], $partOfNamespace, true)) {
1689+
break;
1690+
}
1691+
$functionName = "{$tokens[$i]['content']}{$functionName}";
1692+
}
1693+
return $functionName;
1694+
}
1695+
16611696
/**
16621697
* Return false if the token is definitely not part of a typehint
16631698
*

VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php

+30-2
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,13 @@ class VariableAnalysisSniff implements Sniff
153153
*/
154154
public $allowUnusedVariablesBeforeRequire = false;
155155

156+
/**
157+
* A cache for getPassByReferenceFunctions
158+
*
159+
* @var array<array<int|string>>|null
160+
*/
161+
private $passByRefFunctionsCache = null;
162+
156163
public function __construct()
157164
{
158165
$this->scopeManager = new ScopeManager();
@@ -195,6 +202,18 @@ public function register()
195202
*/
196203
private function getPassByReferenceFunction($functionName)
197204
{
205+
$passByRefFunctions = $this->getPassByReferenceFunctions();
206+
return isset($passByRefFunctions[$functionName]) ? $passByRefFunctions[$functionName] : [];
207+
}
208+
209+
/**
210+
* @return array<array<int|string>>
211+
*/
212+
private function getPassByReferenceFunctions()
213+
{
214+
if (! is_null($this->passByRefFunctionsCache)) {
215+
return $this->passByRefFunctionsCache;
216+
}
198217
$passByRefFunctions = Constants::getPassByReferenceFunctions();
199218
if (!empty($this->sitePassByRefFunctions)) {
200219
$lines = Helpers::splitStringToArray('/\s+/', trim($this->sitePassByRefFunctions));
@@ -206,7 +225,8 @@ private function getPassByReferenceFunction($functionName)
206225
if ($this->allowWordPressPassByRefFunctions) {
207226
$passByRefFunctions = array_merge($passByRefFunctions, Constants::getWordPressPassByReferenceFunctions());
208227
}
209-
return isset($passByRefFunctions[$functionName]) ? $passByRefFunctions[$functionName] : [];
228+
$this->passByRefFunctionsCache = $passByRefFunctions;
229+
return $passByRefFunctions;
210230
}
211231

212232
/**
@@ -1485,7 +1505,15 @@ protected function processVariableAsPassByReferenceFunctionCall(File $phpcsFile,
14851505
$functionName = $tokens[$functionPtr]['content'];
14861506
$refArgs = $this->getPassByReferenceFunction($functionName);
14871507
if (! $refArgs) {
1488-
return false;
1508+
// Check again with the fully namespaced function name.
1509+
$functionName = Helpers::getFunctionNameWithNamespace($phpcsFile, $functionPtr);
1510+
if (! $functionName) {
1511+
return false;
1512+
}
1513+
$refArgs = $this->getPassByReferenceFunction($functionName);
1514+
if (! $refArgs) {
1515+
return false;
1516+
}
14891517
}
14901518

14911519
$argPtrs = Helpers::findFunctionCallArguments($phpcsFile, $stackPtr);

0 commit comments

Comments
 (0)