wishthis/vendor/phpcsstandards/phpcsutils/PHPCSUtils/Utils/Lists.php
2023-09-20 13:52:46 +02:00

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;
}
}