358 lines
16 KiB
PHP
358 lines
16 KiB
PHP
<?php
|
|
/**
|
|
* PHPCSUtils, utility functions and classes for PHP_CodeSniffer sniff developers.
|
|
*
|
|
* @package PHPCSUtils
|
|
* @copyright 2019-2020 PHPCSUtils Contributors
|
|
* @license https://opensource.org/licenses/LGPL-3.0 LGPL3
|
|
* @link https://github.com/PHPCSStandards/PHPCSUtils
|
|
*/
|
|
|
|
namespace PHPCSUtils\Utils;
|
|
|
|
use PHP_CodeSniffer\Exceptions\RuntimeException;
|
|
use PHP_CodeSniffer\Files\File;
|
|
use PHP_CodeSniffer\Util\Tokens;
|
|
use PHPCSUtils\Internal\Cache;
|
|
use PHPCSUtils\Internal\IsShortArrayOrListWithCache;
|
|
use PHPCSUtils\Tokens\Collections;
|
|
use PHPCSUtils\Utils\GetTokensAsString;
|
|
|
|
/**
|
|
* Utility functions to retrieve information when working with lists.
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
final class Lists
|
|
{
|
|
|
|
/**
|
|
* Default values for individual list items.
|
|
*
|
|
* Used by the `getAssignments()` method.
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @var array
|
|
*/
|
|
private static $listItemDefaults = [
|
|
'raw' => '',
|
|
'assignment' => '',
|
|
'is_empty' => false,
|
|
'is_nested_list' => false,
|
|
'variable' => false,
|
|
'assignment_token' => false,
|
|
'assignment_end_token' => false,
|
|
'assign_by_reference' => false,
|
|
'reference_token' => false,
|
|
];
|
|
|
|
/**
|
|
* Determine whether a T_OPEN/CLOSE_SHORT_ARRAY token is a short list() construct.
|
|
*
|
|
* This method also accepts `T_OPEN/CLOSE_SQUARE_BRACKET` tokens to allow it to be
|
|
* PHPCS cross-version compatible as the short array tokenizing has been plagued by
|
|
* a number of bugs over time, which affects the short list determination.
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
|
|
* @param int $stackPtr The position of the short array bracket token.
|
|
*
|
|
* @return bool `TRUE` if the token passed is the open/close bracket of a short list.
|
|
* `FALSE` if the token is a short array bracket or plain square bracket
|
|
* or not one of the accepted tokens.
|
|
*/
|
|
public static function isShortList(File $phpcsFile, $stackPtr)
|
|
{
|
|
return IsShortArrayOrListWithCache::isShortList($phpcsFile, $stackPtr);
|
|
}
|
|
|
|
/**
|
|
* Find the list opener and closer based on a T_LIST or T_OPEN_SHORT_ARRAY token.
|
|
*
|
|
* This method also accepts `T_OPEN_SQUARE_BRACKET` tokens to allow it to be
|
|
* PHPCS cross-version compatible as the short array tokenizing has been plagued by
|
|
* a number of bugs over time, which affects the short list determination.
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
|
|
* @param int $stackPtr The position of the T_LIST or T_OPEN_SHORT_ARRAY
|
|
* token in the stack.
|
|
* @param true|null $isShortList Short-circuit the short list check for T_OPEN_SHORT_ARRAY
|
|
* tokens if it isn't necessary.
|
|
* Efficiency tweak for when this has already been established,
|
|
* i.e. when encountering a nested list while walking the
|
|
* tokens in a list.
|
|
* Use with care.
|
|
*
|
|
* @return array|false An array with the token pointers; or `FALSE` if this is not a (short) list
|
|
* token or if the opener/closer could not be determined.
|
|
* The format of the array return value is:
|
|
* ```php
|
|
* array(
|
|
* 'opener' => integer, // Stack pointer to the list open bracket.
|
|
* 'closer' => integer, // Stack pointer to the list close bracket.
|
|
* )
|
|
* ```
|
|
*/
|
|
public static function getOpenClose(File $phpcsFile, $stackPtr, $isShortList = null)
|
|
{
|
|
$tokens = $phpcsFile->getTokens();
|
|
|
|
// Is this one of the tokens this function handles ?
|
|
if (isset($tokens[$stackPtr]) === false
|
|
|| isset(Collections::listOpenTokensBC()[$tokens[$stackPtr]['code']]) === false
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
switch ($tokens[ $stackPtr ]['code']) {
|
|
case \T_LIST:
|
|
if (isset($tokens[$stackPtr]['parenthesis_opener'])) {
|
|
$opener = $tokens[$stackPtr]['parenthesis_opener'];
|
|
|
|
if (isset($tokens[$opener]['parenthesis_closer'])) {
|
|
$closer = $tokens[$opener]['parenthesis_closer'];
|
|
}
|
|
}
|
|
break;
|
|
|
|
case \T_OPEN_SHORT_ARRAY:
|
|
case \T_OPEN_SQUARE_BRACKET:
|
|
if ($isShortList === true || self::isShortList($phpcsFile, $stackPtr) === true) {
|
|
$opener = $stackPtr;
|
|
$closer = $tokens[$stackPtr]['bracket_closer'];
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (isset($opener, $closer)) {
|
|
return [
|
|
'opener' => $opener,
|
|
'closer' => $closer,
|
|
];
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Retrieves information on the assignments made in the specified (long/short) list.
|
|
*
|
|
* This method also accepts `T_OPEN_SQUARE_BRACKET` tokens to allow it to be
|
|
* PHPCS cross-version compatible as the short array tokenizing has been plagued by
|
|
* a number of bugs over time, which affects the short list determination.
|
|
*
|
|
* The returned array will contain the following basic information for each assignment:
|
|
*
|
|
* ```php
|
|
* 0 => array(
|
|
* 'raw' => string, // The full content of the variable definition,
|
|
* // including whitespace and comments.
|
|
* // This may be an empty string when a list
|
|
* // item is being skipped.
|
|
* 'assignment' => string, // The content of the assignment part,
|
|
* // cleaned of comments.
|
|
* // This may be an empty string for an empty
|
|
* // list item; it could also be a nested list
|
|
* // represented as a string.
|
|
* 'is_empty' => bool, // Whether this is an empty list item, i.e.
|
|
* // the second item in `list($a, , $b)`.
|
|
* 'is_nested_list' => bool, // Whether this is a nested list.
|
|
* 'variable' => string|false, // The base variable being assigned to; or
|
|
* // FALSE in case of a nested list or
|
|
* // a variable variable.
|
|
* // I.e. `$a` in `list($a['key'])`.
|
|
* 'assignment_token' => int|false, // The start pointer for the assignment.
|
|
* // For a nested list, this will be the pointer
|
|
* // to the `list` keyword or the open square
|
|
* // bracket in case of a short list.
|
|
* 'assignment_end_token' => int|false, // The end pointer for the assignment.
|
|
* 'assign_by_reference' => bool, // Is the variable assigned by reference?
|
|
* 'reference_token' => int|false, // The stack pointer to the reference operator;
|
|
* // or FALSE when not a reference assignment.
|
|
* )
|
|
* ```
|
|
*
|
|
* Assignments with keys will have the following additional array indexes set:
|
|
* ```php
|
|
* 'key' => string, // The content of the key, cleaned of comments.
|
|
* 'key_token' => int, // The stack pointer to the start of the key.
|
|
* 'key_end_token' => int, // The stack pointer to the end of the key.
|
|
* 'double_arrow_token' => int, // The stack pointer to the double arrow.
|
|
* ```
|
|
*
|
|
* @since 1.0.0
|
|
*
|
|
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
|
|
* @param int $stackPtr The position in the stack of the function token
|
|
* to acquire the parameters for.
|
|
*
|
|
* @return array An array with information on each assignment made, including skipped assignments (empty),
|
|
* or an empty array if no assignments are made at all (fatal error in PHP >= 7.0).
|
|
*
|
|
* @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified $stackPtr is not of
|
|
* type T_LIST, T_OPEN_SHORT_ARRAY or
|
|
* T_OPEN_SQUARE_BRACKET.
|
|
*/
|
|
public static function getAssignments(File $phpcsFile, $stackPtr)
|
|
{
|
|
$openClose = self::getOpenClose($phpcsFile, $stackPtr);
|
|
if ($openClose === false) {
|
|
// The `getOpenClose()` method does the $stackPtr validation.
|
|
throw new RuntimeException('The Lists::getAssignments() method expects a long/short list token.');
|
|
}
|
|
|
|
if (Cache::isCached($phpcsFile, __METHOD__, $stackPtr) === true) {
|
|
return Cache::get($phpcsFile, __METHOD__, $stackPtr);
|
|
}
|
|
|
|
$opener = $openClose['opener'];
|
|
$closer = $openClose['closer'];
|
|
|
|
$tokens = $phpcsFile->getTokens();
|
|
|
|
$vars = [];
|
|
$start = null;
|
|
$lastNonEmpty = null;
|
|
$reference = null;
|
|
$list = null;
|
|
$lastComma = $opener;
|
|
$keys = [];
|
|
|
|
for ($i = ($opener + 1); $i <= $closer; $i++) {
|
|
if (isset(Tokens::$emptyTokens[$tokens[$i]['code']])) {
|
|
continue;
|
|
}
|
|
|
|
switch ($tokens[$i]['code']) {
|
|
case \T_DOUBLE_ARROW:
|
|
$keys['key'] = GetTokensAsString::compact($phpcsFile, $start, $lastNonEmpty, true);
|
|
$keys['key_token'] = $start;
|
|
$keys['key_end_token'] = $lastNonEmpty;
|
|
$keys['double_arrow_token'] = $i;
|
|
|
|
// Partial reset.
|
|
$start = null;
|
|
$lastNonEmpty = null;
|
|
$list = null; // Prevent confusion when short array was used as the key.
|
|
$reference = null;
|
|
break;
|
|
|
|
case \T_COMMA:
|
|
case $tokens[$closer]['code']:
|
|
// Check if this is the end of the list or only a token with the same type as the list closer.
|
|
if ($tokens[$i]['code'] === $tokens[$closer]['code']) {
|
|
if ($i !== $closer) {
|
|
/*
|
|
* Shouldn't be possible anymore now nested brackets are being skipped over,
|
|
* but keep it just in case.
|
|
*/
|
|
// @codeCoverageIgnoreStart
|
|
$lastNonEmpty = $i;
|
|
break;
|
|
// @codeCoverageIgnoreEnd
|
|
} elseif ($start === null && $lastComma === $opener) {
|
|
// This is an empty list.
|
|
break 2;
|
|
}
|
|
}
|
|
|
|
// Ok, so this is actually the end of the list item.
|
|
$current = self::$listItemDefaults;
|
|
$current['raw'] = \trim(GetTokensAsString::normal($phpcsFile, ($lastComma + 1), ($i - 1)));
|
|
|
|
if ($start === null) {
|
|
$current['is_empty'] = true;
|
|
} else {
|
|
$current['assignment'] = \trim(
|
|
GetTokensAsString::compact($phpcsFile, $start, $lastNonEmpty, true)
|
|
);
|
|
$current['is_nested_list'] = isset($list);
|
|
|
|
$current['variable'] = false;
|
|
if (isset($list) === false && $tokens[$start]['code'] === \T_VARIABLE) {
|
|
$current['variable'] = $tokens[$start]['content'];
|
|
}
|
|
$current['assignment_token'] = $start;
|
|
$current['assignment_end_token'] = $lastNonEmpty;
|
|
|
|
if (isset($reference)) {
|
|
$current['assign_by_reference'] = true;
|
|
$current['reference_token'] = $reference;
|
|
}
|
|
}
|
|
|
|
if (empty($keys) === false) {
|
|
$current += $keys;
|
|
}
|
|
|
|
$vars[] = $current;
|
|
|
|
// Reset.
|
|
$start = null;
|
|
$lastNonEmpty = null;
|
|
$reference = null;
|
|
$list = null;
|
|
$lastComma = $i;
|
|
$keys = [];
|
|
|
|
break;
|
|
|
|
case \T_LIST:
|
|
case \T_OPEN_SHORT_ARRAY:
|
|
if ($start === null) {
|
|
$start = $i;
|
|
}
|
|
|
|
/*
|
|
* As the top level list has an open/close, we know we don't have a parse error and
|
|
* any nested (short) arrays/lists will be tokenized correctly, so no need for extra checks here.
|
|
*/
|
|
$nestedOpenClose = self::getOpenClose($phpcsFile, $i, true);
|
|
$list = $i;
|
|
$i = $nestedOpenClose['closer'];
|
|
|
|
$lastNonEmpty = $i;
|
|
break;
|
|
|
|
case \T_BITWISE_AND:
|
|
$reference = $i;
|
|
$lastNonEmpty = $i;
|
|
break;
|
|
|
|
default:
|
|
if ($start === null) {
|
|
$start = $i;
|
|
}
|
|
|
|
// Skip over everything within all types of brackets which may be used in keys.
|
|
if (isset($tokens[$i]['bracket_opener'], $tokens[$i]['bracket_closer'])
|
|
&& $i === $tokens[$i]['bracket_opener']
|
|
) {
|
|
$i = $tokens[$i]['bracket_closer'];
|
|
} elseif ($tokens[$i]['code'] === \T_OPEN_PARENTHESIS
|
|
&& isset($tokens[$i]['parenthesis_closer'])
|
|
) {
|
|
$i = $tokens[$i]['parenthesis_closer'];
|
|
} elseif (isset($tokens[$i]['scope_condition'], $tokens[$i]['scope_closer'])
|
|
&& $tokens[$i]['scope_condition'] === $i
|
|
) {
|
|
$i = $tokens[$i]['scope_closer'];
|
|
} elseif ($tokens[$i]['code'] === \T_ATTRIBUTE
|
|
&& isset($tokens[$i]['attribute_closer'])
|
|
) {
|
|
$i = $tokens[$i]['attribute_closer'];
|
|
}
|
|
|
|
$lastNonEmpty = $i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
Cache::set($phpcsFile, __METHOD__, $stackPtr, $vars);
|
|
return $vars;
|
|
}
|
|
}
|