VariableAnalysisSniff.php
Same filename in this branch
Namespace
VariableAnalysis\Sniffs\CodeAnalysisFile
-
vendor/
sirbrillig/ phpcs-variable-analysis/ VariableAnalysis/ Sniffs/ CodeAnalysis/ VariableAnalysisSniff.php
View source
<?php
namespace VariableAnalysis\Sniffs\CodeAnalysis;
use VariableAnalysis\Lib\ScopeInfo;
use VariableAnalysis\Lib\ScopeType;
use VariableAnalysis\Lib\VariableInfo;
use VariableAnalysis\Lib\Constants;
use VariableAnalysis\Lib\Helpers;
use VariableAnalysis\Lib\ScopeManager;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
class VariableAnalysisSniff implements Sniff {
/**
* The current phpcsFile being checked.
*
* @var File|null
*/
protected $currentFile = null;
/**
* @var ScopeManager
*/
private $scopeManager;
/**
* A list of for loops, keyed by the index of their first token in this file.
*
* @var array<int, \VariableAnalysis\Lib\ForLoopInfo>
*/
private $forLoops = [];
/**
* A list of enum blocks, keyed by the index of their first token in this file.
*
* @var array<int, \VariableAnalysis\Lib\EnumInfo>
*/
private $enums = [];
/**
* A list of custom functions which pass in variables to be initialized by
* reference (eg `preg_match()`) and therefore should not require those
* variables to be defined ahead of time. The list is space separated and
* each entry is of the form `functionName:1,2`. The function name comes
* first followed by a colon and a comma-separated list of argument numbers
* (starting from 1) which should be considered variable definitions. The
* special value `...` in the arguments list will cause all arguments after
* the last number to be considered variable definitions.
*
* @var string|null
*/
public $sitePassByRefFunctions = null;
/**
* If set, allows common WordPress pass-by-reference functions in addition to
* the standard PHP ones.
*
* @var bool
*/
public $allowWordPressPassByRefFunctions = false;
/**
* Allow exceptions in a catch block to be unused without warning.
*
* @var bool
*/
public $allowUnusedCaughtExceptions = true;
/**
* Allow function parameters to be unused without provoking unused-var warning.
*
* @var bool
*/
public $allowUnusedFunctionParameters = false;
/**
* If set, ignores undefined variables in the file scope (the top-level
* scope of a file).
*
* @var bool
*/
public $allowUndefinedVariablesInFileScope = false;
/**
* If set, ignores unused variables in the file scope (the top-level
* scope of a file).
*
* @var bool
*/
public $allowUnusedVariablesInFileScope = false;
/**
* A space-separated list of names of placeholder variables that you want to
* ignore from unused variable warnings. For example, to ignore the variables
* `$junk` and `$unused`, this could be set to `'junk unused'`.
*
* @var string|null
*/
public $validUnusedVariableNames = null;
/**
* A PHP regexp string for variables that you want to ignore from unused
* variable warnings. For example, to ignore the variables `$_junk` and
* `$_unused`, this could be set to `'/^_/'`.
*
* @var string|null
*/
public $ignoreUnusedRegexp = null;
/**
* A space-separated list of names of placeholder variables that you want to
* ignore from undefined variable warnings. For example, to ignore the variables
* `$post` and `$undefined`, this could be set to `'post undefined'`.
*
* @var string|null
*/
public $validUndefinedVariableNames = null;
/**
* A PHP regexp string for variables that you want to ignore from undefined
* variable warnings. For example, to ignore the variables `$_junk` and
* `$_unused`, this could be set to `'/^_/'`.
*
* @var string|null
*/
public $validUndefinedVariableRegexp = null;
/**
* Allows unused arguments in a function definition if they are
* followed by an argument which is used.
*
* @var bool
*/
public $allowUnusedParametersBeforeUsed = true;
/**
* If set to true, unused values from the `key => value` syntax
* in a `foreach` loop will never be marked as unused.
*
* @var bool
*/
public $allowUnusedForeachVariables = true;
/**
* If set to true, unused variables in a function before a require or import
* statement will not be marked as unused because they may be used in the
* required file.
*
* @var bool
*/
public $allowUnusedVariablesBeforeRequire = false;
public function __construct() {
$this->scopeManager = new ScopeManager();
}
/**
* Decide which tokens to scan.
*
* @return (int|string)[]
*/
public function register() {
$types = [
T_VARIABLE,
T_DOUBLE_QUOTED_STRING,
T_HEREDOC,
T_CLOSE_CURLY_BRACKET,
T_FUNCTION,
T_CLOSURE,
T_STRING,
T_COMMA,
T_SEMICOLON,
T_CLOSE_PARENTHESIS,
T_FOR,
T_ENDFOR,
];
if (defined('T_FN')) {
$types[] = T_FN;
}
if (defined('T_ENUM')) {
$types[] = T_ENUM;
}
return $types;
}
/**
* @param string $functionName
*
* @return array<int|string>
*/
private function getPassByReferenceFunction($functionName) {
$passByRefFunctions = Constants::getPassByReferenceFunctions();
if (!empty($this->sitePassByRefFunctions)) {
$lines = Helpers::splitStringToArray('/\\s+/', trim($this->sitePassByRefFunctions));
foreach ($lines as $line) {
list($function, $args) = explode(':', $line);
$passByRefFunctions[$function] = explode(',', $args);
}
}
if ($this->allowWordPressPassByRefFunctions) {
$passByRefFunctions = array_merge($passByRefFunctions, Constants::getWordPressPassByReferenceFunctions());
}
return isset($passByRefFunctions[$functionName]) ? $passByRefFunctions[$functionName] : [];
}
/**
* Scan and process a token.
*
* This is the main processing function of the sniff. Will run on every token
* for which `register()` returns true.
*
* @param File $phpcsFile
* @param int $stackPtr
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr) {
$tokens = $phpcsFile->getTokens();
$scopeStartTokenTypes = [
T_FUNCTION,
T_CLOSURE,
];
$token = $tokens[$stackPtr];
// Cache the current PHPCS File in an instance variable so it can be more
// easily accessed in other places which aren't passed the object.
if ($this->currentFile !== $phpcsFile) {
$this->currentFile = $phpcsFile;
$this->forLoops = [];
$this->enums = [];
}
// Add the global scope for the current file to our scope indexes.
$scopesForFilename = $this->scopeManager
->getScopesForFilename($phpcsFile->getFilename());
if (empty($scopesForFilename)) {
$this->scopeManager
->recordScopeStartAndEnd($phpcsFile, 0);
}
// Find and process variables to perform two jobs: to record variable
// definition or use, and to report variables as undefined if they are used
// without having been first defined.
if ($token['code'] === T_VARIABLE) {
$this->processVariable($phpcsFile, $stackPtr);
}
// Report variables defined but not used in the current scope as unused
// variables if the current token closes scopes.
$this->searchForAndProcessClosingScopesAt($phpcsFile, $stackPtr);
// Scan variables that were postponed because they exist in the increment
// expression of a for loop if the current token closes a loop.
$this->processClosingForLoopsAt($phpcsFile, $stackPtr);
if ($token['code'] === T_VARIABLE) {
return;
}
if ($token['code'] === T_DOUBLE_QUOTED_STRING || $token['code'] === T_HEREDOC) {
$this->processVariableInString($phpcsFile, $stackPtr);
return;
}
if ($token['code'] === T_STRING && $token['content'] === 'compact') {
$this->processCompact($phpcsFile, $stackPtr);
return;
}
// Record for loop boundaries so we can delay scanning the third for loop
// expression until after the loop has been scanned.
if ($token['code'] === T_FOR) {
$this->recordForLoop($phpcsFile, $stackPtr);
return;
}
// Record enums so we can detect them even before phpcs was able to.
if ($token['content'] === 'enum') {
$enumInfo = Helpers::makeEnumInfo($phpcsFile, $stackPtr);
// The token might not actually be an enum so let's avoid returning if
// it's not.
if ($enumInfo) {
$this->enums[$stackPtr] = $enumInfo;
return;
}
}
// If the current token is a call to `get_defined_vars()`, consider that a
// usage of all variables in the current scope.
if ($this->isGetDefinedVars($phpcsFile, $stackPtr)) {
Helpers::debug('get_defined_vars is being called');
$this->markAllVariablesRead($phpcsFile, $stackPtr);
return;
}
// If the current token starts a scope, record that scope's start and end
// indexes so that we can determine if variables in that scope are defined
// and/or used.
if (in_array($token['code'], $scopeStartTokenTypes, true) || Helpers::isArrowFunction($phpcsFile, $stackPtr)) {
Helpers::debug('found scope condition', $token);
$this->scopeManager
->recordScopeStartAndEnd($phpcsFile, $stackPtr);
return;
}
}
/**
* Record the boundaries of a for loop.
*
* @param File $phpcsFile
* @param int $stackPtr
*
* @return void
*/
private function recordForLoop($phpcsFile, $stackPtr) {
$this->forLoops[$stackPtr] = Helpers::makeForLoopInfo($phpcsFile, $stackPtr);
}
/**
* Find scopes closed by a token and process their variables.
*
* Calls `processScopeClose()` for each closed scope.
*
* @param File $phpcsFile
* @param int $stackPtr
*
* @return void
*/
private function searchForAndProcessClosingScopesAt($phpcsFile, $stackPtr) {
$scopeIndicesThisCloses = $this->scopeManager
->getScopesForScopeEnd($phpcsFile->getFilename(), $stackPtr);
$tokens = $phpcsFile->getTokens();
$token = $tokens[$stackPtr];
$line = $token['line'];
foreach ($scopeIndicesThisCloses as $scopeIndexThisCloses) {
Helpers::debug('found closing scope at index', $stackPtr, 'line', $line, 'for scopes starting at:', $scopeIndexThisCloses->scopeStartIndex);
$this->processScopeClose($phpcsFile, $scopeIndexThisCloses->scopeStartIndex);
}
}
/**
* Scan variables that were postponed because they exist in the increment expression of a for loop.
*
* @param File $phpcsFile
* @param int $stackPtr
*
* @return void
*/
private function processClosingForLoopsAt($phpcsFile, $stackPtr) {
$forLoopsThisCloses = [];
foreach ($this->forLoops as $forLoop) {
if ($forLoop->blockEnd === $stackPtr) {
$forLoopsThisCloses[] = $forLoop;
}
}
foreach ($forLoopsThisCloses as $forLoop) {
foreach ($forLoop->incrementVariables as $varIndex => $varInfo) {
Helpers::debug('processing delayed for loop increment variable at', $varIndex, $varInfo);
$this->processVariable($phpcsFile, $varIndex, [
'ignore-for-loops' => true,
]);
}
}
}
/**
* Return true if the token is a call to `get_defined_vars()`.
*
* @param File $phpcsFile
* @param int $stackPtr
*
* @return bool
*/
protected function isGetDefinedVars(File $phpcsFile, $stackPtr) {
$tokens = $phpcsFile->getTokens();
$token = $tokens[$stackPtr];
if (!$token || $token['content'] !== 'get_defined_vars') {
return false;
}
// Make sure this is a function call
$parenPointer = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true);
if (!$parenPointer || $tokens[$parenPointer]['code'] !== T_OPEN_PARENTHESIS) {
return false;
}
return true;
}
/**
* @return string
*/
protected function getFilename() {
return $this->currentFile ? $this->currentFile
->getFilename() : 'unknown file';
}
/**
* @param int $currScope
*
* @return ScopeInfo
*/
protected function getOrCreateScopeInfo($currScope) {
$scope = $this->scopeManager
->getScopeForScopeStart($this->getFilename(), $currScope);
if (!$scope) {
if (!$this->currentFile) {
throw new \Exception('Cannot create scope info; current file is not set.');
}
$scope = $this->scopeManager
->recordScopeStartAndEnd($this->currentFile, $currScope);
}
return $scope;
}
/**
* @param string $varName
* @param int $currScope
*
* @return VariableInfo|null
*/
protected function getVariableInfo($varName, $currScope) {
$scopeInfo = $this->scopeManager
->getScopeForScopeStart($this->getFilename(), $currScope);
return $scopeInfo && isset($scopeInfo->variables[$varName]) ? $scopeInfo->variables[$varName] : null;
}
/**
* Returns variable data for a variable at an index.
*
* The variable will also be added to the list of variables stored in its
* scope so that its use or non-use can be reported when those scopes end by
* `processScopeClose()`.
*
* @param string $varName
* @param int $currScope
*
* @return VariableInfo
*/
protected function getOrCreateVariableInfo($varName, $currScope) {
Helpers::debug("getOrCreateVariableInfo: starting for '{$varName}'");
$scopeInfo = $this->getOrCreateScopeInfo($currScope);
if (isset($scopeInfo->variables[$varName])) {
Helpers::debug("getOrCreateVariableInfo: found variable for '{$varName}'", $scopeInfo->variables[$varName]);
return $scopeInfo->variables[$varName];
}
Helpers::debug("getOrCreateVariableInfo: creating a new variable for '{$varName}' in scope", $scopeInfo);
$scopeInfo->variables[$varName] = new VariableInfo($varName);
$validUnusedVariableNames = empty($this->validUnusedVariableNames) ? [] : Helpers::splitStringToArray('/\\s+/', trim($this->validUnusedVariableNames));
$validUndefinedVariableNames = empty($this->validUndefinedVariableNames) ? [] : Helpers::splitStringToArray('/\\s+/', trim($this->validUndefinedVariableNames));
if (in_array($varName, $validUnusedVariableNames)) {
$scopeInfo->variables[$varName]->ignoreUnused = true;
}
if (!empty($this->ignoreUnusedRegexp) && preg_match($this->ignoreUnusedRegexp, $varName) === 1) {
$scopeInfo->variables[$varName]->ignoreUnused = true;
}
if ($scopeInfo->scopeStartIndex === 0 && $this->allowUndefinedVariablesInFileScope) {
$scopeInfo->variables[$varName]->ignoreUndefined = true;
}
if (in_array($varName, $validUndefinedVariableNames)) {
$scopeInfo->variables[$varName]->ignoreUndefined = true;
}
if (!empty($this->validUndefinedVariableRegexp) && preg_match($this->validUndefinedVariableRegexp, $varName) === 1) {
$scopeInfo->variables[$varName]->ignoreUndefined = true;
}
Helpers::debug("getOrCreateVariableInfo: scope for '{$varName}' is now", $scopeInfo);
return $scopeInfo->variables[$varName];
}
/**
* Record that a variable has been defined and assigned a value.
*
* If a variable has been defined within a scope, it will not be marked as
* undefined when that variable is later used. If it is not used, it will be
* marked as unused when that scope ends.
*
* Sometimes it's possible to assign something to a variable without
* definining it (eg: assignment to a reference); in that case, use
* `markVariableAssignmentWithoutInitialization()`.
*
* @param string $varName
* @param int $stackPtr
* @param int $currScope
*
* @return void
*/
protected function markVariableAssignment($varName, $stackPtr, $currScope) {
Helpers::debug('markVariableAssignment: starting for', $varName);
$this->markVariableAssignmentWithoutInitialization($varName, $stackPtr, $currScope);
Helpers::debug('markVariableAssignment: marked as assigned without initialization', $varName);
$varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
if (isset($varInfo->firstInitialized) && $varInfo->firstInitialized <= $stackPtr) {
Helpers::debug('markVariableAssignment: variable is already initialized', $varName);
return;
}
$varInfo->firstInitialized = $stackPtr;
Helpers::debug('markVariableAssignment: marked as initialized', $varName);
}
/**
* Record that a variable has been assigned a value.
*
* Does not record that a variable has been defined, which is the usual state
* of affairs. For that, use `markVariableAssignment()`.
*
* This is useful for assignments to references.
*
* @param string $varName
* @param int $stackPtr
* @param int $currScope
*
* @return void
*/
protected function markVariableAssignmentWithoutInitialization($varName, $stackPtr, $currScope) {
$varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
// Is the variable referencing another variable? If so, mark that variable used also.
if ($varInfo->referencedVariableScope !== null && $varInfo->referencedVariableScope !== $currScope) {
Helpers::debug('markVariableAssignmentWithoutInitialization: considering marking referenced variable assigned', $varName);
// Don't do this if the referenced variable does not exist; eg: if it's going to be bound at runtime like in array_walk
if ($this->getVariableInfo($varInfo->name, $varInfo->referencedVariableScope)) {
Helpers::debug('markVariableAssignmentWithoutInitialization: marking referenced variable as assigned also', $varName);
$this->markVariableAssignment($varInfo->name, $stackPtr, $varInfo->referencedVariableScope);
}
else {
Helpers::debug('markVariableAssignmentWithoutInitialization: not marking referenced variable assigned', $varName);
}
}
else {
Helpers::debug('markVariableAssignmentWithoutInitialization: not considering referenced variable', $varName);
}
if (empty($varInfo->scopeType)) {
$varInfo->scopeType = ScopeType::LOCAL;
}
$varInfo->allAssignments[] = $stackPtr;
}
/**
* Record that a variable has been defined within a scope.
*
* @param string $varName
* @param ScopeType::PARAM|ScopeType::BOUND|ScopeType::LOCAL|ScopeType::GLOBALSCOPE|ScopeType::STATICSCOPE $scopeType
* @param ?string $typeHint
* @param int $stackPtr
* @param int $currScope
* @param ?bool $permitMatchingRedeclaration
*
* @return void
*/
protected function markVariableDeclaration($varName, $scopeType, $typeHint, $stackPtr, $currScope, $permitMatchingRedeclaration = false) {
Helpers::debug("marking variable '{$varName}' declared in scope starting at token", $currScope);
$varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
if (!empty($varInfo->scopeType)) {
if ($permitMatchingRedeclaration === false || $varInfo->scopeType !== $scopeType) {
// Issue redeclaration/reuse warning
// Note: we check off scopeType not firstDeclared, this is so that
// we catch declarations that come after implicit declarations like
// use of a variable as a local.
$this->addWarning('Redeclaration of %s %s as %s.', $stackPtr, 'VariableRedeclaration', [
VariableInfo::$scopeTypeDescriptions[$varInfo->scopeType],
"\${$varName}",
VariableInfo::$scopeTypeDescriptions[$scopeType],
]);
}
}
$varInfo->scopeType = $scopeType;
if (isset($typeHint)) {
$varInfo->typeHint = $typeHint;
}
if (isset($varInfo->firstDeclared) && $varInfo->firstDeclared <= $stackPtr) {
Helpers::debug("variable '{$varName}' was already marked declared", $varInfo);
return;
}
$varInfo->firstDeclared = $stackPtr;
$varInfo->allAssignments[] = $stackPtr;
Helpers::debug("variable '{$varName}' marked declared", $varInfo);
}
/**
* @param string $message
* @param int $stackPtr
* @param string $code
* @param string[] $data
*
* @return void
*/
protected function addWarning($message, $stackPtr, $code, $data) {
if (!$this->currentFile) {
throw new \Exception('Cannot add warning; current file is not set.');
}
$this->currentFile
->addWarning($message, $stackPtr, $code, $data);
}
/**
* Record that a variable has been used within a scope.
*
* If the variable has not been defined first, this will still mark it used.
* To display a warning for undefined variables, use
* `markVariableReadAndWarnIfUndefined()`.
*
* @param string $varName
* @param int $stackPtr
* @param int $currScope
*
* @return void
*/
protected function markVariableRead($varName, $stackPtr, $currScope) {
$varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
if (isset($varInfo->firstRead) && $varInfo->firstRead <= $stackPtr) {
return;
}
$varInfo->firstRead = $stackPtr;
}
/**
* Return true if a variable is defined within a scope.
*
* @param string $varName
* @param int $stackPtr
* @param int $currScope
*
* @return bool
*/
protected function isVariableUndefined($varName, $stackPtr, $currScope) {
$varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
Helpers::debug('isVariableUndefined', $varInfo, 'at', $stackPtr);
if ($varInfo->ignoreUndefined) {
return false;
}
if (isset($varInfo->firstDeclared) && $varInfo->firstDeclared <= $stackPtr) {
return false;
}
if (isset($varInfo->firstInitialized) && $varInfo->firstInitialized <= $stackPtr) {
return false;
}
// If we are inside a for loop increment expression, check to see if the
// variable was defined inside the for loop.
foreach ($this->forLoops as $forLoop) {
if ($stackPtr > $forLoop->incrementStart && $stackPtr < $forLoop->incrementEnd) {
Helpers::debug('isVariableUndefined looking at increment expression for loop', $forLoop);
if (isset($varInfo->firstInitialized) && $varInfo->firstInitialized > $forLoop->blockStart && $varInfo->firstInitialized < $forLoop->blockEnd) {
return false;
}
}
}
// If we are inside a for loop body, check to see if the variable was
// defined in that loop's third expression.
foreach ($this->forLoops as $forLoop) {
if ($stackPtr > $forLoop->blockStart && $stackPtr < $forLoop->blockEnd) {
foreach ($forLoop->incrementVariables as $forLoopVarInfo) {
if ($varInfo === $forLoopVarInfo) {
return false;
}
}
}
}
return true;
}
/**
* Record a variable use and report a warning if the variable is undefined.
*
* @param File $phpcsFile
* @param string $varName
* @param int $stackPtr
* @param int $currScope
*
* @return void
*/
protected function markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope) {
$this->markVariableRead($varName, $stackPtr, $currScope);
if ($this->isVariableUndefined($varName, $stackPtr, $currScope) === true) {
Helpers::debug("variable {$varName} looks undefined");
if (Helpers::isVariableArrayPushShortcut($phpcsFile, $stackPtr)) {
$this->warnAboutUndefinedArrayPushShortcut($phpcsFile, $varName, $stackPtr);
// Mark the variable as defined if it's of the form `$x[] = 1;`
$this->markVariableAssignment($varName, $stackPtr, $currScope);
return;
}
if (Helpers::isVariableInsideUnset($phpcsFile, $stackPtr)) {
$this->warnAboutUndefinedUnset($phpcsFile, $varName, $stackPtr);
return;
}
$this->warnAboutUndefinedVariable($phpcsFile, $varName, $stackPtr);
}
}
/**
* Mark all variables within a scope as being used.
*
* This will prevent any of the variables in that scope from being reported
* as unused.
*
* @param File $phpcsFile
* @param int $stackPtr
*
* @return void
*/
protected function markAllVariablesRead(File $phpcsFile, $stackPtr) {
$currScope = Helpers::findVariableScope($phpcsFile, $stackPtr);
if ($currScope === null) {
return;
}
$scopeInfo = $this->getOrCreateScopeInfo($currScope);
$count = count($scopeInfo->variables);
Helpers::debug("marking all {$count} variables in scope as read");
foreach ($scopeInfo->variables as $varInfo) {
$this->markVariableRead($varInfo->name, $stackPtr, $scopeInfo->scopeStartIndex);
}
}
/**
* Process a parameter definition if it is inside a function definition.
*
* This does not include variables imported by a "use" statement.
*
* @param File $phpcsFile
* @param int $stackPtr
* @param string $varName
* @param int $outerScope
*
* @return void
*/
protected function processVariableAsFunctionParameter(File $phpcsFile, $stackPtr, $varName, $outerScope) {
Helpers::debug('processVariableAsFunctionParameter', $stackPtr, $varName);
$tokens = $phpcsFile->getTokens();
$functionPtr = Helpers::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
if (!is_int($functionPtr)) {
throw new \Exception("Function index not found for function argument index {$stackPtr}");
}
Helpers::debug('processVariableAsFunctionParameter found function definition', $tokens[$functionPtr]);
$this->markVariableDeclaration($varName, ScopeType::PARAM, null, $stackPtr, $functionPtr);
// Are we pass-by-reference?
$referencePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true);
if ($referencePtr !== false && $tokens[$referencePtr]['code'] === T_BITWISE_AND) {
Helpers::debug('processVariableAsFunctionParameter found pass-by-reference to scope', $outerScope);
$varInfo = $this->getOrCreateVariableInfo($varName, $functionPtr);
$varInfo->referencedVariableScope = $outerScope;
}
// Are we optional with a default?
if (Helpers::getNextAssignPointer($phpcsFile, $stackPtr) !== null) {
Helpers::debug('processVariableAsFunctionParameter optional with default');
$this->markVariableAssignment($varName, $stackPtr, $functionPtr);
}
// Are we using constructor promotion? If so, that counts as both definition and use.
if (Helpers::isConstructorPromotion($phpcsFile, $stackPtr)) {
Helpers::debug('processVariableAsFunctionParameter constructor promotion');
$this->markVariableRead($varName, $stackPtr, $outerScope);
}
}
/**
* Process a variable definition if it is inside a function's "use" import.
*
* @param File $phpcsFile
* @param int $stackPtr
* @param string $varName
* @param int $outerScope The start of the scope outside the function definition
*
* @return void
*/
protected function processVariableAsUseImportDefinition(File $phpcsFile, $stackPtr, $varName, $outerScope) {
$tokens = $phpcsFile->getTokens();
Helpers::debug('processVariableAsUseImportDefinition', $stackPtr, $varName, $outerScope);
$endOfArgsPtr = $phpcsFile->findPrevious([
T_CLOSE_PARENTHESIS,
], $stackPtr - 1, null);
if (!is_int($endOfArgsPtr)) {
throw new \Exception("Arguments index not found for function use index {$stackPtr} when processing variable {$varName}");
}
$functionPtr = Helpers::getFunctionIndexForFunctionParameter($phpcsFile, $endOfArgsPtr);
if (!is_int($functionPtr)) {
throw new \Exception("Function index not found for function use index {$stackPtr} (using {$endOfArgsPtr}) when processing variable {$varName}");
}
// Use is both a read (in the enclosing scope) and a define (in the function scope)
$this->markVariableRead($varName, $stackPtr, $outerScope);
// If it's undefined in the enclosing scope, the use is wrong
if ($this->isVariableUndefined($varName, $stackPtr, $outerScope) === true) {
Helpers::debug("variable '{$varName}' in function definition looks undefined in scope", $outerScope);
$this->warnAboutUndefinedVariable($phpcsFile, $varName, $stackPtr);
return;
}
$this->markVariableDeclaration($varName, ScopeType::BOUND, null, $stackPtr, $functionPtr);
$this->markVariableAssignment($varName, $stackPtr, $functionPtr);
// Are we pass-by-reference? If so, then any assignment to the variable in
// the function scope also should count for the enclosing scope.
$referencePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true);
if (is_int($referencePtr) && $tokens[$referencePtr]['code'] === T_BITWISE_AND) {
Helpers::debug("variable '{$varName}' in function definition looks passed by reference");
$varInfo = $this->getOrCreateVariableInfo($varName, $functionPtr);
$varInfo->referencedVariableScope = $outerScope;
}
}
/**
* Process a class property that is being defined.
*
* Property definitions are ignored currently because all property access is
* legal, even to undefined properties.
*
* Can be called for any token and will return false if the variable is not
* of this type.
*
* @param File $phpcsFile
* @param int $stackPtr
*
* @return bool
*/
protected function processVariableAsClassProperty(File $phpcsFile, $stackPtr) {
// Make sure we are not in a class method before assuming it's a property.
$tokens = $phpcsFile->getTokens();
/** @var array{conditions?: (int|string)[], content?: string}|null */
$token = $tokens[$stackPtr];
if ($token && !empty($token['conditions']) && !empty($token['content']) && !Helpers::areConditionsWithinFunctionBeforeClass($token)) {
return Helpers::areAnyConditionsAClass($token);
}
return false;
}
/**
* Process a variable that is being accessed inside a catch block.
*
* Can be called for any token and will return false if the variable is not
* of this type.
*
* @param File $phpcsFile
* @param int $stackPtr
* @param string $varName
* @param int $currScope
*
* @return bool
*/
protected function processVariableAsCatchBlock(File $phpcsFile, $stackPtr, $varName, $currScope) {
$tokens = $phpcsFile->getTokens();
// Are we a catch block parameter?
$openPtr = Helpers::findContainingOpeningBracket($phpcsFile, $stackPtr);
if ($openPtr === null) {
return false;
}
$catchPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $openPtr - 1, null, true, null, true);
if ($catchPtr !== false && $tokens[$catchPtr]['code'] === T_CATCH) {
// Scope of the exception var is actually the function, not just the catch block.
$this->markVariableDeclaration($varName, ScopeType::LOCAL, null, $stackPtr, $currScope, true);
$this->markVariableAssignment($varName, $stackPtr, $currScope);
if ($this->allowUnusedCaughtExceptions) {
$varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
$varInfo->ignoreUnused = true;
}
return true;
}
return false;
}
/**
* Process a variable that is being accessed as a member of `$this`.
*
* Looks for variables of the form `$this->myVariable`.
*
* Can be called for any token and will return false if the variable is not
* of this type.
*
* @param File $phpcsFile
* @param int $stackPtr
* @param string $varName
*
* @return bool
*/
protected function processVariableAsThisWithinClass(File $phpcsFile, $stackPtr, $varName) {
$tokens = $phpcsFile->getTokens();
$token = $tokens[$stackPtr];
// Are we $this within a class?
if ($varName !== 'this' || empty($token['conditions'])) {
return false;
}
// Handle enums specially since their condition may not exist in old phpcs.
$inEnum = false;
foreach ($this->enums as $enum) {
if ($stackPtr > $enum->blockStart && $stackPtr < $enum->blockEnd) {
$inEnum = true;
}
}
$inFunction = false;
foreach (array_reverse($token['conditions'], true) as $scopeCode) {
// $this within a closure is valid
if ($scopeCode === T_CLOSURE && $inFunction === false) {
return true;
}
$classlikeCodes = [
T_CLASS,
T_ANON_CLASS,
T_TRAIT,
];
if (defined('T_ENUM')) {
$classlikeCodes[] = T_ENUM;
}
if (in_array($scopeCode, $classlikeCodes, true)) {
return true;
}
if ($scopeCode === T_FUNCTION && $inEnum) {
return true;
}
// Handle nested function declarations.
if ($scopeCode === T_FUNCTION) {
if ($inFunction === true) {
break;
}
$inFunction = true;
}
}
return false;
}
/**
* Process a superglobal variable that is being accessed.
*
* Can be called for any token and will return false if the variable is not
* of this type.
*
* @param string $varName
*
* @return bool
*/
protected function processVariableAsSuperGlobal($varName) {
$superglobals = [
'GLOBALS',
'_SERVER',
'_GET',
'_POST',
'_FILES',
'_COOKIE',
'_SESSION',
'_REQUEST',
'_ENV',
'argv',
'argc',
'http_response_header',
'HTTP_RAW_POST_DATA',
];
// Are we a superglobal variable?
return in_array($varName, $superglobals, true);
}
/**
* Process a variable that is being accessed with static syntax.
*
* That is, this will record the use of a variable of the form
* `MyClass::$myVariable` or `self::$myVariable`.
*
* Can be called for any token and will return false if the variable is not
* of this type.
*
* @param File $phpcsFile
* @param int $stackPtr
*
* @return bool
*/
protected function processVariableAsStaticMember(File $phpcsFile, $stackPtr) {
$tokens = $phpcsFile->getTokens();
$doubleColonPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true);
if ($doubleColonPtr === false || $tokens[$doubleColonPtr]['code'] !== T_DOUBLE_COLON) {
return false;
}
$classNamePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $doubleColonPtr - 1, null, true);
$staticReferences = [
T_STRING,
T_SELF,
T_PARENT,
T_STATIC,
T_VARIABLE,
];
if ($classNamePtr === false || !in_array($tokens[$classNamePtr]['code'], $staticReferences, true)) {
return false;
}
// "When calling static methods, the function call is stronger than the
// static property operator" so look for a function call.
$parenPointer = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true);
if ($parenPointer !== false && $tokens[$parenPointer]['code'] === T_OPEN_PARENTHESIS) {
return false;
}
return true;
}
/**
* @param File $phpcsFile
* @param int $stackPtr
* @param string $varName
*
* @return bool
*/
protected function processVariableAsStaticOutsideClass(File $phpcsFile, $stackPtr, $varName) {
// Are we refering to self:: outside a class?
$tokens = $phpcsFile->getTokens();
/** @var array{conditions?: (int|string)[], content?: string}|null */
$token = $tokens[$stackPtr];
$doubleColonPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true);
if ($doubleColonPtr === false || $tokens[$doubleColonPtr]['code'] !== T_DOUBLE_COLON) {
return false;
}
$classNamePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $doubleColonPtr - 1, null, true);
if ($classNamePtr === false) {
return false;
}
$code = $tokens[$classNamePtr]['code'];
$staticReferences = [
T_SELF,
T_STATIC,
];
if (!in_array($code, $staticReferences, true)) {
return false;
}
$errorClass = $code === T_SELF ? 'SelfOutsideClass' : 'StaticOutsideClass';
$staticRefType = $code === T_SELF ? 'self::' : 'static::';
if (!empty($token['conditions']) && !empty($token['content']) && Helpers::areAnyConditionsAClass($token)) {
return false;
}
$phpcsFile->addError("Use of {$staticRefType}%s outside class definition.", $stackPtr, $errorClass, [
"\${$varName}",
]);
return true;
}
/**
* Process a variable that is being assigned.
*
* This will record that the variable has been defined within a scope so that
* later we can determine if it it unused and we can guarantee that any
* future uses of the variable are not using an undefined variable.
*
* References (on either side of an assignment) behave differently and this
* function handles those cases as well.
*
* @param File $phpcsFile
* @param int $stackPtr
* @param string $varName
* @param int $currScope
*
* @return void
*/
protected function processVariableAsAssignment(File $phpcsFile, $stackPtr, $varName, $currScope) {
Helpers::debug("processVariableAsAssignment: starting for '{$varName}'");
$assignPtr = Helpers::getNextAssignPointer($phpcsFile, $stackPtr);
if (!is_int($assignPtr)) {
return;
}
// If the right-hand-side of the assignment to this variable is a reference
// variable, then this variable is a reference to that one, and as such any
// assignment to this variable (except another assignment by reference,
// which would change the binding) has a side effect of changing the
// referenced variable and therefore should count as both an assignment and
// a read.
$tokens = $phpcsFile->getTokens();
$referencePtr = $phpcsFile->findNext(Tokens::$emptyTokens, $assignPtr + 1, null, true, null, true);
if (is_int($referencePtr) && $tokens[$referencePtr]['code'] === T_BITWISE_AND) {
Helpers::debug("processVariableAsAssignment: found reference variable for '{$varName}'");
$varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
// If the variable was already declared, but was not yet read, it is
// unused because we're about to change the binding; that is, unless we
// are inside a conditional block because in that case the condition may
// never activate.
$scopeInfo = $this->getOrCreateScopeInfo($currScope);
$conditionPointer = Helpers::getClosestConditionPositionIfBeforeOtherConditions($tokens[$referencePtr]['conditions']);
$lastAssignmentPtr = $varInfo->firstDeclared;
if (!$conditionPointer && $lastAssignmentPtr) {
Helpers::debug("processVariableAsAssignment: considering close of scope for '{$varName}' due to reference reassignment");
$this->processScopeCloseForVariable($phpcsFile, $varInfo, $scopeInfo);
}
if ($conditionPointer && $lastAssignmentPtr && $conditionPointer < $lastAssignmentPtr) {
// We may be inside a condition but the last assignment was also inside this condition.
Helpers::debug("processVariableAsAssignment: considering close of scope for '{$varName}' due to reference reassignment ignoring recent condition");
$this->processScopeCloseForVariable($phpcsFile, $varInfo, $scopeInfo);
}
if ($conditionPointer && $lastAssignmentPtr && $conditionPointer > $lastAssignmentPtr) {
Helpers::debug("processVariableAsAssignment: not considering close of scope for '{$varName}' due to reference reassignment because it is conditional");
}
// The referenced variable may have a different name, but we don't
// actually need to mark it as used in this case because the act of this
// assignment will mark it used on the next token.
$varInfo->referencedVariableScope = $currScope;
$this->markVariableDeclaration($varName, ScopeType::LOCAL, null, $stackPtr, $currScope, true);
// An assignment to a reference is a binding and should not count as
// initialization since it doesn't change any values.
$this->markVariableAssignmentWithoutInitialization($varName, $stackPtr, $currScope);
return;
}
Helpers::debug('processVariableAsAssignment: marking as assignment in scope', $currScope);
$this->markVariableAssignment($varName, $stackPtr, $currScope);
// If the left-hand-side of the assignment (the variable we are examining)
// is itself a reference, then that counts as a read as well as a write.
$varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
if ($varInfo->isDynamicReference) {
Helpers::debug('processVariableAsAssignment: also marking as a use because variable is a reference');
$this->markVariableRead($varName, $stackPtr, $currScope);
}
}
/**
* Processes variables destructured from an array using shorthand list assignment.
*
* This will record the definition and assignment of variables defined using
* the format:
*
* ```
* [ $foo, $bar, $baz ] = $ary;
* ```
*
* Can be called for any token and will return false if the variable is not
* of this type.
*
* @param File $phpcsFile
* @param int $stackPtr
* @param string $varName
* @param int $currScope
*
* @return bool
*/
protected function processVariableAsListShorthandAssignment(File $phpcsFile, $stackPtr, $varName, $currScope) {
// OK, are we within a [ ... ] construct?
$openPtr = Helpers::findContainingOpeningSquareBracket($phpcsFile, $stackPtr);
if (!is_int($openPtr)) {
return false;
}
// OK, we're a [ ... ] construct... are we being assigned to?
$assignments = Helpers::getListAssignments($phpcsFile, $openPtr);
if (!$assignments) {
return false;
}
$matchingAssignment = array_reduce($assignments, function ($thisAssignment, $assignment) use ($stackPtr) {
if ($assignment === $stackPtr) {
return $assignment;
}
return $thisAssignment;
});
if (!$matchingAssignment) {
return false;
}
// Yes, we're being assigned.
$this->markVariableAssignment($varName, $stackPtr, $currScope);
return true;
}
/**
* Processes variables destructured from an array using list assignment.
*
* This will record the definition and assignment of variables defined using
* the format:
*
* ```
* list( $foo, $bar, $baz ) = $ary;
* ```
*
* Can be called for any token and will return false if the variable is not
* of this type.
*
* @param File $phpcsFile
* @param int $stackPtr
* @param string $varName
* @param int $currScope
*
* @return bool
*/
protected function processVariableAsListAssignment(File $phpcsFile, $stackPtr, $varName, $currScope) {
$tokens = $phpcsFile->getTokens();
// OK, are we within a list (...) construct?
$openPtr = Helpers::findContainingOpeningBracket($phpcsFile, $stackPtr);
if ($openPtr === null) {
return false;
}
$prevPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $openPtr - 1, null, true, null, true);
if (is_bool($prevPtr) || $tokens[$prevPtr]['code'] !== T_LIST) {
return false;
}
// OK, we're a list (...) construct... are we being assigned to?
$assignments = Helpers::getListAssignments($phpcsFile, $prevPtr);
if (!$assignments) {
return false;
}
$matchingAssignment = array_reduce($assignments, function ($thisAssignment, $assignment) use ($stackPtr) {
if ($assignment === $stackPtr) {
return $assignment;
}
return $thisAssignment;
});
if (!$matchingAssignment) {
return false;
}
// Yes, we're being assigned.
$this->markVariableAssignment($varName, $stackPtr, $currScope);
return true;
}
/**
* Process a variable being defined (imported, really) with the `global` keyword.
*
* Can be called for any token and will return false if the variable is not
* of this type.
*
* @param File $phpcsFile
* @param int $stackPtr
* @param string $varName
* @param int $currScope
*
* @return bool
*/
protected function processVariableAsGlobalDeclaration(File $phpcsFile, $stackPtr, $varName, $currScope) {
$tokens = $phpcsFile->getTokens();
// Are we a global declaration?
// Search backwards for first token that isn't whitespace/comment, comma or variable.
$ignore = Tokens::$emptyTokens;
$ignore[T_VARIABLE] = T_VARIABLE;
$ignore[T_COMMA] = T_COMMA;
$globalPtr = $phpcsFile->findPrevious($ignore, $stackPtr - 1, null, true, null, true);
if ($globalPtr === false || $tokens[$globalPtr]['code'] !== T_GLOBAL) {
return false;
}
// It's a global declaration.
$this->markVariableDeclaration($varName, ScopeType::GLOBALSCOPE, null, $stackPtr, $currScope);
return true;
}
/**
* Process a variable as a static declaration within a function.
*
* Specifically, this looks for variable definitions of the form `static
* $foo = 'hello';` or `static int $foo;` inside a function definition.
*
* This will not operate on variables that are written in a class definition
* outside of a function like `static $foo;` or `public static ?int $foo =
* 'bar';` because class properties (static or instance) are currently not
* tracked by this sniff. This is because a class property might be unused
* inside the class, but used outside the class (we cannot easily know if it
* is unused); this is also because it's common and legal to define class
* properties when they are assigned and that assignment can happen outside a
* class (we cannot easily know if the use of a property is undefined). These
* sorts of checks are better performed by static analysis tools that can see
* a whole project rather than a linter which can only easily see a file or
* some lines.
*
* If found, such a variable will be marked as declared (and possibly
* assigned, if it includes an initial value) within the scope of the
* function body.
*
* This will not operate on variables that use late static binding
* (`static::$foobar`) or the parameters of static methods even though they
* include the word `static` in the same statement.
*
* This only finds the defintions of static variables. Their use is handled
* by `processVariableAsStaticMember()`.
*
* Can be called for any token and will return false if the variable is not
* of this type.
*
* @param File $phpcsFile
* @param int $stackPtr
* @param string $varName
* @param int $currScope
*
* @return bool
*/
protected function processVariableAsStaticDeclaration(File $phpcsFile, $stackPtr, $varName, $currScope) {
$tokens = $phpcsFile->getTokens();
// Search backwards for a `static` keyword that occurs before the start of the statement.
$startOfStatement = $phpcsFile->findPrevious([
T_SEMICOLON,
T_OPEN_CURLY_BRACKET,
T_FN_ARROW,
T_OPEN_PARENTHESIS,
], $stackPtr - 1, null, false, null, true);
$staticPtr = $phpcsFile->findPrevious([
T_STATIC,
], $stackPtr - 1, null, false, null, true);
if (!is_int($startOfStatement)) {
$startOfStatement = 1;
}
if (!is_int($staticPtr)) {
return false;
}
// PHPCS is bad at finding the start of statements so we have to do it ourselves.
if ($staticPtr < $startOfStatement) {
return false;
}
// Is the 'static' keyword an anonymous static function declaration? If so,
// this is not a static variable declaration.
$tokenAfterStatic = $phpcsFile->findNext(Tokens::$emptyTokens, $staticPtr + 1, null, true, null, true);
$functionTokenTypes = [
T_FUNCTION,
T_CLOSURE,
T_FN,
];
if (is_int($tokenAfterStatic) && in_array($tokens[$tokenAfterStatic]['code'], $functionTokenTypes, true)) {
return false;
}
// Is the token inside function parameters? If so, this is not a static
// declaration because we must be inside a function body.
if (Helpers::isTokenFunctionParameter($phpcsFile, $stackPtr)) {
return false;
}
// Is the token inside a function call? If so, this is not a static
// declaration.
if (Helpers::isTokenInsideFunctionCallArgument($phpcsFile, $stackPtr)) {
return false;
}
// Is the keyword a late static binding? If so, this isn't the static
// keyword we're looking for, but since static:: isn't allowed in a
// compile-time constant, we also know we can't be part of a static
// declaration anyway, so there's no need to look any further.
$lateStaticBindingPtr = $phpcsFile->findNext(T_WHITESPACE, $staticPtr + 1, null, true, null, true);
if ($lateStaticBindingPtr !== false && $tokens[$lateStaticBindingPtr]['code'] === T_DOUBLE_COLON) {
return false;
}
$this->markVariableDeclaration($varName, ScopeType::STATICSCOPE, null, $stackPtr, $currScope);
if (Helpers::getNextAssignPointer($phpcsFile, $stackPtr) !== null) {
$this->markVariableAssignment($varName, $stackPtr, $currScope);
}
return true;
}
/**
* @param File $phpcsFile
* @param int $stackPtr
* @param string $varName
* @param int $currScope
*
* @return bool
*/
protected function processVariableAsForeachLoopVar(File $phpcsFile, $stackPtr, $varName, $currScope) {
$tokens = $phpcsFile->getTokens();
// Are we a foreach loopvar?
$openParenPtr = Helpers::findContainingOpeningBracket($phpcsFile, $stackPtr);
if (!is_int($openParenPtr)) {
return false;
}
$foreachPtr = Helpers::getIntOrNull($phpcsFile->findPrevious(Tokens::$emptyTokens, $openParenPtr - 1, null, true));
if (!is_int($foreachPtr)) {
return false;
}
if ($tokens[$foreachPtr]['code'] === T_LIST) {
$openParenPtr = Helpers::findContainingOpeningBracket($phpcsFile, $foreachPtr);
if (!is_int($openParenPtr)) {
return false;
}
$foreachPtr = Helpers::getIntOrNull($phpcsFile->findPrevious(Tokens::$emptyTokens, $openParenPtr - 1, null, true));
if (!is_int($foreachPtr)) {
return false;
}
}
if ($tokens[$foreachPtr]['code'] !== T_FOREACH) {
return false;
}
// Is there an 'as' token between us and the foreach?
if ($phpcsFile->findPrevious(T_AS, $stackPtr - 1, $openParenPtr) === false) {
return false;
}
$this->markVariableAssignment($varName, $stackPtr, $currScope);
$varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
// Is this the value of a key => value foreach?
if ($phpcsFile->findPrevious(T_DOUBLE_ARROW, $stackPtr - 1, $openParenPtr) !== false) {
$varInfo->isForeachLoopAssociativeValue = true;
}
// Are we pass-by-reference?
$referencePtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true);
if ($referencePtr !== false && $tokens[$referencePtr]['code'] === T_BITWISE_AND) {
Helpers::debug('processVariableAsForeachLoopVar: found foreach loop variable assigned by reference');
$varInfo->isDynamicReference = true;
}
return true;
}
/**
* @param File $phpcsFile
* @param int $stackPtr
* @param string $varName
* @param int $currScope
*
* @return bool
*/
protected function processVariableAsPassByReferenceFunctionCall(File $phpcsFile, $stackPtr, $varName, $currScope) {
$tokens = $phpcsFile->getTokens();
// Are we pass-by-reference to known pass-by-reference function?
$functionPtr = Helpers::findFunctionCall($phpcsFile, $stackPtr);
if ($functionPtr === null || !isset($tokens[$functionPtr])) {
return false;
}
// Is our function a known pass-by-reference function?
$functionName = $tokens[$functionPtr]['content'];
$refArgs = $this->getPassByReferenceFunction($functionName);
if (!$refArgs) {
return false;
}
$argPtrs = Helpers::findFunctionCallArguments($phpcsFile, $stackPtr);
// We're within a function call arguments list, find which arg we are.
$argPos = false;
foreach ($argPtrs as $idx => $ptrs) {
if (in_array($stackPtr, $ptrs)) {
$argPos = $idx + 1;
break;
}
}
if ($argPos === false) {
return false;
}
if (!in_array($argPos, $refArgs)) {
// Our arg wasn't mentioned explicitly, are we after an elipsis catch-all?
$elipsis = array_search('...', $refArgs);
if ($elipsis === false) {
return false;
}
$elipsis = (int) $elipsis;
if ($argPos < $refArgs[$elipsis - 1]) {
return false;
}
}
// Our argument position matches that of a pass-by-ref argument,
// check that we're the only part of the argument expression.
foreach ($argPtrs[$argPos - 1] as $ptr) {
if ($ptr === $stackPtr) {
continue;
}
if (isset(Tokens::$emptyTokens[$tokens[$ptr]['code']]) === false) {
return false;
}
}
// Just us, we can mark it as a write.
$this->markVariableAssignment($varName, $stackPtr, $currScope);
// It's a read as well for purposes of used-variables.
$this->markVariableRead($varName, $stackPtr, $currScope);
return true;
}
/**
* @param File $phpcsFile
* @param int $stackPtr
* @param string $varName
* @param int $currScope
*
* @return bool
*/
protected function processVariableAsSymbolicObjectProperty(File $phpcsFile, $stackPtr, $varName, $currScope) {
$tokens = $phpcsFile->getTokens();
// Are we a symbolic object property/function derefeference?
// Search backwards for first token that isn't whitespace, is it a "->" operator?
$objectOperatorPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true);
if ($objectOperatorPtr === false || $tokens[$objectOperatorPtr]['code'] !== T_OBJECT_OPERATOR) {
return false;
}
$this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);
return true;
}
/**
* Process a normal variable in the code.
*
* Most importantly, this function determines if the variable use is a "read"
* (using the variable for something) or a "write" (an assignment) or,
* sometimes, both at once.
*
* It also determines the scope of the variable (where it begins and ends).
*
* Using these two pieces of information, we can determine if the variable is
* being used ("read") without having been defined ("write").
*
* We can also determine, once the scan has hit the end of a scope, if any of
* the variables within that scope have been defined ("write") without being
* used ("read"). That behavior, however, happens in the `processScopeClose()`
* function using the data gathered by this function.
*
* Some variables are used in more complex ways, so there are other similar
* functions to this one, like `processVariableInString`, and
* `processCompact`. They have the same purpose as this function, though.
*
* If the 'ignore-for-loops' option is true, we will ignore the special
* processing for the increment variables of for loops. This will prevent
* infinite loops.
*
* @param File $phpcsFile The PHP_CodeSniffer file where this token was found.
* @param int $stackPtr The position where the token was found.
* @param array<string, bool|string|int> $options See above.
*
* @return void
*/
protected function processVariable(File $phpcsFile, $stackPtr, $options = []) {
$tokens = $phpcsFile->getTokens();
$token = $tokens[$stackPtr];
// Get the name of the variable.
$varName = Helpers::normalizeVarName($token['content']);
Helpers::debug("examining token for variable '{$varName}' on line {$token['line']}", $token);
// Find the start of the current scope.
$currScope = Helpers::findVariableScope($phpcsFile, $stackPtr);
if ($currScope === null) {
Helpers::debug('no scope found');
return;
}
Helpers::debug("start of scope for variable '{$varName}' is", $currScope);
// Determine if variable is being assigned ("write") or used ("read").
// Read methods that preempt assignment:
// Are we a $object->$property type symbolic reference?
// Possible assignment methods:
// Is a mandatory function/closure parameter
// Is an optional function/closure parameter with non-null value
// Is closure use declaration of a variable defined within containing scope
// catch (...) block start
// $this within a class.
// $GLOBALS, $_REQUEST, etc superglobals.
// $var part of class::$var static member
// Assignment via =
// Assignment via list (...) =
// Declares as a global
// Declares as a static
// Assignment via foreach (... as ...) { }
// Pass-by-reference to known pass-by-reference function
// Are we inside the third expression of a for loop? Store such variables
// for processing after the loop ends by `processClosingForLoopsAt()`.
if (empty($options['ignore-for-loops'])) {
$forLoop = Helpers::getForLoopForIncrementVariable($stackPtr, $this->forLoops);
if ($forLoop) {
Helpers::debug('found variable inside for loop third expression');
$varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
$forLoop->incrementVariables[$stackPtr] = $varInfo;
return;
}
}
// Are we a $object->$property type symbolic reference?
if ($this->processVariableAsSymbolicObjectProperty($phpcsFile, $stackPtr, $varName, $currScope)) {
Helpers::debug('found symbolic object property');
return;
}
// Are we a function or closure parameter?
if (Helpers::isTokenFunctionParameter($phpcsFile, $stackPtr)) {
Helpers::debug('found function definition parameter');
$this->processVariableAsFunctionParameter($phpcsFile, $stackPtr, $varName, $currScope);
return;
}
// Are we a variable being imported into a function's scope with "use"?
if (Helpers::isTokenInsideFunctionUseImport($phpcsFile, $stackPtr)) {
Helpers::debug('found use scope import definition');
$this->processVariableAsUseImportDefinition($phpcsFile, $stackPtr, $varName, $currScope);
return;
}
// Are we a catch parameter?
if ($this->processVariableAsCatchBlock($phpcsFile, $stackPtr, $varName, $currScope)) {
Helpers::debug('found catch block');
return;
}
// Are we $this within a class?
if ($this->processVariableAsThisWithinClass($phpcsFile, $stackPtr, $varName)) {
Helpers::debug('found this usage within a class');
return;
}
// Are we a $GLOBALS, $_REQUEST, etc superglobal?
if ($this->processVariableAsSuperGlobal($varName)) {
Helpers::debug('found superglobal');
return;
}
// Check for static members used outside a class
if ($this->processVariableAsStaticOutsideClass($phpcsFile, $stackPtr, $varName)) {
Helpers::debug('found static usage outside of class');
return;
}
// $var part of class::$var static member
if ($this->processVariableAsStaticMember($phpcsFile, $stackPtr)) {
Helpers::debug('found static member');
return;
}
if ($this->processVariableAsClassProperty($phpcsFile, $stackPtr)) {
Helpers::debug('found class property definition');
return;
}
// Is the next non-whitespace an assignment?
if (Helpers::isTokenInsideAssignmentLHS($phpcsFile, $stackPtr)) {
Helpers::debug('found assignment');
$this->processVariableAsAssignment($phpcsFile, $stackPtr, $varName, $currScope);
if (Helpers::isTokenInsideAssignmentRHS($phpcsFile, $stackPtr) || Helpers::isTokenInsideFunctionCallArgument($phpcsFile, $stackPtr)) {
Helpers::debug("found assignment that's also inside an expression");
$this->markVariableRead($varName, $stackPtr, $currScope);
return;
}
return;
}
// OK, are we within a list (...) = construct?
if ($this->processVariableAsListAssignment($phpcsFile, $stackPtr, $varName, $currScope)) {
Helpers::debug('found list assignment');
return;
}
// OK, are we within a [...] = construct?
if ($this->processVariableAsListShorthandAssignment($phpcsFile, $stackPtr, $varName, $currScope)) {
Helpers::debug('found list shorthand assignment');
return;
}
// Are we a global declaration?
if ($this->processVariableAsGlobalDeclaration($phpcsFile, $stackPtr, $varName, $currScope)) {
Helpers::debug('found global declaration');
return;
}
// Are we a static declaration?
if ($this->processVariableAsStaticDeclaration($phpcsFile, $stackPtr, $varName, $currScope)) {
Helpers::debug('found static declaration');
return;
}
// Are we a foreach loopvar?
if ($this->processVariableAsForeachLoopVar($phpcsFile, $stackPtr, $varName, $currScope)) {
Helpers::debug('found foreach loop variable');
return;
}
// Are we pass-by-reference to known pass-by-reference function?
if ($this->processVariableAsPassByReferenceFunctionCall($phpcsFile, $stackPtr, $varName, $currScope)) {
Helpers::debug('found pass by reference');
return;
}
// Are we a numeric variable used for constructs like preg_replace?
if (Helpers::isVariableANumericVariable($varName)) {
Helpers::debug('found numeric variable');
return;
}
if (Helpers::isVariableInsideElseCondition($phpcsFile, $stackPtr) || Helpers::isVariableInsideElseBody($phpcsFile, $stackPtr)) {
Helpers::debug('found variable inside else condition or body');
$this->processVaribleInsideElse($phpcsFile, $stackPtr, $varName, $currScope);
return;
}
// Are we an isset or empty call?
if (Helpers::isVariableInsideIssetOrEmpty($phpcsFile, $stackPtr)) {
Helpers::debug('found isset or empty');
$this->markVariableRead($varName, $stackPtr, $currScope);
return;
}
// OK, we don't appear to be a write to the var, assume we're a read.
Helpers::debug('looks like a variable read');
$this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);
}
/**
* @param File $phpcsFile
* @param int $stackPtr
* @param string $varName
* @param int $currScope
*
* @return void
*/
protected function processVaribleInsideElse(File $phpcsFile, $stackPtr, $varName, $currScope) {
// Find all assignments to this variable inside the current scope.
$varInfo = $this->getOrCreateVariableInfo($varName, $currScope);
$allAssignmentIndices = array_unique($varInfo->allAssignments);
// Find the attached 'if' and 'elseif' block start and end indices.
$blockIndices = Helpers::getAttachedBlockIndicesForElse($phpcsFile, $stackPtr);
// If all of the assignments are within the previous attached blocks, then warn about undefined.
$tokens = $phpcsFile->getTokens();
$assignmentsInsideAttachedBlocks = [];
foreach ($allAssignmentIndices as $index) {
foreach ($blockIndices as $blockIndex) {
$blockToken = $tokens[$blockIndex];
Helpers::debug('for variable inside else, looking at assignment', $index, 'at block index', $blockIndex, 'which is token', $blockToken);
if (isset($blockToken['scope_opener']) && isset($blockToken['scope_closer'])) {
$scopeOpener = $blockToken['scope_opener'];
$scopeCloser = $blockToken['scope_closer'];
}
else {
// If the `if` statement has no scope, it is probably inline, which
// means its scope is from the end of the condition up until the next
// semicolon
$scopeOpener = isset($blockToken['parenthesis_closer']) ? $blockToken['parenthesis_closer'] : $blockIndex + 1;
$scopeCloser = $phpcsFile->findNext([
T_SEMICOLON,
], $scopeOpener);
if (!$scopeCloser) {
throw new \Exception("Cannot find scope for if condition block at index {$stackPtr} while examining variable {$varName}");
}
}
Helpers::debug('for variable inside else, looking at scope', $index, 'between', $scopeOpener, 'and', $scopeCloser);
if (Helpers::isIndexInsideScope($index, $scopeOpener, $scopeCloser)) {
$assignmentsInsideAttachedBlocks[] = $index;
}
}
}
if (count($assignmentsInsideAttachedBlocks) === count($allAssignmentIndices)) {
if (!$varInfo->ignoreUndefined) {
Helpers::debug("variable {$varName} inside else looks undefined");
$this->warnAboutUndefinedVariable($phpcsFile, $varName, $stackPtr);
}
return;
}
Helpers::debug('looks like a variable read inside else');
$this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);
}
/**
* Called to process variables found in double quoted strings.
*
* Note that there may be more than one variable in the string, which will
* result only in one call for the string.
*
* @param File $phpcsFile The PHP_CodeSniffer file where this token was found.
* @param int $stackPtr The position where the double quoted string was found.
*
* @return void
*/
protected function processVariableInString(File $phpcsFile, $stackPtr) {
$tokens = $phpcsFile->getTokens();
$token = $tokens[$stackPtr];
$regexp = Constants::getDoubleQuotedVarRegexp();
if (!empty($regexp) && !preg_match_all($regexp, $token['content'], $matches)) {
Helpers::debug('processVariableInString: no variables found', $token);
return;
}
Helpers::debug('examining token for variable in string', $token);
if (empty($matches)) {
Helpers::debug('processVariableInString: no variables found after search', $token);
return;
}
foreach ($matches[1] as $varName) {
$varName = Helpers::normalizeVarName($varName);
// Are we $this within a class?
if ($this->processVariableAsThisWithinClass($phpcsFile, $stackPtr, $varName)) {
continue;
}
if ($this->processVariableAsSuperGlobal($varName)) {
continue;
}
// Are we a numeric variable used for constructs like preg_replace?
if (Helpers::isVariableANumericVariable($varName)) {
continue;
}
$currScope = Helpers::findVariableScope($phpcsFile, $stackPtr, $varName);
if ($currScope === null) {
continue;
}
$this->markVariableReadAndWarnIfUndefined($phpcsFile, $varName, $stackPtr, $currScope);
}
}
/**
* Called to process variables named in a call to compact().
*
* @param File $phpcsFile The PHP_CodeSniffer file where this token was found.
* @param int $stackPtr The position where the call to compact() was found.
*
* @return void
*/
protected function processCompact(File $phpcsFile, $stackPtr) {
Helpers::debug("processCompact at {$stackPtr}");
$arguments = Helpers::findFunctionCallArguments($phpcsFile, $stackPtr);
$variables = Helpers::getVariablesInsideCompact($phpcsFile, $stackPtr, $arguments);
foreach ($variables as $variable) {
$currScope = Helpers::findVariableScope($phpcsFile, $stackPtr, $variable->name);
if ($currScope === null) {
continue;
}
$variablePosition = $variable->firstRead ? $variable->firstRead : $stackPtr;
$this->markVariableReadAndWarnIfUndefined($phpcsFile, $variable->name, $variablePosition, $currScope);
}
}
/**
* Called to process the end of a scope.
*
* Note that although triggered by the closing curly brace of the scope,
* $stackPtr is the scope conditional, not the closing curly brace.
*
* @param File $phpcsFile The PHP_CodeSniffer file where this token was found.
* @param int $stackPtr The position of the scope conditional.
*
* @return void
*/
protected function processScopeClose(File $phpcsFile, $stackPtr) {
Helpers::debug("processScopeClose at {$stackPtr}");
$scopeInfo = $this->scopeManager
->getScopeForScopeStart($phpcsFile->getFilename(), $stackPtr);
if (is_null($scopeInfo)) {
return;
}
foreach ($scopeInfo->variables as $varInfo) {
$this->processScopeCloseForVariable($phpcsFile, $varInfo, $scopeInfo);
}
}
/**
* Warn about an unused variable if it has not been used within a scope.
*
* @param File $phpcsFile
* @param VariableInfo $varInfo
* @param ScopeInfo $scopeInfo
*
* @return void
*/
protected function processScopeCloseForVariable(File $phpcsFile, VariableInfo $varInfo, ScopeInfo $scopeInfo) {
Helpers::debug('processScopeCloseForVariable', $varInfo);
if ($varInfo->ignoreUnused || isset($varInfo->firstRead)) {
return;
}
if ($this->allowUnusedFunctionParameters && $varInfo->scopeType === ScopeType::PARAM) {
return;
}
if ($this->allowUnusedParametersBeforeUsed && $varInfo->scopeType === ScopeType::PARAM && Helpers::areFollowingArgumentsUsed($varInfo, $scopeInfo)) {
Helpers::debug("variable '{$varInfo->name}' at end of scope has unused following args");
return;
}
if ($this->allowUnusedForeachVariables && $varInfo->isForeachLoopAssociativeValue) {
return;
}
if ($varInfo->referencedVariableScope !== null && isset($varInfo->firstInitialized)) {
Helpers::debug("variable '{$varInfo->name}' at end of scope is a reference and so counts as used");
// If we're pass-by-reference then it's a common pattern to
// use the variable to return data to the caller, so any
// assignment also counts as "variable use" for the purposes
// of "unused variable" warnings.
return;
}
if ($varInfo->scopeType === ScopeType::GLOBALSCOPE && isset($varInfo->firstInitialized)) {
Helpers::debug("variable '{$varInfo->name}' at end of scope is a global and so counts as used");
// If we imported this variable from the global scope, any further use of
// the variable, including assignment, should count as "variable use" for
// the purposes of "unused variable" warnings.
return;
}
if (empty($varInfo->firstDeclared) && empty($varInfo->firstInitialized)) {
return;
}
if ($this->allowUnusedVariablesBeforeRequire && Helpers::isRequireInScopeAfter($phpcsFile, $varInfo, $scopeInfo)) {
return;
}
if ($scopeInfo->scopeStartIndex === 0 && $this->allowUnusedVariablesInFileScope) {
return;
}
if (!empty($varInfo->firstDeclared) && $varInfo->scopeType === ScopeType::PARAM && Helpers::isInAbstractClass($phpcsFile, Helpers::getFunctionIndexForFunctionParameter($phpcsFile, $varInfo->firstDeclared) ?: 0) && Helpers::isFunctionBodyEmpty($phpcsFile, Helpers::getFunctionIndexForFunctionParameter($phpcsFile, $varInfo->firstDeclared) ?: 0)) {
// Allow non-abstract methods inside an abstract class to have unused
// parameters if the method body does nothing. Such methods are
// effectively optional abstract methods so their unused parameters
// should be ignored as we do with abstract method parameters.
return;
}
$this->warnAboutUnusedVariable($phpcsFile, $varInfo);
}
/**
* Register warnings for a variable that is defined but not used.
*
* @param File $phpcsFile
* @param VariableInfo $varInfo
*
* @return void
*/
protected function warnAboutUnusedVariable(File $phpcsFile, VariableInfo $varInfo) {
foreach (array_unique($varInfo->allAssignments) as $indexForWarning) {
Helpers::debug("variable '{$varInfo->name}' at end of scope looks unused");
$phpcsFile->addWarning('Unused %s %s.', $indexForWarning, 'UnusedVariable', [
VariableInfo::$scopeTypeDescriptions[$varInfo->scopeType ?: ScopeType::LOCAL],
"\${$varInfo->name}",
]);
}
}
/**
* @param File $phpcsFile
* @param string $varName
* @param int $stackPtr
*
* @return void
*/
protected function warnAboutUndefinedVariable(File $phpcsFile, $varName, $stackPtr) {
$phpcsFile->addWarning('Variable %s is undefined.', $stackPtr, 'UndefinedVariable', [
"\${$varName}",
]);
}
/**
* @param File $phpcsFile
* @param string $varName
* @param int $stackPtr
*
* @return void
*/
protected function warnAboutUndefinedArrayPushShortcut(File $phpcsFile, $varName, $stackPtr) {
$phpcsFile->addWarning('Array variable %s is undefined.', $stackPtr, 'UndefinedVariable', [
"\${$varName}",
]);
}
/**
* @param File $phpcsFile
* @param string $varName
* @param int $stackPtr
*
* @return void
*/
protected function warnAboutUndefinedUnset(File $phpcsFile, $varName, $stackPtr) {
$phpcsFile->addWarning('Variable %s inside unset call is undefined.', $stackPtr, 'UndefinedUnsetVariable', [
"\${$varName}",
]);
}
}
Classes
Title | Deprecated | Summary |
---|---|---|
VariableAnalysisSniff |