Skip to content

Commit 3fd0109

Browse files
authored
Allow undefined variables inside isset and empty (#204)
* Add tests for isset and empty * Ignore undefined variables inside isset or empty * Make sure we detect the start of arguments * Add test to make sure isset still counts as read * Explicitly do not count variable definitions for getFunctionIndexForFunctionCallArgument * Mark isset/empty usage as a read
1 parent 81da0cf commit 3fd0109

File tree

4 files changed

+125
-0
lines changed

4 files changed

+125
-0
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 IssetTest extends BaseTestCase {
7+
public function testIssetVariableUse() {
8+
$fixtureFile = $this->getFixture('IssetFixture.php');
9+
$phpcsFile = $this->prepareLocalFileForSniffs($fixtureFile);
10+
$phpcsFile->process();
11+
$lines = $this->getWarningLineNumbersFromFile($phpcsFile);
12+
$expectedWarnings = [
13+
4,
14+
23, // ideally this should not be a warning, but will be because it is difficult to know: https://github.com/sirbrillig/phpcs-variable-analysis/issues/202#issuecomment-688507314
15+
];
16+
$this->assertEquals($expectedWarnings, $lines);
17+
}
18+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
function normalFunctionShouldNotIgnoreUndefinedVariable() {
4+
if (isActive($undefinedVar)) { // should report undefined variable $undefinedVar
5+
doSomething();
6+
}
7+
}
8+
9+
function issetShouldIgnoreUndefinedVariable() {
10+
if (isset($undefinedVar)) {
11+
doSomething();
12+
}
13+
}
14+
15+
function emptyShouldIgnoreUndefinedVariable() {
16+
if (! empty($undefinedVar)) {
17+
doSomething();
18+
}
19+
}
20+
21+
function shouldIgnoreUndefinedVariableUseAfterIsset() {
22+
if (isset($undefinedVar)) {
23+
doSomething($undefinedVar); // ideally this should not be a warning, but will be because it is difficult to know: https://github.com/sirbrillig/phpcs-variable-analysis/issues/202#issuecomment-688507314
24+
}
25+
}
26+
27+
function shouldCountVariableUseInsideIssetAsRead($definedVar) {
28+
if (isset($definedVar)) {
29+
doSomething();
30+
}
31+
}
32+
33+
function shouldCountVariableUseInsideEmptyAsRead($definedVar) {
34+
if (empty($definedVar)) {
35+
doSomething();
36+
}
37+
}

VariableAnalysis/Lib/Helpers.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ public static function isTokenInsideFunctionDefinitionArgumentList(File $phpcsFi
107107
*
108108
* Will also work for the parenthesis that make up the function definition's arguments list.
109109
*
110+
* For arguments inside a function call, rather than a definition, use
111+
* `getFunctionIndexForFunctionCallArgument`.
112+
*
110113
* @param File $phpcsFile
111114
* @param int $stackPtr
112115
*
@@ -130,6 +133,10 @@ public static function getFunctionIndexForFunctionArgument(File $phpcsFile, $sta
130133
$startOfArguments = end($startingParenthesis);
131134
}
132135

136+
if (! is_int($startOfArguments)) {
137+
return null;
138+
}
139+
133140
$nonFunctionTokenTypes = array_values(Tokens::$emptyTokens);
134141
$nonFunctionTokenTypes[] = T_STRING;
135142
$nonFunctionTokenTypes[] = T_BITWISE_AND;
@@ -730,4 +737,60 @@ public static function isRequireInScopeAfter(File $phpcsFile, VariableInfo $varI
730737
}
731738
return false;
732739
}
740+
741+
/**
742+
* Find the index of the function keyword for a token in a function call's arguments
743+
*
744+
* @param File $phpcsFile
745+
* @param int $stackPtr
746+
*
747+
* @return ?int
748+
*/
749+
public static function getFunctionIndexForFunctionCallArgument(File $phpcsFile, $stackPtr) {
750+
$tokens = $phpcsFile->getTokens();
751+
$token = $tokens[$stackPtr];
752+
if (empty($token['nested_parenthesis'])) {
753+
return null;
754+
}
755+
$startingParenthesis = array_keys($token['nested_parenthesis']);
756+
$startOfArguments = end($startingParenthesis);
757+
if (! is_int($startOfArguments)) {
758+
return null;
759+
}
760+
761+
$nonFunctionTokenTypes = array_values(Tokens::$emptyTokens);
762+
$functionPtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $startOfArguments - 1, null, true, null, true));
763+
if (! is_int($functionPtr) || ! isset($tokens[$functionPtr]['code'])) {
764+
return null;
765+
}
766+
if ($tokens[$functionPtr]['code'] === 'function' || ($tokens[$functionPtr]['content'] === 'fn' && FunctionDeclarations::isArrowFunction($phpcsFile, $functionPtr))) {
767+
return null;
768+
}
769+
return $functionPtr;
770+
}
771+
772+
/**
773+
* @param File $phpcsFile
774+
* @param int $stackPtr
775+
*
776+
* @return bool
777+
*/
778+
public static function isVariableInsideIssetOrEmpty(File $phpcsFile, $stackPtr) {
779+
$functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr);
780+
if (! is_int($functionIndex)) {
781+
return false;
782+
}
783+
$tokens = $phpcsFile->getTokens();
784+
if (! isset($tokens[$functionIndex])) {
785+
return false;
786+
}
787+
$allowedFunctionNames = [
788+
'isset',
789+
'empty',
790+
];
791+
if (in_array($tokens[$functionIndex]['content'], $allowedFunctionNames, true)) {
792+
return true;
793+
}
794+
return false;
795+
}
733796
}

VariableAnalysis/Sniffs/CodeAnalysis/VariableAnalysisSniff.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1440,6 +1440,13 @@ protected function processVariable(File $phpcsFile, $stackPtr) {
14401440
return;
14411441
}
14421442

1443+
// Are we an isset or empty call?
1444+
if (Helpers::isVariableInsideIssetOrEmpty($phpcsFile, $stackPtr)) {
1445+
Helpers::debug('found isset or empty');
1446+
$this->markVariableRead($varName, $stackPtr, $currScope);
1447+
return;
1448+
}
1449+
14431450
// OK, we don't appear to be a write to the var, assume we're a read.
14441451
Helpers::debug('looks like a variable read');
14451452
$this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);

0 commit comments

Comments
 (0)