*/ 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 */ $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> */ 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> $arguments The stack pointers of each argument; see findFunctionCallArguments * * @return array 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 */ 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 */ 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 */ $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 $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; } }