Skip to main content
Drupal API
User account menu
  • Log in

Breadcrumb

  1. Drupal Core 11.1.x

Helpers.php

Namespace

VariableAnalysis\Lib

File

vendor/sirbrillig/phpcs-variable-analysis/VariableAnalysis/Lib/Helpers.php

View source
<?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']]) && $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;
    }

}

Classes

Title Deprecated Summary
Helpers

API Navigation

  • Drupal Core 11.1.x
  • Topics
  • Classes
  • Functions
  • Constants
  • Globals
  • Files
  • Namespaces
  • Deprecated
  • Services
RSS feed
Powered by Drupal