2064 lines
68 KiB
PHP
2064 lines
68 KiB
PHP
<?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}"]
|
|
);
|
|
}
|
|
}
|