1781 lines
51 KiB
PHP
1781 lines
51 KiB
PHP
<?php
|
|
|
|
namespace VariableAnalysis\Lib;
|
|
|
|
use PHP_CodeSniffer\Files\File;
|
|
use VariableAnalysis\Lib\ScopeInfo;
|
|
use VariableAnalysis\Lib\Constants;
|
|
use VariableAnalysis\Lib\ForLoopInfo;
|
|
use VariableAnalysis\Lib\EnumInfo;
|
|
use VariableAnalysis\Lib\ScopeType;
|
|
use VariableAnalysis\Lib\VariableInfo;
|
|
use PHP_CodeSniffer\Util\Tokens;
|
|
|
|
class Helpers
|
|
{
|
|
/**
|
|
* @return array<int|string>
|
|
*/
|
|
public static function getPossibleEndOfFileTokens()
|
|
{
|
|
return array_merge(
|
|
array_values(Tokens::$emptyTokens),
|
|
[
|
|
T_INLINE_HTML,
|
|
T_CLOSE_TAG,
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param int|bool $value
|
|
*
|
|
* @return ?int
|
|
*/
|
|
public static function getIntOrNull($value)
|
|
{
|
|
return is_int($value) ? $value : null;
|
|
}
|
|
|
|
/**
|
|
* Find the position of the square bracket containing the token at $stackPtr,
|
|
* if any.
|
|
*
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return ?int
|
|
*/
|
|
public static function findContainingOpeningSquareBracket(File $phpcsFile, $stackPtr)
|
|
{
|
|
// Find the previous bracket within this same statement.
|
|
$previousStatementPtr = self::getPreviousStatementPtr($phpcsFile, $stackPtr);
|
|
$openBracketPosition = self::getIntOrNull($phpcsFile->findPrevious([T_OPEN_SHORT_ARRAY, T_OPEN_SQUARE_BRACKET], $stackPtr - 1, $previousStatementPtr));
|
|
if (empty($openBracketPosition)) {
|
|
return null;
|
|
}
|
|
// Make sure we are inside the pair of brackets we found.
|
|
$tokens = $phpcsFile->getTokens();
|
|
$openBracketToken = $tokens[$openBracketPosition];
|
|
if (empty($openBracketToken) || empty($tokens[$openBracketToken['bracket_closer']])) {
|
|
return null;
|
|
}
|
|
$closeBracketPosition = $openBracketToken['bracket_closer'];
|
|
if (empty($closeBracketPosition)) {
|
|
return null;
|
|
}
|
|
if ($stackPtr > $closeBracketPosition) {
|
|
return null;
|
|
}
|
|
return $openBracketPosition;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return int
|
|
*/
|
|
public static function getPreviousStatementPtr(File $phpcsFile, $stackPtr)
|
|
{
|
|
$result = $phpcsFile->findPrevious([T_SEMICOLON, T_CLOSE_CURLY_BRACKET], $stackPtr - 1);
|
|
return is_bool($result) ? 1 : $result;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return ?int
|
|
*/
|
|
public static function findContainingOpeningBracket(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
if (isset($tokens[$stackPtr]['nested_parenthesis'])) {
|
|
/**
|
|
* @var array<int|string|null>
|
|
*/
|
|
$openPtrs = array_keys($tokens[$stackPtr]['nested_parenthesis']);
|
|
return (int)end($openPtrs);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param array{conditions: (int|string)[], content: string} $token
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function areAnyConditionsAClass(array $token)
|
|
{
|
|
$conditions = $token['conditions'];
|
|
$classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT];
|
|
if (defined('T_ENUM')) {
|
|
$classlikeCodes[] = T_ENUM;
|
|
}
|
|
$classlikeCodes[] = 'PHPCS_T_ENUM';
|
|
foreach (array_reverse($conditions, true) as $scopeCode) {
|
|
if (in_array($scopeCode, $classlikeCodes, true)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return true if the token conditions are within a function before they are
|
|
* within a class.
|
|
*
|
|
* @param array{conditions: (int|string)[], content: string} $token
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function areConditionsWithinFunctionBeforeClass(array $token)
|
|
{
|
|
$conditions = $token['conditions'];
|
|
$classlikeCodes = [T_CLASS, T_ANON_CLASS, T_TRAIT];
|
|
if (defined('T_ENUM')) {
|
|
$classlikeCodes[] = T_ENUM;
|
|
}
|
|
$classlikeCodes[] = 'PHPCS_T_ENUM';
|
|
foreach (array_reverse($conditions, true) as $scopeCode) {
|
|
if (in_array($scopeCode, $classlikeCodes)) {
|
|
return false;
|
|
}
|
|
if ($scopeCode === T_FUNCTION) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return true if the token conditions are within an IF/ELSE/ELSEIF block
|
|
* before they are within a class or function.
|
|
*
|
|
* @param (int|string)[] $conditions
|
|
*
|
|
* @return int|string|null
|
|
*/
|
|
public static function getClosestConditionPositionIfBeforeOtherConditions(array $conditions)
|
|
{
|
|
$conditionsInsideOut = array_reverse($conditions, true);
|
|
if (empty($conditions)) {
|
|
return null;
|
|
}
|
|
$scopeCode = reset($conditionsInsideOut);
|
|
$conditionalCodes = [
|
|
T_IF,
|
|
T_ELSE,
|
|
T_ELSEIF,
|
|
];
|
|
if (in_array($scopeCode, $conditionalCodes, true)) {
|
|
return key($conditionsInsideOut);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isTokenFunctionParameter(File $phpcsFile, $stackPtr)
|
|
{
|
|
return is_int(self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr));
|
|
}
|
|
|
|
/**
|
|
* Return true if the token is inside the arguments of a function call.
|
|
*
|
|
* For example, the variable `$foo` in `doSomething($foo)` is inside the
|
|
* arguments to the call to `doSomething()`.
|
|
*
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isTokenInsideFunctionCallArgument(File $phpcsFile, $stackPtr)
|
|
{
|
|
return is_int(self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr));
|
|
}
|
|
|
|
/**
|
|
* Find the index of the function keyword for a token in a function
|
|
* definition's parameters.
|
|
*
|
|
* Does not work for tokens inside the "use".
|
|
*
|
|
* Will also work for the parenthesis that make up the function definition's
|
|
* parameters list.
|
|
*
|
|
* For arguments inside a function call, rather than a definition, use
|
|
* `getFunctionIndexForFunctionCallArgument`.
|
|
*
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return ?int
|
|
*/
|
|
public static function getFunctionIndexForFunctionParameter(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
$token = $tokens[$stackPtr];
|
|
if ($token['code'] === 'PHPCS_T_OPEN_PARENTHESIS') {
|
|
$startOfArguments = $stackPtr;
|
|
} elseif ($token['code'] === 'PHPCS_T_CLOSE_PARENTHESIS') {
|
|
if (empty($token['parenthesis_opener'])) {
|
|
return null;
|
|
}
|
|
$startOfArguments = $token['parenthesis_opener'];
|
|
} else {
|
|
if (empty($token['nested_parenthesis'])) {
|
|
return null;
|
|
}
|
|
$startingParenthesis = array_keys($token['nested_parenthesis']);
|
|
$startOfArguments = end($startingParenthesis);
|
|
}
|
|
|
|
if (! is_int($startOfArguments)) {
|
|
return null;
|
|
}
|
|
|
|
$nonFunctionTokenTypes = Tokens::$emptyTokens;
|
|
$nonFunctionTokenTypes[] = T_STRING;
|
|
$nonFunctionTokenTypes[] = T_BITWISE_AND;
|
|
$functionPtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $startOfArguments - 1, null, true, null, true));
|
|
if (! is_int($functionPtr)) {
|
|
return null;
|
|
}
|
|
$functionToken = $tokens[$functionPtr];
|
|
|
|
$functionTokenTypes = [
|
|
T_FUNCTION,
|
|
T_CLOSURE,
|
|
];
|
|
if (!in_array($functionToken['code'], $functionTokenTypes, true) && ! self::isArrowFunction($phpcsFile, $functionPtr)) {
|
|
return null;
|
|
}
|
|
return $functionPtr;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isTokenInsideFunctionUseImport(File $phpcsFile, $stackPtr)
|
|
{
|
|
return is_int(self::getUseIndexForUseImport($phpcsFile, $stackPtr));
|
|
}
|
|
|
|
/**
|
|
* Find the token index of the "use" for a token inside a function use import
|
|
*
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return ?int
|
|
*/
|
|
public static function getUseIndexForUseImport(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
|
|
$nonUseTokenTypes = Tokens::$emptyTokens;
|
|
$nonUseTokenTypes[] = T_VARIABLE;
|
|
$nonUseTokenTypes[] = T_ELLIPSIS;
|
|
$nonUseTokenTypes[] = T_COMMA;
|
|
$nonUseTokenTypes[] = T_BITWISE_AND;
|
|
$openParenPtr = self::getIntOrNull($phpcsFile->findPrevious($nonUseTokenTypes, $stackPtr - 1, null, true, null, true));
|
|
if (! is_int($openParenPtr) || $tokens[$openParenPtr]['code'] !== T_OPEN_PARENTHESIS) {
|
|
return null;
|
|
}
|
|
|
|
$usePtr = self::getIntOrNull($phpcsFile->findPrevious(array_values($nonUseTokenTypes), $openParenPtr - 1, null, true, null, true));
|
|
if (! is_int($usePtr) || $tokens[$usePtr]['code'] !== T_USE) {
|
|
return null;
|
|
}
|
|
return $usePtr;
|
|
}
|
|
|
|
/**
|
|
* Return the index of a function's name token from inside the function.
|
|
*
|
|
* $stackPtr must be inside the function body or parameters for this to work.
|
|
*
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return ?int
|
|
*/
|
|
public static function findFunctionCall(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
|
|
$openPtr = self::findContainingOpeningBracket($phpcsFile, $stackPtr);
|
|
if (is_int($openPtr)) {
|
|
// First non-whitespace thing and see if it's a T_STRING function name
|
|
$functionPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $openPtr - 1, null, true, null, true);
|
|
if (is_int($functionPtr) && $tokens[$functionPtr]['code'] === T_STRING) {
|
|
return $functionPtr;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return array<int, array<int>>
|
|
*/
|
|
public static function findFunctionCallArguments(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
|
|
// Slight hack: also allow this to find args for array constructor.
|
|
if (($tokens[$stackPtr]['code'] !== T_STRING) && ($tokens[$stackPtr]['code'] !== T_ARRAY)) {
|
|
// Assume $stackPtr is something within the brackets, find our function call
|
|
$stackPtr = self::findFunctionCall($phpcsFile, $stackPtr);
|
|
if ($stackPtr === null) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// $stackPtr is the function name, find our brackets after it
|
|
$openPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true);
|
|
if (($openPtr === false) || ($tokens[$openPtr]['code'] !== T_OPEN_PARENTHESIS)) {
|
|
return [];
|
|
}
|
|
|
|
if (!isset($tokens[$openPtr]['parenthesis_closer'])) {
|
|
return [];
|
|
}
|
|
$closePtr = $tokens[$openPtr]['parenthesis_closer'];
|
|
|
|
$argPtrs = [];
|
|
$lastPtr = $openPtr;
|
|
$lastArgComma = $openPtr;
|
|
$nextPtr = $phpcsFile->findNext([T_COMMA], $lastPtr + 1, $closePtr);
|
|
while (is_int($nextPtr)) {
|
|
if (self::findContainingOpeningBracket($phpcsFile, $nextPtr) === $openPtr) {
|
|
// Comma is at our level of brackets, it's an argument delimiter.
|
|
$range = range($lastArgComma + 1, $nextPtr - 1);
|
|
$range = array_filter($range, function ($element) {
|
|
return is_int($element);
|
|
});
|
|
array_push($argPtrs, $range);
|
|
$lastArgComma = $nextPtr;
|
|
}
|
|
$lastPtr = $nextPtr;
|
|
$nextPtr = $phpcsFile->findNext([T_COMMA], $lastPtr + 1, $closePtr);
|
|
}
|
|
$range = range($lastArgComma + 1, $closePtr - 1);
|
|
$range = array_filter($range, function ($element) {
|
|
return is_int($element);
|
|
});
|
|
array_push($argPtrs, $range);
|
|
|
|
return $argPtrs;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return ?int
|
|
*/
|
|
public static function getNextAssignPointer(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
|
|
// Is the next non-whitespace an assignment?
|
|
$nextPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true);
|
|
if (is_int($nextPtr)
|
|
&& isset(Tokens::$assignmentTokens[$tokens[$nextPtr]['code']])
|
|
// Ignore double arrow to prevent triggering on `foreach ( $array as $k => $v )`.
|
|
&& $tokens[$nextPtr]['code'] !== T_DOUBLE_ARROW
|
|
) {
|
|
return $nextPtr;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param string $varName
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function normalizeVarName($varName)
|
|
{
|
|
$result = preg_replace('/[{}$]/', '', $varName);
|
|
return $result ? $result : $varName;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
* @param string $varName (optional) if it differs from the normalized 'content' of the token at $stackPtr
|
|
*
|
|
* @return ?int
|
|
*/
|
|
public static function findVariableScope(File $phpcsFile, $stackPtr, $varName = null)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
$token = $tokens[$stackPtr];
|
|
$varName = isset($varName) ? $varName : self::normalizeVarName($token['content']);
|
|
|
|
$enclosingScopeIndex = self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr);
|
|
if ($enclosingScopeIndex) {
|
|
$arrowFunctionIndex = self::getContainingArrowFunctionIndex($phpcsFile, $stackPtr, $enclosingScopeIndex);
|
|
$isTokenInsideArrowFunctionBody = is_int($arrowFunctionIndex);
|
|
if ($isTokenInsideArrowFunctionBody) {
|
|
// Get the list of variables defined by the arrow function
|
|
// If this matches any of them, the scope is the arrow function,
|
|
// otherwise, it uses the enclosing scope.
|
|
if ($arrowFunctionIndex) {
|
|
$variableNames = self::getVariablesDefinedByArrowFunction($phpcsFile, $arrowFunctionIndex);
|
|
self::debug('findVariableScope: looking for', $varName, 'in arrow function variables', $variableNames);
|
|
if (in_array($varName, $variableNames, true)) {
|
|
return $arrowFunctionIndex;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return self::findVariableScopeExceptArrowFunctions($phpcsFile, $stackPtr);
|
|
}
|
|
|
|
/**
|
|
* Return the variable names and positions of each variable targetted by a `compact()` call.
|
|
*
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
* @param array<int, array<int>> $arguments The stack pointers of each argument; see findFunctionCallArguments
|
|
*
|
|
* @return array<VariableInfo> each variable's firstRead position and its name; other VariableInfo properties are not set!
|
|
*/
|
|
public static function getVariablesInsideCompact(File $phpcsFile, $stackPtr, $arguments)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
$variablePositionsAndNames = [];
|
|
|
|
foreach ($arguments as $argumentPtrs) {
|
|
$argumentPtrs = array_values(array_filter($argumentPtrs, function ($argumentPtr) use ($tokens) {
|
|
return isset(Tokens::$emptyTokens[$tokens[$argumentPtr]['code']]) === false;
|
|
}));
|
|
if (empty($argumentPtrs)) {
|
|
continue;
|
|
}
|
|
if (!isset($tokens[$argumentPtrs[0]])) {
|
|
continue;
|
|
}
|
|
$argumentFirstToken = $tokens[$argumentPtrs[0]];
|
|
if ($argumentFirstToken['code'] === T_ARRAY) {
|
|
// It's an array argument, recurse.
|
|
$arrayArguments = self::findFunctionCallArguments($phpcsFile, $argumentPtrs[0]);
|
|
$variablePositionsAndNames = array_merge($variablePositionsAndNames, self::getVariablesInsideCompact($phpcsFile, $stackPtr, $arrayArguments));
|
|
continue;
|
|
}
|
|
if (count($argumentPtrs) > 1) {
|
|
// Complex argument, we can't handle it, ignore.
|
|
continue;
|
|
}
|
|
if ($argumentFirstToken['code'] === T_CONSTANT_ENCAPSED_STRING) {
|
|
// Single-quoted string literal, ie compact('whatever').
|
|
// Substr is to strip the enclosing single-quotes.
|
|
$varName = substr($argumentFirstToken['content'], 1, -1);
|
|
$variable = new VariableInfo($varName);
|
|
$variable->firstRead = $argumentPtrs[0];
|
|
$variablePositionsAndNames[] = $variable;
|
|
continue;
|
|
}
|
|
if ($argumentFirstToken['code'] === T_DOUBLE_QUOTED_STRING) {
|
|
// Double-quoted string literal.
|
|
$regexp = Constants::getDoubleQuotedVarRegexp();
|
|
if (! empty($regexp) && preg_match($regexp, $argumentFirstToken['content'])) {
|
|
// Bail if the string needs variable expansion, that's runtime stuff.
|
|
continue;
|
|
}
|
|
// Substr is to strip the enclosing double-quotes.
|
|
$varName = substr($argumentFirstToken['content'], 1, -1);
|
|
$variable = new VariableInfo($varName);
|
|
$variable->firstRead = $argumentPtrs[0];
|
|
$variablePositionsAndNames[] = $variable;
|
|
continue;
|
|
}
|
|
}
|
|
return $variablePositionsAndNames;
|
|
}
|
|
|
|
/**
|
|
* Return the token index of the scope start for a token
|
|
*
|
|
* For a variable within a function body, or a variable within a function
|
|
* definition argument list, this will return the function keyword's index.
|
|
*
|
|
* For a variable within a "use" import list within a function definition,
|
|
* this will return the enclosing scope, not the function keyword. This is
|
|
* important to note because the "use" keyword performs double-duty, defining
|
|
* variables for the function's scope, and consuming the variables in the
|
|
* enclosing scope. Use `getUseIndexForUseImport` to determine if this
|
|
* token needs to be treated as a "use".
|
|
*
|
|
* For a variable within an arrow function definition argument list,
|
|
* this will return the arrow function's keyword index.
|
|
*
|
|
* For a variable in an arrow function body, this will return the enclosing
|
|
* function's index, which may be incorrect.
|
|
*
|
|
* Since a variable in an arrow function's body may be imported from the
|
|
* enclosing scope, it's important to test to see if the variable is in an
|
|
* arrow function and also check its enclosing scope separately.
|
|
*
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return ?int
|
|
*/
|
|
public static function findVariableScopeExceptArrowFunctions(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
$allowedTypes = [
|
|
T_VARIABLE,
|
|
T_DOUBLE_QUOTED_STRING,
|
|
T_HEREDOC,
|
|
T_STRING,
|
|
];
|
|
if (! in_array($tokens[$stackPtr]['code'], $allowedTypes, true)) {
|
|
throw new \Exception("Cannot find variable scope for non-variable {$tokens[$stackPtr]['type']}");
|
|
}
|
|
|
|
$startOfTokenScope = self::getStartOfTokenScope($phpcsFile, $stackPtr);
|
|
if (is_int($startOfTokenScope) && $startOfTokenScope > 0) {
|
|
return $startOfTokenScope;
|
|
}
|
|
|
|
// If there is no "conditions" array, this is a function definition argument.
|
|
if (self::isTokenFunctionParameter($phpcsFile, $stackPtr)) {
|
|
$functionPtr = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
|
|
if (! is_int($functionPtr)) {
|
|
throw new \Exception("Function index not found for function argument index {$stackPtr}");
|
|
}
|
|
return $functionPtr;
|
|
}
|
|
|
|
self::debug('Cannot find function scope for variable at', $stackPtr);
|
|
return $startOfTokenScope;
|
|
}
|
|
|
|
/**
|
|
* Return the token index of the scope start for a variable token
|
|
*
|
|
* This will only work for a variable within a function's body. Otherwise,
|
|
* see `findVariableScope`, which is more complex.
|
|
*
|
|
* Note that if used on a variable in an arrow function, it will return the
|
|
* enclosing function's scope, which may be incorrect.
|
|
*
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return ?int
|
|
*/
|
|
private static function getStartOfTokenScope(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
$token = $tokens[$stackPtr];
|
|
|
|
$inClass = false;
|
|
$conditions = isset($token['conditions']) ? $token['conditions'] : [];
|
|
$functionTokenTypes = [
|
|
T_FUNCTION,
|
|
T_CLOSURE,
|
|
];
|
|
foreach (array_reverse($conditions, true) as $scopePtr => $scopeCode) {
|
|
if (in_array($scopeCode, $functionTokenTypes, true) || self::isArrowFunction($phpcsFile, $scopePtr)) {
|
|
return $scopePtr;
|
|
}
|
|
if (isset(Tokens::$ooScopeTokens[$scopeCode]) === true) {
|
|
$inClass = true;
|
|
}
|
|
}
|
|
|
|
if ($inClass) {
|
|
// If this is inside a class and not inside a function, this is either a
|
|
// class member variable definition, or a function argument. If it is a
|
|
// variable definition, it has no scope on its own (it can only be used
|
|
// with an object reference). If it is a function argument, we need to do
|
|
// more work (see `findVariableScopeExceptArrowFunctions`).
|
|
return null;
|
|
}
|
|
|
|
// If we can't find a scope, let's use the first token of the file.
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isTokenInsideArrowFunctionDefinition(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
$token = $tokens[$stackPtr];
|
|
$openParenIndices = isset($token['nested_parenthesis']) ? $token['nested_parenthesis'] : [];
|
|
if (empty($openParenIndices)) {
|
|
return false;
|
|
}
|
|
$openParenPtr = $openParenIndices[0];
|
|
return self::isArrowFunction($phpcsFile, $openParenPtr - 1);
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
* @param int $enclosingScopeIndex
|
|
*
|
|
* @return ?int
|
|
*/
|
|
public static function getContainingArrowFunctionIndex(File $phpcsFile, $stackPtr, $enclosingScopeIndex)
|
|
{
|
|
$arrowFunctionIndex = self::getPreviousArrowFunctionIndex($phpcsFile, $stackPtr, $enclosingScopeIndex);
|
|
if (! is_int($arrowFunctionIndex)) {
|
|
return null;
|
|
}
|
|
$arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $arrowFunctionIndex);
|
|
if (! $arrowFunctionInfo) {
|
|
return null;
|
|
}
|
|
|
|
// We found the closest arrow function before this token. If the token is
|
|
// within the scope of that arrow function, then return it.
|
|
if ($stackPtr > $arrowFunctionInfo['scope_opener'] && $stackPtr < $arrowFunctionInfo['scope_closer']) {
|
|
return $arrowFunctionIndex;
|
|
}
|
|
|
|
// If the token is after the scope of the closest arrow function, we may
|
|
// still be inside the scope of a nested arrow function, so we need to
|
|
// search further back until we are certain there are no more arrow
|
|
// functions.
|
|
if ($stackPtr > $arrowFunctionInfo['scope_closer']) {
|
|
return self::getContainingArrowFunctionIndex($phpcsFile, $arrowFunctionIndex, $enclosingScopeIndex);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Move back from the stackPtr to the start of the enclosing scope until we
|
|
* find a 'fn' token that starts an arrow function, returning the index of
|
|
* that token. Returns null if there are no arrow functions before stackPtr.
|
|
*
|
|
* Note that this does not guarantee that stackPtr is inside the arrow
|
|
* function scope we find!
|
|
*
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
* @param int $enclosingScopeIndex
|
|
*
|
|
* @return ?int
|
|
*/
|
|
private static function getPreviousArrowFunctionIndex(File $phpcsFile, $stackPtr, $enclosingScopeIndex)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
for ($index = $stackPtr - 1; $index > $enclosingScopeIndex; $index--) {
|
|
$token = $tokens[$index];
|
|
if ($token['content'] === 'fn' && self::isArrowFunction($phpcsFile, $index)) {
|
|
return $index;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isArrowFunction(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
if (defined('T_FN') && $tokens[$stackPtr]['code'] === T_FN) {
|
|
return true;
|
|
}
|
|
if ($tokens[$stackPtr]['content'] !== 'fn') {
|
|
return false;
|
|
}
|
|
// Make sure next non-space token is an open parenthesis
|
|
$openParenIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true);
|
|
if (! is_int($openParenIndex) || $tokens[$openParenIndex]['code'] !== T_OPEN_PARENTHESIS) {
|
|
return false;
|
|
}
|
|
// Find the associated close parenthesis
|
|
$closeParenIndex = $tokens[$openParenIndex]['parenthesis_closer'];
|
|
// Make sure the next token is a fat arrow
|
|
$fatArrowIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenIndex + 1, null, true);
|
|
if (! is_int($fatArrowIndex)) {
|
|
return false;
|
|
}
|
|
if ($tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW && $tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW') {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Find the opening and closing scope positions for an arrow function if the
|
|
* given position is the start of the arrow function (the `fn` keyword
|
|
* token).
|
|
*
|
|
* Returns null if the passed token is not an arrow function keyword.
|
|
*
|
|
* If the token is an arrow function keyword, the scope opener is returned as
|
|
* the provided position.
|
|
*
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return ?array<string, int>
|
|
*/
|
|
public static function getArrowFunctionOpenClose(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
if ($tokens[$stackPtr]['content'] !== 'fn') {
|
|
return null;
|
|
}
|
|
// Make sure next non-space token is an open parenthesis
|
|
$openParenIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $stackPtr + 1, null, true);
|
|
if (! is_int($openParenIndex) || $tokens[$openParenIndex]['code'] !== T_OPEN_PARENTHESIS) {
|
|
return null;
|
|
}
|
|
// Find the associated close parenthesis
|
|
$closeParenIndex = $tokens[$openParenIndex]['parenthesis_closer'];
|
|
// Make sure the next token is a fat arrow or a return type
|
|
$fatArrowIndex = $phpcsFile->findNext(Tokens::$emptyTokens, $closeParenIndex + 1, null, true);
|
|
if (! is_int($fatArrowIndex)) {
|
|
return null;
|
|
}
|
|
if (
|
|
$tokens[$fatArrowIndex]['code'] !== T_DOUBLE_ARROW &&
|
|
$tokens[$fatArrowIndex]['type'] !== 'T_FN_ARROW' &&
|
|
$tokens[$fatArrowIndex]['code'] !== T_COLON
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
// Find the scope closer
|
|
$scopeCloserIndex = null;
|
|
$foundCurlyPairs = 0;
|
|
$foundArrayPairs = 0;
|
|
$foundParenPairs = 0;
|
|
$arrowBodyStart = $tokens[$stackPtr]['parenthesis_closer'] + 1;
|
|
$lastToken = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
|
|
for ($index = $arrowBodyStart; $index < $lastToken; $index++) {
|
|
$token = $tokens[$index];
|
|
if (empty($token['code'])) {
|
|
$scopeCloserIndex = $index;
|
|
break;
|
|
}
|
|
|
|
$code = $token['code'];
|
|
|
|
// A semicolon is always a closer.
|
|
if ($code === T_SEMICOLON) {
|
|
$scopeCloserIndex = $index;
|
|
break;
|
|
}
|
|
|
|
// Track pair opening tokens.
|
|
if ($code === T_OPEN_CURLY_BRACKET) {
|
|
$foundCurlyPairs += 1;
|
|
continue;
|
|
}
|
|
if ($code === T_OPEN_SHORT_ARRAY || $code === T_OPEN_SQUARE_BRACKET) {
|
|
$foundArrayPairs += 1;
|
|
continue;
|
|
}
|
|
if ($code === T_OPEN_PARENTHESIS) {
|
|
$foundParenPairs += 1;
|
|
continue;
|
|
}
|
|
|
|
// A pair closing is only an arrow func closer if there was no matching opening token.
|
|
if ($code === T_CLOSE_CURLY_BRACKET) {
|
|
if ($foundCurlyPairs === 0) {
|
|
$scopeCloserIndex = $index;
|
|
break;
|
|
}
|
|
$foundCurlyPairs -= 1;
|
|
continue;
|
|
}
|
|
if ($code === T_CLOSE_SHORT_ARRAY || $code === T_CLOSE_SQUARE_BRACKET) {
|
|
if ($foundArrayPairs === 0) {
|
|
$scopeCloserIndex = $index;
|
|
break;
|
|
}
|
|
$foundArrayPairs -= 1;
|
|
continue;
|
|
}
|
|
if ($code === T_CLOSE_PARENTHESIS) {
|
|
if ($foundParenPairs === 0) {
|
|
$scopeCloserIndex = $index;
|
|
break;
|
|
}
|
|
$foundParenPairs -= 1;
|
|
continue;
|
|
}
|
|
|
|
// A comma is a closer only if we are not inside an opening token.
|
|
if ($code === T_COMMA) {
|
|
if (empty($foundArrayPairs) && empty($foundParenPairs) && empty($foundCurlyPairs)) {
|
|
$scopeCloserIndex = $index;
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (! is_int($scopeCloserIndex)) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'scope_opener' => $stackPtr,
|
|
'scope_closer' => $scopeCloserIndex,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Determine if a token is a list opener for list assignment/destructuring.
|
|
*
|
|
* The index provided can be either the opening square brace of a short list
|
|
* assignment like the first character of `[$a] = $b;` or the `list` token of
|
|
* an expression like `list($a) = $b;` or the opening parenthesis of that
|
|
* expression.
|
|
*
|
|
* @param File $phpcsFile
|
|
* @param int $listOpenerIndex
|
|
*
|
|
* @return bool
|
|
*/
|
|
private static function isListAssignment(File $phpcsFile, $listOpenerIndex)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
// Match `[$a] = $b;` except for when the previous token is a parenthesis.
|
|
if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SHORT_ARRAY) {
|
|
return true;
|
|
}
|
|
// Match `list($a) = $b;`
|
|
if ($tokens[$listOpenerIndex]['code'] === T_LIST) {
|
|
return true;
|
|
}
|
|
|
|
// If $listOpenerIndex is the open parenthesis of `list($a) = $b;`, then
|
|
// match that too.
|
|
if ($tokens[$listOpenerIndex]['code'] === T_OPEN_PARENTHESIS) {
|
|
$previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true);
|
|
if (
|
|
isset($tokens[$previousTokenPtr])
|
|
&& $tokens[$previousTokenPtr]['code'] === T_LIST
|
|
) {
|
|
return true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// If the list opener token is a square bracket that is preceeded by a
|
|
// close parenthesis that has an owner which is a scope opener, then this
|
|
// is a list assignment and not an array access.
|
|
//
|
|
// Match `if (true) [$a] = $b;`
|
|
if ($tokens[$listOpenerIndex]['code'] === T_OPEN_SQUARE_BRACKET) {
|
|
$previousTokenPtr = $phpcsFile->findPrevious(Tokens::$emptyTokens, $listOpenerIndex - 1, null, true);
|
|
if (
|
|
isset($tokens[$previousTokenPtr])
|
|
&& $tokens[$previousTokenPtr]['code'] === T_CLOSE_PARENTHESIS
|
|
&& isset($tokens[$previousTokenPtr]['parenthesis_owner'])
|
|
&& isset(Tokens::$scopeOpeners[$tokens[$tokens[$previousTokenPtr]['parenthesis_owner']]['code']])
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return a list of indices for variables assigned within a list assignment.
|
|
*
|
|
* The index provided can be either the opening square brace of a short list
|
|
* assignment like the first character of `[$a] = $b;` or the `list` token of
|
|
* an expression like `list($a) = $b;` or the opening parenthesis of that
|
|
* expression.
|
|
*
|
|
* @param File $phpcsFile
|
|
* @param int $listOpenerIndex
|
|
*
|
|
* @return ?array<int>
|
|
*/
|
|
public static function getListAssignments(File $phpcsFile, $listOpenerIndex)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
self::debug('getListAssignments', $listOpenerIndex, $tokens[$listOpenerIndex]);
|
|
|
|
// First find the end of the list
|
|
$closePtr = null;
|
|
if (isset($tokens[$listOpenerIndex]['parenthesis_closer'])) {
|
|
$closePtr = $tokens[$listOpenerIndex]['parenthesis_closer'];
|
|
}
|
|
if (isset($tokens[$listOpenerIndex]['bracket_closer'])) {
|
|
$closePtr = $tokens[$listOpenerIndex]['bracket_closer'];
|
|
}
|
|
if (! $closePtr) {
|
|
return null;
|
|
}
|
|
|
|
// Find the assignment (equals sign) which, if this is a list assignment, should be the next non-space token
|
|
$assignPtr = $phpcsFile->findNext(Tokens::$emptyTokens, $closePtr + 1, null, true);
|
|
|
|
// If the next token isn't an assignment, check for nested brackets because we might be a nested assignment
|
|
if (! is_int($assignPtr) || $tokens[$assignPtr]['code'] !== T_EQUAL) {
|
|
// Collect the enclosing list open/close tokens ($parents is an assoc array keyed by opener index and the value is the closer index)
|
|
$parents = isset($tokens[$listOpenerIndex]['nested_parenthesis']) ? $tokens[$listOpenerIndex]['nested_parenthesis'] : [];
|
|
// There's no record of nested brackets for short lists; we'll have to find the parent ourselves
|
|
if (empty($parents)) {
|
|
$parentSquareBracketPtr = self::findContainingOpeningSquareBracket($phpcsFile, $listOpenerIndex);
|
|
if (is_int($parentSquareBracketPtr)) {
|
|
// Make sure that the parent is really a parent by checking that its
|
|
// closing index is outside of the current bracket's closing index.
|
|
$parentSquareBracketToken = $tokens[$parentSquareBracketPtr];
|
|
$parentSquareBracketClosePtr = $parentSquareBracketToken['bracket_closer'];
|
|
if ($parentSquareBracketClosePtr && $parentSquareBracketClosePtr > $closePtr) {
|
|
self::debug("found enclosing bracket for {$listOpenerIndex}: {$parentSquareBracketPtr}");
|
|
// Collect the opening index, but we don't actually need the closing paren index so just make that 0
|
|
$parents = [$parentSquareBracketPtr => 0];
|
|
}
|
|
}
|
|
}
|
|
// If we have no parents, this is not a nested assignment and therefore is not an assignment
|
|
if (empty($parents)) {
|
|
return null;
|
|
}
|
|
|
|
// Recursively check to see if the parent is a list assignment (we only need to check one level due to the recursion)
|
|
$isNestedAssignment = null;
|
|
$parentListOpener = array_keys(array_reverse($parents, true))[0];
|
|
$isNestedAssignment = self::getListAssignments($phpcsFile, $parentListOpener);
|
|
if ($isNestedAssignment === null) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
$variablePtrs = [];
|
|
|
|
$currentPtr = $listOpenerIndex;
|
|
$variablePtr = 0;
|
|
while ($currentPtr < $closePtr && is_int($variablePtr)) {
|
|
$variablePtr = $phpcsFile->findNext([T_VARIABLE], $currentPtr + 1, $closePtr);
|
|
if (is_int($variablePtr)) {
|
|
$variablePtrs[] = $variablePtr;
|
|
}
|
|
++$currentPtr;
|
|
}
|
|
|
|
if (! self::isListAssignment($phpcsFile, $listOpenerIndex)) {
|
|
return null;
|
|
}
|
|
|
|
return $variablePtrs;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return string[]
|
|
*/
|
|
public static function getVariablesDefinedByArrowFunction(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
$arrowFunctionToken = $tokens[$stackPtr];
|
|
$variableNames = [];
|
|
self::debug('looking for variables in arrow function token', $arrowFunctionToken);
|
|
for ($index = $arrowFunctionToken['parenthesis_opener']; $index < $arrowFunctionToken['parenthesis_closer']; $index++) {
|
|
$token = $tokens[$index];
|
|
if ($token['code'] === T_VARIABLE) {
|
|
$variableNames[] = self::normalizeVarName($token['content']);
|
|
}
|
|
}
|
|
self::debug('found these variables in arrow function token', $variableNames);
|
|
return $variableNames;
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
public static function debug()
|
|
{
|
|
$messages = func_get_args();
|
|
if (! defined('PHP_CODESNIFFER_VERBOSITY')) {
|
|
return;
|
|
}
|
|
if (PHP_CODESNIFFER_VERBOSITY <= 3) {
|
|
return;
|
|
}
|
|
$output = PHP_EOL . 'VariableAnalysisSniff: DEBUG:';
|
|
foreach ($messages as $message) {
|
|
if (is_string($message) || is_numeric($message)) {
|
|
$output .= ' "' . $message . '"';
|
|
continue;
|
|
}
|
|
$output .= PHP_EOL . var_export($message, true) . PHP_EOL;
|
|
}
|
|
$output .= PHP_EOL;
|
|
echo $output;
|
|
}
|
|
|
|
/**
|
|
* @param string $pattern
|
|
* @param string $value
|
|
*
|
|
* @return string[]
|
|
*/
|
|
public static function splitStringToArray($pattern, $value)
|
|
{
|
|
if (empty($pattern)) {
|
|
return [];
|
|
}
|
|
$result = preg_split($pattern, $value);
|
|
return is_array($result) ? $result : [];
|
|
}
|
|
|
|
/**
|
|
* @param string $varName
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isVariableANumericVariable($varName)
|
|
{
|
|
return is_numeric(substr($varName, 0, 1));
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isVariableInsideElseCondition(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
$nonFunctionTokenTypes = Tokens::$emptyTokens;
|
|
$nonFunctionTokenTypes[] = T_OPEN_PARENTHESIS;
|
|
$nonFunctionTokenTypes[] = T_INLINE_HTML;
|
|
$nonFunctionTokenTypes[] = T_CLOSE_TAG;
|
|
$nonFunctionTokenTypes[] = T_VARIABLE;
|
|
$nonFunctionTokenTypes[] = T_ELLIPSIS;
|
|
$nonFunctionTokenTypes[] = T_COMMA;
|
|
$nonFunctionTokenTypes[] = T_STRING;
|
|
$nonFunctionTokenTypes[] = T_BITWISE_AND;
|
|
$elsePtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $stackPtr - 1, null, true, null, true));
|
|
$elseTokenTypes = [
|
|
T_ELSE,
|
|
T_ELSEIF,
|
|
];
|
|
if (is_int($elsePtr) && in_array($tokens[$elsePtr]['code'], $elseTokenTypes, true)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isVariableInsideElseBody(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
$token = $tokens[$stackPtr];
|
|
$conditions = isset($token['conditions']) ? $token['conditions'] : [];
|
|
$elseTokenTypes = [
|
|
T_ELSE,
|
|
T_ELSEIF,
|
|
];
|
|
foreach (array_reverse($conditions, true) as $scopeCode) {
|
|
if (in_array($scopeCode, $elseTokenTypes, true)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Some else body code will not have conditions because it is inline (no
|
|
// curly braces) so we have to look in other ways.
|
|
$previousSemicolonPtr = $phpcsFile->findPrevious([T_SEMICOLON], $stackPtr - 1);
|
|
if (! is_int($previousSemicolonPtr)) {
|
|
$previousSemicolonPtr = 0;
|
|
}
|
|
$elsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1, $previousSemicolonPtr);
|
|
if (is_int($elsePtr)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return int[]
|
|
*/
|
|
public static function getAttachedBlockIndicesForElse(File $phpcsFile, $stackPtr)
|
|
{
|
|
$currentElsePtr = $phpcsFile->findPrevious([T_ELSE, T_ELSEIF], $stackPtr - 1);
|
|
if (! is_int($currentElsePtr)) {
|
|
throw new \Exception("Cannot find expected else at {$stackPtr}");
|
|
}
|
|
|
|
$ifPtr = $phpcsFile->findPrevious([T_IF], $currentElsePtr - 1);
|
|
if (! is_int($ifPtr)) {
|
|
throw new \Exception("Cannot find if for else at {$stackPtr}");
|
|
}
|
|
$blockIndices = [$ifPtr];
|
|
|
|
$previousElseIfPtr = $currentElsePtr;
|
|
do {
|
|
$elseIfPtr = $phpcsFile->findPrevious([T_ELSEIF], $previousElseIfPtr - 1, $ifPtr);
|
|
if (is_int($elseIfPtr)) {
|
|
$blockIndices[] = $elseIfPtr;
|
|
$previousElseIfPtr = $elseIfPtr;
|
|
}
|
|
} while (is_int($elseIfPtr));
|
|
|
|
return $blockIndices;
|
|
}
|
|
|
|
/**
|
|
* @param int $needle
|
|
* @param int $scopeStart
|
|
* @param int $scopeEnd
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isIndexInsideScope($needle, $scopeStart, $scopeEnd)
|
|
{
|
|
return ($needle > $scopeStart && $needle < $scopeEnd);
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $scopeStartIndex
|
|
*
|
|
* @return int
|
|
*/
|
|
public static function getScopeCloseForScopeOpen(File $phpcsFile, $scopeStartIndex)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
$scopeCloserIndex = isset($tokens[$scopeStartIndex]['scope_closer']) ? $tokens[$scopeStartIndex]['scope_closer'] : 0;
|
|
|
|
if (self::isArrowFunction($phpcsFile, $scopeStartIndex)) {
|
|
$arrowFunctionInfo = self::getArrowFunctionOpenClose($phpcsFile, $scopeStartIndex);
|
|
$scopeCloserIndex = $arrowFunctionInfo ? $arrowFunctionInfo['scope_closer'] : $scopeCloserIndex;
|
|
}
|
|
|
|
if ($scopeStartIndex === 0) {
|
|
$scopeCloserIndex = self::getLastNonEmptyTokenIndexInFile($phpcsFile);
|
|
}
|
|
return $scopeCloserIndex;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
*
|
|
* @return int
|
|
*/
|
|
public static function getLastNonEmptyTokenIndexInFile(File $phpcsFile)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
foreach (array_reverse($tokens, true) as $index => $token) {
|
|
if (! in_array($token['code'], self::getPossibleEndOfFileTokens(), true)) {
|
|
return $index;
|
|
}
|
|
}
|
|
self::debug('no non-empty token found for end of file');
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @param VariableInfo $varInfo
|
|
* @param ScopeInfo $scopeInfo
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function areFollowingArgumentsUsed(VariableInfo $varInfo, ScopeInfo $scopeInfo)
|
|
{
|
|
$foundVarPosition = false;
|
|
foreach ($scopeInfo->variables as $variable) {
|
|
if ($variable === $varInfo) {
|
|
$foundVarPosition = true;
|
|
continue;
|
|
}
|
|
if (! $foundVarPosition) {
|
|
continue;
|
|
}
|
|
if ($variable->scopeType !== ScopeType::PARAM) {
|
|
continue;
|
|
}
|
|
if ($variable->firstRead) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param VariableInfo $varInfo
|
|
* @param ScopeInfo $scopeInfo
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isRequireInScopeAfter(File $phpcsFile, VariableInfo $varInfo, ScopeInfo $scopeInfo)
|
|
{
|
|
$requireTokens = [
|
|
T_REQUIRE,
|
|
T_REQUIRE_ONCE,
|
|
T_INCLUDE,
|
|
T_INCLUDE_ONCE,
|
|
];
|
|
$indexToStartSearch = $varInfo->firstDeclared;
|
|
if (! empty($varInfo->firstInitialized)) {
|
|
$indexToStartSearch = $varInfo->firstInitialized;
|
|
}
|
|
$tokens = $phpcsFile->getTokens();
|
|
$indexToStopSearch = isset($tokens[$scopeInfo->scopeStartIndex]['scope_closer']) ? $tokens[$scopeInfo->scopeStartIndex]['scope_closer'] : null;
|
|
if (! is_int($indexToStartSearch) || ! is_int($indexToStopSearch)) {
|
|
return false;
|
|
}
|
|
$requireTokenIndex = $phpcsFile->findNext($requireTokens, $indexToStartSearch + 1, $indexToStopSearch);
|
|
if (is_int($requireTokenIndex)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Find the index of the function keyword for a token in a function call's arguments
|
|
*
|
|
* For the variable `$foo` in the expression `doSomething($foo)`, this will
|
|
* return the index of the `doSomething` token.
|
|
*
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return ?int
|
|
*/
|
|
public static function getFunctionIndexForFunctionCallArgument(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
$token = $tokens[$stackPtr];
|
|
if (empty($token['nested_parenthesis'])) {
|
|
return null;
|
|
}
|
|
/**
|
|
* @var array<int|string|null>
|
|
*/
|
|
$startingParenthesis = array_keys($token['nested_parenthesis']);
|
|
$startOfArguments = end($startingParenthesis);
|
|
if (! is_int($startOfArguments)) {
|
|
return null;
|
|
}
|
|
|
|
$nonFunctionTokenTypes = Tokens::$emptyTokens;
|
|
$functionPtr = self::getIntOrNull($phpcsFile->findPrevious($nonFunctionTokenTypes, $startOfArguments - 1, null, true, null, true));
|
|
if (! is_int($functionPtr) || ! isset($tokens[$functionPtr]['code'])) {
|
|
return null;
|
|
}
|
|
if (
|
|
$tokens[$functionPtr]['content'] === 'function'
|
|
|| ($tokens[$functionPtr]['content'] === 'fn' && self::isArrowFunction($phpcsFile, $functionPtr))
|
|
) {
|
|
// If there is a function/fn keyword before the beginning of the parens,
|
|
// this is a function definition and not a function call.
|
|
return null;
|
|
}
|
|
if (! empty($tokens[$functionPtr]['scope_opener'])) {
|
|
// If the alleged function name has a scope, this is not a function call.
|
|
return null;
|
|
}
|
|
|
|
$functionNameType = $tokens[$functionPtr]['code'];
|
|
if (! in_array($functionNameType, Tokens::$functionNameTokens, true)) {
|
|
// If the alleged function name is not a variable or a string, this is
|
|
// not a function call.
|
|
return null;
|
|
}
|
|
|
|
if ($tokens[$functionPtr]['level'] !== $tokens[$stackPtr]['level']) {
|
|
// If the variable is inside a different scope than the function name,
|
|
// the function call doesn't apply to the variable.
|
|
return null;
|
|
}
|
|
|
|
return $functionPtr;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isVariableInsideIssetOrEmpty(File $phpcsFile, $stackPtr)
|
|
{
|
|
$functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr);
|
|
if (! is_int($functionIndex)) {
|
|
return false;
|
|
}
|
|
$tokens = $phpcsFile->getTokens();
|
|
if (! isset($tokens[$functionIndex])) {
|
|
return false;
|
|
}
|
|
$allowedFunctionNames = [
|
|
'isset',
|
|
'empty',
|
|
];
|
|
if (in_array($tokens[$functionIndex]['content'], $allowedFunctionNames, true)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isVariableArrayPushShortcut(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
$nonFunctionTokenTypes = Tokens::$emptyTokens;
|
|
|
|
$arrayPushOperatorIndex1 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $stackPtr + 1, null, true, null, true));
|
|
if (! is_int($arrayPushOperatorIndex1)) {
|
|
return false;
|
|
}
|
|
if (! isset($tokens[$arrayPushOperatorIndex1]['content']) || $tokens[$arrayPushOperatorIndex1]['content'] !== '[') {
|
|
return false;
|
|
}
|
|
|
|
$arrayPushOperatorIndex2 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex1 + 1, null, true, null, true));
|
|
if (! is_int($arrayPushOperatorIndex2)) {
|
|
return false;
|
|
}
|
|
if (! isset($tokens[$arrayPushOperatorIndex2]['content']) || $tokens[$arrayPushOperatorIndex2]['content'] !== ']') {
|
|
return false;
|
|
}
|
|
|
|
$arrayPushOperatorIndex3 = self::getIntOrNull($phpcsFile->findNext($nonFunctionTokenTypes, $arrayPushOperatorIndex2 + 1, null, true, null, true));
|
|
if (! is_int($arrayPushOperatorIndex3)) {
|
|
return false;
|
|
}
|
|
if (! isset($tokens[$arrayPushOperatorIndex3]['content']) || $tokens[$arrayPushOperatorIndex3]['content'] !== '=') {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isVariableInsideUnset(File $phpcsFile, $stackPtr)
|
|
{
|
|
$functionIndex = self::getFunctionIndexForFunctionCallArgument($phpcsFile, $stackPtr);
|
|
if (! is_int($functionIndex)) {
|
|
return false;
|
|
}
|
|
$tokens = $phpcsFile->getTokens();
|
|
if (! isset($tokens[$functionIndex])) {
|
|
return false;
|
|
}
|
|
if ($tokens[$functionIndex]['content'] === 'unset') {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isTokenInsideAssignmentRHS(File $phpcsFile, $stackPtr)
|
|
{
|
|
$previousStatementPtr = $phpcsFile->findPrevious([T_SEMICOLON, T_CLOSE_CURLY_BRACKET, T_OPEN_CURLY_BRACKET, T_COMMA], $stackPtr - 1);
|
|
if (! is_int($previousStatementPtr)) {
|
|
$previousStatementPtr = 1;
|
|
}
|
|
$previousTokenPtr = $phpcsFile->findPrevious([T_EQUAL], $stackPtr - 1, $previousStatementPtr);
|
|
if (is_int($previousTokenPtr)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isTokenInsideAssignmentLHS(File $phpcsFile, $stackPtr)
|
|
{
|
|
// Is the next non-whitespace an assignment?
|
|
$assignPtr = self::getNextAssignPointer($phpcsFile, $stackPtr);
|
|
if (! is_int($assignPtr)) {
|
|
return false;
|
|
}
|
|
|
|
// Is this a variable variable? If so, it's not an assignment to the current variable.
|
|
if (self::isTokenVariableVariable($phpcsFile, $stackPtr)) {
|
|
self::debug('found variable variable');
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isTokenVariableVariable(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
|
|
$prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
|
|
if ($prev === false) {
|
|
return false;
|
|
}
|
|
if ($tokens[$prev]['code'] === T_DOLLAR) {
|
|
return true;
|
|
}
|
|
if ($tokens[$prev]['code'] !== T_OPEN_CURLY_BRACKET) {
|
|
return false;
|
|
}
|
|
|
|
$prevPrev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true);
|
|
if ($prevPrev !== false && $tokens[$prevPrev]['code'] === T_DOLLAR) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return EnumInfo|null
|
|
*/
|
|
public static function makeEnumInfo(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
$token = $tokens[$stackPtr];
|
|
|
|
if (isset($token['scope_opener'])) {
|
|
$blockStart = $token['scope_opener'];
|
|
$blockEnd = $token['scope_closer'];
|
|
} else {
|
|
// Enums before phpcs could detect them do not have scopes so we have to
|
|
// find them ourselves.
|
|
|
|
$blockStart = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET], $stackPtr + 1);
|
|
if (! is_int($blockStart)) {
|
|
return null;
|
|
}
|
|
$blockEnd = $tokens[$blockStart]['bracket_closer'];
|
|
}
|
|
|
|
return new EnumInfo(
|
|
$stackPtr,
|
|
$blockStart,
|
|
$blockEnd
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return ForLoopInfo
|
|
*/
|
|
public static function makeForLoopInfo(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
$token = $tokens[$stackPtr];
|
|
$forIndex = $stackPtr;
|
|
$blockStart = $token['parenthesis_closer'];
|
|
if (isset($token['scope_opener'])) {
|
|
$blockStart = $token['scope_opener'];
|
|
$blockEnd = $token['scope_closer'];
|
|
} else {
|
|
// Some for loop blocks will not have scope positions because it they are
|
|
// inline (no curly braces) so we have to find the end of their scope by
|
|
// looking for the end of the next statement.
|
|
$nextSemicolonIndex = $phpcsFile->findNext([T_SEMICOLON], $token['parenthesis_closer']);
|
|
if (! is_int($nextSemicolonIndex)) {
|
|
$nextSemicolonIndex = $token['parenthesis_closer'] + 1;
|
|
}
|
|
$blockEnd = $nextSemicolonIndex;
|
|
}
|
|
$initStart = intval($token['parenthesis_opener']) + 1;
|
|
$initEnd = null;
|
|
$conditionStart = null;
|
|
$conditionEnd = null;
|
|
$incrementStart = null;
|
|
$incrementEnd = $token['parenthesis_closer'] - 1;
|
|
|
|
$semicolonCount = 0;
|
|
$forLoopLevel = $tokens[$forIndex]['level'];
|
|
$forLoopNestedParensCount = 1;
|
|
|
|
if (isset($tokens[$forIndex]['nested_parenthesis'])) {
|
|
$forLoopNestedParensCount = count($tokens[$forIndex]['nested_parenthesis']) + 1;
|
|
}
|
|
|
|
for ($i = $initStart; ($i <= $incrementEnd && $semicolonCount < 2); $i++) {
|
|
if ($tokens[$i]['code'] !== T_SEMICOLON) {
|
|
continue;
|
|
}
|
|
|
|
if ($tokens[$i]['level'] !== $forLoopLevel) {
|
|
continue;
|
|
}
|
|
|
|
if (count($tokens[$i]['nested_parenthesis']) !== $forLoopNestedParensCount) {
|
|
continue;
|
|
}
|
|
|
|
switch ($semicolonCount) {
|
|
case 0:
|
|
$initEnd = $i;
|
|
$conditionStart = $initEnd + 1;
|
|
break;
|
|
case 1:
|
|
$conditionEnd = $i;
|
|
$incrementStart = $conditionEnd + 1;
|
|
break;
|
|
}
|
|
$semicolonCount += 1;
|
|
}
|
|
|
|
if ($initEnd === null || $conditionStart === null || $conditionEnd === null || $incrementStart === null) {
|
|
throw new \Exception("Cannot parse for loop at position {$forIndex}");
|
|
}
|
|
|
|
return new ForLoopInfo(
|
|
$forIndex,
|
|
$blockStart,
|
|
$blockEnd,
|
|
$initStart,
|
|
$initEnd,
|
|
$conditionStart,
|
|
$conditionEnd,
|
|
$incrementStart,
|
|
$incrementEnd
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param int $stackPtr
|
|
* @param array<int, ForLoopInfo> $forLoops
|
|
* @return ForLoopInfo|null
|
|
*/
|
|
public static function getForLoopForIncrementVariable($stackPtr, $forLoops)
|
|
{
|
|
foreach ($forLoops as $forLoop) {
|
|
if ($stackPtr > $forLoop->incrementStart && $stackPtr < $forLoop->incrementEnd) {
|
|
return $forLoop;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Return true if the token looks like constructor promotion.
|
|
*
|
|
* Call on a parameter variable token only.
|
|
*
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isConstructorPromotion(File $phpcsFile, $stackPtr)
|
|
{
|
|
// If we are not in a function's parameters, this is not promotion.
|
|
$functionIndex = self::getFunctionIndexForFunctionParameter($phpcsFile, $stackPtr);
|
|
if (! $functionIndex) {
|
|
return false;
|
|
}
|
|
|
|
$tokens = $phpcsFile->getTokens();
|
|
|
|
// Move backwards from the token, ignoring whitespace, typehints, and the
|
|
// 'readonly' keyword, and return true if the previous token is a
|
|
// visibility keyword (eg: `public`).
|
|
for ($i = $stackPtr - 1; $i > $functionIndex; $i--) {
|
|
if (in_array($tokens[$i]['code'], Tokens::$scopeModifiers, true)) {
|
|
return true;
|
|
}
|
|
if (in_array($tokens[$i]['code'], Tokens::$emptyTokens, true)) {
|
|
continue;
|
|
}
|
|
if ($tokens[$i]['content'] === 'readonly') {
|
|
continue;
|
|
}
|
|
if (self::isTokenPartOfTypehint($phpcsFile, $i)) {
|
|
continue;
|
|
}
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return false if the token is definitely not part of a typehint
|
|
*
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return bool
|
|
*/
|
|
private static function isTokenPossiblyPartOfTypehint(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
$token = $tokens[$stackPtr];
|
|
if ($token['code'] === 'PHPCS_T_NULLABLE') {
|
|
return true;
|
|
}
|
|
if ($token['code'] === T_NS_SEPARATOR) {
|
|
return true;
|
|
}
|
|
if ($token['code'] === T_STRING) {
|
|
return true;
|
|
}
|
|
if ($token['code'] === T_TRUE) {
|
|
return true;
|
|
}
|
|
if ($token['code'] === T_FALSE) {
|
|
return true;
|
|
}
|
|
if ($token['code'] === T_NULL) {
|
|
return true;
|
|
}
|
|
if ($token['content'] === '|') {
|
|
return true;
|
|
}
|
|
if (in_array($token['code'], Tokens::$emptyTokens)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return true if the token is inside a typehint
|
|
*
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isTokenPartOfTypehint(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
|
|
if (! self::isTokenPossiblyPartOfTypehint($phpcsFile, $stackPtr)) {
|
|
return false;
|
|
}
|
|
|
|
// Examine every following token, ignoring everything that might be part of
|
|
// a typehint. If we find a variable at the end, this is part of a
|
|
// typehint.
|
|
$i = $stackPtr;
|
|
while (true) {
|
|
$i += 1;
|
|
if (! isset($tokens[$i])) {
|
|
return false;
|
|
}
|
|
if (! self::isTokenPossiblyPartOfTypehint($phpcsFile, $i)) {
|
|
return ($tokens[$i]['code'] === T_VARIABLE);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return true if the token is inside an abstract class.
|
|
*
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isInAbstractClass(File $phpcsFile, $stackPtr)
|
|
{
|
|
$classIndex = $phpcsFile->getCondition($stackPtr, T_CLASS);
|
|
if (! is_int($classIndex)) {
|
|
return false;
|
|
}
|
|
$classProperties = $phpcsFile->getClassProperties($classIndex);
|
|
return $classProperties['is_abstract'];
|
|
}
|
|
|
|
/**
|
|
* Return true if the function body is empty or contains only `return;`
|
|
*
|
|
* @param File $phpcsFile
|
|
* @param int $stackPtr The index of the function keyword.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isFunctionBodyEmpty(File $phpcsFile, $stackPtr)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
if ($tokens[$stackPtr]['code'] !== T_FUNCTION) {
|
|
return false;
|
|
}
|
|
$functionScopeStart = $tokens[$stackPtr]['scope_opener'];
|
|
$functionScopeEnd = $tokens[$stackPtr]['scope_closer'];
|
|
$tokensToIgnore = array_merge(
|
|
Tokens::$emptyTokens,
|
|
[
|
|
T_RETURN,
|
|
T_SEMICOLON,
|
|
T_OPEN_CURLY_BRACKET,
|
|
T_CLOSE_CURLY_BRACKET,
|
|
]
|
|
);
|
|
for ($i = $functionScopeStart; $i < $functionScopeEnd; $i++) {
|
|
if (! in_array($tokens[$i]['code'], $tokensToIgnore, true)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|