wishthis/vendor/phpcsstandards/phpcsutils/PHPCSUtils/AbstractSniffs/AbstractArrayDeclarationSniff.php

552 lines
20 KiB
PHP
Raw Normal View History

2023-09-19 17:26:46 +00:00
<?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\AbstractSniffs;
use PHP_CodeSniffer\Exceptions\RuntimeException;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\Tokens\Collections;
use PHPCSUtils\Utils\Arrays;
use PHPCSUtils\Utils\Numbers;
use PHPCSUtils\Utils\PassedParameters;
use PHPCSUtils\Utils\TextStrings;
/**
* Abstract sniff to easily examine all parts of an array declaration.
*
* @since 1.0.0
*/
abstract class AbstractArrayDeclarationSniff implements Sniff
{
/**
* The stack pointer to the array keyword or the short array open token.
*
* @since 1.0.0
*
* @var int
*/
protected $stackPtr;
/**
* The token stack for the current file being examined.
*
* @since 1.0.0
*
* @var array
*/
protected $tokens;
/**
* The stack pointer to the array opener.
*
* @since 1.0.0
*
* @var int
*/
protected $arrayOpener;
/**
* The stack pointer to the array closer.
*
* @since 1.0.0
*
* @var int
*/
protected $arrayCloser;
/**
* A multi-dimentional array with information on each array item.
*
* The array index is 1-based and contains the following information on each array item:
* ```php
* 1 => array(
* 'start' => int, // The stack pointer to the first token in the array item.
* 'end' => int, // The stack pointer to the last token in the array item.
* 'raw' => string, // A string with the contents of all tokens between `start` and `end`.
* 'clean' => string, // Same as `raw`, but all comment tokens have been stripped out.
* )
* ```
*
* @since 1.0.0
*
* @var array
*/
protected $arrayItems;
/**
* How many items are in the array.
*
* @since 1.0.0
*
* @var int
*/
protected $itemCount = 0;
/**
* Whether or not the array is single line.
*
* @since 1.0.0
*
* @var bool
*/
protected $singleLine;
/**
* List of tokens which can safely be used with an eval() expression.
*
* This list gets enhanced with additional token groups in the constructor.
*
* @since 1.0.0
*
* @var array
*/
private $acceptedTokens = [
\T_NULL => \T_NULL,
\T_TRUE => \T_TRUE,
\T_FALSE => \T_FALSE,
\T_LNUMBER => \T_LNUMBER,
\T_DNUMBER => \T_DNUMBER,
\T_CONSTANT_ENCAPSED_STRING => \T_CONSTANT_ENCAPSED_STRING,
\T_STRING_CONCAT => \T_STRING_CONCAT,
\T_INLINE_THEN => \T_INLINE_THEN,
\T_INLINE_ELSE => \T_INLINE_ELSE,
\T_BOOLEAN_NOT => \T_BOOLEAN_NOT,
];
/**
* Set up this class.
*
* @since 1.0.0
*
* @codeCoverageIgnore
*
* @return void
*/
final public function __construct()
{
// Enhance the list of accepted tokens.
$this->acceptedTokens += Tokens::$assignmentTokens;
$this->acceptedTokens += Tokens::$comparisonTokens;
$this->acceptedTokens += Tokens::$arithmeticTokens;
$this->acceptedTokens += Tokens::$operators;
$this->acceptedTokens += Tokens::$booleanOperators;
$this->acceptedTokens += Tokens::$castTokens;
$this->acceptedTokens += Tokens::$bracketTokens;
$this->acceptedTokens += Tokens::$heredocTokens;
}
/**
* Returns an array of tokens this test wants to listen for.
*
* @since 1.0.0
*
* @codeCoverageIgnore
*
* @return array
*/
public function register()
{
return Collections::arrayOpenTokensBC();
}
/**
* Processes this test when one of its tokens is encountered.
*
* This method fills the properties with relevant information for examining the array
* and then passes off to the {@see AbstractArrayDeclarationSniff::processArray()} method.
*
* @since 1.0.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* token was found.
* @param int $stackPtr The position in the PHP_CodeSniffer
* file's token stack where the token
* was found.
*
* @return void
*/
final public function process(File $phpcsFile, $stackPtr)
{
try {
$this->arrayItems = PassedParameters::getParameters($phpcsFile, $stackPtr);
} catch (RuntimeException $e) {
// Parse error, short list, real square open bracket or incorrectly tokenized short array token.
return;
}
$openClose = Arrays::getOpenClose($phpcsFile, $stackPtr, true);
if ($openClose === false) {
// Parse error or live coding.
return;
}
$this->stackPtr = $stackPtr;
$this->tokens = $phpcsFile->getTokens();
$this->arrayOpener = $openClose['opener'];
$this->arrayCloser = $openClose['closer'];
$this->itemCount = \count($this->arrayItems);
$this->singleLine = true;
if ($this->tokens[$openClose['opener']]['line'] !== $this->tokens[$openClose['closer']]['line']) {
$this->singleLine = false;
}
$this->processArray($phpcsFile);
// Reset select properties between calls to this sniff to lower memory usage.
$this->tokens = [];
$this->arrayItems = [];
}
/**
* Process every part of the array declaration.
*
* Controller which calls the individual `process...()` methods for each part of the array.
*
* The method starts by calling the {@see AbstractArrayDeclarationSniff::processOpenClose()} method
* and subsequently calls the following methods for each array item:
*
* Unkeyed arrays | Keyed arrays
* -------------- | ------------
* processNoKey() | processKey()
* - | processArrow()
* processValue() | processValue()
* processComma() | processComma()
*
* This is the default logic for the sniff, but can be overloaded in a concrete child class
* if needed.
*
* @since 1.0.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* token was found.
*
* @return void
*/
public function processArray(File $phpcsFile)
{
if ($this->processOpenClose($phpcsFile, $this->arrayOpener, $this->arrayCloser) === true) {
return;
}
if ($this->itemCount === 0) {
return;
}
foreach ($this->arrayItems as $itemNr => $arrayItem) {
try {
$arrowPtr = Arrays::getDoubleArrowPtr($phpcsFile, $arrayItem['start'], $arrayItem['end']);
} catch (RuntimeException $e) {
// Parse error: empty array item. Ignore.
continue;
}
if ($arrowPtr !== false) {
if ($this->processKey($phpcsFile, $arrayItem['start'], ($arrowPtr - 1), $itemNr) === true) {
return;
}
if ($this->processArrow($phpcsFile, $arrowPtr, $itemNr) === true) {
return;
}
if ($this->processValue($phpcsFile, ($arrowPtr + 1), $arrayItem['end'], $itemNr) === true) {
return;
}
} else {
if ($this->processNoKey($phpcsFile, $arrayItem['start'], $itemNr) === true) {
return;
}
if ($this->processValue($phpcsFile, $arrayItem['start'], $arrayItem['end'], $itemNr) === true) {
return;
}
}
$commaPtr = ($arrayItem['end'] + 1);
if ($itemNr < $this->itemCount || $this->tokens[$commaPtr]['code'] === \T_COMMA) {
if ($this->processComma($phpcsFile, $commaPtr, $itemNr) === true) {
return;
}
}
}
}
/**
* Process the array opener and closer.
*
* Optional method to be implemented in concrete child classes. By default, this method does nothing.
*
* @since 1.0.0
*
* @codeCoverageIgnore
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* token was found.
* @param int $openPtr The position of the array opener token in the token stack.
* @param int $closePtr The position of the array closer token in the token stack.
*
* @return true|void Returning `TRUE` will short-circuit the sniff and stop processing.
* In effect, this means that the sniff will not examine the individual
* array items if `TRUE` is returned.
*/
public function processOpenClose(File $phpcsFile, $openPtr, $closePtr)
{
}
/**
* Process the tokens in an array key.
*
* Optional method to be implemented in concrete child classes. By default, this method does nothing.
*
* Note: The `$startPtr` and `$endPtr` do not discount whitespace or comments, but are all inclusive
* to allow for examining all tokens in an array key.
*
* @since 1.0.0
*
* @codeCoverageIgnore
*
* @see \PHPCSUtils\AbstractSniffs\AbstractArrayDeclarationSniff::getActualArrayKey() Optional helper function.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* token was found.
* @param int $startPtr The stack pointer to the first token in the "key" part of
* an array item.
* @param int $endPtr The stack pointer to the last token in the "key" part of
* an array item.
* @param int $itemNr Which item in the array is being handled.
* 1-based, i.e. the first item is item 1, the second 2 etc.
*
* @return true|void Returning `TRUE` will short-circuit the array item loop and stop processing.
* In effect, this means that the sniff will not examine the double arrow, the array
* value or comma for this array item and will not process any array items after this one.
*/
public function processKey(File $phpcsFile, $startPtr, $endPtr, $itemNr)
{
}
/**
* Process an array item without an array key.
*
* Optional method to be implemented in concrete child classes. By default, this method does nothing.
*
* Note: This method is _not_ intended for processing the array _value_. Use the
* {@see AbstractArrayDeclarationSniff::processValue()} method to implement processing of the array value.
*
* @since 1.0.0
*
* @codeCoverageIgnore
*
* @see \PHPCSUtils\AbstractSniffs\AbstractArrayDeclarationSniff::processValue() Method to process the array value.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* token was found.
* @param int $startPtr The stack pointer to the first token in the array item,
* which in this case will be the first token of the array
* value part of the array item.
* @param int $itemNr Which item in the array is being handled.
* 1-based, i.e. the first item is item 1, the second 2 etc.
*
* @return true|void Returning `TRUE` will short-circuit the array item loop and stop processing.
* In effect, this means that the sniff will not examine the array value or
* comma for this array item and will not process any array items after this one.
*/
public function processNoKey(File $phpcsFile, $startPtr, $itemNr)
{
}
/**
* Process the double arrow.
*
* Optional method to be implemented in concrete child classes. By default, this method does nothing.
*
* @since 1.0.0
*
* @codeCoverageIgnore
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* token was found.
* @param int $arrowPtr The stack pointer to the double arrow for the array item.
* @param int $itemNr Which item in the array is being handled.
* 1-based, i.e. the first item is item 1, the second 2 etc.
*
* @return true|void Returning `TRUE` will short-circuit the array item loop and stop processing.
* In effect, this means that the sniff will not examine the array value or
* comma for this array item and will not process any array items after this one.
*/
public function processArrow(File $phpcsFile, $arrowPtr, $itemNr)
{
}
/**
* Process the tokens in an array value.
*
* Optional method to be implemented in concrete child classes. By default, this method does nothing.
*
* Note: The `$startPtr` and `$endPtr` do not discount whitespace or comments, but are all inclusive
* to allow for examining all tokens in an array value.
*
* @since 1.0.0
*
* @codeCoverageIgnore
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* token was found.
* @param int $startPtr The stack pointer to the first token in the "value" part of
* an array item.
* @param int $endPtr The stack pointer to the last token in the "value" part of
* an array item.
* @param int $itemNr Which item in the array is being handled.
* 1-based, i.e. the first item is item 1, the second 2 etc.
*
* @return true|void Returning `TRUE` will short-circuit the array item loop and stop processing.
* In effect, this means that the sniff will not examine the comma for this
* array item and will not process any array items after this one.
*/
public function processValue(File $phpcsFile, $startPtr, $endPtr, $itemNr)
{
}
/**
* Process the comma after an array item.
*
* Optional method to be implemented in concrete child classes. By default, this method does nothing.
*
* @since 1.0.0
*
* @codeCoverageIgnore
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* token was found.
* @param int $commaPtr The stack pointer to the comma.
* @param int $itemNr Which item in the array is being handled.
* 1-based, i.e. the first item is item 1, the second 2 etc.
*
* @return true|void Returning `TRUE` will short-circuit the array item loop and stop processing.
* In effect, this means that the sniff will not process any array items
* after this one.
*/
public function processComma(File $phpcsFile, $commaPtr, $itemNr)
{
}
/**
* Determine what the actual array key would be.
*
* Helper function for processsing array keys in the processKey() function.
* Using this method is up to the sniff implementation in the child class.
*
* @since 1.0.0
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the
* token was found.
* @param int $startPtr The stack pointer to the first token in the "key" part of
* an array item.
* @param int $endPtr The stack pointer to the last token in the "key" part of
* an array item.
*
* @return string|int|void The string or integer array key or void if the array key could not
* reliably be determined.
*/
public function getActualArrayKey(File $phpcsFile, $startPtr, $endPtr)
{
/*
* Determine the value of the key.
*/
$firstNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $startPtr, null, true);
$lastNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, $endPtr, null, true);
$content = '';
for ($i = $firstNonEmpty; $i <= $lastNonEmpty; $i++) {
if (isset(Tokens::$commentTokens[$this->tokens[$i]['code']]) === true) {
continue;
}
if ($this->tokens[$i]['code'] === \T_WHITESPACE) {
$content .= ' ';
continue;
}
if (isset($this->acceptedTokens[$this->tokens[$i]['code']]) === false) {
// This is not a key we can evaluate. Might be a variable or constant.
return;
}
// Take PHP 7.4 numeric literal separators into account.
if ($this->tokens[$i]['code'] === \T_LNUMBER || $this->tokens[$i]['code'] === \T_DNUMBER) {
$number = Numbers::getCompleteNumber($phpcsFile, $i);
$content .= $number['content'];
$i = $number['last_token'];
continue;
}
// Account for heredoc with vars.
if ($this->tokens[$i]['code'] === \T_START_HEREDOC) {
$text = TextStrings::getCompleteTextString($phpcsFile, $i);
// Check if there's a variable in the heredoc.
if ($text !== TextStrings::stripEmbeds($text)) {
return;
}
for ($j = $i; $j <= $this->tokens[$i]['scope_closer']; $j++) {
$content .= $this->tokens[$j]['content'];
}
$i = $this->tokens[$i]['scope_closer'];
continue;
}
$content .= $this->tokens[$i]['content'];
}
// The PHP_EOL is to prevent getting parse errors when the key is a heredoc/nowdoc.
$key = eval('return ' . $content . ';' . \PHP_EOL);
/*
* Ok, so now we know the base value of the key, let's determine whether it is
* an acceptable index key for an array and if not, what it would turn into.
*/
switch (\gettype($key)) {
case 'NULL':
// An array key of `null` will become an empty string.
return '';
case 'boolean':
return ($key === true) ? 1 : 0;
case 'integer':
return $key;
case 'double':
return (int) $key; // Will automatically cut off the decimal part.
case 'string':
if (Numbers::isDecimalInt($key) === true) {
return (int) $key;
}
return $key;
default:
/*
* Shouldn't be possible. Either way, if it's not one of the above types,
* this is not a key we can handle.
*/
return; // @codeCoverageIgnore
}
}
}