From 2845669127d9d96192a38404eabe3b176c6ba423 Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Fri, 20 Jan 2023 19:00:52 +0100 Subject: [PATCH] Bump/install phpcsutils to 1.0.1 (4fd2e30) Those utilities were previously part of the PHPCompatibility standard and now, as many of them and a lot new, are general-purpose, reusable by other standards, they are a separate (meta) standard. --- .../AbstractArrayDeclarationSniff.php | 545 ++++++++++++ PHPCSUtils/BackCompat/BCFile.php | 765 +++++++++++++++++ PHPCSUtils/BackCompat/BCTokens.php | 123 +++ PHPCSUtils/BackCompat/Helper.php | 197 +++++ PHPCSUtils/Exceptions/InvalidTokenArray.php | 44 + PHPCSUtils/Exceptions/TestFileNotFound.php | 47 + PHPCSUtils/Exceptions/TestMarkerNotFound.php | 43 + PHPCSUtils/Exceptions/TestTargetNotFound.php | 50 ++ PHPCSUtils/Fixers/SpacesFixer.php | 246 ++++++ PHPCSUtils/Internal/Cache.php | 217 +++++ PHPCSUtils/Internal/IsShortArrayOrList.php | 700 +++++++++++++++ .../Internal/IsShortArrayOrListWithCache.php | 269 ++++++ PHPCSUtils/Internal/NoFileCache.php | 164 ++++ .../TestUtils/UtilityMethodTestCase.php | 419 +++++++++ PHPCSUtils/Tokens/Collections.php | 803 +++++++++++++++++ PHPCSUtils/Tokens/TokenHelper.php | 55 ++ PHPCSUtils/Utils/Arrays.php | 218 +++++ PHPCSUtils/Utils/Conditions.php | 156 ++++ PHPCSUtils/Utils/Context.php | 232 +++++ PHPCSUtils/Utils/ControlStructures.php | 274 ++++++ PHPCSUtils/Utils/FunctionDeclarations.php | 808 ++++++++++++++++++ PHPCSUtils/Utils/GetTokensAsString.php | 262 ++++++ PHPCSUtils/Utils/Lists.php | 332 +++++++ PHPCSUtils/Utils/MessageHelper.php | 145 ++++ PHPCSUtils/Utils/Namespaces.php | 389 +++++++++ PHPCSUtils/Utils/NamingConventions.php | 117 +++ PHPCSUtils/Utils/Numbers.php | 322 +++++++ PHPCSUtils/Utils/ObjectDeclarations.php | 360 ++++++++ PHPCSUtils/Utils/Operators.php | 252 ++++++ PHPCSUtils/Utils/Orthography.php | 120 +++ PHPCSUtils/Utils/Parentheses.php | 419 +++++++++ PHPCSUtils/Utils/PassedParameters.php | 502 +++++++++++ PHPCSUtils/Utils/Scopes.php | 143 ++++ PHPCSUtils/Utils/TextStrings.php | 331 +++++++ PHPCSUtils/Utils/UseStatements.php | 431 ++++++++++ PHPCSUtils/Utils/Variables.php | 331 +++++++ PHPCSUtils/ruleset.xml | 4 + phpcs/CodeSniffer.conf | 2 +- readme_moodle.txt | 21 +- thirdpartylibs.xml | 7 + 40 files changed, 10863 insertions(+), 2 deletions(-) create mode 100644 PHPCSUtils/AbstractSniffs/AbstractArrayDeclarationSniff.php create mode 100644 PHPCSUtils/BackCompat/BCFile.php create mode 100644 PHPCSUtils/BackCompat/BCTokens.php create mode 100644 PHPCSUtils/BackCompat/Helper.php create mode 100644 PHPCSUtils/Exceptions/InvalidTokenArray.php create mode 100644 PHPCSUtils/Exceptions/TestFileNotFound.php create mode 100644 PHPCSUtils/Exceptions/TestMarkerNotFound.php create mode 100644 PHPCSUtils/Exceptions/TestTargetNotFound.php create mode 100644 PHPCSUtils/Fixers/SpacesFixer.php create mode 100644 PHPCSUtils/Internal/Cache.php create mode 100644 PHPCSUtils/Internal/IsShortArrayOrList.php create mode 100644 PHPCSUtils/Internal/IsShortArrayOrListWithCache.php create mode 100644 PHPCSUtils/Internal/NoFileCache.php create mode 100644 PHPCSUtils/TestUtils/UtilityMethodTestCase.php create mode 100644 PHPCSUtils/Tokens/Collections.php create mode 100644 PHPCSUtils/Tokens/TokenHelper.php create mode 100644 PHPCSUtils/Utils/Arrays.php create mode 100644 PHPCSUtils/Utils/Conditions.php create mode 100644 PHPCSUtils/Utils/Context.php create mode 100644 PHPCSUtils/Utils/ControlStructures.php create mode 100644 PHPCSUtils/Utils/FunctionDeclarations.php create mode 100644 PHPCSUtils/Utils/GetTokensAsString.php create mode 100644 PHPCSUtils/Utils/Lists.php create mode 100644 PHPCSUtils/Utils/MessageHelper.php create mode 100644 PHPCSUtils/Utils/Namespaces.php create mode 100644 PHPCSUtils/Utils/NamingConventions.php create mode 100644 PHPCSUtils/Utils/Numbers.php create mode 100644 PHPCSUtils/Utils/ObjectDeclarations.php create mode 100644 PHPCSUtils/Utils/Operators.php create mode 100644 PHPCSUtils/Utils/Orthography.php create mode 100644 PHPCSUtils/Utils/Parentheses.php create mode 100644 PHPCSUtils/Utils/PassedParameters.php create mode 100644 PHPCSUtils/Utils/Scopes.php create mode 100644 PHPCSUtils/Utils/TextStrings.php create mode 100644 PHPCSUtils/Utils/UseStatements.php create mode 100644 PHPCSUtils/Utils/Variables.php create mode 100644 PHPCSUtils/ruleset.xml diff --git a/PHPCSUtils/AbstractSniffs/AbstractArrayDeclarationSniff.php b/PHPCSUtils/AbstractSniffs/AbstractArrayDeclarationSniff.php new file mode 100644 index 00000000..271c9af7 --- /dev/null +++ b/PHPCSUtils/AbstractSniffs/AbstractArrayDeclarationSniff.php @@ -0,0 +1,545 @@ + 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; + } + + $this->stackPtr = $stackPtr; + $this->tokens = $phpcsFile->getTokens(); + $openClose = Arrays::getOpenClose($phpcsFile, $stackPtr, true); + $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. + unset($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 + } + } +} diff --git a/PHPCSUtils/BackCompat/BCFile.php b/PHPCSUtils/BackCompat/BCFile.php new file mode 100644 index 00000000..d70bfbff --- /dev/null +++ b/PHPCSUtils/BackCompat/BCFile.php @@ -0,0 +1,765 @@ + + * @author Jaroslav HanslĂ­k + * @author jdavis + * @author Klaus Purer + * @author Juliette Reinders Folmer + * @author Nick Wilde + * @author Martin Hujer + * @author Chris Wilkinson + * + * With documentation contributions from: + * @author Pascal Borreli + * @author Diogo Oliveira de Melo + * @author Stefano Kowalke + * @author George Mponos + * @author Tyson Andre + * @author Klaus Purer + * + * @copyright 2006-2019 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHPCSUtils\BackCompat; + +use PHP_CodeSniffer\Exceptions\RuntimeException; +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Tokens\Collections; + +/** + * PHPCS native utility functions. + * + * Backport of the latest versions of PHPCS native utility functions to make them + * available in older PHPCS versions without the bugs and other quirks that the + * older versions of the native functions had. + * + * Additionally, this class works round the following tokenizer issues for + * any affected utility functions: + * - None at this time. + * + * Most functions in this class will have a related twin-function in the relevant + * class in the `PHPCSUtils\Utils` namespace. + * These will be indicated with `@see` tags in the docblock of the function. + * + * The PHPCSUtils native twin-functions will often have additional features and/or + * improved functionality, but will generally be fully compatible with the PHPCS + * native functions. + * The differences between the functions here and the twin functions are documented + * in the docblock of the respective twin-function. + * + * @see \PHP_CodeSniffer\Files\File Original source of these utility methods. + * + * @since 1.0.0 + */ +final class BCFile +{ + + /** + * Returns the declaration name for classes, interfaces, traits, enums, and functions. + * + * PHPCS cross-version compatible version of the `File::getDeclarationName()` method. + * + * Changelog for the PHPCS native function: + * - Introduced in PHPCS 0.0.5. + * - The upstream method has received no significant updates since PHPCS 3.7.1. + * + * @see \PHP_CodeSniffer\Files\File::getDeclarationName() Original source. + * @see \PHPCSUtils\Utils\ObjectDeclarations::getName() PHPCSUtils native improved version. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the declaration token + * which declared the class, interface, + * trait, enum or function. + * + * @return string|null The name of the class, interface, trait, enum, or function; + * or `NULL` if the function or class is anonymous or + * in case of a parse error/live coding. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token is not of type + * `T_FUNCTION`, `T_CLASS`, `T_ANON_CLASS`, + * `T_CLOSURE`, `T_TRAIT`, `T_ENUM` or `T_INTERFACE`. + */ + public static function getDeclarationName(File $phpcsFile, $stackPtr) + { + return $phpcsFile->getDeclarationName($stackPtr); + } + + /** + * Returns the method parameters for the specified function token. + * + * Also supports passing in a `T_USE` token for a closure use group. + * + * Each parameter is in the following format: + * ```php + * 0 => array( + * 'name' => '$var', // The variable name. + * 'token' => integer, // The stack pointer to the variable name. + * 'content' => string, // The full content of the variable definition. + * 'has_attributes' => boolean, // Does the parameter have one or more attributes attached ? + * 'pass_by_reference' => boolean, // Is the variable passed by reference? + * 'reference_token' => integer, // The stack pointer to the reference operator + * // or FALSE if the param is not passed by reference. + * 'variable_length' => boolean, // Is the param of variable length through use of `...` ? + * 'variadic_token' => integer, // The stack pointer to the ... operator + * // or FALSE if the param is not variable length. + * 'type_hint' => string, // The type hint for the variable. + * 'type_hint_token' => integer, // The stack pointer to the start of the type hint + * // or FALSE if there is no type hint. + * 'type_hint_end_token' => integer, // The stack pointer to the end of the type hint + * // or FALSE if there is no type hint. + * 'nullable_type' => boolean, // TRUE if the var type is preceded by the nullability + * // operator. + * 'comma_token' => integer, // The stack pointer to the comma after the param + * // or FALSE if this is the last param. + * ) + * ``` + * + * Parameters with default values have the following additional array indexes: + * ```php + * 'default' => string, // The full content of the default value. + * 'default_token' => integer, // The stack pointer to the start of the default value. + * 'default_equal_token' => integer, // The stack pointer to the equals sign. + * ``` + * + * Parameters declared using PHP 8 constructor property promotion, have these additional array indexes: + * ```php + * 'property_visibility' => string, // The property visibility as declared. + * 'visibility_token' => integer, // The stack pointer to the visibility modifier token. + * 'property_readonly' => bool, // TRUE if the readonly keyword was found. + * 'readonly_token' => integer, // The stack pointer to the readonly modifier token. + * ``` + * + * PHPCS cross-version compatible version of the `File::getMethodParameters()` method. + * + * Changelog for the PHPCS native function: + * - Introduced in PHPCS 0.0.5. + * - The upstream method has received no significant updates since PHPCS 3.7.1. + * + * @see \PHP_CodeSniffer\Files\File::getMethodParameters() Original source. + * @see \PHPCSUtils\Utils\FunctionDeclarations::getParameters() PHPCSUtils native improved version. + * + * @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 + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified `$stackPtr` is not of + * type `T_FUNCTION`, `T_CLOSURE`, `T_USE`, + * or `T_FN`. + */ + public static function getMethodParameters(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset(Collections::functionDeclarationTokens()[$tokens[$stackPtr]['code']]) === false + && $tokens[$stackPtr]['code'] !== T_USE + ) { + throw new RuntimeException('$stackPtr must be of type T_FUNCTION or T_CLOSURE or T_USE or T_FN'); + } + + if ($tokens[$stackPtr]['code'] === T_USE) { + $opener = $phpcsFile->findNext(T_OPEN_PARENTHESIS, ($stackPtr + 1)); + if ($opener === false + || (isset($tokens[$opener]['parenthesis_owner']) === true + // BC: as of PHPCS 4.x, closure use tokens are parentheses owners. + && $tokens[$opener]['parenthesis_owner'] !== $stackPtr) + ) { + throw new RuntimeException('$stackPtr was not a valid T_USE'); + } + } else { + if (isset($tokens[$stackPtr]['parenthesis_opener']) === false) { + // Live coding or syntax error, so no params to find. + return []; + } + + $opener = $tokens[$stackPtr]['parenthesis_opener']; + } + + if (isset($tokens[$opener]['parenthesis_closer']) === false) { + // Live coding or syntax error, so no params to find. + return []; + } + + $closer = $tokens[$opener]['parenthesis_closer']; + + $vars = []; + $currVar = null; + $paramStart = ($opener + 1); + $defaultStart = null; + $equalToken = null; + $paramCount = 0; + $hasAttributes = false; + $passByReference = false; + $referenceToken = false; + $variableLength = false; + $variadicToken = false; + $typeHint = ''; + $typeHintToken = false; + $typeHintEndToken = false; + $nullableType = false; + $visibilityToken = null; + $readonlyToken = null; + + for ($i = $paramStart; $i <= $closer; $i++) { + // Check to see if this token has a parenthesis or bracket opener. If it does + // it's likely to be an array which might have arguments in it. This + // could cause problems in our parsing below, so lets just skip to the + // end of it. + if (isset($tokens[$i]['parenthesis_opener']) === true) { + // Don't do this if it's the close parenthesis for the method. + if ($i !== $tokens[$i]['parenthesis_closer']) { + $i = ($tokens[$i]['parenthesis_closer'] + 1); + } + } + + if (isset($tokens[$i]['bracket_opener']) === true) { + // Don't do this if it's the close parenthesis for the method. + if ($i !== $tokens[$i]['bracket_closer']) { + $i = ($tokens[$i]['bracket_closer'] + 1); + } + } + + switch ($tokens[$i]['code']) { + case T_ATTRIBUTE: + $hasAttributes = true; + + // Skip to the end of the attribute. + $i = $tokens[$i]['attribute_closer']; + break; + case T_BITWISE_AND: + if ($defaultStart === null) { + $passByReference = true; + $referenceToken = $i; + } + break; + case T_VARIABLE: + $currVar = $i; + break; + case T_ELLIPSIS: + $variableLength = true; + $variadicToken = $i; + break; + case T_CALLABLE: + if ($typeHintToken === false) { + $typeHintToken = $i; + } + + $typeHint .= $tokens[$i]['content']; + $typeHintEndToken = $i; + break; + case T_SELF: + case T_PARENT: + case T_STATIC: + // Self and parent are valid, static invalid, but was probably intended as type hint. + if (isset($defaultStart) === false) { + if ($typeHintToken === false) { + $typeHintToken = $i; + } + + $typeHint .= $tokens[$i]['content']; + $typeHintEndToken = $i; + } + break; + case T_STRING: + case T_NAME_QUALIFIED: + case T_NAME_FULLY_QUALIFIED: + case T_NAME_RELATIVE: + // This is an identifier name, so it may be a type declaration, but it could + // also be a constant used as a default value. + $prevComma = false; + for ($t = $i; $t >= $opener; $t--) { + if ($tokens[$t]['code'] === T_COMMA) { + $prevComma = $t; + break; + } + } + + if ($prevComma !== false) { + $nextEquals = false; + for ($t = $prevComma; $t < $i; $t++) { + if ($tokens[$t]['code'] === T_EQUAL) { + $nextEquals = $t; + break; + } + } + + if ($nextEquals !== false) { + break; + } + } + + if ($defaultStart === null) { + if ($typeHintToken === false) { + $typeHintToken = $i; + } + + $typeHint .= $tokens[$i]['content']; + $typeHintEndToken = $i; + } + break; + case T_NAMESPACE: + case T_NS_SEPARATOR: + case T_TYPE_UNION: + case T_TYPE_INTERSECTION: + case T_FALSE: + case T_NULL: + // Part of a type hint or default value. + if ($defaultStart === null) { + if ($typeHintToken === false) { + $typeHintToken = $i; + } + + $typeHint .= $tokens[$i]['content']; + $typeHintEndToken = $i; + } + break; + case T_NULLABLE: + if ($defaultStart === null) { + $nullableType = true; + $typeHint .= $tokens[$i]['content']; + $typeHintEndToken = $i; + } + break; + case T_PUBLIC: + case T_PROTECTED: + case T_PRIVATE: + if ($defaultStart === null) { + $visibilityToken = $i; + } + break; + case T_READONLY: + if ($defaultStart === null) { + $readonlyToken = $i; + } + break; + case T_CLOSE_PARENTHESIS: + case T_COMMA: + // If it's null, then there must be no parameters for this + // method. + if ($currVar === null) { + continue 2; + } + + $vars[$paramCount] = []; + $vars[$paramCount]['token'] = $currVar; + $vars[$paramCount]['name'] = $tokens[$currVar]['content']; + $vars[$paramCount]['content'] = trim($phpcsFile->getTokensAsString($paramStart, ($i - $paramStart))); + + if ($defaultStart !== null) { + $vars[$paramCount]['default'] = trim($phpcsFile->getTokensAsString($defaultStart, ($i - $defaultStart))); + $vars[$paramCount]['default_token'] = $defaultStart; + $vars[$paramCount]['default_equal_token'] = $equalToken; + } + + $vars[$paramCount]['has_attributes'] = $hasAttributes; + $vars[$paramCount]['pass_by_reference'] = $passByReference; + $vars[$paramCount]['reference_token'] = $referenceToken; + $vars[$paramCount]['variable_length'] = $variableLength; + $vars[$paramCount]['variadic_token'] = $variadicToken; + $vars[$paramCount]['type_hint'] = $typeHint; + $vars[$paramCount]['type_hint_token'] = $typeHintToken; + $vars[$paramCount]['type_hint_end_token'] = $typeHintEndToken; + $vars[$paramCount]['nullable_type'] = $nullableType; + + if ($visibilityToken !== null) { + $vars[$paramCount]['property_visibility'] = $tokens[$visibilityToken]['content']; + $vars[$paramCount]['visibility_token'] = $visibilityToken; + $vars[$paramCount]['property_readonly'] = false; + } + + if ($readonlyToken !== null) { + $vars[$paramCount]['property_readonly'] = true; + $vars[$paramCount]['readonly_token'] = $readonlyToken; + } + + if ($tokens[$i]['code'] === T_COMMA) { + $vars[$paramCount]['comma_token'] = $i; + } else { + $vars[$paramCount]['comma_token'] = false; + } + + // Reset the vars, as we are about to process the next parameter. + $currVar = null; + $paramStart = ($i + 1); + $defaultStart = null; + $equalToken = null; + $hasAttributes = false; + $passByReference = false; + $referenceToken = false; + $variableLength = false; + $variadicToken = false; + $typeHint = ''; + $typeHintToken = false; + $typeHintEndToken = false; + $nullableType = false; + $visibilityToken = null; + $readonlyToken = null; + + ++$paramCount; + break; + case T_EQUAL: + $defaultStart = $phpcsFile->findNext(Tokens::$emptyTokens, ($i + 1), null, true); + $equalToken = $i; + break; + } + } + + return $vars; + } + + /** + * Returns the visibility and implementation properties of a method. + * + * The format of the return value is: + * ```php + * array( + * 'scope' => 'public', // Public, private, or protected + * 'scope_specified' => true, // TRUE if the scope keyword was found. + * 'return_type' => '', // The return type of the method. + * 'return_type_token' => integer, // The stack pointer to the start of the return type + * // or FALSE if there is no return type. + * 'return_type_end_token' => integer, // The stack pointer to the end of the return type + * // or FALSE if there is no return type. + * 'nullable_return_type' => false, // TRUE if the return type is preceded by + * // the nullability operator. + * 'is_abstract' => false, // TRUE if the abstract keyword was found. + * 'is_final' => false, // TRUE if the final keyword was found. + * 'is_static' => false, // TRUE if the static keyword was found. + * 'has_body' => false, // TRUE if the method has a body + * ); + * ``` + * + * PHPCS cross-version compatible version of the `File::getMethodProperties()` method. + * + * Changelog for the PHPCS native function: + * - Introduced in PHPCS 0.0.5. + * - The upstream method has received no significant updates since PHPCS 3.7.1. + * + * @see \PHP_CodeSniffer\Files\File::getMethodProperties() Original source. + * @see \PHPCSUtils\Utils\FunctionDeclarations::getProperties() PHPCSUtils native improved version. + * + * @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 properties for. + * + * @return array + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a + * `T_FUNCTION`, `T_CLOSURE`, or `T_FN` token. + */ + public static function getMethodProperties(File $phpcsFile, $stackPtr) + { + return $phpcsFile->getMethodProperties($stackPtr); + } + + /** + * Returns the visibility and implementation properties of a class member var. + * + * The format of the return value is: + * ```php + * array( + * 'scope' => string, // Public, private, or protected. + * 'scope_specified' => boolean, // TRUE if the scope was explicitly specified. + * 'is_static' => boolean, // TRUE if the static keyword was found. + * 'is_readonly' => boolean, // TRUE if the readonly keyword was found. + * 'type' => string, // The type of the var (empty if no type specified). + * 'type_token' => integer, // The stack pointer to the start of the type + * // or FALSE if there is no type. + * 'type_end_token' => integer, // The stack pointer to the end of the type + * // or FALSE if there is no type. + * 'nullable_type' => boolean, // TRUE if the type is preceded by the + * // nullability operator. + * ); + * ``` + * + * PHPCS cross-version compatible version of the `File::getMemberProperties() method. + * + * Changelog for the PHPCS native function: + * - Introduced in PHPCS 0.0.5. + * - The upstream method has received no significant updates since PHPCS 3.7.1. + * + * @see \PHP_CodeSniffer\Files\File::getMemberProperties() Original source. + * @see \PHPCSUtils\Utils\Variables::getMemberProperties() PHPCSUtils native improved version. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position in the stack of the `T_VARIABLE` token to + * acquire the properties for. + * + * @return array + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a + * `T_VARIABLE` token, or if the position is not + * a class member variable. + */ + public static function getMemberProperties(File $phpcsFile, $stackPtr) + { + return $phpcsFile->getMemberProperties($stackPtr); + } + + /** + * Returns the implementation properties of a class. + * + * The format of the return value is: + * ```php + * array( + * 'is_abstract' => false, // TRUE if the abstract keyword was found. + * 'is_final' => false, // TRUE if the final keyword was found. + * ); + * ``` + * + * PHPCS cross-version compatible version of the `File::getClassProperties()` method. + * + * Changelog for the PHPCS native function: + * - Introduced in PHPCS 1.3.0. + * - The upstream method has received no significant updates since PHPCS 3.7.1. + * + * @see \PHP_CodeSniffer\Files\File::getClassProperties() Original source. + * @see \PHPCSUtils\Utils\ObjectDeclarations::getClassProperties() PHPCSUtils native improved version. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position in the stack of the `T_CLASS` + * token to acquire the properties for. + * + * @return array + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a + * `T_CLASS` token. + */ + public static function getClassProperties(File $phpcsFile, $stackPtr) + { + return $phpcsFile->getClassProperties($stackPtr); + } + + /** + * Determine if the passed token is a reference operator. + * + * PHPCS cross-version compatible version of the `File::isReference()` method. + * + * Changelog for the PHPCS native function: + * - Introduced in PHPCS 0.0.5. + * - The upstream method has received no significant updates since PHPCS 3.7.1. + * + * @see \PHP_CodeSniffer\Files\File::isReference() Original source. + * @see \PHPCSUtils\Utils\Operators::isReference() PHPCSUtils native improved version. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the `T_BITWISE_AND` token. + * + * @return bool `TRUE` if the specified token position represents a reference. + * `FALSE` if the token represents a bitwise operator. + */ + public static function isReference(File $phpcsFile, $stackPtr) + { + return $phpcsFile->isReference($stackPtr); + } + + /** + * Returns the content of the tokens from the specified start position in + * the token stack for the specified length. + * + * PHPCS cross-version compatible version of the `File::getTokensAsString()` method. + * + * Changelog for the PHPCS native function: + * - Introduced in PHPCS 0.0.5. + * - The upstream method has received no significant updates since PHPCS 3.7.1. + * + * @see \PHP_CodeSniffer\Files\File::getTokensAsString() Original source. + * @see \PHPCSUtils\Utils\GetTokensAsString Related set of functions. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $start The position to start from in the token stack. + * @param int $length The length of tokens to traverse from the start pos. + * @param bool $origContent Whether the original content or the tab replaced + * content should be used. + * + * @return string The token contents. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified start position does not exist. + */ + public static function getTokensAsString(File $phpcsFile, $start, $length, $origContent = false) + { + return $phpcsFile->getTokensAsString($start, $length, $origContent); + } + + /** + * Returns the position of the first non-whitespace token in a statement. + * + * PHPCS cross-version compatible version of the `File::findStartOfStatement()` method. + * + * Changelog for the PHPCS native function: + * - Introduced in PHPCS 2.1.0. + * - The upstream method has received no significant updates since PHPCS 3.7.1. + * + * @see \PHP_CodeSniffer\Files\File::findStartOfStatement() Original source. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $start The position to start searching from in the token stack. + * @param int|string|array $ignore Token types that should not be considered stop points. + * + * @return int + */ + public static function findStartOfStatement(File $phpcsFile, $start, $ignore = null) + { + return $phpcsFile->findStartOfStatement($start, $ignore); + } + + /** + * Returns the position of the last non-whitespace token in a statement. + * + * PHPCS cross-version compatible version of the `File::findEndOfStatement() method. + * + * Changelog for the PHPCS native function: + * - Introduced in PHPCS 2.1.0. + * - The upstream method has received no significant updates since PHPCS 3.7.1. + * + * @see \PHP_CodeSniffer\Files\File::findEndOfStatement() Original source. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $start The position to start searching from in the token stack. + * @param int|string|array $ignore Token types that should not be considered stop points. + * + * @return int + */ + public static function findEndOfStatement(File $phpcsFile, $start, $ignore = null) + { + return $phpcsFile->findEndOfStatement($start, $ignore); + } + + /** + * Determine if the passed token has a condition of one of the passed types. + * + * PHPCS cross-version compatible version of the `File::hasCondition()` method. + * + * Changelog for the PHPCS native function: + * - Introduced in PHPCS 0.0.5. + * - The upstream method has received no significant updates since PHPCS 3.7.1. + * + * @see \PHP_CodeSniffer\Files\File::hasCondition() Original source. + * @see \PHPCSUtils\Utils\Conditions::hasCondition() PHPCSUtils native alternative. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the token we are checking. + * @param int|string|array $types The type(s) of tokens to search for. + * + * @return bool + */ + public static function hasCondition(File $phpcsFile, $stackPtr, $types) + { + return $phpcsFile->hasCondition($stackPtr, $types); + } + + /** + * Return the position of the condition for the passed token. + * + * PHPCS cross-version compatible version of the `File::getCondition()` method. + * + * Changelog for the PHPCS native function: + * - Introduced in PHPCS 1.3.0. + * - The upstream method has received no significant updates since PHPCS 3.7.1. + * + * @see \PHP_CodeSniffer\Files\File::getCondition() Original source. + * @see \PHPCSUtils\Utils\Conditions::getCondition() More versatile alternative. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the token we are checking. + * @param int|string $type The type of token to search for. + * @param bool $first If `true`, will return the matched condition + * furthest away from the passed token. + * If `false`, will return the matched condition + * closest to the passed token. + * + * @return int|false Integer stack pointer to the condition or `FALSE` if the token + * does not have the condition. + */ + public static function getCondition(File $phpcsFile, $stackPtr, $type, $first = true) + { + return $phpcsFile->getCondition($stackPtr, $type, $first); + } + + /** + * Returns the name of the class that the specified class extends. + * (works for classes, anonymous classes and interfaces) + * + * PHPCS cross-version compatible version of the `File::findExtendedClassName()` method. + * + * Changelog for the PHPCS native function: + * - Introduced in PHPCS 1.2.0. + * - The upstream method has received no significant updates since PHPCS 3.7.1. + * + * @see \PHP_CodeSniffer\Files\File::findExtendedClassName() Original source. + * @see \PHPCSUtils\Utils\ObjectDeclarations::findExtendedClassName() PHPCSUtils native improved version. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The stack position of the class or interface. + * + * @return string|false The extended class name or `FALSE` on error or if there + * is no extended class name. + */ + public static function findExtendedClassName(File $phpcsFile, $stackPtr) + { + return $phpcsFile->findExtendedClassName($stackPtr); + } + + /** + * Returns the names of the interfaces that the specified class or enum implements. + * + * PHPCS cross-version compatible version of the `File::findImplementedInterfaceNames()` method. + * + * Changelog for the PHPCS native function: + * - Introduced in PHPCS 2.7.0. + * - The upstream method has received no significant updates since PHPCS 3.7.1. + * + * @see \PHP_CodeSniffer\Files\File::findImplementedInterfaceNames() Original source. + * @see \PHPCSUtils\Utils\ObjectDeclarations::findImplementedInterfaceNames() PHPCSUtils native improved version. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The stack position of the class or enum token. + * + * @return array|false Array with names of the implemented interfaces or `FALSE` on + * error or if there are no implemented interface names. + */ + public static function findImplementedInterfaceNames(File $phpcsFile, $stackPtr) + { + return $phpcsFile->findImplementedInterfaceNames($stackPtr); + } +} diff --git a/PHPCSUtils/BackCompat/BCTokens.php b/PHPCSUtils/BackCompat/BCTokens.php new file mode 100644 index 00000000..78591164 --- /dev/null +++ b/PHPCSUtils/BackCompat/BCTokens.php @@ -0,0 +1,123 @@ + `PHPCSUtils\BackCompat\BCTokens::emptyTokens()` + * - `PHP_CodeSniffer\Util\Tokens::$operators` => `PHPCSUtils\BackCompat\BCTokens::operators()` + * - ... etc + * + * The order of the tokens in the arrays may differ between the PHPCS native token arrays and + * the token arrays returned by this class. + * + * @since 1.0.0 + * + * @method static array arithmeticTokens() Tokens that represent arithmetic operators. + * @method static array assignmentTokens() Tokens that represent assignments. + * @method static array blockOpeners() Tokens that open code blocks. + * @method static array booleanOperators() Tokens that perform boolean operations. + * @method static array bracketTokens() Tokens that represent brackets and parenthesis. + * @method static array castTokens() Tokens that represent type casting. + * @method static array commentTokens() Tokens that are comments. + * @method static array comparisonTokens() Tokens that represent comparison operator. + * @method static array contextSensitiveKeywords() Tokens representing context sensitive keywords in PHP. + * @method static array emptyTokens() Tokens that don't represent code. + * @method static array equalityTokens() Tokens that represent equality comparisons. + * @method static array heredocTokens() Tokens that make up a heredoc string. + * @method static array includeTokens() Tokens that include files. + * @method static array magicConstants() Tokens representing PHP magic constants. + * @method static array methodPrefixes() Tokens that can prefix a method name. + * @method static array ooScopeTokens() Tokens that open class and object scopes. + * @method static array operators() Tokens that perform operations. + * @method static array parenthesisOpeners() Token types that open parenthesis. + * @method static array phpcsCommentTokens() Tokens that are comments containing PHPCS instructions. + * @method static array scopeModifiers() Tokens that represent scope modifiers. + * @method static array scopeOpeners() Tokens that are allowed to open scopes. + * @method static array stringTokens() Tokens that represent strings. + * Note that `T_STRING`s are NOT represented in this list as this list + * is about _text_ strings. + * @method static array textStringTokens() Tokens that represent text strings. + */ +final class BCTokens +{ + + /** + * Handle calls to (undeclared) methods for token arrays which haven't received any + * changes since PHPCS 3.7.1. + * + * @since 1.0.0 + * + * @param string $name The name of the method which has been called. + * @param array $args Any arguments passed to the method. + * Unused as none of the methods take arguments. + * + * @return array => Token array + * + * @throws \PHPCSUtils\Exceptions\InvalidTokenArray When an invalid token array is requested. + */ + public static function __callStatic($name, $args) + { + if (isset(Tokens::${$name})) { + return Tokens::${$name}; + } + + // Unknown token array requested. + throw InvalidTokenArray::create($name); + } + + /** + * Tokens that represent the names of called functions. + * + * Retrieve the PHPCS function name tokens array in a cross-version compatible manner. + * + * Changelog for the PHPCS native array: + * - Introduced in PHPCS 2.3.3. + * - PHPCS 3.7.2: `T_PARENT` added to the array. + * - PHPCS 4.0.0: `T_NAME_QUALIFIED`, `T_NAME_FULLY_QUALIFIED` and `T_NAME_RELATIVE` added to the array. + * + * @see \PHP_CodeSniffer\Util\Tokens::$functionNameTokens Original array. + * + * @since 1.0.0 + * + * @return array => Token array. + */ + public static function functionNameTokens() + { + $tokens = Tokens::$functionNameTokens; + $tokens += Collections::ooHierarchyKeywords(); + $tokens += Collections::nameTokens(); + + return $tokens; + } +} diff --git a/PHPCSUtils/BackCompat/Helper.php b/PHPCSUtils/BackCompat/Helper.php new file mode 100644 index 00000000..c3a30257 --- /dev/null +++ b/PHPCSUtils/BackCompat/Helper.php @@ -0,0 +1,197 @@ +config` property should work + * in PHPCS 3.x and higher. + * + * @return bool Whether the setting of the data was successfull. + */ + public static function setConfigData($key, $value, $temp = false, $config = null) + { + if (isset($config) === true) { + // PHPCS 3.x and 4.x. + return $config->setConfigData($key, $value, $temp); + } + + if (\version_compare(self::getVersion(), '3.99.99', '>') === true) { + throw new RuntimeException('Passing the $config parameter is required in PHPCS 4.x'); + } + + // PHPCS 3.x. + return Config::setConfigData($key, $value, $temp); + } + + /** + * Get the value of a single PHP_CodeSniffer config key. + * + * @see Helper::getCommandLineData() Alternative for the same which is more reliable + * if the `$phpcsFile` object is available. + * + * @since 1.0.0 + * + * @param string $key The name of the config value. + * + * @return string|null + */ + public static function getConfigData($key) + { + return Config::getConfigData($key); + } + + /** + * Get the value of a CLI overrulable single PHP_CodeSniffer config key. + * + * Use this for config keys which can be set in the `CodeSniffer.conf` file, + * on the command-line or in a ruleset. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being processed. + * @param string $key The name of the config value. + * + * @return string|null + */ + public static function getCommandLineData(File $phpcsFile, $key) + { + if (isset($phpcsFile->config->{$key})) { + return $phpcsFile->config->{$key}; + } + + return null; + } + + /** + * Get the applicable tab width as passed to PHP_CodeSniffer from the + * command-line or the ruleset. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being processed. + * + * @return int Tab width. Defaults to the PHPCS native default of 4. + */ + public static function getTabWidth(File $phpcsFile) + { + $tabWidth = self::getCommandLineData($phpcsFile, 'tabWidth'); + if ($tabWidth > 0) { + return (int) $tabWidth; + } + + return self::DEFAULT_TABWIDTH; + } + + /** + * Get the applicable (file) encoding as passed to PHP_CodeSniffer from the + * command-line or the ruleset. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File|null $phpcsFile Optional. The current file being processed. + * + * @return string Encoding. Defaults to the PHPCS native default, which is 'utf-8' + * for PHPCS 3.x. + */ + public static function getEncoding(File $phpcsFile = null) + { + $default = 'utf-8'; + + if ($phpcsFile instanceof File) { + // Most reliable. + $encoding = self::getCommandLineData($phpcsFile, 'encoding'); + if ($encoding === null) { + $encoding = $default; + } + + return $encoding; + } + + // Less reliable. + $encoding = self::getConfigData('encoding'); + if ($encoding === null) { + $encoding = $default; + } + + return $encoding; + } + + /** + * Check whether the "--ignore-annotations" option is in effect. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File|null $phpcsFile Optional. The current file being processed. + * + * @return bool `TRUE` if annotations should be ignored, `FALSE` otherwise. + */ + public static function ignoreAnnotations(File $phpcsFile = null) + { + if (isset($phpcsFile, $phpcsFile->config->annotations)) { + return ! $phpcsFile->config->annotations; + } + + $annotations = Config::getConfigData('annotations'); + if (isset($annotations)) { + return ! $annotations; + } + + return false; + } +} diff --git a/PHPCSUtils/Exceptions/InvalidTokenArray.php b/PHPCSUtils/Exceptions/InvalidTokenArray.php new file mode 100644 index 00000000..8de6bd52 --- /dev/null +++ b/PHPCSUtils/Exceptions/InvalidTokenArray.php @@ -0,0 +1,44 @@ +getTokens(); + + /* + * Validate the received function input. + */ + + if (isset($tokens[$stackPtr], $tokens[$secondPtr]) === false + || $tokens[$stackPtr]['code'] === \T_WHITESPACE + || $tokens[$secondPtr]['code'] === \T_WHITESPACE + ) { + throw new RuntimeException('The $stackPtr and the $secondPtr token must exist and not be whitespace'); + } + + $expected = false; + if ($expectedSpaces === 'newline') { + $expected = $expectedSpaces; + } elseif (\is_int($expectedSpaces) === true && $expectedSpaces >= 0) { + $expected = $expectedSpaces; + } elseif (\is_string($expectedSpaces) === true && Numbers::isDecimalInt($expectedSpaces) === true) { + $expected = (int) $expectedSpaces; + } + + if ($expected === false) { + throw new RuntimeException( + 'The $expectedSpaces setting should be either "newline", 0 or a positive integer' + ); + } + + $ptrA = $stackPtr; + $ptrB = $secondPtr; + if ($stackPtr > $secondPtr) { + $ptrA = $secondPtr; + $ptrB = $stackPtr; + } + + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($ptrA + 1), null, true); + if ($nextNonEmpty < $ptrB) { + throw new RuntimeException( + 'The $stackPtr and the $secondPtr token must be adjacent tokens separated only' + . ' by whitespace and/or comments' + ); + } + + /* + * Determine how many spaces are between the two tokens. + */ + + $found = 0; + $foundPhrase = 'no spaces'; + if ($tokens[$ptrA]['line'] !== $tokens[$ptrB]['line']) { + $found = 'newline'; + $foundPhrase = 'a new line'; + if (($tokens[$ptrA]['line'] + 1) !== $tokens[$ptrB]['line']) { + $foundPhrase = 'multiple new lines'; + } + } elseif (($ptrA + 1) !== $ptrB) { + if ($tokens[($ptrA + 1)]['code'] === \T_WHITESPACE) { + $found = $tokens[($ptrA + 1)]['length']; + $foundPhrase = $found . (($found === 1) ? ' space' : ' spaces'); + } else { + $found = 'non-whitespace tokens'; + $foundPhrase = 'non-whitespace tokens'; + } + } + + if ($metricName !== '') { + $phpcsFile->recordMetric($stackPtr, $metricName, $foundPhrase); + } + + if ($found === $expected) { + return; + } + + /* + * Handle the violation message. + */ + + $expectedPhrase = 'no space'; + if ($expected === 'newline') { + $expectedPhrase = 'a new line'; + } elseif ($expected === 1) { + $expectedPhrase = $expected . ' space'; + } elseif ($expected > 1) { + $expectedPhrase = $expected . ' spaces'; + } + + $fixable = true; + $nextNonWhitespace = $phpcsFile->findNext(\T_WHITESPACE, ($ptrA + 1), null, true); + if ($nextNonWhitespace !== $ptrB) { + // Comment found between the tokens and we don't know where it should go, so don't auto-fix. + $fixable = false; + } + + if ($found === 'newline' + && $tokens[$ptrA]['code'] === \T_COMMENT + && \substr($tokens[$ptrA]['content'], -2) !== '*/' + ) { + /* + * $ptrA is a slash-style trailing comment, removing the new line would comment out + * the code, so don't auto-fix. + */ + $fixable = false; + } + + $method = 'add'; + $method .= ($fixable === true) ? 'Fixable' : ''; + $method .= ($errorType === 'error') ? 'Error' : 'Warning'; + + $recorded = $phpcsFile->$method( + $errorTemplate, + $stackPtr, + $errorCode, + [$expectedPhrase, $foundPhrase], + $errorSeverity + ); + + if ($fixable === false || $recorded === false) { + return; + } + + /* + * Fix the violation. + */ + + $phpcsFile->fixer->beginChangeset(); + + /* + * Remove existing whitespace. No need to check if it's whitespace as otherwise the fixer + * wouldn't have kicked in. + */ + for ($i = ($ptrA + 1); $i < $ptrB; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + // If necessary: add the correct amount whitespace. + if ($expected !== 0) { + if ($expected === 'newline') { + $phpcsFile->fixer->addContent($ptrA, $phpcsFile->eolChar); + } else { + $replacement = $tokens[$ptrA]['content'] . \str_repeat(' ', $expected); + $phpcsFile->fixer->replaceToken($ptrA, $replacement); + } + } + + $phpcsFile->fixer->endChangeset(); + } +} diff --git a/PHPCSUtils/Internal/Cache.php b/PHPCSUtils/Internal/Cache.php new file mode 100644 index 00000000..3843f03d --- /dev/null +++ b/PHPCSUtils/Internal/Cache.php @@ -0,0 +1,217 @@ +> Format: $cache[$loop][$fileName][$key][$id] = mixed $value; + */ + private static $cache = []; + + /** + * Check whether a result has been cached for a certain utility function. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param string $key The key to identify a particular set of results. + * It is recommended to pass __METHOD__ to this parameter. + * @param int|string $id Unique identifier for these results. + * Generally speaking this will be the $stackPtr passed + * to the utility function, but it can also something else, + * like a serialization of args passed to a function or an + * md5 hash of an input. + * + * @return bool + */ + public static function isCached(File $phpcsFile, $key, $id) + { + if (self::$enabled === false) { + return false; + } + + $fileName = $phpcsFile->getFilename(); + $loop = $phpcsFile->fixer->enabled === true ? $phpcsFile->fixer->loops : 0; + + return isset(self::$cache[$loop][$fileName][$key]) + && \array_key_exists($id, self::$cache[$loop][$fileName][$key]); + } + + /** + * Retrieve a previously cached result for a certain utility function. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param string $key The key to identify a particular set of results. + * It is recommended to pass __METHOD__ to this parameter. + * @param int|string $id Unique identifier for these results. + * Generally speaking this will be the $stackPtr passed + * to the utility function, but it can also something else, + * like a serialization of args passed to a function or an + * md5 hash of an input. + * + * @return mixed + */ + public static function get(File $phpcsFile, $key, $id) + { + if (self::$enabled === false) { + return null; + } + + $fileName = $phpcsFile->getFilename(); + $loop = $phpcsFile->fixer->enabled === true ? $phpcsFile->fixer->loops : 0; + + if (isset(self::$cache[$loop][$fileName][$key]) + && \array_key_exists($id, self::$cache[$loop][$fileName][$key]) + ) { + return self::$cache[$loop][$fileName][$key][$id]; + } + + return null; + } + + /** + * Retrieve all previously cached results for a certain utility function and a certain file. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param string $key The key to identify a particular set of results. + * It is recommended to pass __METHOD__ to this parameter. + * + * @return array + */ + public static function getForFile(File $phpcsFile, $key) + { + if (self::$enabled === false) { + return []; + } + + $fileName = $phpcsFile->getFilename(); + $loop = $phpcsFile->fixer->enabled === true ? $phpcsFile->fixer->loops : 0; + + if (isset(self::$cache[$loop][$fileName]) + && \array_key_exists($key, self::$cache[$loop][$fileName]) + ) { + return self::$cache[$loop][$fileName][$key]; + } + + return []; + } + + /** + * Cache the result for a certain utility function. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param string $key The key to identify a particular set of results. + * It is recommended to pass __METHOD__ to this parameter. + * @param int|string $id Unique identifier for these results. + * Generally speaking this will be the $stackPtr passed + * to the utility function, but it can also something else, + * like a serialization of args passed to a function or an + * md5 hash of an input. + * @param mixed $value An arbitrary value to write to the cache. + * + * @return mixed + */ + public static function set(File $phpcsFile, $key, $id, $value) + { + if (self::$enabled === false) { + return; + } + + $fileName = $phpcsFile->getFilename(); + $loop = $phpcsFile->fixer->enabled === true ? $phpcsFile->fixer->loops : 0; + + /* + * If this is a phpcbf run and we've reached the next loop, clear the cache + * of all previous loops to free up memory. + */ + if (isset(self::$cache[$loop]) === false + && empty(self::$cache) === false + ) { + self::clear(); + } + + self::$cache[$loop][$fileName][$key][$id] = $value; + } + + /** + * Clear the cache. + * + * @since 1.0.0 + * + * @return void + */ + public static function clear() + { + self::$cache = []; + } +} diff --git a/PHPCSUtils/Internal/IsShortArrayOrList.php b/PHPCSUtils/Internal/IsShortArrayOrList.php new file mode 100644 index 00000000..a693754a --- /dev/null +++ b/PHPCSUtils/Internal/IsShortArrayOrList.php @@ -0,0 +1,700 @@ + => + */ + private $openBrackets; + + /** + * Constructor. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the short array opener token. + * + * @return void + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the token passed is not one of the + * accepted types or doesn't exist. + */ + public function __construct(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + $openBrackets = Collections::shortArrayListOpenTokensBC(); + + if (isset($tokens[$stackPtr]) === false + || isset($openBrackets[$tokens[$stackPtr]['code']]) === false + ) { + throw new RuntimeException( + 'The IsShortArrayOrList class expects to be passed a T_OPEN_SHORT_ARRAY or T_OPEN_SQUARE_BRACKET token.' + ); + } + + $this->phpcsFile = $phpcsFile; + $this->tokens = $tokens; + $this->opener = $stackPtr; + + $this->closer = $stackPtr; + if (isset($this->tokens[$stackPtr]['bracket_closer'])) { + $this->closer = $this->tokens[$stackPtr]['bracket_closer']; + } + + $this->beforeOpener = $this->phpcsFile->findPrevious(Tokens::$emptyTokens, ($this->opener - 1), null, true); + $this->afterCloser = $this->phpcsFile->findNext(Tokens::$emptyTokens, ($this->closer + 1), null, true); + + $this->phpcsVersion = Helper::getVersion(); + $this->openBrackets = $openBrackets; + } + + /** + * Determine whether the bracket is a short array, short list or real square bracket. + * + * @since 1.0.0 + * + * @return string Either 'short array', 'short list' or 'square brackets'. + */ + public function solve() + { + if ($this->isSquareBracket() === true) { + return self::SQUARE_BRACKETS; + } + + if ($this->afterCloser === false) { + // Live coding. Short array until told differently. + return self::SHORT_ARRAY; + } + + // If the bracket closer is followed by an equals sign, it's always a short list. + if ($this->tokens[$this->afterCloser]['code'] === \T_EQUAL) { + return self::SHORT_LIST; + } + + // Attributes can only contain constant expressions, i.e. lists not allowed. + if (Context::inAttribute($this->phpcsFile, $this->opener) === true) { + return self::SHORT_ARRAY; + } + + $type = $this->isInForeach(); + if ($type !== false) { + return $type; + } + + /* + * Check if this can be a nested set of brackets used as a value. + * That's the only "confusing" syntax left. In all other cases, it will be a short array. + */ + $hasRiskyTokenBeforeOpener = false; + if (isset($this->openBrackets[$this->tokens[$this->beforeOpener]['code']]) === true + || $this->tokens[$this->beforeOpener]['code'] === \T_COMMA + || $this->tokens[$this->beforeOpener]['code'] === \T_DOUBLE_ARROW + ) { + $hasRiskyTokenBeforeOpener = true; + } + + $hasRiskyTokenAfterCloser = false; + if ($this->tokens[$this->afterCloser]['code'] === \T_COMMA + || $this->tokens[$this->afterCloser]['code'] === \T_CLOSE_SHORT_ARRAY + || $this->tokens[$this->afterCloser]['code'] === \T_CLOSE_SQUARE_BRACKET + ) { + $hasRiskyTokenAfterCloser = true; + } + + if ($hasRiskyTokenBeforeOpener === false || $hasRiskyTokenAfterCloser === false) { + return self::SHORT_ARRAY; + } + + /* + * Check if this is the first/last item in a "parent" set of brackets. + * If so, skip straight to the parent and determine the type of that, the type + * of the inner set of brackets will be the same (as all other options have + * already been eliminated). + */ + if (isset($this->openBrackets[$this->tokens[$this->beforeOpener]['code']]) === true) { + return IsShortArrayOrListWithCache::getType($this->phpcsFile, $this->beforeOpener); + } + + $nextEffectiveAfterCloser = $this->afterCloser; + if ($this->tokens[$this->afterCloser]['code'] === \T_COMMA) { + // Skip over potential trailing commas. + $nextEffectiveAfterCloser = $this->phpcsFile->findNext( + Tokens::$emptyTokens, + ($this->afterCloser + 1), + null, + true + ); + } + + if ($this->tokens[$nextEffectiveAfterCloser]['code'] === \T_CLOSE_SHORT_ARRAY + || $this->tokens[$nextEffectiveAfterCloser]['code'] === \T_CLOSE_SQUARE_BRACKET + ) { + return IsShortArrayOrListWithCache::getType($this->phpcsFile, $nextEffectiveAfterCloser); + } + + /* + * Okay, so as of here, we know this set of brackets is preceded by a comma or double arrow + * and followed by a comma. This is the only ambiguous syntax left. + */ + + /* + * Check if this could be a (nested) short list at all. + * A list must have at least one variable inside and can not be empty. + * An array, however, cannot contain empty items. + */ + $type = $this->walkInside($this->opener); + if ($type !== false) { + return $type; + } + + // Last resort: walk up in the file to see if we can find a set of parent brackets... + $type = $this->walkOutside(); + if ($type !== false) { + return $type; + } + + // If everything failed, this will be a short array (shouldn't be possible). + return self::SHORT_ARRAY; // @codeCoverageIgnore + } + + /** + * Check if the brackets are in actual fact real square brackets. + * + * @since 1.0.0 + * + * @return bool TRUE if these are real square brackets; FALSE otherwise. + */ + private function isSquareBracket() + { + if ($this->opener === $this->closer) { + // Parse error (unclosed bracket) or live coding. Bow out. + return true; + } + + // Check if this is a bracket we need to examine or a mistokenization. + return ($this->isShortArrayBracket() === false); + } + + /** + * Verify that the current set of brackets is not affected by known PHPCS cross-version tokenizer issues. + * + * List of current tokenizer issues which affect the short array/short list tokenization: + * - {@link https://github.com/squizlabs/PHP_CodeSniffer/pull/3632 PHPCS#3632} (PHPCS < 3.7.2) + * + * List of previous tokenizer issues which affected the short array/short list tokenization for reference: + * - {@link https://github.com/squizlabs/PHP_CodeSniffer/issues/1284 PHPCS#1284} (PHPCS < 2.8.1) + * - {@link https://github.com/squizlabs/PHP_CodeSniffer/issues/1381 PHPCS#1381} (PHPCS < 2.9.0) + * - {@link https://github.com/squizlabs/PHP_CodeSniffer/issues/1971 PHPCS#1971} (PHPCS 2.8.0 - 3.2.3) + * - {@link https://github.com/squizlabs/PHP_CodeSniffer/pull/3013 PHPCS#3013} (PHPCS < 3.5.6) + * - {@link https://github.com/squizlabs/PHP_CodeSniffer/pull/3172 PHPCS#3172} (PHPCS < 3.6.0) + * + * @since 1.0.0 + * + * @return bool TRUE if this is actually a short array bracket which needs to be examined, + * FALSE if it is an (incorrectly tokenized) square bracket. + */ + private function isShortArrayBracket() + { + if ($this->tokens[$this->opener]['code'] === \T_OPEN_SQUARE_BRACKET) { + if (\version_compare($this->phpcsVersion, '3.7.2', '>=') === true) { + // These will just be properly tokenized, plain square brackets. No need for further checks. + return false; + } + + /* + * BC: Work around a bug in the tokenizer of PHPCS < 3.7.2, where a `[` would be + * tokenized as T_OPEN_SQUARE_BRACKET instead of T_OPEN_SHORT_ARRAY if it was + * preceded by the close parenthesis of a non-braced control structure. + * + * @link https://github.com/squizlabs/PHP_CodeSniffer/issues/3632 + */ + if ($this->tokens[$this->beforeOpener]['code'] === \T_CLOSE_PARENTHESIS + && isset($this->tokens[$this->beforeOpener]['parenthesis_owner']) === true + // phpcs:ignore Generic.Files.LineLength.TooLong + && isset(Tokens::$scopeOpeners[$this->tokens[$this->tokens[$this->beforeOpener]['parenthesis_owner']]['code']]) === true + ) { + return true; + } + + // These are really just plain square brackets. + return false; + } + + return true; + } + + /** + * Check is this set of brackets is used within a foreach expression. + * + * @since 1.0.0 + * + * @return string|false The determined type or FALSE if undetermined. + */ + private function isInForeach() + { + $inForeach = Context::inForeachCondition($this->phpcsFile, $this->opener); + if ($inForeach === false) { + return false; + } + + switch ($inForeach) { + case 'beforeAs': + if ($this->tokens[$this->afterCloser]['code'] === \T_AS) { + return self::SHORT_ARRAY; + } + + break; + + case 'afterAs': + if ($this->tokens[$this->afterCloser]['code'] === \T_CLOSE_PARENTHESIS) { + $owner = Parentheses::getOwner($this->phpcsFile, $this->afterCloser); + if ($owner !== false && $this->tokens[$owner]['code'] === \T_FOREACH) { + return self::SHORT_LIST; + } + } + + break; + } + + /* + * Everything else will be a nested set of brackets (provided we're talking valid PHP), + * so disregard as it can not be determined yet. + */ + return false; + } + + /** + * Walk the first part of the contents between the brackets to see if we can determine if this + * is a short array or short list based on its contents. + * + * Short lists can only have another (nested) list or variable assignments, including property assignments + * and array index assignment, as the value inside the brackets. + * + * This won't walk the complete contents as that could be a huge performance drain. Just the first x items. + * + * @since 1.0.0 + * + * @param int $opener The position of the short array open bracket token. + * @param int $recursions Optional. Keep track of how often we've recursed into this methd. + * Prevent infinite loops for extremely deeply nested arrays. + * Defaults to 0. + * + * @return string|false The determined type or FALSE if undetermined. + */ + private function walkInside($opener, $recursions = 0) + { + // Get the first 5 "parameters" and ignore the "is short array" check. + $items = PassedParameters::getParameters($this->phpcsFile, $opener, self::ITEM_LIMIT, true); + + if ($items === []) { + /* + * A list can not be empty, so this must be an array, however as this is a nested + * set of brackets, let the outside brackets be the decider as it may be + * a coding error which a sniff needs to flag. + */ + return false; + } + + // Make sure vars assigned by reference are handled correctly. + $skip = Tokens::$emptyTokens; + $skip[] = \T_BITWISE_AND; + + $skipNames = Collections::namespacedNameTokens() + Collections::ooHierarchyKeywords(); + + foreach ($items as $item) { + /* + * If we encounter a completely empty item, this must be a short list as arrays cannot contain + * empty items. + */ + if ($item['raw'] === '') { + return self::SHORT_LIST; + } + + /* + * If the "value" part of the entry doesn't start with a variable, a (nested) short list/array, + * or a static property assignment, we know for sure that the outside brackets will be an array. + */ + $arrow = Arrays::getDoubleArrowPtr($this->phpcsFile, $item['start'], $item['end']); + if ($arrow === false) { + $firstNonEmptyInValue = $this->phpcsFile->findNext($skip, $item['start'], ($item['end'] + 1), true); + } else { + $firstNonEmptyInValue = $this->phpcsFile->findNext($skip, ($arrow + 1), ($item['end'] + 1), true); + } + + if ($this->tokens[$firstNonEmptyInValue]['code'] !== \T_VARIABLE + && isset(Collections::namespacedNameTokens()[$this->tokens[$firstNonEmptyInValue]['code']]) === false + && isset(Collections::ooHierarchyKeywords()[$this->tokens[$firstNonEmptyInValue]['code']]) === false + && isset($this->openBrackets[$this->tokens[$firstNonEmptyInValue]['code']]) === false + ) { + return self::SHORT_ARRAY; + } + + /* + * Check if this is a potential list assignment to a static variable. + * If not, again, we can be sure it will be a short array. + */ + if (isset(Collections::namespacedNameTokens()[$this->tokens[$firstNonEmptyInValue]['code']]) === true + || isset(Collections::ooHierarchyKeywords()[$this->tokens[$firstNonEmptyInValue]['code']]) === true + ) { + $nextAfter = $this->phpcsFile->findNext($skipNames, ($firstNonEmptyInValue + 1), null, true); + + if ($this->tokens[$nextAfter]['code'] !== \T_DOUBLE_COLON) { + return self::SHORT_ARRAY; + } else { + /* + * Double colon, so make sure there is a variable after it. + * If not, it's constant or function call, i.e. a short array. + */ + $nextNextAfter = $this->phpcsFile->findNext(Tokens::$emptyTokens, ($nextAfter + 1), null, true); + if ($this->tokens[$nextNextAfter]['code'] !== \T_VARIABLE) { + return self::SHORT_ARRAY; + } + } + + continue; + } + + if (isset($this->openBrackets[$this->tokens[$firstNonEmptyInValue]['code']]) === true) { + /* + * If the "value" part starts with an open bracket, but has other tokens after it, the current, + * outside set of brackets will always be an array (the brackets in the value can still be both, + * but that's not the concern of the current determination). + */ + $lastNonEmptyInValue = $this->phpcsFile->findPrevious( + Tokens::$emptyTokens, + $item['end'], + $item['start'], + true + ); + if (isset($this->tokens[$firstNonEmptyInValue]['bracket_closer']) === true + && $this->tokens[$firstNonEmptyInValue]['bracket_closer'] !== $lastNonEmptyInValue + ) { + return self::SHORT_ARRAY; + } + + /* + * Recursively check the inner set of brackets for contents indicating this is not a short list. + */ + if ($recursions < self::RECURSION_LIMIT) { + $innerType = $this->walkInside($firstNonEmptyInValue, ($recursions + 1)); + if ($innerType !== false) { + return $innerType; + } + } + } + } + + // Undetermined. + return false; + } + + /** + * Walk up in the file to try and find an "outer" set of brackets for an ambiguous, potentially + * nested set of brackets. + * + * This should really be the last resort, if all else fails to determine the type of the brackets. + * + * @since 1.0.0 + * + * @return string|false The determined type or FALSE if undetermined. + */ + private function walkOutside() + { + $stopPoints = Collections::phpOpenTags(); + $stopPoints[\T_SEMICOLON] = \T_SEMICOLON; + + for ($i = ($this->opener - 1); $i >= 0; $i--) { + // Skip over block comments (just in case). + if ($this->tokens[$i]['code'] === \T_DOC_COMMENT_CLOSE_TAG) { + $i = $this->tokens[$i]['comment_opener']; + continue; + } + + if (isset(Tokens::$emptyTokens[$this->tokens[$i]['code']]) === true) { + continue; + } + + // Stop on an end of statement. + if (isset($stopPoints[$this->tokens[$i]['code']]) === true) { + // End of previous statement or start of document. + return self::SHORT_ARRAY; + } + + if (isset($this->tokens[$i]['scope_opener'], $this->tokens[$i]['scope_closer']) === true) { + if ($i === $this->tokens[$i]['scope_opener'] + && $this->tokens[$i]['scope_closer'] > $this->closer + ) { + // Found a scope wrapping this set of brackets before finding a outer set of brackets. + // This will be a short array. + return self::SHORT_ARRAY; + } + + if ($i === $this->tokens[$i]['scope_closer'] + && isset($this->tokens[$i]['scope_condition']) === true + ) { + $i = $this->tokens[$i]['scope_condition']; + continue; + } + + // Scope opener without scope condition shouldn't be possible, but just in case. + // @codeCoverageIgnoreStart + $i = $this->tokens[$i]['scope_opener']; + continue; + // @codeCoverageIgnoreEnd + } + + if (isset($this->tokens[$i]['parenthesis_opener'], $this->tokens[$i]['parenthesis_closer']) === true) { + if ($i === $this->tokens[$i]['parenthesis_opener'] + && $this->tokens[$i]['parenthesis_closer'] > $this->closer + ) { + // Found parentheses wrapping this set of brackets before finding a outer set of brackets. + // This will be a short array. + return self::SHORT_ARRAY; + } + + if ($i === $this->tokens[$i]['parenthesis_closer']) { + if (isset($this->tokens[$i]['parenthesis_owner']) === true) { + $i = $this->tokens[$i]['parenthesis_owner']; + continue; + } + } + + // Parenthesis closer without owner (function call and such). + $i = $this->tokens[$i]['parenthesis_opener']; + continue; + } + + /* + * Skip over attributes. + * No special handling needed, brackets within attributes won't reach this + * method as they are already handled within the solve() method. + */ + if (isset($this->tokens[$i]['attribute_opener'], $this->tokens[$i]['attribute_closer']) === true + && $i === $this->tokens[$i]['attribute_closer'] + ) { + $i = $this->tokens[$i]['attribute_opener']; + continue; + } + + /* + * This is a close bracket, but it's not the outer wrapper. + * As we skip over parentheses and curlies above, we *know* this will be a + * set of brackets at the same bracket "nesting level" as the set we are examining. + */ + if (isset($this->tokens[$i]['bracket_opener'], $this->tokens[$i]['bracket_closer']) === true + && $i === $this->tokens[$i]['bracket_closer'] + ) { + /* + * Now, if the set of brackets follows the same code pattern (comma or double arrow before, + * comma after), this will be an adjacent set of potentially nested brackets. + * If so, check if the type of the previous set of brackets has already been determined + * as adjacent sets of brackets will have the same type. + */ + $adjOpener = $this->tokens[$i]['bracket_opener']; + $prevNonEmpty = $this->phpcsFile->findPrevious(Tokens::$emptyTokens, ($adjOpener - 1), null, true); + $nextNonEmpty = $this->phpcsFile->findNext(Tokens::$emptyTokens, ($i + 1), null, true); + + if ($this->tokens[$prevNonEmpty]['code'] === $this->tokens[$this->beforeOpener]['code'] + && $this->tokens[$nextNonEmpty]['code'] === $this->tokens[$this->afterCloser]['code'] + && Cache::isCached($this->phpcsFile, IsShortArrayOrListWithCache::CACHE_KEY, $adjOpener) === true + ) { + return Cache::get($this->phpcsFile, IsShortArrayOrListWithCache::CACHE_KEY, $adjOpener); + } + + // If not, skip over it. + $i = $this->tokens[$i]['bracket_opener']; + continue; + } + + // Open bracket. + if (isset($this->openBrackets[$this->tokens[$i]['code']]) === true) { + if (isset($this->tokens[$i]['bracket_closer']) === false + || $this->tokens[$i]['code'] === \T_OPEN_SQUARE_BRACKET + ) { + /* + * If the type of the unclosed "outer" brackets cannot be determined or + * they are identified as plain square brackets, the inner brackets + * we are examining should be regarded as a short array. + */ + return self::SHORT_ARRAY; + } + + if ($this->tokens[$i]['bracket_closer'] > $this->closer) { + // This is one we have to examine further as an outer set of brackets. + // As all the other checks have already failed to get a result, we know that + // whatever the outer set is, the inner set will be the same. + return IsShortArrayOrListWithCache::getType($this->phpcsFile, $i); + } + } + } + + // Reached the start of the file without finding an outer set of brackets. + // Shouldn't be possible, but just in case. + return false; // @codeCoverageIgnore + } +} diff --git a/PHPCSUtils/Internal/IsShortArrayOrListWithCache.php b/PHPCSUtils/Internal/IsShortArrayOrListWithCache.php new file mode 100644 index 00000000..e004b854 --- /dev/null +++ b/PHPCSUtils/Internal/IsShortArrayOrListWithCache.php @@ -0,0 +1,269 @@ +process(); + } + + /** + * Constructor. + * + * @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 void + */ + private function __construct(File $phpcsFile, $stackPtr) + { + $this->phpcsFile = $phpcsFile; + $this->tokens = $phpcsFile->getTokens(); + $this->stackPtr = $stackPtr; + } + + /** + * Determine whether a T_[OPEN|CLOSE}_[SHORT_ARRAY|SQUARE_BRACKET] token is a short array + * or short list construct using previously cached results whenever possible. + * + * @since 1.0.0 + * + * @return string|false The type of construct this bracket was determined to be. + * Either 'short array', 'short list' or 'square brackets'. + * Or FALSE is this was not a bracket token. + */ + private function process() + { + if ($this->isValidStackPtr() === false) { + return false; + } + + $this->opener = $this->getOpener(); + + /* + * Check the cache in case we've seen this token before. + */ + $type = $this->getFromCache(); + if ($type !== false) { + return $type; + } + + /* + * If we've not seen the token before, try and solve it and cache the results. + * + * Make sure to safeguard against unopened/unclosed square brackets (parse error), + * which should always be regarded as real square brackets. + */ + $type = IsShortArrayOrList::SQUARE_BRACKETS; + if (isset($this->tokens[$this->stackPtr]['bracket_opener'], $this->tokens[$this->stackPtr]['bracket_closer'])) { + $solver = new IsShortArrayOrList($this->phpcsFile, $this->opener); + $type = $solver->solve(); + } + + $this->updateCache($type); + + return $type; + } + + /** + * Verify the passed token could potentially be a short array or short list token. + * + * @since 1.0.0 + * + * @return bool + */ + private function isValidStackPtr() + { + return (isset($this->tokens[$this->stackPtr]) === true + && isset(Collections::shortArrayTokensBC()[$this->tokens[$this->stackPtr]['code']]) === true); + } + + /** + * Get the stack pointer to the short array/list opener. + * + * @since 1.0.0 + * + * @return int + */ + private function getOpener() + { + $opener = $this->stackPtr; + if (isset($this->tokens[$this->stackPtr]['bracket_opener'])) { + $opener = $this->tokens[$this->stackPtr]['bracket_opener']; + } + + return $opener; + } + + /** + * Retrieve the bracket "type" of a token from the cache. + * + * @since 1.0.0 + * + * @return string|false The previously determined type (which could be an empty string) + * or FALSE if no cache entry was found for this token. + */ + private function getFromCache() + { + if (Cache::isCached($this->phpcsFile, self::CACHE_KEY, $this->opener) === true) { + return Cache::get($this->phpcsFile, self::CACHE_KEY, $this->opener); + } + + return false; + } + + /** + * Update the cache with information about a particular bracket token. + * + * @since 1.0.0 + * + * @param string $type The type this bracket has been determined to be. + * Either 'short array', 'short list' or 'square brackets'. + * + * @return void + */ + private function updateCache($type) + { + Cache::set($this->phpcsFile, self::CACHE_KEY, $this->opener, $type); + } +} diff --git a/PHPCSUtils/Internal/NoFileCache.php b/PHPCSUtils/Internal/NoFileCache.php new file mode 100644 index 00000000..6e7d08dd --- /dev/null +++ b/PHPCSUtils/Internal/NoFileCache.php @@ -0,0 +1,164 @@ +> Format: $cache[$key][$id] = mixed $value; + */ + private static $cache = []; + + /** + * Check whether a result has been cached for a certain utility function. + * + * @since 1.0.0 + * + * @param string $key The key to identify a particular set of results. + * It is recommended to pass `__METHOD__` to this parameter. + * @param int|string $id Unique identifier for these results. + * It is recommended for this to be a serialization of arguments passed + * to the function or an md5 hash of an input. + * + * @return bool + */ + public static function isCached($key, $id) + { + return self::$enabled === true && isset(self::$cache[$key]) && \array_key_exists($id, self::$cache[$key]); + } + + /** + * Retrieve a previously cached result for a certain utility function. + * + * @since 1.0.0 + * + * @param string $key The key to identify a particular set of results. + * It is recommended to pass `__METHOD__` to this parameter. + * @param int|string $id Unique identifier for these results. + * It is recommended for this to be a serialization of arguments passed + * to the function or an md5 hash of an input. + * + * @return mixed + */ + public static function get($key, $id) + { + if (self::$enabled === true && isset(self::$cache[$key]) && \array_key_exists($id, self::$cache[$key])) { + return self::$cache[$key][$id]; + } + + return null; + } + + /** + * Retrieve all previously cached results for a certain utility function. + * + * @since 1.0.0 + * + * @param string $key The key to identify a particular set of results. + * It is recommended to pass `__METHOD__` to this parameter. + * + * @return array + */ + public static function getForKey($key) + { + if (self::$enabled === true && \array_key_exists($key, self::$cache)) { + return self::$cache[$key]; + } + + return []; + } + + /** + * Cache the result for a certain utility function. + * + * @since 1.0.0 + * + * @param string $key The key to identify a particular set of results. + * It is recommended to pass `__METHOD__` to this parameter. + * @param int|string $id Unique identifier for these results. + * It is recommended for this to be a serialization of arguments passed + * to the function or an md5 hash of an input. + * @param mixed $value An arbitrary value to write to the cache. + * + * @return mixed + */ + public static function set($key, $id, $value) + { + if (self::$enabled === false) { + return; + } + + self::$cache[$key][$id] = $value; + } + + /** + * Clear the cache. + * + * @since 1.0.0 + * + * @return void + */ + public static function clear() + { + self::$cache = []; + } +} diff --git a/PHPCSUtils/TestUtils/UtilityMethodTestCase.php b/PHPCSUtils/TestUtils/UtilityMethodTestCase.php new file mode 100644 index 00000000..fcc1e75a --- /dev/null +++ b/PHPCSUtils/TestUtils/UtilityMethodTestCase.php @@ -0,0 +1,419 @@ +getTargetToken($commentString, [\T_TOKEN_CONSTANT, \T_ANOTHER_TOKEN]); + * $class = new ClassUnderTest(); + * $result = $class->MyMethod(self::$phpcsFile, $stackPtr); + * // Or for static utility methods: + * $result = ClassUnderTest::MyMethod(self::$phpcsFile, $stackPtr); + * + * $this->assertSame($expected, $result); + * } + * + * /** + * * Data Provider. + * * + * * @see ClassUnderTestUnitTest::testMyMethod() For the array format. + * * + * * @return array + * * / + * public function dataMyMethod() + * { + * return array( + * array('/* testTestCaseDescription * /', false), + * ); + * } + * } + * ``` + * + * Note: + * - Remove the space between the comment closers `* /` for a working example. + * - Each test case separator comment MUST start with `/* test`. + * This is to allow the {@see UtilityMethodTestCase::getTargetToken()} method to + * distinquish between the test separation comments and comments which may be part + * of the test case. + * - The test case file and unit test file should be placed in the same directory. + * - For working examples using this abstract class, have a look at the unit tests + * for the PHPCSUtils utility functions themselves. + * + * @since 1.0.0 + */ +abstract class UtilityMethodTestCase extends TestCase +{ + + /** + * The PHPCS version the tests are being run on. + * + * @since 1.0.0 + * + * @var string + */ + protected static $phpcsVersion = '0'; + + /** + * The file extension of the test case file (without leading dot). + * + * This allows concrete test classes to overrule the default `"inc"` with, for instance, + * `"js"` or `"css"` when applicable. + * + * @since 1.0.0 + * + * @var string + */ + protected static $fileExtension = 'inc'; + + /** + * Full path to the test case file associated with the concrete test class. + * + * Optional. If left empty, the case file will be presumed to be in + * the same directory and named the same as the test class, but with an + * `"inc"` file extension. + * + * @since 1.0.0 + * + * @var string + */ + protected static $caseFile = ''; + + /** + * The tab width setting to use when tokenizing the file. + * + * This allows for test case files to use a different tab width than the default. + * + * @since 1.0.0 + * + * @var int + */ + protected static $tabWidth = 4; + + /** + * The \PHP_CodeSniffer\Files\File object containing the parsed contents of the test case file. + * + * @since 1.0.0 + * + * @var \PHP_CodeSniffer\Files\File + */ + protected static $phpcsFile; + + /** + * Set the name of a sniff to pass to PHPCS to limit the run (and force it to record errors). + * + * Normally, this propery won't need to be overloaded, but for utility methods which record + * violations and contain fixers, setting a dummy sniff name equal to the sniff name passed + * in the error code for `addError()`/`addWarning()` during the test, will allow for testing + * the recording of these violations, as well as testing the fixer. + * + * @since 1.0.0 + * + * @var array + */ + protected static $selectedSniff = ['Dummy.Dummy.Dummy']; + + /** + * Initialize PHPCS & tokenize the test case file. + * + * The test case file for a unit test class has to be in the same directory + * directory and use the same file name as the test class, using the `.inc` extension + * or be explicitly set using the {@see UtilityMethodTestCase::$fileExtension}/ + * {@see UtilityMethodTestCase::$caseFile} properties. + * + * Note: This is a PHPUnit cross-version compatible {@see \PHPUnit\Framework\TestCase::setUpBeforeClass()} + * method. + * + * @since 1.0.0 + * + * @beforeClass + * + * @return void + */ + public static function setUpTestFile() + { + parent::setUpBeforeClass(); + + self::$phpcsVersion = Helper::getVersion(); + + $caseFile = static::$caseFile; + if (\is_string($caseFile) === false || $caseFile === '') { + $testClass = \get_called_class(); + $testFile = (new ReflectionClass($testClass))->getFileName(); + $caseFile = \substr($testFile, 0, -3) . static::$fileExtension; + } + + if (\is_readable($caseFile) === false) { + parent::fail("Test case file missing. Expected case file location: $caseFile"); + } + + $contents = \file_get_contents($caseFile); + + $config = new \PHP_CodeSniffer\Config(); + + /* + * We just need to provide a standard so PHPCS will tokenize the file. + * The standard itself doesn't actually matter for testing utility methods, + * so use the smallest one to get the fastest results. + */ + $config->standards = ['PSR1']; + + /* + * Limiting the run to just one sniff will make it, yet again, slightly faster. + * Picked the simplest/fastest sniff available which is registered in PSR1. + */ + $config->sniffs = static::$selectedSniff; + + // Disable caching. + $config->cache = false; + + // Also set a tab-width to enable testing tab-replaced vs `orig_content`. + $config->tabWidth = static::$tabWidth; + + $ruleset = new \PHP_CodeSniffer\Ruleset($config); + + // Make sure the file gets parsed correctly based on the file type. + $contents = 'phpcs_input_file: ' . $caseFile . \PHP_EOL . $contents; + + self::$phpcsFile = new \PHP_CodeSniffer\Files\DummyFile($contents, $ruleset, $config); + + // Only tokenize the file, do not process it. + try { + self::$phpcsFile->parse(); + } catch (TokenizerException $e) { + // PHPCS 3.5.0 and higher. This is handled below. + } + + // Fail the test if the case file failed to tokenize. + if (self::$phpcsFile->numTokens === 0) { + parent::fail("Tokenizing of the test case file failed for case file: $caseFile"); + } + } + + /** + * Skip JS and CSS related tests on PHPCS 4.x. + * + * PHPCS 4.x drops support for the JS and CSS tokenizers. + * This method takes care of automatically skipping tests involving JS/CSS case files + * when the tests are being run with PHPCS 4.x. + * + * Note: This is a PHPUnit cross-version compatible {@see \PHPUnit\Framework\TestCase::setUp()} + * method. + * + * @since 1.0.0 + * + * @before + * + * @return void + */ + public function skipJSCSSTestsOnPHPCS4() + { + if (static::$fileExtension !== 'js' && static::$fileExtension !== 'css') { + return; + } + + if (\version_compare(self::$phpcsVersion, '3.99.99', '<=')) { + return; + } + + $this->markTestSkipped('JS and CSS support has been removed in PHPCS 4.'); + } + + /** + * Clean up after finished test by resetting all static properties to their default values. + * + * Note: This is a PHPUnit cross-version compatible {@see \PHPUnit\Framework\TestCase::tearDownAfterClass()} + * method. + * + * @since 1.0.0 + * + * @afterClass + * + * @return void + */ + public static function resetTestFile() + { + self::$phpcsVersion = '0'; + self::$fileExtension = 'inc'; + self::$caseFile = ''; + self::$tabWidth = 4; + self::$phpcsFile = null; + self::$selectedSniff = ['Dummy.Dummy.Dummy']; + } + + /** + * Check whether or not the PHP 8.0 identifier name tokens will be in use. + * + * The expected token positions/token counts for certain tokens will differ depending + * on whether the PHP 8.0 identifier name tokenization is used or the PHP < 8.0 + * identifier name tokenization. + * + * Tests can use this method to determine which flavour of tokenization to expect and + * to set test expectations accordingly. + * + * @codeCoverageIgnore Nothing to test. + * + * @since 1.0.0 + * + * @return bool + */ + public static function usesPhp8NameTokens() + { + return \version_compare(Helper::getVersion(), '3.99.99', '>='); + } + + /** + * Get the token pointer for a target token based on a specific comment. + * + * Note: the test delimiter comment MUST start with `/* test` to allow this function to + * distinguish between comments used *in* a test and test delimiters. + * + * @since 1.0.0 + * + * @param string $commentString The complete delimiter comment to look for as a string. + * This string should include the comment opener and closer. + * @param int|string|array $tokenType The type of token(s) to look for. + * @param string $tokenContent Optional. The token content for the target token. + * + * @return int + * + * @throws \PHPCSUtils\Exceptions\TestMarkerNotFound When the delimiter comment for the test was not found. + * @throws \PHPCSUtils\Exceptions\TestTargetNotFound When the target token cannot be found. + */ + public static function getTargetToken($commentString, $tokenType, $tokenContent = null) + { + if ((self::$phpcsFile instanceof File) === false) { + throw new TestFileNotFound(); + } + + $start = (self::$phpcsFile->numTokens - 1); + $comment = self::$phpcsFile->findPrevious( + \T_COMMENT, + $start, + null, + false, + $commentString + ); + + if ($comment === false) { + throw TestMarkerNotFound::create($commentString, self::$phpcsFile->getFilename()); + } + + $tokens = self::$phpcsFile->getTokens(); + $end = ($start + 1); + + // Limit the token finding to between this and the next delimiter comment. + for ($i = ($comment + 1); $i < $end; $i++) { + if ($tokens[$i]['code'] !== \T_COMMENT) { + continue; + } + + if (\stripos($tokens[$i]['content'], '/* test') === 0) { + $end = $i; + break; + } + } + + $target = self::$phpcsFile->findNext( + $tokenType, + ($comment + 1), + $end, + false, + $tokenContent + ); + + if ($target === false) { + throw TestTargetNotFound::create($commentString, $tokenContent, self::$phpcsFile->getFilename()); + } + + return $target; + } + + /** + * Helper method to tell PHPUnit to expect a PHPCS Exception in a PHPUnit and PHPCS cross-version + * compatible manner. + * + * @since 1.0.0 + * + * @param string $msg The expected exception message. + * @param string $type The PHPCS native exception type to expect. Either 'runtime' or 'tokenizer'. + * Defaults to 'runtime'. + * + * @return void + */ + public function expectPhpcsException($msg, $type = 'runtime') + { + $exception = 'PHP_CodeSniffer\Exceptions\RuntimeException'; + if ($type === 'tokenizer') { + $exception = 'PHP_CodeSniffer\Exceptions\TokenizerException'; + } + + if (\method_exists($this, 'expectException')) { + // PHPUnit 5+. + $this->expectException($exception); + $this->expectExceptionMessage($msg); + } else { + // PHPUnit 4. + $this->setExpectedException($exception, $msg); + } + } +} diff --git a/PHPCSUtils/Tokens/Collections.php b/PHPCSUtils/Tokens/Collections.php new file mode 100644 index 00000000..b1434463 --- /dev/null +++ b/PHPCSUtils/Tokens/Collections.php @@ -0,0 +1,803 @@ + => + */ + private static $alternativeControlStructureSyntaxes = [ + \T_IF => \T_IF, + \T_ELSEIF => \T_ELSEIF, + \T_ELSE => \T_ELSE, + \T_FOR => \T_FOR, + \T_FOREACH => \T_FOREACH, + \T_SWITCH => \T_SWITCH, + \T_WHILE => \T_WHILE, + \T_DECLARE => \T_DECLARE, + ]; + + /** + * Tokens representing alternative control structure syntax closer keywords. + * + * @since 1.0.0 Use the {@see Collections::alternativeControlStructureSyntaxClosers()} method for access. + * + * @var array => + */ + private static $alternativeControlStructureSyntaxClosers = [ + \T_ENDIF => \T_ENDIF, + \T_ENDFOR => \T_ENDFOR, + \T_ENDFOREACH => \T_ENDFOREACH, + \T_ENDWHILE => \T_ENDWHILE, + \T_ENDSWITCH => \T_ENDSWITCH, + \T_ENDDECLARE => \T_ENDDECLARE, + ]; + + /** + * Tokens which can open an array (PHPCS cross-version compatible). + * + * Includes `T_OPEN_SQUARE_BRACKET` to allow for handling intermittent tokenizer issues related + * to the retokenization to `T_OPEN_SHORT_ARRAY`. + * Should only be used selectively. + * + * @see \PHPCSUtils\Tokens\Collections::arrayTokensBC() Related method to retrieve tokens used + * for arrays (PHPCS cross-version). + * @see \PHPCSUtils\Tokens\Collections::shortArrayTokensBC() Related method to retrieve only tokens used + * for short arrays (PHPCS cross-version). + * + * @since 1.0.0 Use the {@see Collections::arrayOpenTokensBC()} method for access. + * + * @return array => + */ + private static $arrayOpenTokensBC = [ + \T_ARRAY => \T_ARRAY, + \T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY, + \T_OPEN_SQUARE_BRACKET => \T_OPEN_SQUARE_BRACKET, + ]; + + /** + * Tokens which are used to create arrays. + * + * @see \PHPCSUtils\Tokens\Collections::arrayOpenTokensBC() Related method to retrieve only the "open" tokens + * used for arrays (PHPCS cross-version compatible). + * @see \PHPCSUtils\Tokens\Collections::arrayTokensBC() Related method to retrieve tokens used + * for arrays (PHPCS cross-version compatible). + * @see \PHPCSUtils\Tokens\Collections::shortArrayTokens() Related method to retrieve only tokens used + * for short arrays. + * + * @since 1.0.0 Use the {@see Collections::arrayTokens()} method for access. + * + * @var array => + */ + private static $arrayTokens = [ + \T_ARRAY => \T_ARRAY, + \T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY, + \T_CLOSE_SHORT_ARRAY => \T_CLOSE_SHORT_ARRAY, + ]; + + /** + * Tokens which are used to create arrays (PHPCS cross-version compatible). + * + * Includes `T_OPEN_SQUARE_BRACKET` and `T_CLOSE_SQUARE_BRACKET` to allow for handling + * intermittent tokenizer issues related to the retokenization to `T_OPEN_SHORT_ARRAY`. + * Should only be used selectively. + * + * @see \PHPCSUtils\Tokens\Collections::arrayOpenTokensBC() Related method to retrieve only the "open" tokens + * used for arrays (PHPCS cross-version compatible). + * @see \PHPCSUtils\Tokens\Collections::shortArrayTokensBC() Related method to retrieve only tokens used + * for short arrays (PHPCS cross-version compatible). + * + * @since 1.0.0 Use the {@see Collections::arrayTokensBC()} method for access. + * + * @var array => + */ + private static $arrayTokensBC = [ + \T_ARRAY => \T_ARRAY, + \T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY, + \T_CLOSE_SHORT_ARRAY => \T_CLOSE_SHORT_ARRAY, + \T_OPEN_SQUARE_BRACKET => \T_OPEN_SQUARE_BRACKET, + \T_CLOSE_SQUARE_BRACKET => \T_CLOSE_SQUARE_BRACKET, + ]; + + /** + * Modifier keywords which can be used for a class declaration. + * + * @since 1.0.0 Use the {@see Collections::classModifierKeywords()} method for access. + * + * @var array => + */ + private static $classModifierKeywords = [ + \T_FINAL => \T_FINAL, + \T_ABSTRACT => \T_ABSTRACT, + \T_READONLY => \T_READONLY, + ]; + + /** + * List of tokens which represent "closed" scopes. + * + * I.e. anything declared within that scope - except for other closed scopes - is + * outside of the global namespace. + * + * This list doesn't contain the `T_NAMESPACE` token on purpose as variables declared + * within a namespace scope are still global and not limited to that namespace. + * + * @since 1.0.0 Use the {@see Collections::closedScopes()} method for access. + * + * @var array => + */ + private static $closedScopes = [ + \T_CLASS => \T_CLASS, + \T_ANON_CLASS => \T_ANON_CLASS, + \T_INTERFACE => \T_INTERFACE, + \T_TRAIT => \T_TRAIT, + \T_ENUM => \T_ENUM, + \T_FUNCTION => \T_FUNCTION, + \T_CLOSURE => \T_CLOSURE, + ]; + + /** + * Modifier keywords which can be used for constant declarations (in OO structures). + * + * - PHP 7.1 added class constants visibility support. + * - PHP 8.1 added support for final class constants. + * + * @since 1.0.0 Use the {@see Collections::constantModifierKeywords()} method for access. + * + * @var array => + */ + private static $constantModifierKeywords = [ + \T_PUBLIC => \T_PUBLIC, + \T_PRIVATE => \T_PRIVATE, + \T_PROTECTED => \T_PROTECTED, + \T_FINAL => \T_FINAL, + ]; + + /** + * Control structure tokens. + * + * @since 1.0.0 Use the {@see Collections::controlStructureTokens()} method for access. + * + * @var array => + */ + private static $controlStructureTokens = [ + \T_IF => \T_IF, + \T_ELSEIF => \T_ELSEIF, + \T_ELSE => \T_ELSE, + \T_FOR => \T_FOR, + \T_FOREACH => \T_FOREACH, + \T_SWITCH => \T_SWITCH, + \T_DO => \T_DO, + \T_WHILE => \T_WHILE, + \T_DECLARE => \T_DECLARE, + \T_MATCH => \T_MATCH, + ]; + + /** + * Tokens which represent a keyword which starts a function declaration. + * + * @since 1.0.0 Use the {@see Collections::functionDeclarationTokens()} method for access. + * + * @return array => + */ + private static $functionDeclarationTokens = [ + \T_FUNCTION => \T_FUNCTION, + \T_CLOSURE => \T_CLOSURE, + \T_FN => \T_FN, + ]; + + /** + * Increment/decrement operator tokens. + * + * @since 1.0.0 Use the {@see Collections::incrementDecrementOperators()} method for access. + * + * @var array => + */ + private static $incrementDecrementOperators = [ + \T_DEC => \T_DEC, + \T_INC => \T_INC, + ]; + + /** + * Tokens which can open a list construct (PHPCS cross-version compatible). + * + * Includes `T_OPEN_SQUARE_BRACKET` to allow for handling intermittent tokenizer issues related + * to the retokenization to `T_OPEN_SHORT_ARRAY`. + * Should only be used selectively. + * + * @see \PHPCSUtils\Tokens\Collections::listTokensBC() Related method to retrieve tokens used + * for lists (PHPCS cross-version). + * @see \PHPCSUtils\Tokens\Collections::shortListTokensBC() Related method to retrieve only tokens used + * for short lists (PHPCS cross-version). + * + * @since 1.0.0 Use the {@see Collections::listOpenTokensBC()} method for access. + * + * @return array => + */ + private static $listOpenTokensBC = [ + \T_LIST => \T_LIST, + \T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY, + \T_OPEN_SQUARE_BRACKET => \T_OPEN_SQUARE_BRACKET, + ]; + + /** + * Tokens which are used to create lists. + * + * @see \PHPCSUtils\Tokens\Collections::listTokensBC() Related method to retrieve tokens used + * for lists (PHPCS cross-version). + * @see \PHPCSUtils\Tokens\Collections::shortListTokens() Related method to retrieve only tokens used + * for short lists. + * + * @since 1.0.0 Use the {@see Collections::listTokens()} method for access. + * + * @var array => + */ + private static $listTokens = [ + \T_LIST => \T_LIST, + \T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY, + \T_CLOSE_SHORT_ARRAY => \T_CLOSE_SHORT_ARRAY, + ]; + + /** + * Tokens which are used to create lists (PHPCS cross-version compatible). + * + * Includes `T_OPEN_SQUARE_BRACKET` and `T_CLOSE_SQUARE_BRACKET` to allow for handling + * intermittent tokenizer issues related to the retokenization to `T_OPEN_SHORT_ARRAY`. + * Should only be used selectively. + * + * @see \PHPCSUtils\Tokens\Collections::shortListTokensBC() Related method to retrieve only tokens used + * for short lists (cross-version). + * + * @since 1.0.0 Use the {@see Collections::listTokensBC()} method for access. + * + * @var array => + */ + private static $listTokensBC = [ + \T_LIST => \T_LIST, + \T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY, + \T_CLOSE_SHORT_ARRAY => \T_CLOSE_SHORT_ARRAY, + \T_OPEN_SQUARE_BRACKET => \T_OPEN_SQUARE_BRACKET, + \T_CLOSE_SQUARE_BRACKET => \T_CLOSE_SQUARE_BRACKET, + ]; + + /** + * List of tokens which can end a namespace declaration statement. + * + * @since 1.0.0 Use the {@see Collections::namespaceDeclarationClosers()} method for access. + * + * @var array => + */ + private static $namespaceDeclarationClosers = [ + \T_SEMICOLON => \T_SEMICOLON, + \T_OPEN_CURLY_BRACKET => \T_OPEN_CURLY_BRACKET, + \T_CLOSE_TAG => \T_CLOSE_TAG, + ]; + + /** + * Tokens used for "names", be it namespace, OO, function or constant names. + * + * Includes the tokens introduced in PHP 8.0 for "Namespaced names as single token". + * + * Note: the PHP 8.0 namespaced name tokens are backfilled in PHPCS since PHPCS 3.5.7, + * but are not used yet (the PHP 8.0 tokenization is "undone" in PHPCS). + * As of PHPCS 4.0.0, these tokens _will_ be used and the PHP 8.0 tokenization is respected. + * + * @link https://wiki.php.net/rfc/namespaced_names_as_token PHP RFC on namespaced names as single token + * + * @since 1.0.0 Use the {@see Collections::nameTokens()} method for access. + * + * @return array => + */ + private static $nameTokens = [ + \T_STRING => \T_STRING, + \T_NAME_QUALIFIED => \T_NAME_QUALIFIED, + \T_NAME_FULLY_QUALIFIED => \T_NAME_FULLY_QUALIFIED, + \T_NAME_RELATIVE => \T_NAME_RELATIVE, + ]; + + /** + * Object operator tokens. + * + * @since 1.0.0 Use the {@see Collections::objectOperators()} method for access. + * + * @var array => + */ + private static $objectOperators = [ + \T_DOUBLE_COLON => \T_DOUBLE_COLON, + \T_OBJECT_OPERATOR => \T_OBJECT_OPERATOR, + \T_NULLSAFE_OBJECT_OPERATOR => \T_NULLSAFE_OBJECT_OPERATOR, + ]; + + /** + * OO structures which can use the "extends" keyword. + * + * @since 1.0.0 Use the {@see Collections::ooCanExtend()} method for access. + * + * @var array => + */ + private static $ooCanExtend = [ + \T_CLASS => \T_CLASS, + \T_ANON_CLASS => \T_ANON_CLASS, + \T_INTERFACE => \T_INTERFACE, + ]; + + /** + * OO structures which can use the "implements" keyword. + * + * @since 1.0.0 Use the {@see Collections::ooCanImplement()} method for access. + * + * @var array => + */ + private static $ooCanImplement = [ + \T_CLASS => \T_CLASS, + \T_ANON_CLASS => \T_ANON_CLASS, + \T_ENUM => \T_ENUM, + ]; + + /** + * OO scopes in which constants can be declared. + * + * Note: traits can only declare constants since PHP 8.2. + * + * @since 1.0.0 Use the {@see Collections::ooConstantScopes()} method for access. + * + * @var array => + */ + private static $ooConstantScopes = [ + \T_CLASS => \T_CLASS, + \T_ANON_CLASS => \T_ANON_CLASS, + \T_INTERFACE => \T_INTERFACE, + \T_ENUM => \T_ENUM, + \T_TRAIT => \T_TRAIT, + ]; + + /** + * Tokens types used for "forwarding" calls within OO structures. + * + * @link https://www.php.net/language.oop5.paamayim-nekudotayim PHP Manual on OO forwarding calls + * + * @since 1.0.0 Use the {@see Collections::ooHierarchyKeywords()} method for access. + * + * @var array => + */ + private static $ooHierarchyKeywords = [ + \T_PARENT => \T_PARENT, + \T_SELF => \T_SELF, + \T_STATIC => \T_STATIC, + ]; + + /** + * OO scopes in which properties can be declared. + * + * Note: interfaces can not declare properties. + * + * @since 1.0.0 Use the {@see Collections::ooPropertyScopes()} method for access. + * + * @var array => + */ + private static $ooPropertyScopes = [ + \T_CLASS => \T_CLASS, + \T_ANON_CLASS => \T_ANON_CLASS, + \T_TRAIT => \T_TRAIT, + ]; + + /** + * Token types which can be encountered in a parameter type declaration. + * + * @since 1.0.0 Use the {@see Collections::parameterTypeTokens()} method for access. + * + * @var array => + */ + private static $parameterTypeTokens = [ + \T_CALLABLE => \T_CALLABLE, + \T_SELF => \T_SELF, + \T_PARENT => \T_PARENT, + \T_FALSE => \T_FALSE, + \T_TRUE => \T_TRUE, + \T_NULL => \T_NULL, + \T_TYPE_UNION => \T_TYPE_UNION, + \T_TYPE_INTERSECTION => \T_TYPE_INTERSECTION, + ]; + + /** + * Tokens which open PHP. + * + * @since 1.0.0 Use the {@see Collections::phpOpenTags()} method for access. + * + * @return array => + */ + private static $phpOpenTags = [ + \T_OPEN_TAG => \T_OPEN_TAG, + \T_OPEN_TAG_WITH_ECHO => \T_OPEN_TAG_WITH_ECHO, + ]; + + /** + * Modifier keywords which can be used for a property declaration. + * + * @since 1.0.0 Use the {@see Collections::propertyModifierKeywords()} method for access. + * + * @var array => + */ + private static $propertyModifierKeywords = [ + \T_PUBLIC => \T_PUBLIC, + \T_PRIVATE => \T_PRIVATE, + \T_PROTECTED => \T_PROTECTED, + \T_STATIC => \T_STATIC, + \T_VAR => \T_VAR, + \T_READONLY => \T_READONLY, + ]; + + /** + * Token types which can be encountered in a property type declaration. + * + * @since 1.0.0 Use the {@see Collections::propertyTypeTokens()} method for access. + * + * @var array => + */ + private static $propertyTypeTokens = [ + \T_CALLABLE => \T_CALLABLE, + \T_SELF => \T_SELF, + \T_PARENT => \T_PARENT, + \T_FALSE => \T_FALSE, + \T_TRUE => \T_TRUE, + \T_NULL => \T_NULL, + \T_TYPE_UNION => \T_TYPE_UNION, + \T_TYPE_INTERSECTION => \T_TYPE_INTERSECTION, + ]; + + /** + * Token types which can be encountered in a return type declaration. + * + * @since 1.0.0 Use the {@see Collections::returnTypeTokens()} method for access. + * + * @var array => + */ + private static $returnTypeTokens = [ + \T_CALLABLE => \T_CALLABLE, + \T_FALSE => \T_FALSE, + \T_TRUE => \T_TRUE, + \T_NULL => \T_NULL, + \T_TYPE_UNION => \T_TYPE_UNION, + \T_TYPE_INTERSECTION => \T_TYPE_INTERSECTION, + ]; + + /** + * Tokens which can open a short array or short list (PHPCS cross-version compatible). + * + * Includes `T_OPEN_SQUARE_BRACKET` to allow for handling intermittent tokenizer issues related + * to the retokenization to `T_OPEN_SHORT_ARRAY`. + * Should only be used selectively. + * + * @since 1.0.0 Use the {@see Collections::shortArrayListOpenTokensBC()} method for access. + * + * @return array => + */ + private static $shortArrayListOpenTokensBC = [ + \T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY, + \T_OPEN_SQUARE_BRACKET => \T_OPEN_SQUARE_BRACKET, + ]; + + /** + * Tokens which are used for short arrays. + * + * @see \PHPCSUtils\Tokens\Collections::arrayTokens() Related method to retrieve all tokens used for arrays. + * + * @since 1.0.0 Use the {@see Collections::shortArrayTokens()} method for access. + * + * @var array => + */ + private static $shortArrayTokens = [ + \T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY, + \T_CLOSE_SHORT_ARRAY => \T_CLOSE_SHORT_ARRAY, + ]; + + /** + * Tokens which are used for short arrays (PHPCS cross-version compatible). + * + * Includes `T_OPEN_SQUARE_BRACKET` and `T_CLOSE_SQUARE_BRACKET` to allow for handling + * intermittent tokenizer issues related to the retokenization to `T_OPEN_SHORT_ARRAY`. + * Should only be used selectively. + * + * @see \PHPCSUtils\Tokens\Collections::arrayTokensBC() Related method to retrieve all tokens used for arrays + * (cross-version). + * + * @since 1.0.0 Use the {@see Collections::shortArrayTokensBC()} method for access. + * + * @var array => + */ + private static $shortArrayTokensBC = [ + \T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY, + \T_CLOSE_SHORT_ARRAY => \T_CLOSE_SHORT_ARRAY, + \T_OPEN_SQUARE_BRACKET => \T_OPEN_SQUARE_BRACKET, + \T_CLOSE_SQUARE_BRACKET => \T_CLOSE_SQUARE_BRACKET, + ]; + + /** + * Tokens which are used for short lists. + * + * @see \PHPCSUtils\Tokens\Collections::listTokens() Related method to retrieve all tokens used for lists. + * + * @since 1.0.0 Use the {@see Collections::shortListTokens()} method for access. + * + * @var array => + */ + private static $shortListTokens = [ + \T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY, + \T_CLOSE_SHORT_ARRAY => \T_CLOSE_SHORT_ARRAY, + ]; + + /** + * Tokens which are used for short lists (PHPCS cross-version compatible). + * + * Includes `T_OPEN_SQUARE_BRACKET` and `T_CLOSE_SQUARE_BRACKET` to allow for handling + * intermittent tokenizer issues related to the retokenization to `T_OPEN_SHORT_ARRAY`. + * Should only be used selectively. + * + * @see \PHPCSUtils\Tokens\Collections::listTokensBC() Related method to retrieve all tokens used for lists + * (cross-version). + * + * @since 1.0.0 Use the {@see Collections::shortListTokensBC()} method for access. + * + * @var array => + */ + private static $shortListTokensBC = [ + \T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY, + \T_CLOSE_SHORT_ARRAY => \T_CLOSE_SHORT_ARRAY, + \T_OPEN_SQUARE_BRACKET => \T_OPEN_SQUARE_BRACKET, + \T_CLOSE_SQUARE_BRACKET => \T_CLOSE_SQUARE_BRACKET, + ]; + + /** + * Tokens which can start a - potentially multi-line - text string. + * + * @since 1.0.0 Use the {@see Collections::textStringStartTokens()} method for access. + * + * @var array => + */ + private static $textStringStartTokens = [ + \T_START_HEREDOC => \T_START_HEREDOC, + \T_START_NOWDOC => \T_START_NOWDOC, + \T_CONSTANT_ENCAPSED_STRING => \T_CONSTANT_ENCAPSED_STRING, + \T_DOUBLE_QUOTED_STRING => \T_DOUBLE_QUOTED_STRING, + ]; + + /** + * Handle calls to (undeclared) methods for token arrays which don't need special handling. + * + * @since 1.0.0 + * + * @param string $name The name of the method which has been called. + * @param array $args Any arguments passed to the method. + * Unused as none of the methods take arguments. + * + * @return array => Token array + * + * @throws \PHPCSUtils\Exceptions\InvalidTokenArray When an invalid token array is requested. + */ + public static function __callStatic($name, $args) + { + if (isset(self::${$name})) { + return self::${$name}; + } + + // Unknown token array requested. + throw InvalidTokenArray::create($name); + } + + /** + * Throw a deprecation notice with a standardized deprecation message. + * + * @since 1.0.0 + * + * @param string $method The name of the method which is deprecated. + * @param string $version The version since which the method is deprecated. + * @param string $replacement What to use instead. + * + * @return void + */ + private static function triggerDeprecation($method, $version, $replacement) + { + \trigger_error( + \sprintf( + 'The %1$s::%2$s() method is deprecated since PHPCSUtils %3$s.' + . ' Use %4$s instead.', + __CLASS__, + $method, + $version, + $replacement + ), + \E_USER_DEPRECATED + ); + } + + /** + * Tokens which can represent function calls and function-call-like language constructs. + * + * @see \PHPCSUtils\Tokens\Collections::parameterPassingTokens() Related method. + * + * @since 1.0.0 + * + * @return array => + */ + public static function functionCallTokens() + { + // Function calls and class instantiation. + $tokens = self::$nameTokens; + $tokens[\T_VARIABLE] = \T_VARIABLE; + + // Class instantiation only. + $tokens[\T_ANON_CLASS] = \T_ANON_CLASS; + $tokens += self::$ooHierarchyKeywords; + + return $tokens; + } + + /** + * Tokens types which can be encountered in a fully, partially or unqualified name. + * + * Example: + * ```php + * echo namespace\Sub\ClassName::method(); + * ``` + * + * @since 1.0.0 + * + * @return array => + */ + public static function namespacedNameTokens() + { + $tokens = [ + \T_NS_SEPARATOR => \T_NS_SEPARATOR, + \T_NAMESPACE => \T_NAMESPACE, + ]; + + $tokens += self::$nameTokens; + + return $tokens; + } + + /** + * Tokens which can be passed to the methods in the PassedParameter class. + * + * @see \PHPCSUtils\Utils\PassedParameters + * + * @since 1.0.0 + * + * @return array => + */ + public static function parameterPassingTokens() + { + // Function call and class instantiation tokens. + $tokens = self::functionCallTokens(); + + // Function-look-a-like language constructs which can take multiple "parameters". + $tokens[\T_ISSET] = \T_ISSET; + $tokens[\T_UNSET] = \T_UNSET; + + // Array tokens. + $tokens += self::$arrayOpenTokensBC; + + return $tokens; + } + + /** + * Token types which can be encountered in a parameter type declaration. + * + * @since 1.0.0 + * + * @return array => + */ + public static function parameterTypeTokens() + { + $tokens = self::$parameterTypeTokens; + $tokens += self::namespacedNameTokens(); + + return $tokens; + } + + /** + * Token types which can be encountered in a property type declaration. + * + * @since 1.0.0 + * + * @return array => + */ + public static function propertyTypeTokens() + { + $tokens = self::$propertyTypeTokens; + $tokens += self::namespacedNameTokens(); + + return $tokens; + } + + /** + * Token types which can be encountered in a return type declaration. + * + * @since 1.0.0 + * + * @return array => + */ + public static function returnTypeTokens() + { + $tokens = self::$returnTypeTokens; + $tokens += self::$ooHierarchyKeywords; + $tokens += self::namespacedNameTokens(); + + return $tokens; + } +} diff --git a/PHPCSUtils/Tokens/TokenHelper.php b/PHPCSUtils/Tokens/TokenHelper.php new file mode 100644 index 00000000..4466b776 --- /dev/null +++ b/PHPCSUtils/Tokens/TokenHelper.php @@ -0,0 +1,55 @@ += 9.3 (which uses PHP-Parser), + * this logic breaks because PHP-Parser also polyfills tokens. + * This method takes potentially polyfilled tokens from PHP-Parser into account and will regard the token + * as undefined if it was declared by PHP-Parser. + * + * Note: this method only _needs_ to be used for PHP native tokens, not for PHPCS specific tokens. + * Also, realistically, it only needs to be used for tokens introduced in PHP in recent versions (PHP 7.4 and up). + * Having said that, the method _will_ also work correctly when a name of a PHPCS native token is passed or + * of an older PHP native token. + * + * {@internal PHP native tokens have a positive integer value. PHPCS polyfilled tokens are strings. + * PHP-Parser polyfilled tokens will always have a negative integer value < 0, which is how + * these are filtered out.} + * + * @link https://github.com/sebastianbergmann/php-code-coverage/issues/798 PHP-Code-Coverage#798 + * @link https://github.com/nikic/PHP-Parser/blob/master/lib/PhpParser/Lexer.php PHP-Parser Lexer code + * + * @since 1.0.0 + * + * @param string $name The token name. + * + * @return bool + */ + public static function tokenExists($name) + { + return (\defined($name) && (\is_int(\constant($name)) === false || \constant($name) > 0)); + } +} diff --git a/PHPCSUtils/Utils/Arrays.php b/PHPCSUtils/Utils/Arrays.php new file mode 100644 index 00000000..ce321b04 --- /dev/null +++ b/PHPCSUtils/Utils/Arrays.php @@ -0,0 +1,218 @@ + => + */ + private static $doubleArrowTargets = [ + \T_DOUBLE_ARROW => \T_DOUBLE_ARROW, + + // Nested arrays. + \T_ARRAY => \T_ARRAY, + \T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY, + + // Inline function, control structures and other things to skip over. + \T_FN => \T_FN, + \T_MATCH => \T_MATCH, + \T_ATTRIBUTE => \T_ATTRIBUTE, + ]; + + /** + * Determine whether a T_OPEN/CLOSE_SHORT_ARRAY token is a short array construct + * and not a short list. + * + * 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. + * + * @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 array. + * `FALSE` if the token is a short list bracket, a plain square bracket + * or not one of the accepted tokens. + */ + public static function isShortArray(File $phpcsFile, $stackPtr) + { + return IsShortArrayOrListWithCache::isShortArray($phpcsFile, $stackPtr); + } + + /** + * Find the array opener and closer based on a T_ARRAY 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 array determination. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the `T_ARRAY` or `T_OPEN_SHORT_ARRAY` + * token in the stack. + * @param true|null $isShortArray Short-circuit the short array 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 array while walking the + * tokens in an array. + * Use with care. + * + * @return array|false An array with the token pointers; or `FALSE` if this is not a + * (short) array 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 array open bracket. + * 'closer' => integer, // Stack pointer to the array close bracket. + * ) + * ``` + */ + public static function getOpenClose(File $phpcsFile, $stackPtr, $isShortArray = null) + { + $tokens = $phpcsFile->getTokens(); + + // Is this one of the tokens this function handles ? + if (isset($tokens[$stackPtr]) === false + || isset(Collections::arrayOpenTokensBC()[$tokens[$stackPtr]['code']]) === false + ) { + return false; + } + + switch ($tokens[$stackPtr]['code']) { + case \T_ARRAY: + 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 ($isShortArray === true || self::isShortArray($phpcsFile, $stackPtr) === true) { + $opener = $stackPtr; + $closer = $tokens[$stackPtr]['bracket_closer']; + } + break; + } + + if (isset($opener, $closer)) { + return [ + 'opener' => $opener, + 'closer' => $closer, + ]; + } + + return false; + } + + /** + * Get the stack pointer position of the double arrow within an array item. + * + * Expects to be passed the array item start and end tokens as retrieved via + * {@see \PHPCSUtils\Utils\PassedParameters::getParameters()}. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being examined. + * @param int $start Stack pointer to the start of the array item. + * @param int $end Stack pointer to the last token in the array item. + * + * @return int|false Stack pointer to the double arrow if this array item has a key; or `FALSE` otherwise. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the start or end positions are invalid. + */ + public static function getDoubleArrowPtr(File $phpcsFile, $start, $end) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$start], $tokens[$end]) === false || $start > $end) { + throw new RuntimeException( + 'Invalid start and/or end position passed to getDoubleArrowPtr().' + . ' Received: $start ' . $start . ', $end ' . $end + ); + } + + $cacheId = "$start-$end"; + if (Cache::isCached($phpcsFile, __METHOD__, $cacheId) === true) { + return Cache::get($phpcsFile, __METHOD__, $cacheId); + } + + $targets = self::$doubleArrowTargets; + $targets += Collections::closedScopes(); + + $doubleArrow = ($start - 1); + $returnValue = false; + ++$end; + do { + $doubleArrow = $phpcsFile->findNext( + $targets, + ($doubleArrow + 1), + $end + ); + + if ($doubleArrow === false) { + break; + } + + if ($tokens[$doubleArrow]['code'] === \T_DOUBLE_ARROW) { + $returnValue = $doubleArrow; + break; + } + + // Skip over closed scopes which may contain foreach structures or generators. + if ((isset(Collections::closedScopes()[$tokens[$doubleArrow]['code']]) === true + || $tokens[$doubleArrow]['code'] === \T_FN + || $tokens[$doubleArrow]['code'] === \T_MATCH) + && isset($tokens[$doubleArrow]['scope_closer']) === true + ) { + $doubleArrow = $tokens[$doubleArrow]['scope_closer']; + continue; + } + + // Skip over attributes which may contain arrays as a passed parameters. + if ($tokens[$doubleArrow]['code'] === \T_ATTRIBUTE + && isset($tokens[$doubleArrow]['attribute_closer']) + ) { + $doubleArrow = $tokens[$doubleArrow]['attribute_closer']; + continue; + } + + // Start of nested long/short array. + break; + } while ($doubleArrow < $end); + + Cache::set($phpcsFile, __METHOD__, $cacheId, $returnValue); + return $returnValue; + } +} diff --git a/PHPCSUtils/Utils/Conditions.php b/PHPCSUtils/Utils/Conditions.php new file mode 100644 index 00000000..aba769e3 --- /dev/null +++ b/PHPCSUtils/Utils/Conditions.php @@ -0,0 +1,156 @@ +getTokens(); + + // Check for the existence of the token. + if (isset($tokens[$stackPtr]) === false) { + return false; + } + + // Make sure the token has conditions. + if (empty($tokens[$stackPtr]['conditions'])) { + return false; + } + + $types = (array) $types; + $conditions = $tokens[$stackPtr]['conditions']; + + if (empty($types) === true) { + // No types specified, just return the first/last condition pointer. + if ($first === false) { + \end($conditions); + } else { + \reset($conditions); + } + + return \key($conditions); + } + + if ($first === false) { + $conditions = \array_reverse($conditions, true); + } + + foreach ($conditions as $ptr => $type) { + if (isset($tokens[$ptr]) === true + && \in_array($type, $types, true) === true + ) { + // We found a token with the required type. + return $ptr; + } + } + + return false; + } + + /** + * Determine if the passed token has a condition of one of the passed types. + * + * This method is not significantly different from the PHPCS native version. + * + * @see \PHP_CodeSniffer\Files\File::hasCondition() Original source. + * @see \PHPCSUtils\BackCompat\BCFile::hasCondition() Cross-version compatible version of the original. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the token we are checking. + * @param int|string|array $types The type(s) of tokens to search for. + * + * @return bool + */ + public static function hasCondition(File $phpcsFile, $stackPtr, $types) + { + return (self::getCondition($phpcsFile, $stackPtr, $types) !== false); + } + + /** + * Return the position of the first (outermost) condition of a certain type for the passed token. + * + * If no types are specified, the first condition for the token, independently of type, + * will be returned. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position of the token we are checking. + * @param int|string|array $types Optional. The type(s) of tokens to search for. + * + * @return int|false Integer stack pointer to the condition; or `FALSE` if the token + * does not have the condition or has no conditions at all. + */ + public static function getFirstCondition(File $phpcsFile, $stackPtr, $types = []) + { + return self::getCondition($phpcsFile, $stackPtr, $types, true); + } + + /** + * Return the position of the last (innermost) condition of a certain type for the passed token. + * + * If no types are specified, the last condition for the token, independently of type, + * will be returned. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position of the token we are checking. + * @param int|string|array $types Optional. The type(s) of tokens to search for. + * + * @return int|false Integer stack pointer to the condition; or `FALSE` if the token + * does not have the condition or has no conditions at all. + */ + public static function getLastCondition(File $phpcsFile, $stackPtr, $types = []) + { + return self::getCondition($phpcsFile, $stackPtr, $types, false); + } +} diff --git a/PHPCSUtils/Utils/Context.php b/PHPCSUtils/Utils/Context.php new file mode 100644 index 00000000..c0f1f947 --- /dev/null +++ b/PHPCSUtils/Utils/Context.php @@ -0,0 +1,232 @@ +getTokens(); + + // Check for the existence of the token. + if (isset($tokens[$stackPtr]) === false) { + return false; + } + + if (isset($tokens[$stackPtr]['attribute_opener'], $tokens[$stackPtr]['attribute_closer']) === false) { + return false; + } + + return ($stackPtr !== $tokens[$stackPtr]['attribute_opener'] + && $stackPtr !== $tokens[$stackPtr]['attribute_closer']); + } + + /** + * Check whether an arbitrary token is in a foreach condition and if so, in which part: + * before or after the "as". + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the token we are checking. + * + * @return string|false String `'beforeAs'`, `'as'` or `'afterAs'` when the token is within + * a `foreach` condition. + * `FALSE` in all other cases, including for parse errors. + */ + public static function inForeachCondition(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + // Check for the existence of the token. + if (isset($tokens[$stackPtr]) === false) { + return false; + } + + $foreach = Parentheses::getLastOwner($phpcsFile, $stackPtr, \T_FOREACH); + if ($foreach === false) { + return false; + } + + if ($tokens[$stackPtr]['code'] === \T_AS) { + return 'as'; + } + + $asPtr = $phpcsFile->findNext( + \T_AS, + ($tokens[$foreach]['parenthesis_opener'] + 1), + $tokens[$foreach]['parenthesis_closer'] + ); + + if ($asPtr === false) { + // Parse error or live coding. + return false; + } + + if ($stackPtr < $asPtr) { + return 'beforeAs'; + } + + return 'afterAs'; + } + + /** + * Check whether an arbitrary token is in a for condition and if so, in which part: + * the first, second or third expression. + * + * Note: the semicolons separating the conditions are regarded as belonging with the + * expression before it. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the token we are checking. + * + * @return string|false String `'expr1'`, `'expr2'` or `'expr3'` when the token is within + * a `for` condition. + * `FALSE` in all other cases, including for parse errors. + */ + public static function inForCondition(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + // Check for the existence of the token. + if (isset($tokens[$stackPtr]) === false) { + return false; + } + + $for = Parentheses::getLastOwner($phpcsFile, $stackPtr, \T_FOR); + if ($for === false) { + return false; + } + + $semicolons = []; + $count = 0; + $opener = $tokens[$for]['parenthesis_opener']; + $closer = $tokens[$for]['parenthesis_closer']; + $level = $tokens[$for]['level']; + $parens = 1; + + if (isset($tokens[$for]['nested_parenthesis'])) { + $parens = (\count($tokens[$for]['nested_parenthesis']) + 1); + } + + for ($i = ($opener + 1); $i < $closer; $i++) { + if ($tokens[$i]['code'] !== \T_SEMICOLON) { + continue; + } + + if ($tokens[$i]['level'] !== $level + || \count($tokens[$i]['nested_parenthesis']) !== $parens + ) { + // Disregard semi-colons at lower nesting/condition levels. + continue; + } + + ++$count; + $semicolons[$count] = $i; + } + + if ($count !== 2) { + return false; + } + + foreach ($semicolons as $key => $ptr) { + if ($stackPtr <= $ptr) { + return 'expr' . $key; + } + } + + return 'expr3'; + } +} diff --git a/PHPCSUtils/Utils/ControlStructures.php b/PHPCSUtils/Utils/ControlStructures.php new file mode 100644 index 00000000..0e8180ad --- /dev/null +++ b/PHPCSUtils/Utils/ControlStructures.php @@ -0,0 +1,274 @@ +getTokens(); + + // Check for the existence of the token. + if (isset($tokens[$stackPtr]) === false + || isset(Collections::controlStructureTokens()[$tokens[$stackPtr]['code']]) === false + ) { + return false; + } + + // Handle `else if`. + if ($tokens[$stackPtr]['code'] === \T_ELSE && isset($tokens[$stackPtr]['scope_opener']) === false) { + $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if ($next !== false && $tokens[$next]['code'] === \T_IF) { + $stackPtr = $next; + } + } + + /* + * The scope markers are set. This is the simplest situation. + */ + if (isset($tokens[$stackPtr]['scope_opener']) === true) { + if ($allowEmpty === true) { + return true; + } + + // Check whether the body is empty. + $start = ($tokens[$stackPtr]['scope_opener'] + 1); + $end = ($phpcsFile->numTokens + 1); + if (isset($tokens[$stackPtr]['scope_closer']) === true) { + $end = $tokens[$stackPtr]['scope_closer']; + } + + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $start, $end, true); + if ($nextNonEmpty !== false) { + return true; + } + + return false; + } + + /* + * Control structure without scope markers. + * Either single line statement or inline control structure. + * + * - Single line statement doesn't have a body and is therefore always empty. + * - Inline control structure has to have a body and can never be empty. + * + * This code also needs to take live coding into account where a scope opener is found, but + * no scope closer. + */ + $searchStart = ($stackPtr + 1); + if (isset($tokens[$stackPtr]['parenthesis_closer']) === true) { + $searchStart = ($tokens[$stackPtr]['parenthesis_closer'] + 1); + } + + $nextNonEmpty = $phpcsFile->findNext( + Tokens::$emptyTokens, + $searchStart, + null, + true + ); + if ($nextNonEmpty === false + || $tokens[$nextNonEmpty]['code'] === \T_SEMICOLON + || $tokens[$nextNonEmpty]['code'] === \T_CLOSE_TAG + ) { + // Parse error or single line statement. + return false; + } + + if ($tokens[$nextNonEmpty]['code'] === \T_OPEN_CURLY_BRACKET) { + if ($allowEmpty === true) { + return true; + } + + // Unrecognized scope opener due to parse error. + $nextNext = $phpcsFile->findNext( + Tokens::$emptyTokens, + ($nextNonEmpty + 1), + null, + true + ); + + if ($nextNext === false) { + return false; + } + + return true; + } + + return true; + } + + /** + * Check whether an IF or ELSE token is part of an "else if". + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the token we are checking. + * + * @return bool + */ + public static function isElseIf(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + // Check for the existence of the token. + if (isset($tokens[$stackPtr]) === false) { + return false; + } + + if ($tokens[$stackPtr]['code'] === \T_ELSEIF) { + return true; + } + + if ($tokens[$stackPtr]['code'] !== \T_ELSE && $tokens[$stackPtr]['code'] !== \T_IF) { + return false; + } + + if ($tokens[$stackPtr]['code'] === \T_ELSE && isset($tokens[$stackPtr]['scope_opener']) === true) { + return false; + } + + switch ($tokens[$stackPtr]['code']) { + case \T_ELSE: + $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if ($next !== false && $tokens[$next]['code'] === \T_IF) { + return true; + } + break; + + case \T_IF: + $previous = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + if ($previous !== false && $tokens[$previous]['code'] === \T_ELSE) { + return true; + } + break; + } + + return false; + } + + /** + * Retrieve the exception(s) being caught in a CATCH condition. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the token we are checking. + * + * @return array Array with information about the caught Exception(s). + * The returned array will contain the following information for + * each caught exception: + * ```php + * 0 => array( + * 'type' => string, // The type declaration for the exception being caught. + * 'type_token' => integer, // The stack pointer to the start of the type declaration. + * 'type_end_token' => integer, // The stack pointer to the end of the type declaration. + * ) + * ``` + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified `$stackPtr` is not of + * type `T_CATCH` or doesn't exist. + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If no parenthesis opener or closer can be + * determined (parse error). + */ + public static function getCaughtExceptions(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]) === false + || $tokens[$stackPtr]['code'] !== \T_CATCH + ) { + throw new RuntimeException('$stackPtr must be of type T_CATCH'); + } + + if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_closer']) === false) { + throw new RuntimeException('Parentheses opener/closer of the T_CATCH could not be determined'); + } + + $opener = $tokens[$stackPtr]['parenthesis_opener']; + $closer = $tokens[$stackPtr]['parenthesis_closer']; + $exceptions = []; + + $foundName = ''; + $firstToken = null; + $lastToken = null; + + for ($i = ($opener + 1); $i <= $closer; $i++) { + if (isset(Tokens::$emptyTokens[$tokens[$i]['code']])) { + continue; + } + + if (isset(Collections::namespacedNameTokens()[$tokens[$i]['code']]) === false) { + // Add the current exception to the result array. + $exceptions[] = [ + 'type' => $foundName, + 'type_token' => $firstToken, + 'type_end_token' => $lastToken, + ]; + + if ($tokens[$i]['code'] === \T_BITWISE_OR) { + // Multi-catch. Reset and continue. + $foundName = ''; + $firstToken = null; + $lastToken = null; + continue; + } + + break; + } + + if (isset($firstToken) === false) { + $firstToken = $i; + } + + $foundName .= $tokens[$i]['content']; + $lastToken = $i; + } + + return $exceptions; + } +} diff --git a/PHPCSUtils/Utils/FunctionDeclarations.php b/PHPCSUtils/Utils/FunctionDeclarations.php new file mode 100644 index 00000000..2b16b557 --- /dev/null +++ b/PHPCSUtils/Utils/FunctionDeclarations.php @@ -0,0 +1,808 @@ + => + */ + public static $magicFunctions = [ + '__autoload' => 'autoload', + ]; + + /** + * A list of all PHP magic methods. + * + * The array keys contain the method names. The values contain the name without the double underscore. + * + * The method names are listed in lowercase as these method names in PHP are case-insensitive + * and comparisons against this list should therefore always be done in a case-insensitive manner. + * + * @since 1.0.0 + * + * @var array => + */ + public static $magicMethods = [ + '__construct' => 'construct', + '__destruct' => 'destruct', + '__call' => 'call', + '__callstatic' => 'callstatic', + '__get' => 'get', + '__set' => 'set', + '__isset' => 'isset', + '__unset' => 'unset', + '__sleep' => 'sleep', + '__wakeup' => 'wakeup', + '__tostring' => 'tostring', + '__set_state' => 'set_state', + '__clone' => 'clone', + '__invoke' => 'invoke', + '__debuginfo' => 'debuginfo', // PHP >= 5.6. + '__serialize' => 'serialize', // PHP >= 7.4. + '__unserialize' => 'unserialize', // PHP >= 7.4. + ]; + + /** + * A list of all PHP native non-magic methods starting with a double underscore. + * + * These come from PHP modules such as SOAPClient. + * + * The array keys are the method names, the values the name of the PHP class containing + * the function. + * + * The method names are listed in lowercase as function names in PHP are case-insensitive + * and comparisons against this list should therefore always be done in a case-insensitive manner. + * + * @since 1.0.0 + * + * @var array => + */ + public static $methodsDoubleUnderscore = [ + '__dorequest' => 'SOAPClient', + '__getcookies' => 'SOAPClient', + '__getfunctions' => 'SOAPClient', + '__getlastrequest' => 'SOAPClient', + '__getlastrequestheaders' => 'SOAPClient', + '__getlastresponse' => 'SOAPClient', + '__getlastresponseheaders' => 'SOAPClient', + '__gettypes' => 'SOAPClient', + '__setcookie' => 'SOAPClient', + '__setlocation' => 'SOAPClient', + '__setsoapheaders' => 'SOAPClient', + '__soapcall' => 'SOAPClient', + ]; + + /** + * Returns the declaration name for a function. + * + * Alias for the {@see \PHPCSUtils\Utils\ObjectDeclarations::getName()} method. + * + * @see \PHPCSUtils\BackCompat\BCFile::getDeclarationName() Original function. + * @see \PHPCSUtils\Utils\ObjectDeclarations::getName() PHPCSUtils native improved version. + * + * @since 1.0.0 + * + * @codeCoverageIgnore + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the function keyword token. + * + * @return string|null The name of the function; or `NULL` if the passed token doesn't exist, + * the function is anonymous or in case of a parse error/live coding. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token is not of type + * `T_FUNCTION`. + */ + public static function getName(File $phpcsFile, $stackPtr) + { + return ObjectDeclarations::getName($phpcsFile, $stackPtr); + } + + /** + * Retrieves the visibility and implementation properties of a method. + * + * Main differences with the PHPCS version: + * - Bugs fixed: + * - Handling of PHPCS annotations. + * - `"has_body"` index could be set to `true` for functions without body in the case of + * parse errors or live coding. + * - Defensive coding against incorrect calls to this method. + * - More efficient checking whether a function has a body. + * - Support for PHP 8.0 identifier name tokens in return types, cross-version PHP & PHPCS. + * - Support for the PHP 8.2 `true` type. + * + * @see \PHP_CodeSniffer\Files\File::getMethodProperties() Original source. + * @see \PHPCSUtils\BackCompat\BCFile::getMethodProperties() Cross-version compatible version of the original. + * + * @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 properties for. + * + * @return array Array with information about a function declaration. + * The format of the return value is: + * ```php + * array( + * 'scope' => string, // Public, private, or protected + * 'scope_specified' => bool, // TRUE if the scope keyword was found. + * 'return_type' => string, // The return type of the method. + * 'return_type_token' => int|false, // The stack pointer to the start of the return type + * // or FALSE if there is no return type. + * 'return_type_end_token' => int|false, // The stack pointer to the end of the return type + * // or FALSE if there is no return type. + * 'nullable_return_type' => bool, // TRUE if the return type is preceded + * // by the nullability operator. + * 'is_abstract' => bool, // TRUE if the abstract keyword was found. + * 'is_final' => bool, // TRUE if the final keyword was found. + * 'is_static' => bool, // TRUE if the static keyword was found. + * 'has_body' => bool, // TRUE if the method has a body + * ); + * ``` + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a T_FUNCTION + * or T_CLOSURE token, nor an arrow function. + */ + public static function getProperties(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]) === false + || isset(Collections::functionDeclarationTokens()[$tokens[$stackPtr]['code']]) === false + ) { + throw new RuntimeException('$stackPtr must be of type T_FUNCTION or T_CLOSURE or an arrow function'); + } + + if ($tokens[$stackPtr]['code'] === \T_FUNCTION) { + $valid = Tokens::$methodPrefixes; + } else { + $valid = [\T_STATIC => \T_STATIC]; + } + + $valid += Tokens::$emptyTokens; + + $scope = 'public'; + $scopeSpecified = false; + $isAbstract = false; + $isFinal = false; + $isStatic = false; + + for ($i = ($stackPtr - 1); $i > 0; $i--) { + if (isset($valid[$tokens[$i]['code']]) === false) { + break; + } + + switch ($tokens[$i]['code']) { + case \T_PUBLIC: + $scope = 'public'; + $scopeSpecified = true; + break; + case \T_PRIVATE: + $scope = 'private'; + $scopeSpecified = true; + break; + case \T_PROTECTED: + $scope = 'protected'; + $scopeSpecified = true; + break; + case \T_ABSTRACT: + $isAbstract = true; + break; + case \T_FINAL: + $isFinal = true; + break; + case \T_STATIC: + $isStatic = true; + break; + } + } + + $returnType = ''; + $returnTypeToken = false; + $returnTypeEndToken = false; + $nullableReturnType = false; + $hasBody = false; + $returnTypeTokens = Collections::returnTypeTokens(); + + /* + * BC PHPCS < 3.x.x: The union type separator is not (yet) retokenized correctly + * for union types containing the `true` type. + */ + $returnTypeTokens[\T_BITWISE_OR] = \T_BITWISE_OR; + + $parenthesisCloser = null; + if (isset($tokens[$stackPtr]['parenthesis_closer']) === true) { + $parenthesisCloser = $tokens[$stackPtr]['parenthesis_closer']; + } + + if (isset($parenthesisCloser) === true) { + $scopeOpener = null; + if (isset($tokens[$stackPtr]['scope_opener']) === true) { + $scopeOpener = $tokens[$stackPtr]['scope_opener']; + } + + for ($i = $parenthesisCloser; $i < $phpcsFile->numTokens; $i++) { + if ($i === $scopeOpener) { + // End of function definition. + $hasBody = true; + break; + } + + if ($scopeOpener === null && $tokens[$i]['code'] === \T_SEMICOLON) { + // End of abstract/interface function definition. + break; + } + + if ($tokens[$i]['code'] === \T_NULLABLE) { + $nullableReturnType = true; + } + + if (isset($returnTypeTokens[$tokens[$i]['code']]) === true) { + if ($returnTypeToken === false) { + $returnTypeToken = $i; + } + + $returnType .= $tokens[$i]['content']; + $returnTypeEndToken = $i; + } + } + } + + if ($returnType !== '' && $nullableReturnType === true) { + $returnType = '?' . $returnType; + } + + return [ + 'scope' => $scope, + 'scope_specified' => $scopeSpecified, + 'return_type' => $returnType, + 'return_type_token' => $returnTypeToken, + 'return_type_end_token' => $returnTypeEndToken, + 'nullable_return_type' => $nullableReturnType, + 'is_abstract' => $isAbstract, + 'is_final' => $isFinal, + 'is_static' => $isStatic, + 'has_body' => $hasBody, + ]; + } + + /** + * Retrieves the method parameters for the specified function token. + * + * Also supports passing in a `T_USE` token for a closure use group. + * + * The returned array will contain the following information for each parameter: + * + * ```php + * 0 => array( + * 'name' => string, // The variable name. + * 'token' => int, // The stack pointer to the variable name. + * 'content' => string, // The full content of the variable definition. + * 'has_attributes' => bool, // Does the parameter have one or more attributes attached ? + * 'pass_by_reference' => bool, // Is the variable passed by reference? + * 'reference_token' => int, // The stack pointer to the reference operator + * // or FALSE if the param is not passed by reference. + * 'variable_length' => bool, // Is the param of variable length through use of `...` ? + * 'variadic_token' => int, // The stack pointer to the ... operator + * // or FALSE if the param is not variable length. + * 'type_hint' => string, // The type hint for the variable. + * 'type_hint_token' => int, // The stack pointer to the start of the type hint + * // or FALSE if there is no type hint. + * 'type_hint_end_token' => int, // The stack pointer to the end of the type hint + * // or FALSE if there is no type hint. + * 'nullable_type' => bool, // TRUE if the var type is preceded by the nullability + * // operator. + * 'comma_token' => int, // The stack pointer to the comma after the param + * // or FALSE if this is the last param. + * ) + * ``` + * + * Parameters with default values have the following additional array indexes: + * ```php + * 'default' => string, // The full content of the default value. + * 'default_token' => int, // The stack pointer to the start of the default value. + * 'default_equal_token' => int, // The stack pointer to the equals sign. + * ``` + * + * Parameters declared using PHP 8 constructor property promotion, have these additional array indexes: + * ```php + * 'property_visibility' => string, // The property visibility as declared. + * 'visibility_token' => int, // The stack pointer to the visibility modifier token. + * 'property_readonly' => bool, // TRUE if the readonly keyword was found. + * 'readonly_token' => int, // The stack pointer to the readonly modifier token. + * ``` + * + * Main differences with the PHPCS version: + * - Defensive coding against incorrect calls to this method. + * - More efficient and more stable checking whether a `T_USE` token is a closure use. + * - More efficient and more stable looping of the default value. + * - Clearer exception message when a non-closure use token was passed to the function. + * - Support for PHP 8.0 identifier name tokens in parameter types, cross-version PHP & PHPCS. + * - Support for the PHP 8.2 `true` type. + * - The results of this function call are cached during a PHPCS run for faster response times. + * + * @see \PHP_CodeSniffer\Files\File::getMethodParameters() Original source. + * @see \PHPCSUtils\BackCompat\BCFile::getMethodParameters() Cross-version compatible version of the original. + * + * @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 + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified $stackPtr is not of + * type `T_FUNCTION`, `T_CLOSURE` or `T_USE`, + * nor an arrow function. + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If a passed `T_USE` token is not a closure + * use token. + */ + public static function getParameters(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]) === false + || (isset(Collections::functionDeclarationTokens()[$tokens[$stackPtr]['code']]) === false + && $tokens[$stackPtr]['code'] !== \T_USE) + ) { + throw new RuntimeException('$stackPtr must be of type T_FUNCTION, T_CLOSURE or T_USE or an arrow function'); + } + + if ($tokens[$stackPtr]['code'] === \T_USE) { + // This will work PHPCS 3.x/4.x cross-version without much overhead. + $opener = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if ($opener === false + || $tokens[$opener]['code'] !== \T_OPEN_PARENTHESIS + || UseStatements::isClosureUse($phpcsFile, $stackPtr) === false + ) { + throw new RuntimeException('$stackPtr was not a valid closure T_USE'); + } + } else { + if (isset($tokens[$stackPtr]['parenthesis_opener']) === false) { + // Live coding or syntax error, so no params to find. + return []; + } + + $opener = $tokens[$stackPtr]['parenthesis_opener']; + } + + if (isset($tokens[$opener]['parenthesis_closer']) === false) { + // Live coding or syntax error, so no params to find. + return []; + } + + if (Cache::isCached($phpcsFile, __METHOD__, $stackPtr) === true) { + return Cache::get($phpcsFile, __METHOD__, $stackPtr); + } + + $closer = $tokens[$opener]['parenthesis_closer']; + + $vars = []; + $currVar = null; + $paramStart = ($opener + 1); + $defaultStart = null; + $equalToken = null; + $paramCount = 0; + $hasAttributes = false; + $passByReference = false; + $referenceToken = false; + $variableLength = false; + $variadicToken = false; + $typeHint = ''; + $typeHintToken = false; + $typeHintEndToken = false; + $nullableType = false; + $visibilityToken = null; + $readonlyToken = null; + + $parameterTypeTokens = Collections::parameterTypeTokens(); + + /* + * BC PHPCS < 3.x.x: The union type separator is not (yet) retokenized correctly + * for union types containing the `true` type. + */ + $parameterTypeTokens[\T_BITWISE_OR] = \T_BITWISE_OR; + + for ($i = $paramStart; $i <= $closer; $i++) { + if (isset($parameterTypeTokens[$tokens[$i]['code']]) === true + /* + * Self and parent are valid, static invalid, but was probably intended as type declaration. + * Note: constructor property promotion does not support static properties, so this should + * still be a valid assumption. + */ + || $tokens[$i]['code'] === \T_STATIC + ) { + if ($typeHintToken === false) { + $typeHintToken = $i; + } + + $typeHint .= $tokens[$i]['content']; + $typeHintEndToken = $i; + continue; + } + + switch ($tokens[$i]['code']) { + case \T_ATTRIBUTE: + $hasAttributes = true; + + // Skip to the end of the attribute. + $i = $tokens[$i]['attribute_closer']; + break; + + case \T_BITWISE_AND: + $passByReference = true; + $referenceToken = $i; + break; + + case \T_VARIABLE: + $currVar = $i; + break; + + case \T_ELLIPSIS: + $variableLength = true; + $variadicToken = $i; + break; + + case \T_NULLABLE: + $nullableType = true; + $typeHint .= $tokens[$i]['content']; + $typeHintEndToken = $i; + break; + + case \T_PUBLIC: + case \T_PROTECTED: + case \T_PRIVATE: + $visibilityToken = $i; + break; + + case \T_READONLY: + $readonlyToken = $i; + break; + + case \T_CLOSE_PARENTHESIS: + case \T_COMMA: + // If it's null, then there must be no parameters for this + // method. + if ($currVar === null) { + continue 2; + } + + $vars[$paramCount] = []; + $vars[$paramCount]['token'] = $currVar; + $vars[$paramCount]['name'] = $tokens[$currVar]['content']; + $vars[$paramCount]['content'] = \trim( + GetTokensAsString::normal($phpcsFile, $paramStart, ($i - 1)) + ); + + if ($defaultStart !== null) { + $vars[$paramCount]['default'] = \trim( + GetTokensAsString::normal($phpcsFile, $defaultStart, ($i - 1)) + ); + $vars[$paramCount]['default_token'] = $defaultStart; + $vars[$paramCount]['default_equal_token'] = $equalToken; + } + + $vars[$paramCount]['has_attributes'] = $hasAttributes; + $vars[$paramCount]['pass_by_reference'] = $passByReference; + $vars[$paramCount]['reference_token'] = $referenceToken; + $vars[$paramCount]['variable_length'] = $variableLength; + $vars[$paramCount]['variadic_token'] = $variadicToken; + $vars[$paramCount]['type_hint'] = $typeHint; + $vars[$paramCount]['type_hint_token'] = $typeHintToken; + $vars[$paramCount]['type_hint_end_token'] = $typeHintEndToken; + $vars[$paramCount]['nullable_type'] = $nullableType; + + if ($visibilityToken !== null) { + $vars[$paramCount]['property_visibility'] = $tokens[$visibilityToken]['content']; + $vars[$paramCount]['visibility_token'] = $visibilityToken; + $vars[$paramCount]['property_readonly'] = false; + } + + if ($readonlyToken !== null) { + $vars[$paramCount]['property_readonly'] = true; + $vars[$paramCount]['readonly_token'] = $readonlyToken; + } + + if ($tokens[$i]['code'] === \T_COMMA) { + $vars[$paramCount]['comma_token'] = $i; + } else { + $vars[$paramCount]['comma_token'] = false; + } + + // Reset the vars, as we are about to process the next parameter. + $currVar = null; + $paramStart = ($i + 1); + $defaultStart = null; + $equalToken = null; + $hasAttributes = false; + $passByReference = false; + $referenceToken = false; + $variableLength = false; + $variadicToken = false; + $typeHint = ''; + $typeHintToken = false; + $typeHintEndToken = false; + $nullableType = false; + $visibilityToken = null; + $readonlyToken = null; + + ++$paramCount; + break; + + case \T_EQUAL: + $defaultStart = $phpcsFile->findNext(Tokens::$emptyTokens, ($i + 1), null, true); + $equalToken = $i; + + // Skip past everything in the default value before going into the next switch loop. + for ($j = ($i + 1); $j <= $closer; $j++) { + // Skip past array()'s et al as default values. + if (isset($tokens[$j]['parenthesis_opener'], $tokens[$j]['parenthesis_closer'])) { + $j = $tokens[$j]['parenthesis_closer']; + + if ($j === $closer) { + // Found the end of the parameter. + break; + } + + continue; + } + + // Skip past short arrays et al as default values. + if (isset($tokens[$j]['bracket_opener'])) { + $j = $tokens[$j]['bracket_closer']; + continue; + } + + if ($tokens[$j]['code'] === \T_COMMA) { + break; + } + } + + $i = ($j - 1); + break; + } + } + + Cache::set($phpcsFile, __METHOD__, $stackPtr, $vars); + return $vars; + } + + /** + * Checks if a given function is a PHP magic function. + * + * @todo Add check for the function declaration being namespaced! + * + * @see \PHPCSUtils\Utils\FunctionDeclaration::$magicFunctions List of names of magic functions. + * @see \PHPCSUtils\Utils\FunctionDeclaration::isMagicFunctionName() For when you already know the name of the + * function and scope checking is done in the + * sniff. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The `T_FUNCTION` token to check. + * + * @return bool + */ + public static function isMagicFunction(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_FUNCTION) { + return false; + } + + if (Scopes::isOOMethod($phpcsFile, $stackPtr) === true) { + return false; + } + + $name = self::getName($phpcsFile, $stackPtr); + return self::isMagicFunctionName($name); + } + + /** + * Verify if a given function name is the name of a PHP magic function. + * + * @see \PHPCSUtils\Utils\FunctionDeclaration::$magicFunctions List of names of magic functions. + * + * @since 1.0.0 + * + * @param string $name The full function name. + * + * @return bool + */ + public static function isMagicFunctionName($name) + { + $name = \strtolower($name); + return (isset(self::$magicFunctions[$name]) === true); + } + + /** + * Checks if a given function is a PHP magic method. + * + * @see \PHPCSUtils\Utils\FunctionDeclaration::$magicMethods List of names of magic methods. + * @see \PHPCSUtils\Utils\FunctionDeclaration::isMagicMethodName() For when you already know the name of the + * method and scope checking is done in the + * sniff. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The `T_FUNCTION` token to check. + * + * @return bool + */ + public static function isMagicMethod(File $phpcsFile, $stackPtr) + { + if (Scopes::isOOMethod($phpcsFile, $stackPtr) === false) { + return false; + } + + $name = self::getName($phpcsFile, $stackPtr); + return self::isMagicMethodName($name); + } + + /** + * Verify if a given function name is the name of a PHP magic method. + * + * @see \PHPCSUtils\Utils\FunctionDeclaration::$magicMethods List of names of magic methods. + * + * @since 1.0.0 + * + * @param string $name The full function name. + * + * @return bool + */ + public static function isMagicMethodName($name) + { + $name = \strtolower($name); + return (isset(self::$magicMethods[$name]) === true); + } + + /** + * Checks if a given function is a non-magic PHP native double underscore method. + * + * @see \PHPCSUtils\Utils\FunctionDeclaration::$methodsDoubleUnderscore List of the PHP native non-magic + * double underscore method names. + * @see \PHPCSUtils\Utils\FunctionDeclaration::isPHPDoubleUnderscoreMethodName() For when you already know the + * name of the method and scope + * checking is done in the sniff. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The `T_FUNCTION` token to check. + * + * @return bool + */ + public static function isPHPDoubleUnderscoreMethod(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_FUNCTION) { + return false; + } + + $scopePtr = Scopes::validDirectScope($phpcsFile, $stackPtr, Tokens::$ooScopeTokens); + if ($scopePtr === false) { + return false; + } + + /* + * If this is a class, make sure it extends something, as otherwise, the methods + * still can't be overloads for the SOAPClient methods. + * For a trait/interface we don't know the concrete implementation context, so skip + * this check. + */ + if ($tokens[$scopePtr]['code'] === \T_CLASS || $tokens[$scopePtr]['code'] === \T_ANON_CLASS) { + $extends = ObjectDeclarations::findExtendedClassName($phpcsFile, $scopePtr); + if ($extends === false) { + return false; + } + } + + $name = self::getName($phpcsFile, $stackPtr); + return self::isPHPDoubleUnderscoreMethodName($name); + } + + /** + * Verify if a given function name is the name of a non-magic PHP native double underscore method. + * + * @see \PHPCSUtils\Utils\FunctionDeclaration::$methodsDoubleUnderscore List of the PHP native non-magic + * double underscore method names. + * + * @since 1.0.0 + * + * @param string $name The full function name. + * + * @return bool + */ + public static function isPHPDoubleUnderscoreMethodName($name) + { + $name = \strtolower($name); + return (isset(self::$methodsDoubleUnderscore[$name]) === true); + } + + /** + * Checks if a given function is a magic method or a PHP native double underscore method. + * + * {@internal Not the most efficient way of checking this, but less efficient ways will get + * less reliable results or introduce a lot of code duplication.} + * + * @see \PHPCSUtils\Utils\FunctionDeclaration::isSpecialMethodName() For when you already know the name of the + * method and scope checking is done in the + * sniff. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The `T_FUNCTION` token to check. + * + * @return bool + */ + public static function isSpecialMethod(File $phpcsFile, $stackPtr) + { + if (self::isMagicMethod($phpcsFile, $stackPtr) === true) { + return true; + } + + if (self::isPHPDoubleUnderscoreMethod($phpcsFile, $stackPtr) === true) { + return true; + } + + return false; + } + + /** + * Verify if a given function name is the name of a magic method or a PHP native double underscore method. + * + * @since 1.0.0 + * + * @param string $name The full function name. + * + * @return bool + */ + public static function isSpecialMethodName($name) + { + $name = \strtolower($name); + return (isset(self::$magicMethods[$name]) === true || isset(self::$methodsDoubleUnderscore[$name]) === true); + } +} diff --git a/PHPCSUtils/Utils/GetTokensAsString.php b/PHPCSUtils/Utils/GetTokensAsString.php new file mode 100644 index 00000000..f18b0c03 --- /dev/null +++ b/PHPCSUtils/Utils/GetTokensAsString.php @@ -0,0 +1,262 @@ +getTokens(); + + if (\is_int($start) === false || isset($tokens[$start]) === false) { + throw new RuntimeException( + 'The $start position for GetTokensAsString methods must exist in the token stack' + ); + } + + if (\is_int($end) === false || $end < $start) { + return ''; + } + + $str = ''; + if ($end >= $phpcsFile->numTokens) { + $end = ($phpcsFile->numTokens - 1); + } + + $lastAdded = null; + for ($i = $start; $i <= $end; $i++) { + if ($stripComments === true && isset(Tokens::$commentTokens[$tokens[$i]['code']])) { + continue; + } + + if ($stripWhitespace === true && $tokens[$i]['code'] === \T_WHITESPACE) { + continue; + } + + if ($compact === true && $tokens[$i]['code'] === \T_WHITESPACE) { + if (isset($lastAdded) === false || $tokens[$lastAdded]['code'] !== \T_WHITESPACE) { + $str .= ' '; + $lastAdded = $i; + } + continue; + } + + // If tabs are being converted to spaces by the tokenizer, the + // original content should be used instead of the converted content. + if ($origContent === true && isset($tokens[$i]['orig_content']) === true) { + $str .= $tokens[$i]['orig_content']; + } else { + $str .= $tokens[$i]['content']; + } + + $lastAdded = $i; + } + + return $str; + } +} diff --git a/PHPCSUtils/Utils/Lists.php b/PHPCSUtils/Utils/Lists.php new file mode 100644 index 00000000..6213bab1 --- /dev/null +++ b/PHPCSUtils/Utils/Lists.php @@ -0,0 +1,332 @@ + '', + '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; + $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) { + $lastNonEmpty = $i; + break; + } 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 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; + } + + $lastNonEmpty = $i; + break; + } + } + + Cache::set($phpcsFile, __METHOD__, $stackPtr, $vars); + return $vars; + } +} diff --git a/PHPCSUtils/Utils/MessageHelper.php b/PHPCSUtils/Utils/MessageHelper.php new file mode 100644 index 00000000..388f2a09 --- /dev/null +++ b/PHPCSUtils/Utils/MessageHelper.php @@ -0,0 +1,145 @@ +addError($message, $stackPtr, $code, $data, $severity); + } + + return $phpcsFile->addWarning($message, $stackPtr, $code, $data, $severity); + } + + /** + * Add a PHPCS message to the output stack as either a fixable warning or a fixable error. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param string $message The message. + * @param int $stackPtr The position of the token + * the message relates to. + * @param bool $isError Whether to report the message as an + * 'error' or 'warning'. + * Defaults to true (error). + * @param string $code The error code for the message. + * Defaults to 'Found'. + * @param array $data Optional input for the data replacements. + * @param int $severity Optional. Severity level. Defaults to 0 which will + * translate to the PHPCS default severity level. + * + * @return bool + */ + public static function addFixableMessage( + File $phpcsFile, + $message, + $stackPtr, + $isError = true, + $code = 'Found', + array $data = [], + $severity = 0 + ) { + if ($isError === true) { + return $phpcsFile->addFixableError($message, $stackPtr, $code, $data, $severity); + } + + return $phpcsFile->addFixableWarning($message, $stackPtr, $code, $data, $severity); + } + + /** + * Convert an arbitrary text string to an alphanumeric string with underscores. + * + * Pre-empt issues in XML and PHP when arbitrary strings are being used as error codes. + * + * @since 1.0.0 + * + * @param string $text Arbitrary text string intended to be used in an error code. + * @param bool $strtolower Whether or not to convert the text string to lowercase. + * + * @return string + */ + public static function stringToErrorcode($text, $strtolower = false) + { + $text = \preg_replace('`[^a-z0-9_]`i', '_', $text); + + if ($strtolower === true) { + $text = \strtolower($text); + } + + return $text; + } + + /** + * Make the whitespace escape codes used in an arbitrary text string visible. + * + * At times, it is useful to show a code snippet in an error message. + * If such a code snippet contains new lines and/or tab or space characters, those would be + * displayed as-is in the command-line report, often breaking the layout of the report + * or making the report harder to read. + * + * This method will convert these characters to their escape codes, making them visible in the + * display string without impacting the report layout. + * + * @see \PHPCSUtils\Utils\GetTokensToString Methods to retrieve a multi-token code snippet. + * @see \PHP_CodeSniffer\Util\Common\prepareForOutput() Similar PHPCS native method. + * + * @since 1.0.0 + * + * @param string $text Arbitrary text string. + * + * @return string + */ + public static function showEscapeChars($text) + { + $search = ["\n", "\r", "\t"]; + $replace = ['\n', '\r', '\t']; + + return \str_replace($search, $replace, $text); + } +} diff --git a/PHPCSUtils/Utils/Namespaces.php b/PHPCSUtils/Utils/Namespaces.php new file mode 100644 index 00000000..4056e037 --- /dev/null +++ b/PHPCSUtils/Utils/Namespaces.php @@ -0,0 +1,389 @@ +getTokens(); + + if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_NAMESPACE) { + throw new RuntimeException('$stackPtr must be of type T_NAMESPACE'); + } + + $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if ($next === false) { + // Live coding or parse error. + return ''; + } + + if (empty($tokens[$stackPtr]['conditions']) === false + || empty($tokens[$stackPtr]['nested_parenthesis']) === false + ) { + /* + * Namespace declarations are only allowed at top level, so this can definitely not + * be a namespace declaration. + */ + if ($tokens[$next]['code'] === \T_NS_SEPARATOR) { + return 'operator'; + } + + return ''; + } + + $start = BCFile::findStartOfStatement($phpcsFile, $stackPtr); + if ($start === $stackPtr + && ($tokens[$next]['code'] === \T_STRING + || $tokens[$next]['code'] === \T_NAME_QUALIFIED + || $tokens[$next]['code'] === \T_OPEN_CURLY_BRACKET) + ) { + return 'declaration'; + } + + if (($tokens[$next]['code'] === \T_NS_SEPARATOR + || $tokens[$next]['code'] === \T_NAME_FULLY_QUALIFIED) // PHP 8.0 parse error. + && ($start !== $stackPtr + || $phpcsFile->findNext($findAfter, ($stackPtr + 1), null, false, null, true) !== false) + ) { + return 'operator'; + } + + return ''; + } + + /** + * Determine whether a T_NAMESPACE token is the keyword for a namespace declaration. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of a `T_NAMESPACE` token. + * + * @return bool `TRUE` if the token passed is the keyword for a namespace declaration. + * `FALSE` if not. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is + * not a `T_NAMESPACE` token. + */ + public static function isDeclaration(File $phpcsFile, $stackPtr) + { + return (self::getType($phpcsFile, $stackPtr) === 'declaration'); + } + + /** + * Determine whether a T_NAMESPACE token is used as an operator. + * + * @link https://www.php.net/language.namespaces.nsconstants PHP Manual about the use of the + * namespace keyword as an operator. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of a `T_NAMESPACE` token. + * + * @return bool `TRUE` if the namespace token passed is used as an operator. `FALSE` if not. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is + * not a `T_NAMESPACE` token. + */ + public static function isOperator(File $phpcsFile, $stackPtr) + { + return (self::getType($phpcsFile, $stackPtr) === 'operator'); + } + + /** + * Get the complete namespace name as declared. + * + * For hierarchical namespaces, the namespace name will be composed of several tokens, + * i.e. "MyProject\Sub\Level", which will be returned together as one string. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of a `T_NAMESPACE` token. + * @param bool $clean Optional. Whether to get the name stripped + * of potentially interlaced whitespace and/or + * comments. Defaults to `true`. + * + * @return string|false The namespace name; or `FALSE` if the specified position is not a + * `T_NAMESPACE` token, the token points to a namespace operator + * or when parse errors are encountered/during live coding. + * > Note: The name can be an empty string for a valid global + * namespace declaration. + */ + public static function getDeclaredName(File $phpcsFile, $stackPtr, $clean = true) + { + try { + if (self::isDeclaration($phpcsFile, $stackPtr) === false) { + // Not a namespace declaration. + return false; + } + } catch (RuntimeException $e) { + // Non-existent token or not a namespace keyword token. + return false; + } + + $endOfStatement = $phpcsFile->findNext(Collections::namespaceDeclarationClosers(), ($stackPtr + 1)); + if ($endOfStatement === false) { + // Live coding or parse error. + return false; + } + + $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), ($endOfStatement + 1), true); + if ($next === $endOfStatement) { + // Declaration of global namespace. I.e.: namespace {}. + // If not a scoped {} namespace declaration, no name/global declarations are invalid + // and result in parse errors, but that's not our concern. + return ''; + } + + if ($clean === false) { + return \trim(GetTokensAsString::origContent($phpcsFile, $next, ($endOfStatement - 1))); + } + + return \trim(GetTokensAsString::noEmpties($phpcsFile, $next, ($endOfStatement - 1))); + } + + /** + * Determine the namespace an arbitrary token lives in. + * + * Take note: + * 1. When a namespace declaration token or a token which is part of the namespace + * name is passed to this method, the result will be `false` as technically, these tokens + * are not _within_ a namespace. + * 2. This method has no opinion on whether the token passed is actually _subject_ + * to namespacing. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The token for which to determine + * the namespace. + * + * @return int|false Token pointer to the namespace keyword for the applicable namespace + * declaration; or `FALSE` if it couldn't be determined or + * if no namespace applies. + */ + public static function findNamespacePtr(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + // Check for the existence of the token. + if (isset($tokens[$stackPtr]) === false) { + return false; + } + + // The namespace keyword in a namespace declaration is itself not namespaced. + if ($tokens[$stackPtr]['code'] === \T_NAMESPACE + && self::isDeclaration($phpcsFile, $stackPtr) === true + ) { + return false; + } + + // Check for scoped namespace {}. + $namespacePtr = Conditions::getCondition($phpcsFile, $stackPtr, \T_NAMESPACE); + if ($namespacePtr !== false) { + return $namespacePtr; + } + + /* + * Not in a scoped namespace, so let's see if we can find a non-scoped namespace instead. + * Keeping in mind that: + * - there can be multiple non-scoped namespaces in a file (bad practice, but is allowed); + * - the namespace keyword can also be used as an operator; + * - a non-named namespace resolves to the global namespace; + * - and that namespace declarations can't be nested in anything, so we can skip over any + * nesting structures. + */ + if (Cache::isCached($phpcsFile, __METHOD__, $stackPtr) === true) { + return Cache::get($phpcsFile, __METHOD__, $stackPtr); + } + + // Start by breaking out of any scoped structures this token is in. + $prev = $stackPtr; + $firstCondition = Conditions::getFirstCondition($phpcsFile, $stackPtr); + if ($firstCondition !== false) { + $prev = $firstCondition; + } + + // And break out of any surrounding parentheses as well. + $firstParensOpener = Parentheses::getFirstOpener($phpcsFile, $prev); + if ($firstParensOpener !== false) { + $prev = $firstParensOpener; + } + + $find = [ + \T_NAMESPACE, + \T_CLOSE_CURLY_BRACKET, + \T_CLOSE_PARENTHESIS, + \T_CLOSE_SHORT_ARRAY, + \T_CLOSE_SQUARE_BRACKET, + \T_DOC_COMMENT_CLOSE_TAG, + \T_ATTRIBUTE_END, + ]; + $returnValue = false; + + do { + $prev = $phpcsFile->findPrevious($find, ($prev - 1)); + if ($prev === false) { + break; + } + + if ($tokens[$prev]['code'] === \T_CLOSE_CURLY_BRACKET) { + // Stop if we encounter a scoped namespace declaration as we already know we're not in one. + if (isset($tokens[$prev]['scope_condition']) === true + && $tokens[$tokens[$prev]['scope_condition']]['code'] === \T_NAMESPACE + ) { + break; + } + + // Skip over other scoped structures for efficiency. + if (isset($tokens[$prev]['scope_condition']) === true) { + $prev = $tokens[$prev]['scope_condition']; + } elseif (isset($tokens[$prev]['scope_opener']) === true) { + // Shouldn't be possible, but just in case. + $prev = $tokens[$prev]['scope_opener']; // @codeCoverageIgnore + } + + continue; + } + + // Skip over other nesting structures for efficiency. + if (isset($tokens[$prev]['bracket_opener']) === true) { + $prev = $tokens[$prev]['bracket_opener']; + continue; + } + + if (isset($tokens[$prev]['parenthesis_owner']) === true) { + $prev = $tokens[$prev]['parenthesis_owner']; + continue; + } elseif (isset($tokens[$prev]['parenthesis_opener']) === true) { + $prev = $tokens[$prev]['parenthesis_opener']; + continue; + } + + // Skip over potentially large attributes. + if (isset($tokens[$prev]['attribute_opener'])) { + $prev = $tokens[$prev]['attribute_opener']; + continue; + } + + // Skip over potentially large docblocks. + if (isset($tokens[$prev]['comment_opener'])) { + $prev = $tokens[$prev]['comment_opener']; + continue; + } + + // So this is a namespace keyword, check if it's a declaration. + if ($tokens[$prev]['code'] === \T_NAMESPACE + && self::isDeclaration($phpcsFile, $prev) === true + ) { + // Now make sure the token was not part of the declaration. + $endOfStatement = $phpcsFile->findNext(Collections::namespaceDeclarationClosers(), ($prev + 1)); + if ($endOfStatement > $stackPtr) { + // Token is part of the declaration, return false. + break; + } + + $returnValue = $prev; + break; + } + } while (true); + + Cache::set($phpcsFile, __METHOD__, $stackPtr, $returnValue); + return $returnValue; + } + + /** + * Determine the namespace name an arbitrary token lives in. + * + * Note: this method has no opinion on whether the token passed is actually _subject_ + * to namespacing. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The token for which to determine + * the namespace. + * + * @return string Namespace name; or an empty string if the namespace couldn't be + * determined or when no namespace applies. + */ + public static function determineNamespace(File $phpcsFile, $stackPtr) + { + $namespacePtr = self::findNamespacePtr($phpcsFile, $stackPtr); + if ($namespacePtr === false) { + return ''; + } + + $namespace = self::getDeclaredName($phpcsFile, $namespacePtr); + if ($namespace !== false) { + return $namespace; + } + + return ''; + } +} diff --git a/PHPCSUtils/Utils/NamingConventions.php b/PHPCSUtils/Utils/NamingConventions.php new file mode 100644 index 00000000..c13d0a21 --- /dev/null +++ b/PHPCSUtils/Utils/NamingConventions.php @@ -0,0 +1,117 @@ + Note: for variable names, the leading dollar sign - `$` - needs to be + * removed prior to passing the name to this method. + * + * @return bool + */ + public static function isValidIdentifierName($name) + { + if (\is_string($name) === false || $name === '' || \strpos($name, ' ') !== false) { + return false; + } + + return (\preg_match(self::PHP_LABEL_REGEX, $name) === 1); + } + + /** + * Check if two arbitrary identifier names will be seen as the same in PHP. + * + * This method should not be used for variable or constant names, but *should* be used + * when comparing namespace, class/trait/interface and function names. + * + * Variable and constant names in PHP are case-sensitive, except for constants explicitely + * declared case-insensitive using the third parameter for + * {@link https://www.php.net/function.define `define()`}. + * + * All other names are case-insensitive for the most part, but as it's PHP, not completely. + * Basically ASCII chars used are case-insensitive, but anything from 0x80 up is case-sensitive. + * + * This method takes this case-(in)sensitivity into account when comparing identifier names. + * + * Note: this method does not check whether the passed names would be valid for identifiers! + * The {@see \PHPCSUtils\Utils\NamingConventions::isValidIdentifierName()} method should be used + * to verify that, if necessary. + * + * @since 1.0.0 + * + * @param string $nameA The first identifier name. + * @param string $nameB The second identifier name. + * + * @return bool `TRUE` if these names would be considered the same in PHP; `FALSE` otherwise. + */ + public static function isEqual($nameA, $nameB) + { + // Simple quick check first. + if ($nameA === $nameB) { + return true; + } + + // OK, so these may be different names or they may be the same name with case differences. + $nameA = \strtr($nameA, self::AZ_UPPER, self::AZ_LOWER); + $nameB = \strtr($nameB, self::AZ_UPPER, self::AZ_LOWER); + + return ($nameA === $nameB); + } +} diff --git a/PHPCSUtils/Utils/Numbers.php b/PHPCSUtils/Utils/Numbers.php new file mode 100644 index 00000000..053643d7 --- /dev/null +++ b/PHPCSUtils/Utils/Numbers.php @@ -0,0 +1,322 @@ +[0-9]+) + | + (?P([0-9]*\.(?P>LNUM)|(?P>LNUM)\.[0-9]*)) + ) + [e][+-]?(?P>LNUM) + ) + | + (?P>DNUM) + | + (?:0|[1-9][0-9]*) + )$ + `ixD'; + + /** + * Retrieve information about a number token. + * + * Helper function to deal with numeric literals, potentially with underscore separators + * and/or explicit octal notation. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of a T_LNUMBER or T_DNUMBER token. + * + * @return array An array with information about the number. + * The format of the array return value is: + * ```php + * array( + * 'orig_content' => string, // The original content of the token(s); + * 'content' => string, // The content, underscore(s) removed; + * 'code' => int, // The token code of the number, either + * // T_LNUMBER or T_DNUMBER. + * 'type' => string, // The token type, either 'T_LNUMBER' + * // or 'T_DNUMBER'. + * 'decimal' => string, // The decimal value of the number; + * 'last_token' => int, // The stackPtr to the last token which was + * // part of the number. + * // At this time, this will be always be the original + * // stackPtr. This may change in the future if + * // new numeric syntaxes would be added to PHP. + * ) + * ``` + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token is not of type + * `T_LNUMBER` or `T_DNUMBER`. + */ + public static function getCompleteNumber(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]) === false + || ($tokens[$stackPtr]['code'] !== \T_LNUMBER && $tokens[$stackPtr]['code'] !== \T_DNUMBER) + ) { + throw new RuntimeException( + 'Token type "' . $tokens[$stackPtr]['type'] . '" is not T_LNUMBER or T_DNUMBER' + ); + } + + $content = $tokens[$stackPtr]['content']; + return [ + 'orig_content' => $content, + 'content' => \str_replace('_', '', $content), + 'code' => $tokens[$stackPtr]['code'], + 'type' => $tokens[$stackPtr]['type'], + 'decimal' => self::getDecimalValue($content), + 'last_token' => $stackPtr, + ]; + } + + /** + * Get the decimal number value of a numeric string. + * + * Takes PHP 7.4 numeric literal separators and explicit octal literals in numbers into account. + * + * @since 1.0.0 + * + * @param string $textString Arbitrary text string. + * This text string should be the (combined) token content of + * one or more tokens which together represent a number in PHP. + * + * @return string|false Decimal number as a string or `FALSE` if the passed parameter + * was not a numeric string. + * > Note: floating point numbers with exponent will not be expanded, + * but returned as-is. + */ + public static function getDecimalValue($textString) + { + if (\is_string($textString) === false || $textString === '') { + return false; + } + + /* + * Remove potential PHP 7.4 numeric literal separators. + * + * {@internal While the is..() functions also do this, this is still needed + * here to allow the hexdec(), bindec() functions to work correctly and for + * the decimal/float to return a cross-version compatible decimal value.} + */ + $textString = \str_replace('_', '', $textString); + + if (self::isDecimalInt($textString) === true) { + return $textString; + } + + if (self::isHexidecimalInt($textString) === true) { + return (string) \hexdec($textString); + } + + if (self::isBinaryInt($textString) === true) { + return (string) \bindec($textString); + } + + if (self::isOctalInt($textString) === true) { + return (string) \octdec($textString); + } + + if (self::isFloat($textString) === true) { + return $textString; + } + + return false; + } + + /** + * Verify whether the contents of an arbitrary string represents a decimal integer. + * + * Takes PHP 7.4 numeric literal separators in numbers into account. + * + * @since 1.0.0 + * + * @param string $textString Arbitrary string. + * + * @return bool + */ + public static function isDecimalInt($textString) + { + if (\is_string($textString) === false || $textString === '') { + return false; + } + + // Remove potential PHP 7.4 numeric literal separators. + $textString = \str_replace('_', '', $textString); + + return (\preg_match(self::REGEX_DECIMAL_INT, $textString) === 1); + } + + /** + * Verify whether the contents of an arbitrary string represents a hexidecimal integer. + * + * Takes PHP 7.4 numeric literal separators in numbers into account. + * + * @since 1.0.0 + * + * @param string $textString Arbitrary string. + * + * @return bool + */ + public static function isHexidecimalInt($textString) + { + if (\is_string($textString) === false || $textString === '') { + return false; + } + + // Remove potential PHP 7.4 numeric literal separators. + $textString = \str_replace('_', '', $textString); + + return (\preg_match(self::REGEX_HEX_INT, $textString) === 1); + } + + /** + * Verify whether the contents of an arbitrary string represents a binary integer. + * + * Takes PHP 7.4 numeric literal separators in numbers into account. + * + * @since 1.0.0 + * + * @param string $textString Arbitrary string. + * + * @return bool + */ + public static function isBinaryInt($textString) + { + if (\is_string($textString) === false || $textString === '') { + return false; + } + + // Remove potential PHP 7.4 numeric literal separators. + $textString = \str_replace('_', '', $textString); + + return (\preg_match(self::REGEX_BINARY_INT, $textString) === 1); + } + + /** + * Verify whether the contents of an arbitrary string represents an octal integer. + * + * Takes PHP 7.4 numeric literal separators and explicit octal literals in numbers into account. + * + * @since 1.0.0 + * + * @param string $textString Arbitrary string. + * + * @return bool + */ + public static function isOctalInt($textString) + { + if (\is_string($textString) === false || $textString === '') { + return false; + } + + // Remove potential PHP 7.4 numeric literal separators. + $textString = \str_replace('_', '', $textString); + + return (\preg_match(self::REGEX_OCTAL_INT, $textString) === 1); + } + + /** + * Verify whether the contents of an arbitrary string represents a floating point number. + * + * Takes PHP 7.4 numeric literal separators in numbers into account. + * + * @since 1.0.0 + * + * @param string $textString Arbitrary string. + * + * @return bool + */ + public static function isFloat($textString) + { + if (\is_string($textString) === false || $textString === '') { + return false; + } + + // Remove potential PHP 7.4 numeric literal separators. + $textString = \str_replace('_', '', $textString); + + return (\preg_match(self::REGEX_FLOAT, $textString) === 1); + } +} diff --git a/PHPCSUtils/Utils/ObjectDeclarations.php b/PHPCSUtils/Utils/ObjectDeclarations.php new file mode 100644 index 00000000..4b4c05ca --- /dev/null +++ b/PHPCSUtils/Utils/ObjectDeclarations.php @@ -0,0 +1,360 @@ +getTokens(); + + if (isset($tokens[$stackPtr]) === false + || ($tokens[$stackPtr]['code'] === \T_ANON_CLASS || $tokens[$stackPtr]['code'] === \T_CLOSURE) + ) { + return null; + } + + $tokenCode = $tokens[$stackPtr]['code']; + + if ($tokenCode !== \T_FUNCTION + && $tokenCode !== \T_CLASS + && $tokenCode !== \T_INTERFACE + && $tokenCode !== \T_TRAIT + && $tokenCode !== \T_ENUM + ) { + throw new RuntimeException( + 'Token type "' . $tokens[$stackPtr]['type'] + . '" is not T_FUNCTION, T_CLASS, T_INTERFACE, T_TRAIT or T_ENUM' + ); + } + + if ($tokenCode === \T_FUNCTION + && \strtolower($tokens[$stackPtr]['content']) !== 'function' + ) { + // This is a function declared without the "function" keyword. + // So this token is the function name. + return $tokens[$stackPtr]['content']; + } + + /* + * Determine the name. Note that we cannot simply look for the first T_STRING + * because an (invalid) class name starting with a number will be multiple tokens. + * Whitespace or comment are however not allowed within a name. + */ + + $stopPoint = $phpcsFile->numTokens; + if ($tokenCode === \T_FUNCTION && isset($tokens[$stackPtr]['parenthesis_opener']) === true) { + $stopPoint = $tokens[$stackPtr]['parenthesis_opener']; + } elseif (isset($tokens[$stackPtr]['scope_opener']) === true) { + $stopPoint = $tokens[$stackPtr]['scope_opener']; + } + + $exclude = Tokens::$emptyTokens; + $exclude[] = \T_OPEN_PARENTHESIS; + $exclude[] = \T_OPEN_CURLY_BRACKET; + $exclude[] = \T_BITWISE_AND; + $exclude[] = \T_COLON; // Backed enums. + + $nameStart = $phpcsFile->findNext($exclude, ($stackPtr + 1), $stopPoint, true); + if ($nameStart === false) { + // Live coding or parse error. + return null; + } + + $tokenAfterNameEnd = $phpcsFile->findNext($exclude, $nameStart, $stopPoint); + + if ($tokenAfterNameEnd === false) { + return $tokens[$nameStart]['content']; + } + + // Name starts with number, so is composed of multiple tokens. + return GetTokensAsString::noEmpties($phpcsFile, $nameStart, ($tokenAfterNameEnd - 1)); + } + + /** + * Retrieves the implementation properties of a class. + * + * Main differences with the PHPCS version: + * - Bugs fixed: + * - Handling of PHPCS annotations. + * - Handling of unorthodox docblock placement. + * - Defensive coding against incorrect calls to this method. + * - Support for PHP 8.2 readonly classes. + * - Additional `'abstract_token'`, `'final_token'`, and `'readonly_token'` indexes in the return array. + * + * @see \PHP_CodeSniffer\Files\File::getClassProperties() Original source. + * @see \PHPCSUtils\BackCompat\BCFile::getClassProperties() Cross-version compatible version of the original. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position in the stack of the `T_CLASS` + * token to acquire the properties for. + * + * @return array Array with implementation properties of a class. + * The format of the return value is: + * ```php + * array( + * 'is_abstract' => bool, // TRUE if the abstract keyword was found. + * 'abstract_token' => int|false, // The stack pointer to the `abstract` keyword or + * // FALSE if the abstract keyword was not found. + * 'is_final' => bool, // TRUE if the final keyword was found. + * 'final_token' => int|false, // The stack pointer to the `final` keyword or + * // FALSE if the abstract keyword was not found. + * 'is_readonly' => bool, // TRUE if the readonly keyword was found. + * 'readonly_token' => int|false, // The stack pointer to the `readonly` keyword or + * // FALSE if the abstract keyword was not found. + * ); + * ``` + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a + * `T_CLASS` token. + */ + public static function getClassProperties(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_CLASS) { + throw new RuntimeException('$stackPtr must be of type T_CLASS'); + } + + $valid = Collections::classModifierKeywords() + Tokens::$emptyTokens; + $properties = [ + 'is_abstract' => false, + 'abstract_token' => false, + 'is_final' => false, + 'final_token' => false, + 'is_readonly' => false, + 'readonly_token' => false, + ]; + + for ($i = ($stackPtr - 1); $i > 0; $i--) { + if (isset($valid[$tokens[$i]['code']]) === false) { + break; + } + + switch ($tokens[$i]['code']) { + case \T_ABSTRACT: + $properties['is_abstract'] = true; + $properties['abstract_token'] = $i; + break; + + case \T_FINAL: + $properties['is_final'] = true; + $properties['final_token'] = $i; + break; + + case \T_READONLY: + $properties['is_readonly'] = true; + $properties['readonly_token'] = $i; + break; + } + } + + return $properties; + } + + /** + * Retrieves the name of the class that the specified class extends. + * + * Works for classes, anonymous classes and interfaces, though it is strongly recommended + * to use the {@see \PHPCSUtils\Utils\ObjectDeclarations::findExtendedInterfaceNames()} + * method to examine interfaces instead. Interfaces can extend multiple parent interfaces, + * and that use-case is not handled by this method. + * + * Main differences with the PHPCS version: + * - Bugs fixed: + * - Handling of PHPCS annotations. + * - Handling of comments. + * - Handling of the namespace keyword used as operator. + * - Improved handling of parse errors. + * - The returned name will be clean of superfluous whitespace and/or comments. + * - Support for PHP 8.0 tokenization of identifier/namespaced names, cross-version PHP & PHPCS. + * + * @see \PHP_CodeSniffer\Files\File::findExtendedClassName() Original source. + * @see \PHPCSUtils\BackCompat\BCFile::findExtendedClassName() Cross-version compatible version of + * the original. + * @see \PHPCSUtils\Utils\ObjectDeclarations::findExtendedInterfaceNames() Similar method for extended interfaces. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The stack position of the class or interface. + * + * @return string|false The extended class name or `FALSE` on error or if there + * is no extended class name. + */ + public static function findExtendedClassName(File $phpcsFile, $stackPtr) + { + $names = self::findNames($phpcsFile, $stackPtr, \T_EXTENDS, Collections::ooCanExtend()); + if ($names === false) { + return false; + } + + // Classes can only extend one parent class. + return \array_shift($names); + } + + /** + * Retrieves the names of the interfaces that the specified class or enum implements. + * + * Main differences with the PHPCS version: + * - Bugs fixed: + * - Handling of PHPCS annotations. + * - Handling of comments. + * - Handling of the namespace keyword used as operator. + * - Improved handling of parse errors. + * - The returned name(s) will be clean of superfluous whitespace and/or comments. + * - Support for PHP 8.0 tokenization of identifier/namespaced names, cross-version PHP & PHPCS. + * + * @see \PHP_CodeSniffer\Files\File::findImplementedInterfaceNames() Original source. + * @see \PHPCSUtils\BackCompat\BCFile::findImplementedInterfaceNames() Cross-version compatible version of + * the original. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The stack position of the class or enum token. + * + * @return array|false Array with names of the implemented interfaces or `FALSE` on + * error or if there are no implemented interface names. + */ + public static function findImplementedInterfaceNames(File $phpcsFile, $stackPtr) + { + return self::findNames($phpcsFile, $stackPtr, \T_IMPLEMENTS, Collections::ooCanImplement()); + } + + /** + * Retrieves the names of the interfaces that the specified interface extends. + * + * @see \PHPCSUtils\Utils\ObjectDeclarations::findExtendedClassName() Similar method for extended classes. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The stack position of the interface keyword. + * + * @return array|false Array with names of the extended interfaces or `FALSE` on + * error or if there are no extended interface names. + */ + public static function findExtendedInterfaceNames(File $phpcsFile, $stackPtr) + { + return self::findNames( + $phpcsFile, + $stackPtr, + \T_EXTENDS, + [\T_INTERFACE => \T_INTERFACE] + ); + } + + /** + * Retrieves the names of the extended classes or interfaces or the implemented + * interfaces that the specific class/interface declaration extends/implements. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The stack position of the + * class/interface declaration keyword. + * @param int $keyword The token constant for the keyword to examine. + * Either `T_EXTENDS` or `T_IMPLEMENTS`. + * @param array $allowedFor Array of OO types for which use of the keyword + * is allowed. + * + * @return array|false Returns an array of names or `FALSE` on error or when the object + * being declared does not extend/implement another object. + */ + private static function findNames(File $phpcsFile, $stackPtr, $keyword, array $allowedFor) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]) === false + || isset($allowedFor[$tokens[$stackPtr]['code']]) === false + || isset($tokens[$stackPtr]['scope_opener']) === false + ) { + return false; + } + + $scopeOpener = $tokens[$stackPtr]['scope_opener']; + $keywordPtr = $phpcsFile->findNext($keyword, ($stackPtr + 1), $scopeOpener); + if ($keywordPtr === false) { + return false; + } + + $find = Collections::namespacedNameTokens() + Tokens::$emptyTokens; + $names = []; + $end = $keywordPtr; + do { + $start = ($end + 1); + $end = $phpcsFile->findNext($find, $start, ($scopeOpener + 1), true); + $name = GetTokensAsString::noEmpties($phpcsFile, $start, ($end - 1)); + + if (\trim($name) !== '') { + $names[] = $name; + } + } while ($tokens[$end]['code'] === \T_COMMA); + + if (empty($names)) { + return false; + } + + return $names; + } +} diff --git a/PHPCSUtils/Utils/Operators.php b/PHPCSUtils/Utils/Operators.php new file mode 100644 index 00000000..ae155c13 --- /dev/null +++ b/PHPCSUtils/Utils/Operators.php @@ -0,0 +1,252 @@ + => + */ + private static $extraUnaryIndicators = [ + \T_STRING_CONCAT => true, + \T_RETURN => true, + \T_EXIT => true, + \T_CONTINUE => true, + \T_BREAK => true, + \T_ECHO => true, + \T_PRINT => true, + \T_YIELD => true, + \T_COMMA => true, + \T_OPEN_PARENTHESIS => true, + \T_OPEN_SQUARE_BRACKET => true, + \T_OPEN_SHORT_ARRAY => true, + \T_OPEN_CURLY_BRACKET => true, + \T_COLON => true, + \T_INLINE_THEN => true, + \T_INLINE_ELSE => true, + \T_CASE => true, + \T_FN_ARROW => true, + \T_MATCH_ARROW => true, + ]; + + /** + * Determine if the passed token is a reference operator. + * + * Main differences with the PHPCS version: + * - Defensive coding against incorrect calls to this method. + * + * @see \PHP_CodeSniffer\Files\File::isReference() Original source. + * @see \PHPCSUtils\BackCompat\BCFile::isReference() Cross-version compatible version of the original. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the `T_BITWISE_AND` token. + * + * @return bool `TRUE` if the specified token position represents a reference. + * `FALSE` if the token represents a bitwise operator. + */ + public static function isReference(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_BITWISE_AND) { + return false; + } + + $tokenBefore = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + + if (isset(Collections::functionDeclarationTokens()[$tokens[$tokenBefore]['code']]) === true) { + // Function returns a reference. + return true; + } + + if ($tokens[$tokenBefore]['code'] === \T_DOUBLE_ARROW) { + // Inside a foreach loop or array assignment, this is a reference. + return true; + } + + if ($tokens[$tokenBefore]['code'] === \T_AS) { + // Inside a foreach loop, this is a reference. + return true; + } + + if (isset(Tokens::$assignmentTokens[$tokens[$tokenBefore]['code']]) === true) { + // This is directly after an assignment. It's a reference. Even if + // it is part of an operation, the other tests will handle it. + return true; + } + + $tokenAfter = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + + if ($tokens[$tokenAfter]['code'] === \T_NEW) { + return true; + } + + $lastOpener = Parentheses::getLastOpener($phpcsFile, $stackPtr); + if ($lastOpener !== false) { + $lastOwner = Parentheses::getOwner($phpcsFile, $lastOpener); + + if (isset(Collections::functionDeclarationTokens()[$tokens[$lastOwner]['code']]) === true + // As of PHPCS 4.x, `T_USE` is a parenthesis owner. + || $tokens[$lastOwner]['code'] === \T_USE + ) { + $params = FunctionDeclarations::getParameters($phpcsFile, $lastOwner); + foreach ($params as $param) { + if ($param['reference_token'] === $stackPtr) { + // Function parameter declared to be passed by reference. + return true; + } + } + } + } + + /* + * Pass by reference in function calls, assign by reference in arrays and + * closure use by reference in PHPCS 3.x. + */ + if ($tokens[$tokenBefore]['code'] === \T_OPEN_PARENTHESIS + || $tokens[$tokenBefore]['code'] === \T_COMMA + || $tokens[$tokenBefore]['code'] === \T_OPEN_SHORT_ARRAY + ) { + if ($tokens[$tokenAfter]['code'] === \T_VARIABLE) { + return true; + } else { + $skip = Tokens::$emptyTokens; + $skip += Collections::namespacedNameTokens(); + $skip += Collections::ooHierarchyKeywords(); + $skip[] = \T_DOUBLE_COLON; + + $nextSignificantAfter = $phpcsFile->findNext( + $skip, + ($stackPtr + 1), + null, + true + ); + if ($tokens[$nextSignificantAfter]['code'] === \T_VARIABLE) { + return true; + } + } + } + + return false; + } + + /** + * Determine whether a T_MINUS/T_PLUS token is a unary operator. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the plus/minus token. + * + * @return bool `TRUE` if the token passed is a unary operator. + * `FALSE` otherwise, i.e. if the token is an arithmetic operator, + * or if the token is not a `T_PLUS`/`T_MINUS` token. + */ + public static function isUnaryPlusMinus(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]) === false + || ($tokens[$stackPtr]['code'] !== \T_PLUS + && $tokens[$stackPtr]['code'] !== \T_MINUS) + ) { + return false; + } + + $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if ($next === false) { + // Live coding or parse error. + return false; + } + + if (isset(Tokens::$operators[$tokens[$next]['code']]) === true) { + // Next token is an operator, so this is not a unary. + return false; + } + + $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + + /* + * Check the preceeding token for an indication that this is not an arithmetic operation. + */ + if (isset(Tokens::$operators[$tokens[$prev]['code']]) === true + || isset(Tokens::$comparisonTokens[$tokens[$prev]['code']]) === true + || isset(Tokens::$booleanOperators[$tokens[$prev]['code']]) === true + || isset(Tokens::$assignmentTokens[$tokens[$prev]['code']]) === true + || isset(Tokens::$castTokens[$tokens[$prev]['code']]) === true + || isset(self::$extraUnaryIndicators[$tokens[$prev]['code']]) === true + ) { + return true; + } + + return false; + } + + /** + * Determine whether a ternary is a short ternary/elvis operator, i.e. without "middle". + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the ternary then/else + * operator in the stack. + * + * @return bool `TRUE` if short ternary; or `FALSE` otherwise. + */ + public static function isShortTernary(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + if (isset($tokens[$stackPtr]) === false) { + return false; + } + + if ($tokens[$stackPtr]['code'] === \T_INLINE_THEN) { + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if ($nextNonEmpty !== false && $tokens[$nextNonEmpty]['code'] === \T_INLINE_ELSE) { + return true; + } + } + + if ($tokens[$stackPtr]['code'] === \T_INLINE_ELSE) { + $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + if ($prevNonEmpty !== false && $tokens[$prevNonEmpty]['code'] === \T_INLINE_THEN) { + return true; + } + } + + // Not a ternary operator token. + return false; + } +} diff --git a/PHPCSUtils/Utils/Orthography.php b/PHPCSUtils/Utils/Orthography.php new file mode 100644 index 00000000..cd9f35b1 --- /dev/null +++ b/PHPCSUtils/Utils/Orthography.php @@ -0,0 +1,120 @@ + An orthography is a set of conventions for writing a language. It includes norms of spelling, + * > hyphenation, capitalization, word breaks, emphasis, and punctuation. + * > Source: https://en.wikipedia.org/wiki/Orthography + * + * @since 1.0.0 + */ +final class Orthography +{ + + /** + * Characters which are considered terminal points for a sentence. + * + * @link https://www.thepunctuationguide.com/terminal-points.html Punctuation guide on terminal points. + * + * @since 1.0.0 + * + * @var string + */ + const TERMINAL_POINTS = '.?!'; + + /** + * Check if the first character of an arbitrary text string is a capital letter. + * + * Letter characters which do not have a concept of lower/uppercase will + * be accepted as correctly capitalized. + * + * @since 1.0.0 + * + * @param string $textString The text string to examine. + * This can be the contents of a text string token, + * but also, for instance, a comment text. + * Potential text delimiter quotes should be stripped + * off a text string before passing it to this method. + * Also see: {@see \PHPCSUtils\Utils\TextStrings::stripQuotes()}. + * + * @return bool `TRUE` when the first character is a capital letter or a letter + * which doesn't have a concept of capitalization. + * `FALSE` otherwise, including for non-letter characters. + */ + public static function isFirstCharCapitalized($textString) + { + $textString = \ltrim($textString); + return (\preg_match('`^[\p{Lu}\p{Lt}\p{Lo}]`u', $textString) > 0); + } + + /** + * Check if the first character of an arbitrary text string is a lowercase letter. + * + * @since 1.0.0 + * + * @param string $textString The text string to examine. + * This can be the contents of a text string token, + * but also, for instance, a comment text. + * Potential text delimiter quotes should be stripped + * off a text string before passing it to this method. + * Also see: {@see \PHPCSUtils\Utils\TextStrings::stripQuotes()}. + * + * @return bool `TRUE` when the first character is a lowercase letter. + * `FALSE` otherwise, including for letters which don't have a concept of + * capitalization and for non-letter characters. + */ + public static function isFirstCharLowercase($textString) + { + $textString = \ltrim($textString); + return (\preg_match('`^\p{Ll}`u', $textString) > 0); + } + + /** + * Check if the last character of an arbitrary text string is a valid punctuation character. + * + * @since 1.0.0 + * + * @param string $textString The text string to examine. + * This can be the contents of a text string token, + * but also, for instance, a comment text. + * Potential text delimiter quotes should be stripped + * off a text string before passing it to this method. + * Also see: {@see \PHPCSUtils\Utils\TextStrings::stripQuotes()}. + * @param string $allowedChars Characters which are considered valid punctuation + * to end the text string. + * Defaults to `'.?!'`, i.e. a full stop, question mark + * or exclamation mark. + * + * @return bool + */ + public static function isLastCharPunctuation($textString, $allowedChars = self::TERMINAL_POINTS) + { + $encoding = Helper::getEncoding(); + $textString = \rtrim($textString); + + if (\function_exists('iconv_substr') === true) { + $lastChar = \iconv_substr($textString, -1, 1, $encoding); + } else { + $lastChar = \substr($textString, -1); + } + + if (\function_exists('iconv_strpos') === true) { + return (\iconv_strpos($allowedChars, $lastChar, 0, $encoding) !== false); + } else { + return (\strpos($allowedChars, $lastChar) !== false); + } + } +} diff --git a/PHPCSUtils/Utils/Parentheses.php b/PHPCSUtils/Utils/Parentheses.php new file mode 100644 index 00000000..2e7384e0 --- /dev/null +++ b/PHPCSUtils/Utils/Parentheses.php @@ -0,0 +1,419 @@ + => + */ + private static $extraParenthesesOwners = [ + \T_ISSET => \T_ISSET, + \T_UNSET => \T_UNSET, + \T_EMPTY => \T_EMPTY, + \T_EXIT => \T_EXIT, + \T_EVAL => \T_EVAL, + ]; + + /** + * Get the stack pointer to the parentheses owner of an open/close parenthesis. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position of `T_OPEN/CLOSE_PARENTHESIS` token. + * + * @return int|false Integer stack pointer to the parentheses owner; or `FALSE` if the + * parenthesis does not have a (direct) owner or if the token passed + * was not a parenthesis. + */ + public static function getOwner(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]['parenthesis_owner'])) { + return $tokens[$stackPtr]['parenthesis_owner']; + } + + /* + * As the 'parenthesis_owner' index is only set on parentheses, we didn't need to do any + * input validation before, but now we do. + */ + if (isset($tokens[$stackPtr]) === false + || ($tokens[$stackPtr]['code'] !== \T_OPEN_PARENTHESIS + && $tokens[$stackPtr]['code'] !== \T_CLOSE_PARENTHESIS) + ) { + return false; + } + + if ($tokens[$stackPtr]['code'] === \T_CLOSE_PARENTHESIS) { + $stackPtr = $tokens[$stackPtr]['parenthesis_opener']; + } + + $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + if ($prevNonEmpty !== false + && isset(self::$extraParenthesesOwners[$tokens[$prevNonEmpty]['code']]) === true + ) { + return $prevNonEmpty; + } + + return false; + } + + /** + * Check whether the parenthesis owner of an open/close parenthesis is within a limited + * set of valid owners. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position of `T_OPEN/CLOSE_PARENTHESIS` token. + * @param int|string|array $validOwners Array of token constants for the owners + * which should be considered valid. + * + * @return bool `TRUE` if the owner is within the list of `$validOwners`; `FALSE` if not and + * if the parenthesis does not have a (direct) owner. + */ + public static function isOwnerIn(File $phpcsFile, $stackPtr, $validOwners) + { + $owner = self::getOwner($phpcsFile, $stackPtr); + if ($owner === false) { + return false; + } + + $tokens = $phpcsFile->getTokens(); + $validOwners = (array) $validOwners; + + return \in_array($tokens[$owner]['code'], $validOwners, true); + } + + /** + * Check whether the passed token is nested within parentheses owned by one of the valid owners. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position of the token we are checking. + * @param int|string|array $validOwners Array of token constants for the owners + * which should be considered valid. + * + * @return bool + */ + public static function hasOwner(File $phpcsFile, $stackPtr, $validOwners) + { + return (self::nestedParensWalker($phpcsFile, $stackPtr, $validOwners) !== false); + } + + /** + * Retrieve the stack pointer to the parentheses opener of the first (outer) set of parentheses + * an arbitrary token is wrapped in. + * + * If the optional `$validOwners` parameter is passed, the stack pointer to the opener to + * the first set of parentheses, which has an owner which is in the list of valid owners, + * will be returned. This may be a nested set of parentheses. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position of the token we are checking. + * @param int|string|array $validOwners Array of token constants for the owners + * which should be considered valid. + * + * @return int|false Integer stack pointer to the parentheses opener; or `FALSE` if the token + * does not have parentheses owned by any of the valid owners or if + * the token is not nested in parentheses at all. + */ + public static function getFirstOpener(File $phpcsFile, $stackPtr, $validOwners = []) + { + return self::nestedParensWalker($phpcsFile, $stackPtr, $validOwners, false); + } + + /** + * Retrieve the stack pointer to the parentheses closer of the first (outer) set of parentheses + * an arbitrary token is wrapped in. + * + * If the optional `$validOwners` parameter is passed, the stack pointer to the closer to + * the first set of parentheses, which has an owner which is in the list of valid owners, + * will be returned. This may be a nested set of parentheses. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position of the token we are checking. + * @param int|string|array $validOwners Array of token constants for the owners + * which should be considered valid. + * + * @return int|false Integer stack pointer to the parentheses closer; or `FALSE` if the token + * does not have parentheses owned by any of the valid owners or if + * the token is not nested in parentheses at all. + */ + public static function getFirstCloser(File $phpcsFile, $stackPtr, $validOwners = []) + { + $opener = self::getFirstOpener($phpcsFile, $stackPtr, $validOwners); + $tokens = $phpcsFile->getTokens(); + if ($opener !== false && isset($tokens[$opener]['parenthesis_closer']) === true) { + return $tokens[$opener]['parenthesis_closer']; + } + + return false; + } + + /** + * Retrieve the stack pointer to the parentheses owner of the first (outer) set of parentheses + * an arbitrary token is wrapped in. + * + * If the optional `$validOwners` parameter is passed, the stack pointer to the owner of + * the first set of parentheses, which has an owner which is in the list of valid owners, + * will be returned. This may be a nested set of parentheses. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position of the token we are checking. + * @param int|string|array $validOwners Array of token constants for the owners + * which should be considered valid. + * + * @return int|false Integer stack pointer to the parentheses owner; or `FALSE` if the token + * does not have parentheses owned by any of the valid owners or if + * the token is not nested in parentheses at all. + */ + public static function getFirstOwner(File $phpcsFile, $stackPtr, $validOwners = []) + { + $opener = self::getFirstOpener($phpcsFile, $stackPtr, $validOwners); + if ($opener !== false) { + return self::getOwner($phpcsFile, $opener); + } + + return false; + } + + /** + * Retrieve the stack pointer to the parentheses opener of the last (inner) set of parentheses + * an arbitrary token is wrapped in. + * + * If the optional `$validOwners` parameter is passed, the stack pointer to the opener to + * the last set of parentheses, which has an owner which is in the list of valid owners, + * will be returned. This may be a set of parentheses higher up. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position of the token we are checking. + * @param int|string|array $validOwners Array of token constants for the owners + * which should be considered valid. + * + * @return int|false Integer stack pointer to the parentheses opener; or `FALSE` if the token + * does not have parentheses owned by any of the valid owners or if + * the token is not nested in parentheses at all. + */ + public static function getLastOpener(File $phpcsFile, $stackPtr, $validOwners = []) + { + return self::nestedParensWalker($phpcsFile, $stackPtr, $validOwners, true); + } + + /** + * Retrieve the stack pointer to the parentheses closer of the last (inner) set of parentheses + * an arbitrary token is wrapped in. + * + * If the optional `$validOwners` parameter is passed, the stack pointer to the closer to + * the last set of parentheses, which has an owner which is in the list of valid owners, + * will be returned. This may be a set of parentheses higher up. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position of the token we are checking. + * @param int|string|array $validOwners Array of token constants for the owners + * which should be considered valid. + * + * @return int|false Integer stack pointer to the parentheses closer; or `FALSE` if the token + * does not have parentheses owned by any of the valid owners or if + * the token is not nested in parentheses at all. + */ + public static function getLastCloser(File $phpcsFile, $stackPtr, $validOwners = []) + { + $opener = self::getLastOpener($phpcsFile, $stackPtr, $validOwners); + $tokens = $phpcsFile->getTokens(); + if ($opener !== false && isset($tokens[$opener]['parenthesis_closer']) === true) { + return $tokens[$opener]['parenthesis_closer']; + } + + return false; + } + + /** + * Retrieve the stack pointer to the parentheses owner of the last (inner) set of parentheses + * an arbitrary token is wrapped in. + * + * If the optional `$validOwners` parameter is passed, the stack pointer to the owner of + * the last set of parentheses, which has an owner which is in the list of valid owners, + * will be returned. This may be a set of parentheses higher up. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position of the token we are checking. + * @param int|string|array $validOwners Array of token constants for the owners + * which should be considered valid. + * + * @return int|false Integer stack pointer to the parentheses owner; or `FALSE` if the token + * does not have parentheses owned by any of the valid owners or if + * the token is not nested in parentheses at all. + */ + public static function getLastOwner(File $phpcsFile, $stackPtr, $validOwners = []) + { + $opener = self::getLastOpener($phpcsFile, $stackPtr, $validOwners); + if ($opener !== false) { + return self::getOwner($phpcsFile, $opener); + } + + return false; + } + + /** + * Check whether the owner of the outermost wrapping set of parentheses of an arbitrary token + * is within a limited set of acceptable token types. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position in the stack of the + * token to verify. + * @param int|string|array $validOwners Array of token constants for the owners + * which should be considered valid. + * + * @return int|false Integer stack pointer to the valid parentheses owner; or `FALSE` if + * the token was not wrapped in parentheses or if the outermost set + * of parentheses in which the token is wrapped does not have an owner + * within the set of owners considered valid. + */ + public static function firstOwnerIn(File $phpcsFile, $stackPtr, $validOwners) + { + $opener = self::getFirstOpener($phpcsFile, $stackPtr); + + if ($opener !== false && self::isOwnerIn($phpcsFile, $opener, $validOwners) === true) { + return self::getOwner($phpcsFile, $opener); + } + + return false; + } + + /** + * Check whether the owner of the innermost wrapping set of parentheses of an arbitrary token + * is within a limited set of acceptable token types. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position in the stack of the + * token to verify. + * @param int|string|array $validOwners Array of token constants for the owners + * which should be considered valid. + * + * @return int|false Integer stack pointer to the valid parentheses owner; or `FALSE` if + * the token was not wrapped in parentheses or if the innermost set + * of parentheses in which the token is wrapped does not have an owner + * within the set of owners considered valid. + */ + public static function lastOwnerIn(File $phpcsFile, $stackPtr, $validOwners) + { + $opener = self::getLastOpener($phpcsFile, $stackPtr); + + if ($opener !== false && self::isOwnerIn($phpcsFile, $opener, $validOwners) === true) { + return self::getOwner($phpcsFile, $opener); + } + + return false; + } + + /** + * Helper method. Retrieve the position of a parentheses opener for an arbitrary passed token. + * + * If no `$validOwners` are specified, the opener to the first set of parentheses surrounding + * the token - or if `$reverse = true`, the last set of parentheses - will be returned. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position of the token we are checking. + * @param int|string|array $validOwners Optional. Array of token constants for the owners + * which should be considered valid. + * @param bool $reverse Optional. Whether to search for the first/outermost + * (`false`) or the last/innermost (`true`) set of + * parentheses with the specified owner(s). + * + * @return int|false Integer stack pointer to the parentheses opener; or `FALSE` if the token + * does not have parentheses owned by any of the valid owners or if + * the token is not nested in parentheses at all. + */ + private static function nestedParensWalker(File $phpcsFile, $stackPtr, $validOwners = [], $reverse = false) + { + $tokens = $phpcsFile->getTokens(); + + // Check for the existence of the token. + if (isset($tokens[$stackPtr]) === false) { + return false; + } + + // Make sure the token is nested in parenthesis. + if (empty($tokens[$stackPtr]['nested_parenthesis']) === true) { + return false; + } + + $validOwners = (array) $validOwners; + $parentheses = $tokens[$stackPtr]['nested_parenthesis']; + + if (empty($validOwners) === true) { + // No owners specified, just return the first/last parentheses opener. + if ($reverse === true) { + \end($parentheses); + } else { + \reset($parentheses); + } + + return \key($parentheses); + } + + if ($reverse === true) { + $parentheses = \array_reverse($parentheses, true); + } + + foreach ($parentheses as $opener => $closer) { + if (self::isOwnerIn($phpcsFile, $opener, $validOwners) === true) { + // We found a token with a valid owner. + return $opener; + } + } + + return false; + } +} diff --git a/PHPCSUtils/Utils/PassedParameters.php b/PHPCSUtils/Utils/PassedParameters.php new file mode 100644 index 00000000..a40bc544 --- /dev/null +++ b/PHPCSUtils/Utils/PassedParameters.php @@ -0,0 +1,502 @@ + => + */ + private static $callParsingStopPoints = [ + \T_COMMA => \T_COMMA, + \T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY, + \T_OPEN_SQUARE_BRACKET => \T_OPEN_SQUARE_BRACKET, + \T_OPEN_PARENTHESIS => \T_OPEN_PARENTHESIS, + \T_DOC_COMMENT_OPEN_TAG => \T_DOC_COMMENT_OPEN_TAG, + \T_ATTRIBUTE => \T_ATTRIBUTE, + ]; + + /** + * Checks if any parameters have been passed. + * + * - If passed a `T_STRING`, `T_NAME_FULLY_QUALIFIED`, `T_NAME_RELATIVE`, `T_NAME_QUALIFIED` + * or `T_VARIABLE` stack pointer, it will treat it as a function call. + * If a `T_STRING` or `T_VARIABLE` which is *not* a function call is passed, the behaviour is + * undetermined. + * - If passed a `T_ANON_CLASS` stack pointer, it will accept it as a class instantiation. + * - If passed a `T_SELF`, `T_STATIC` or `T_PARENT` stack pointer, it will accept it as a + * class instantiation function call when used like `new self()`. + * - If passed a `T_ARRAY` or `T_OPEN_SHORT_ARRAY` stack pointer, it will detect + * whether the array has values or is empty. + * For purposes of backward-compatibility with older PHPCS versions, `T_OPEN_SQUARE_BRACKET` + * tokens will also be accepted and will be checked whether they are in reality + * a short array opener. + * - If passed a `T_ISSET` or `T_UNSET` stack pointer, it will detect whether those + * language constructs have "parameters". + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position of function call name, + * language construct or array open token. + * @param true|null $isShortArray Optional. Short-circuit the short array check for + * `T_OPEN_SHORT_ARRAY` tokens if it isn't necessary. + * Efficiency tweak for when this has already been established, + * Use with EXTREME care. + * + * @return bool + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the token passed is not one of the + * accepted types or doesn't exist. + */ + public static function hasParameters(File $phpcsFile, $stackPtr, $isShortArray = null) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]) === false + || isset(Collections::parameterPassingTokens()[$tokens[$stackPtr]['code']]) === false + ) { + throw new RuntimeException( + 'The hasParameters() method expects a function call, array, isset or unset token to be passed.' + ); + } + + if (isset(Collections::ooHierarchyKeywords()[$tokens[$stackPtr]['code']]) === true) { + $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + if ($tokens[$prev]['code'] !== \T_NEW) { + throw new RuntimeException( + 'The hasParameters() method expects a function call, array, isset or unset token to be passed.' + ); + } + } + + if (isset(Collections::shortArrayListOpenTokensBC()[$tokens[$stackPtr]['code']]) === true + && $isShortArray !== true + && Arrays::isShortArray($phpcsFile, $stackPtr) === false + ) { + throw new RuntimeException( + 'The hasParameters() method expects a function call, array, isset or unset token to be passed.' + ); + } + + $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if ($next === false) { + return false; + } + + // Deal with short array syntax. + if (isset(Collections::shortArrayListOpenTokensBC()[$tokens[$stackPtr]['code']]) === true) { + if ($next === $tokens[$stackPtr]['bracket_closer']) { + // No parameters. + return false; + } + + return true; + } + + // Deal with function calls, long arrays, isset and unset. + // Next non-empty token should be the open parenthesis. + if ($tokens[$next]['code'] !== \T_OPEN_PARENTHESIS) { + return false; + } + + if (isset($tokens[$next]['parenthesis_closer']) === false) { + return false; + } + + $ignore = Tokens::$emptyTokens; + $ignore[\T_ELLIPSIS] = \T_ELLIPSIS; // Prevent PHP 8.1 first class callables from being seen as function calls. + + $closeParenthesis = $tokens[$next]['parenthesis_closer']; + $nextNextNonEmpty = $phpcsFile->findNext($ignore, ($next + 1), ($closeParenthesis + 1), true); + + if ($nextNextNonEmpty === $closeParenthesis) { + // No parameters. + return false; + } + + return true; + } + + /** + * Get information on all parameters passed. + * + * See {@see PassedParameters::hasParameters()} for information on the supported constructs. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position of function call name, + * language construct or array open token. + * @param int $limit Optional. Limit the parameter retrieval to the first # + * parameters/array entries. + * Use with care on function calls, as this can break + * support for named parameters! + * @param true|null $isShortArray Optional. Short-circuit the short array check for + * `T_OPEN_SHORT_ARRAY` tokens if it isn't necessary. + * Efficiency tweak for when this has already been established, + * Use with EXTREME care. + * + * @return array A multi-dimentional array with information on each parameter/array item. + * The information gathered about each parameter/array item is in the following format: + * ```php + * 1 => array( + * 'start' => int, // The stack pointer to the first token in the parameter/array item. + * 'end' => int, // The stack pointer to the last token in the parameter/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. + * ) + * ``` + * If a named parameter is encountered in a function call, the top-level index will not be + * the parameter _position_, but the _parameter name_ and the array will include two extra keys: + * ```php + * 'parameter_name' => array( + * 'name' => string, // The parameter name (without the colon). + * 'name_token' => int, // The stack pointer to the parameter name token. + * ... + * ) + * ``` + * The `'start'`, `'end'`, `'raw'` and `'clean'` indexes will always contain just and only + * information on the parameter value. + * _Note: The array starts at index 1 for positional parameters._ + * _The key for named parameters will be the parameter name._ + * If no parameters/array items are found, an empty array will be returned. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the token passed is not one of the + * accepted types or doesn't exist. + */ + public static function getParameters(File $phpcsFile, $stackPtr, $limit = 0, $isShortArray = null) + { + if (self::hasParameters($phpcsFile, $stackPtr, $isShortArray) === false) { + return []; + } + + $effectiveLimit = (\is_int($limit) && $limit > 0) ? $limit : 0; + + if (Cache::isCached($phpcsFile, __METHOD__, "$stackPtr-$effectiveLimit") === true) { + return Cache::get($phpcsFile, __METHOD__, "$stackPtr-$effectiveLimit"); + } + + if ($effectiveLimit !== 0 && Cache::isCached($phpcsFile, __METHOD__, "$stackPtr-0") === true) { + return \array_slice(Cache::get($phpcsFile, __METHOD__, "$stackPtr-0"), 0, $effectiveLimit, true); + } + + // Ok, we know we have a valid token with parameters and valid open & close brackets/parenthesis. + $tokens = $phpcsFile->getTokens(); + + // Mark the beginning and end tokens. + if (isset(Collections::shortArrayListOpenTokensBC()[$tokens[$stackPtr]['code']]) === true) { + $opener = $stackPtr; + $closer = $tokens[$stackPtr]['bracket_closer']; + } else { + $opener = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + $closer = $tokens[$opener]['parenthesis_closer']; + } + + $mayHaveNames = (isset(Collections::functionCallTokens()[$tokens[$stackPtr]['code']]) === true); + + $parameters = []; + $nextComma = $opener; + $paramStart = ($opener + 1); + $cnt = 1; + $stopPoints = self::$callParsingStopPoints + Tokens::$scopeOpeners; + $stopPoints[] = $tokens[$closer]['code']; + + while (($nextComma = $phpcsFile->findNext($stopPoints, ($nextComma + 1), ($closer + 1))) !== false) { + // Ignore anything within square brackets. + if (isset($tokens[$nextComma]['bracket_opener'], $tokens[$nextComma]['bracket_closer']) + && $nextComma === $tokens[$nextComma]['bracket_opener'] + ) { + $nextComma = $tokens[$nextComma]['bracket_closer']; + continue; + } + + // Skip past nested arrays, function calls and arbitrary groupings. + if ($tokens[$nextComma]['code'] === \T_OPEN_PARENTHESIS + && isset($tokens[$nextComma]['parenthesis_closer']) + ) { + $nextComma = $tokens[$nextComma]['parenthesis_closer']; + continue; + } + + // Skip past closures, anonymous classes and anything else scope related. + if (isset($tokens[$nextComma]['scope_condition'], $tokens[$nextComma]['scope_closer']) + && $tokens[$nextComma]['scope_condition'] === $nextComma + ) { + $nextComma = $tokens[$nextComma]['scope_closer']; + continue; + } + + // Skip over potentially large docblocks. + if ($tokens[$nextComma]['code'] === \T_DOC_COMMENT_OPEN_TAG + && isset($tokens[$nextComma]['comment_closer']) + ) { + $nextComma = $tokens[$nextComma]['comment_closer']; + continue; + } + + // Skip over attributes. + if ($tokens[$nextComma]['code'] === \T_ATTRIBUTE + && isset($tokens[$nextComma]['attribute_closer']) + ) { + $nextComma = $tokens[$nextComma]['attribute_closer']; + continue; + } + + if ($tokens[$nextComma]['code'] !== \T_COMMA + && $tokens[$nextComma]['code'] !== $tokens[$closer]['code'] + ) { + // Just in case. + continue; // @codeCoverageIgnore + } + + // Ok, we've reached the end of the parameter. + $paramEnd = ($nextComma - 1); + $key = $cnt; + + if ($mayHaveNames === true) { + $firstNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $paramStart, ($paramEnd + 1), true); + if ($firstNonEmpty !== $paramEnd) { + $secondNonEmpty = $phpcsFile->findNext( + Tokens::$emptyTokens, + ($firstNonEmpty + 1), + ($paramEnd + 1), + true + ); + + if ($tokens[$secondNonEmpty]['code'] === \T_COLON + && $tokens[$firstNonEmpty]['code'] === \T_PARAM_NAME + ) { + if (isset($parameters[$tokens[$firstNonEmpty]['content']]) === false) { + // Set the key to be the name, but only if we've not seen this name before. + $key = $tokens[$firstNonEmpty]['content']; + } + + $parameters[$key]['name'] = $tokens[$firstNonEmpty]['content']; + $parameters[$key]['name_token'] = $firstNonEmpty; + $paramStart = ($secondNonEmpty + 1); + } + } + } + + $parameters[$key]['start'] = $paramStart; + $parameters[$key]['end'] = $paramEnd; + $parameters[$key]['raw'] = \trim(GetTokensAsString::normal($phpcsFile, $paramStart, $paramEnd)); + $parameters[$key]['clean'] = \trim(GetTokensAsString::noComments($phpcsFile, $paramStart, $paramEnd)); + + // Check if there are more tokens before the closing parenthesis. + // Prevents function calls with trailing comma's from setting an extra parameter: + // `functionCall( $param1, $param2, );`. + $hasNextParam = $phpcsFile->findNext( + Tokens::$emptyTokens, + ($nextComma + 1), + $closer, + true + ); + if ($hasNextParam === false) { + // Reached the end, so for the purpose of caching, this should be saved as if no limit was set. + $effectiveLimit = 0; + break; + } + + // Stop if there is a valid limit and the limit has been reached. + if ($effectiveLimit !== 0 && $cnt === $effectiveLimit) { + break; + } + + // Prepare for the next parameter. + $paramStart = ($nextComma + 1); + ++$cnt; + } + + if ($effectiveLimit !== 0 && $cnt === $effectiveLimit) { + Cache::set($phpcsFile, __METHOD__, "$stackPtr-$effectiveLimit", $parameters); + } else { + // Limit is 0 or total items is less than effective limit. + Cache::set($phpcsFile, __METHOD__, "$stackPtr-0", $parameters); + } + + return $parameters; + } + + /** + * Get information on a specific parameter passed. + * + * See {@see PassedParameters::hasParameters()} for information on the supported constructs. + * + * @see PassedParameters::getParameterFromStack() For when the parameter stack of a function call is + * already retrieved. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position of function call name, + * language construct or array open token. + * @param int $paramOffset The 1-based index position of the parameter to retrieve. + * @param string|string[] $paramNames Optional. Either the name of the target parameter + * to retrieve as a string or an array of names for the + * same target parameter. + * Only relevant for function calls. + * An arrays of names is supported to allow for functions + * for which the parameter names have undergone name + * changes over time. + * When specified, the name will take precedence over the + * offset. + * For PHP 8 support, it is STRONGLY recommended to + * always pass both the offset as well as the parameter + * name when examining function calls. + * + * @return array|false Array with information on the parameter/array item at the specified offset, + * or with the specified name. + * Or `FALSE` if the specified parameter/array item is not found. + * See {@see PassedParameters::getParameters()} for the format of the returned + * (single-dimensional) array. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the token passed is not one of the + * accepted types or doesn't exist. + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If a function call parameter is requested and + * the `$paramName` parameter is not passed. + */ + public static function getParameter(File $phpcsFile, $stackPtr, $paramOffset, $paramNames = []) + { + $tokens = $phpcsFile->getTokens(); + + if (empty($paramNames) === true) { + $parameters = self::getParameters($phpcsFile, $stackPtr, $paramOffset); + } else { + $parameters = self::getParameters($phpcsFile, $stackPtr); + } + + /* + * Non-function calls. + */ + if (isset(Collections::functionCallTokens()[$tokens[$stackPtr]['code']]) === false) { + if (isset($parameters[$paramOffset]) === true) { + return $parameters[$paramOffset]; + } + + return false; + } + + /* + * Function calls. + */ + return self::getParameterFromStack($parameters, $paramOffset, $paramNames); + } + + /** + * Count the number of parameters which have been passed. + * + * See {@see PassedParameters::hasParameters()} for information on the supported constructs. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position of function call name, + * language construct or array open token. + * + * @return int + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the token passed is not one of the + * accepted types or doesn't exist. + */ + public static function getParameterCount(File $phpcsFile, $stackPtr) + { + if (self::hasParameters($phpcsFile, $stackPtr) === false) { + return 0; + } + + return \count(self::getParameters($phpcsFile, $stackPtr)); + } + + /** + * Get information on a specific function call parameter passed. + * + * This is an efficiency method to correctly handle positional versus named parameters + * for function calls when multiple parameters need to be examined. + * + * See {@see PassedParameters::hasParameters()} for information on the supported constructs. + * + * @since 1.0.0 + * + * @param array $parameters The output of a previous call to {@see PassedParameters::getParameters()}. + * @param int $paramOffset The 1-based index position of the parameter to retrieve. + * @param string|string[] $paramNames Either the name of the target parameter to retrieve + * as a string or an array of names for the same target parameter. + * An array of names is supported to allow for functions + * for which the parameter names have undergone name + * changes over time. + * The name will take precedence over the offset. + * + * @return array|false Array with information on the parameter at the specified offset, + * or with the specified name. + * Or `FALSE` if the specified parameter is not found. + * See {@see PassedParameters::getParameters()} for the format of the returned + * (single-dimensional) array. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the `$paramNames` parameter is not passed + * and the requested parameter was not passed + * as a positional parameter in the function call + * being examined. + */ + public static function getParameterFromStack(array $parameters, $paramOffset, $paramNames) + { + if (empty($parameters) === true) { + return false; + } + + // First check for a named parameter. + if (empty($paramNames) === false) { + $paramNames = (array) $paramNames; + foreach ($paramNames as $name) { + // Note: parameter names are case-sensitive!. + if (isset($parameters[$name]) === true) { + return $parameters[$name]; + } + } + } + + // Next check for positional parameters. + if (isset($parameters[$paramOffset]) === true + && isset($parameters[$paramOffset]['name']) === false + ) { + return $parameters[$paramOffset]; + } + + if (empty($paramNames) === true) { + throw new RuntimeException( + 'To allow for support for PHP 8 named parameters, the $paramNames parameter must be passed.' + ); + } + + return false; + } +} diff --git a/PHPCSUtils/Utils/Scopes.php b/PHPCSUtils/Utils/Scopes.php new file mode 100644 index 00000000..32947c9e --- /dev/null +++ b/PHPCSUtils/Utils/Scopes.php @@ -0,0 +1,143 @@ +getTokens(); + $validScopes = (array) $validScopes; + + if (\in_array($tokens[$ptr]['code'], $validScopes, true) === true) { + return $ptr; + } + } + + return false; + } + + /** + * Check whether a T_CONST token is a class/interface/trait/enum constant declaration. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position in the stack of the + * `T_CONST` token to verify. + * + * @return bool + */ + public static function isOOConstant(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_CONST) { + return false; + } + + if (self::validDirectScope($phpcsFile, $stackPtr, Collections::ooConstantScopes()) !== false) { + return true; + } + + return false; + } + + /** + * Check whether a T_VARIABLE token is a class/trait property declaration. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position in the stack of the + * `T_VARIABLE` token to verify. + * + * @return bool + */ + public static function isOOProperty(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_VARIABLE) { + return false; + } + + $scopePtr = self::validDirectScope($phpcsFile, $stackPtr, Collections::ooPropertyScopes()); + if ($scopePtr !== false) { + // Make sure it's not a method parameter. + $deepestOpen = Parentheses::getLastOpener($phpcsFile, $stackPtr); + if ($deepestOpen === false + || $deepestOpen < $scopePtr + || Parentheses::isOwnerIn($phpcsFile, $deepestOpen, \T_FUNCTION) === false + ) { + return true; + } + } + + return false; + } + + /** + * Check whether a T_FUNCTION token is a class/interface/trait/enum method declaration. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position in the stack of the + * `T_FUNCTION` token to verify. + * + * @return bool + */ + public static function isOOMethod(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_FUNCTION) { + return false; + } + + if (self::validDirectScope($phpcsFile, $stackPtr, Tokens::$ooScopeTokens) !== false) { + return true; + } + + return false; + } +} diff --git a/PHPCSUtils/Utils/TextStrings.php b/PHPCSUtils/Utils/TextStrings.php new file mode 100644 index 00000000..76592a5c --- /dev/null +++ b/PHPCSUtils/Utils/TextStrings.php @@ -0,0 +1,331 @@ +[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)(?:\??->(?P>varname)|\[[^\]\'"\s]+\])?`'; + + /** + * Get the complete contents of a - potentially multi-line - text string. + * + * PHPCS tokenizes multi-line text strings with a single token for each line. + * This method can be used to retrieve the text string as it would be received and + * processed in PHP itself. + * + * This method is particularly useful for sniffs which examine the contents of text strings, + * where the content matching might result in false positives/false negatives if the text + * were to be examined line by line. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr Pointer to the first text string token + * of a - potentially multi-line - text string + * or to a Nowdoc/Heredoc opener. + * @param bool $stripQuotes Optional. Whether to strip text delimiter + * quotes off the resulting text string. + * Defaults to `true`. + * + * @return string The contents of the complete text string. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a + * valid text string token. + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token is not the _first_ + * token in a text string. + */ + public static function getCompleteTextString(File $phpcsFile, $stackPtr, $stripQuotes = true) + { + $tokens = $phpcsFile->getTokens(); + $end = self::getEndOfCompleteTextString($phpcsFile, $stackPtr); + + $stripNewline = false; + if ($tokens[$stackPtr]['code'] === \T_START_HEREDOC || $tokens[$stackPtr]['code'] === \T_START_NOWDOC) { + $stripQuotes = false; + $stripNewline = true; + $stackPtr = ($stackPtr + 1); + } + + $contents = GetTokensAsString::normal($phpcsFile, $stackPtr, $end); + + if ($stripNewline === true) { + // Heredoc/nowdoc: strip the new line at the end of the string to emulate how PHP sees the string. + $contents = \rtrim($contents, "\r\n"); + } + + if ($stripQuotes === true) { + return self::stripQuotes($contents); + } + + return $contents; + } + + /** + * Get the stack pointer to the end of a - potentially multi-line - text string. + * + * @see \PHPCSUtils\Utils\TextStrings::getCompleteTextString() Retrieve the contents of a complete - potentially + * multi-line - text string. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr Pointer to the first text string token + * of a - potentially multi-line - text string + * or to a Nowdoc/Heredoc opener. + * + * @return int Stack pointer to the last token in the text string. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a + * valid text string token. + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token is not the _first_ + * token in a text string. + */ + public static function getEndOfCompleteTextString(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + // Must be the start of a text string token. + if (isset($tokens[$stackPtr], Collections::textStringStartTokens()[$tokens[$stackPtr]['code']]) === false) { + throw new RuntimeException( + '$stackPtr must be of type T_START_HEREDOC, T_START_NOWDOC, T_CONSTANT_ENCAPSED_STRING' + . ' or T_DOUBLE_QUOTED_STRING' + ); + } + + if (isset(Tokens::$stringTokens[$tokens[$stackPtr]['code']]) === true) { + $prev = $phpcsFile->findPrevious(\T_WHITESPACE, ($stackPtr - 1), null, true); + if ($tokens[$stackPtr]['code'] === $tokens[$prev]['code']) { + throw new RuntimeException('$stackPtr must be the start of the text string'); + } + } + + if (Cache::isCached($phpcsFile, __METHOD__, $stackPtr) === true) { + return Cache::get($phpcsFile, __METHOD__, $stackPtr); + } + + switch ($tokens[$stackPtr]['code']) { + case \T_START_HEREDOC: + $targetType = \T_HEREDOC; + $current = ($stackPtr + 1); + break; + + case \T_START_NOWDOC: + $targetType = \T_NOWDOC; + $current = ($stackPtr + 1); + break; + + default: + $targetType = $tokens[$stackPtr]['code']; + $current = $stackPtr; + break; + } + + while (isset($tokens[$current]) && $tokens[$current]['code'] === $targetType) { + ++$current; + } + + $lastPtr = ($current - 1); + + Cache::set($phpcsFile, __METHOD__, $stackPtr, $lastPtr); + return $lastPtr; + } + + /** + * Strip text delimiter quotes from an arbitrary text string. + * + * Intended for use with the "content" of a `T_CONSTANT_ENCAPSED_STRING` / `T_DOUBLE_QUOTED_STRING`. + * + * - Prevents stripping mis-matched quotes. + * - Prevents stripping quotes from the textual content of the text string. + * + * @since 1.0.0 + * + * @param string $textString The raw text string. + * + * @return string Text string without quotes around it. + */ + public static function stripQuotes($textString) + { + return \preg_replace('`^([\'"])(.*)\1$`Ds', '$2', $textString); + } + + /** + * Get the embedded variables/expressions from an arbitrary string. + * + * Note: this function gets the complete variables/expressions _as they are embedded_, + * i.e. including potential curly brace wrappers, array access, method calls etc. + * + * @since 1.0.0 + * + * @param string $text The contents of a T_DOUBLE_QUOTED_STRING or T_HEREDOC token. + * + * @return array Array of encountered variable names/expressions with the offset at which + * the variable/expression was found in the string, as the key. + */ + public static function getEmbeds($text) + { + return self::getStripEmbeds($text)['embeds']; + } + + /** + * Strip embedded variables/expressions from an arbitrary string. + * + * @since 1.0.0 + * + * @param string $text The contents of a T_DOUBLE_QUOTED_STRING or T_HEREDOC token. + * + * @return string String without variables/expressions in it. + */ + public static function stripEmbeds($text) + { + return self::getStripEmbeds($text)['remaining']; + } + + /** + * Split an arbitrary text string into embedded variables/expressions and remaining text. + * + * PHP contains four types of embedding syntaxes: + * 1. Directly embedding variables ("$foo"); + * 2. Braces outside the variable ("{$foo}"); + * 3. Braces after the dollar sign ("${foo}"); + * 4. Variable variables ("${expr}", equivalent to (string) ${expr}). + * + * Type 3 and 4 are deprecated as of PHP 8.2 and will be removed in PHP 9.0. + * + * This method handles all types of embeds, including recognition of whether an embed is escaped or not. + * + * @link https://www.php.net/language.types.string#language.types.string.parsing PHP Manual on string parsing + * @link https://wiki.php.net/rfc/deprecate_dollar_brace_string_interpolation PHP RFC on deprecating select + * string interpolation syntaxes + * + * @since 1.0.0 + * + * @param string $text The contents of a T_DOUBLE_QUOTED_STRING or T_HEREDOC token. + * + * @return array Array containing two values: + * 1. An array containing a string representation of each embed encountered. + * The keys in this array are the integer offset within the original string + * where the embed was found. + * 2. The textual contents, embeds stripped out of it. + * The format of the array return value is: + * ```php + * array( + * 'embeds' => array, + * 'remaining' => string, + * ) + * ``` + */ + public static function getStripEmbeds($text) + { + if (\strpos($text, '$') === false) { + return [ + 'embeds' => [], + 'remaining' => $text, + ]; + } + + $textHash = \md5($text); + if (NoFileCache::isCached(__METHOD__, $textHash) === true) { + return NoFileCache::get(__METHOD__, $textHash); + } + + $offset = 0; + $strLen = \strlen($text); // Use iconv ? + $stripped = ''; + $variables = []; + + while (\preg_match(self::START_OF_EMBED, $text, $matches, \PREG_OFFSET_CAPTURE, $offset) === 1) { + $stripped .= \substr($text, $offset, ($matches[2][1] - $offset)); + + $matchedExpr = $matches[2][0]; + $matchedOffset = $matches[2][1]; + $braces = \substr_count($matchedExpr, '{'); + $newOffset = $matchedOffset + \strlen($matchedExpr); + + if ($braces === 0) { + /* + * Type 1: simple variable embed. + * Regex will always return a match due to the look ahead in the above regex. + */ + \preg_match(self::TYPE1_EMBED_AFTER_DOLLAR, $text, $endMatch, 0, $newOffset); + $matchedExpr .= $endMatch[0]; + $variables[$matchedOffset] = $matchedExpr; + $offset = $newOffset + \strlen($endMatch[0]); + continue; + } + + for (; $newOffset < $strLen; $newOffset++) { + if ($text[$newOffset] === '{') { + ++$braces; + continue; + } + + if ($text[$newOffset] === '}') { + --$braces; + if ($braces === 0) { + $matchedExpr = \substr($text, $matchedOffset, (1 + $newOffset - $matchedOffset)); + $variables[$matchedOffset] = $matchedExpr; + $offset = ($newOffset + 1); + break; + } + } + } + } + + if ($offset < $strLen) { + // Add the end of the string. + $stripped .= \substr($text, $offset); + } + + $returnValue = [ + 'embeds' => $variables, + 'remaining' => $stripped, + ]; + + NoFileCache::set(__METHOD__, $textHash, $returnValue); + return $returnValue; + } +} diff --git a/PHPCSUtils/Utils/UseStatements.php b/PHPCSUtils/Utils/UseStatements.php new file mode 100644 index 00000000..4d0e6d6d --- /dev/null +++ b/PHPCSUtils/Utils/UseStatements.php @@ -0,0 +1,431 @@ +getTokens(); + + if (isset($tokens[$stackPtr]) === false + || $tokens[$stackPtr]['code'] !== \T_USE + ) { + throw new RuntimeException('$stackPtr must be of type T_USE'); + } + + $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if ($next === false) { + // Live coding or parse error. + return ''; + } + + // More efficient & simpler check for closure use in PHPCS 4.x. + if (isset($tokens[$stackPtr]['parenthesis_owner']) + && $tokens[$stackPtr]['parenthesis_owner'] === $stackPtr + ) { + return 'closure'; + } + + $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + if ($prev !== false && $tokens[$prev]['code'] === \T_CLOSE_PARENTHESIS + && Parentheses::isOwnerIn($phpcsFile, $prev, \T_CLOSURE) === true + ) { + return 'closure'; + } + + $lastCondition = Conditions::getLastCondition($phpcsFile, $stackPtr); + if ($lastCondition === false || $tokens[$lastCondition]['code'] === \T_NAMESPACE) { + // Global or scoped namespace and not a closure use statement. + return 'import'; + } + + $traitScopes = Tokens::$ooScopeTokens; + // Only classes, traits and enums can import traits. + unset($traitScopes[\T_INTERFACE]); + + if (isset($traitScopes[$tokens[$lastCondition]['code']]) === true) { + return 'trait'; + } + + return ''; + } + + /** + * Determine whether a T_USE token represents a closure use statement. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the `T_USE` token. + * + * @return bool `TRUE` if the token passed is a closure use statement. + * `FALSE` if it's not. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a + * `T_USE` token. + */ + public static function isClosureUse(File $phpcsFile, $stackPtr) + { + return (self::getType($phpcsFile, $stackPtr) === 'closure'); + } + + /** + * Determine whether a T_USE token represents a class/function/constant import use statement. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the `T_USE` token. + * + * @return bool `TRUE` if the token passed is an import use statement. + * `FALSE` if it's not. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a + * `T_USE` token. + */ + public static function isImportUse(File $phpcsFile, $stackPtr) + { + return (self::getType($phpcsFile, $stackPtr) === 'import'); + } + + /** + * Determine whether a T_USE token represents a trait use statement. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the `T_USE` token. + * + * @return bool `TRUE` if the token passed is a trait use statement. + * `FALSE` if it's not. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a + * `T_USE` token. + */ + public static function isTraitUse(File $phpcsFile, $stackPtr) + { + return (self::getType($phpcsFile, $stackPtr) === 'trait'); + } + + /** + * Split an import use statement into individual imports. + * + * Handles single import, multi-import and group-import use statements. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position in the stack of the `T_USE` token. + * + * @return array A multi-level array containing information about the use statement. + * The first level is `'name'`, `'function'` and `'const'`. These keys will always exist. + * If any statements are found for any of these categories, the second level + * will contain the alias/name as the key and the full original use name as the + * value for each of the found imports or an empty array if no imports were found + * in this use statement for a particular category. + * + * For example, for this function group use statement: + * ```php + * use function Vendor\Package\{ + * LevelA\Name as Alias, + * LevelB\Another_Name, + * }; + * ``` + * the return value would look like this: + * ```php + * array( + * 'name' => array(), + * 'function' => array( + * 'Alias' => 'Vendor\Package\LevelA\Name', + * 'Another_Name' => 'Vendor\Package\LevelB\Another_Name', + * ), + * 'const' => array(), + * ) + * ``` + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a + * `T_USE` token. + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the `T_USE` token is not for an import + * use statement. + */ + public static function splitImportUseStatement(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_USE) { + throw new RuntimeException('$stackPtr must be of type T_USE'); + } + + if (self::isImportUse($phpcsFile, $stackPtr) === false) { + throw new RuntimeException('$stackPtr must be an import use statement'); + } + + $statements = [ + 'name' => [], + 'function' => [], + 'const' => [], + ]; + + $endOfStatement = $phpcsFile->findNext([\T_SEMICOLON, \T_CLOSE_TAG], ($stackPtr + 1)); + if ($endOfStatement === false) { + // Live coding or parse error. + return $statements; + } + + if (Cache::isCached($phpcsFile, __METHOD__, $stackPtr) === true) { + return Cache::get($phpcsFile, __METHOD__, $stackPtr); + } + + ++$endOfStatement; + + $start = true; + $useGroup = false; + $hasAlias = false; + $baseName = ''; + $name = ''; + $type = ''; + $fixedType = false; + $alias = ''; + + for ($i = ($stackPtr + 1); $i < $endOfStatement; $i++) { + if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) { + continue; + } + + $tokenCode = $tokens[$i]['code']; + switch ($tokenCode) { + case \T_STRING: + // Only when either at the start of the statement or at the start of a new sub within a group. + if ($start === true && $fixedType === false) { + $content = \strtolower($tokens[$i]['content']); + if ($content === 'function' + || $content === 'const' + ) { + $type = $content; + $start = false; + if ($useGroup === false) { + $fixedType = true; + } + + break; + } else { + $type = 'name'; + } + } + + $start = false; + + if ($hasAlias === false) { + $name .= $tokens[$i]['content']; + } + + $alias = $tokens[$i]['content']; + break; + + case \T_NAME_QUALIFIED: + case \T_NAME_FULLY_QUALIFIED: // This would be a parse error, but handle it anyway. + /* + * PHPCS 4.x. + * + * These tokens can only be encountered when either at the start of the statement + * or at the start of a new sub within a group. + */ + if ($start === true && $fixedType === false) { + $type = 'name'; + } + + $start = false; + + if ($hasAlias === false) { + $name .= $tokens[$i]['content']; + } + + $alias = \substr($tokens[$i]['content'], (\strrpos($tokens[$i]['content'], '\\') + 1)); + break; + + case \T_AS: + $hasAlias = true; + break; + + case \T_OPEN_USE_GROUP: + $start = true; + $useGroup = true; + $baseName = $name; + $name = ''; + break; + + case \T_SEMICOLON: + case \T_CLOSE_TAG: + case \T_CLOSE_USE_GROUP: + case \T_COMMA: + if ($name !== '') { + if ($useGroup === true) { + $statements[$type][$alias] = $baseName . $name; + } else { + $statements[$type][$alias] = $name; + } + } + + if ($tokenCode !== \T_COMMA) { + break 2; + } + + // Reset. + $start = true; + $name = ''; + $hasAlias = false; + if ($fixedType === false) { + $type = ''; + } + break; + + case \T_NS_SEPARATOR: + $name .= $tokens[$i]['content']; + break; + + /* + * Fall back in case reserved keyword is (illegally) used in name. + * Parse error, but not our concern. + */ + default: + if ($hasAlias === false) { + // Defensive coding, just in case. Should no longer be possible since PHPCS 3.7.0. + $name .= $tokens[$i]['content']; // @codeCoverageIgnore + } + + $alias = $tokens[$i]['content']; + break; + } + } + + Cache::set($phpcsFile, __METHOD__, $stackPtr, $statements); + return $statements; + } + + /** + * Split an import use statement into individual imports and merge it with an array of previously + * seen import use statements. + * + * Beware: this method should only be used to combine the import use statements found in *one* file. + * Do NOT combine the statements of multiple files as the result will be inaccurate and unreliable. + * + * In most cases when tempted to use this method, the {@see \PHPCSUtils\AbstractSniffs\AbstractFileContextSniff} + * (upcoming) should be used instead. + * + * @see \PHPCSUtils\AbstractSniffs\AbstractFileContextSniff + * @see \PHPCSUtils\Utils\UseStatements::splitImportUseStatement() + * @see \PHPCSUtils\Utils\UseStatements::mergeImportUseStatements() + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position in the stack of the `T_USE` token. + * @param array $previousUseStatements The import `use` statements collected so far. + * This should be either the output of a + * previous call to this method or the output of + * an earlier call to the + * {@see UseStatements::splitImportUseStatement()} + * method. + * + * @return array A multi-level array containing information about the current `use` statement combined with + * the previously collected `use` statement information. + * See {@see UseStatements::splitImportUseStatement()} for more details about the array format. + */ + public static function splitAndMergeImportUseStatement(File $phpcsFile, $stackPtr, array $previousUseStatements) + { + try { + $useStatements = self::splitImportUseStatement($phpcsFile, $stackPtr); + $previousUseStatements = self::mergeImportUseStatements($previousUseStatements, $useStatements); + } catch (RuntimeException $e) { + // Not an import use statement. + } + + return $previousUseStatements; + } + + /** + * Merge two import use statement arrays. + * + * Beware: this method should only be used to combine the import use statements found in *one* file. + * Do NOT combine the statements of multiple files as the result will be inaccurate and unreliable. + * + * @see \PHPCSUtils\Utils\UseStatements::splitImportUseStatement() + * + * @since 1.0.0 + * + * @param array $previousUseStatements The import `use` statements collected so far. + * This should be either the output of a + * previous call to this method or the output of + * an earlier call to the + * {@see UseStatements::splitImportUseStatement()} + * method. + * @param array $currentUseStatement The parsed import `use` statements to merge with + * the previously collected use statements. + * This should be the output of a call to the + * {@see UseStatements::splitImportUseStatement()} + * method. + * + * @return array A multi-level array containing information about the current `use` statement combined with + * the previously collected `use` statement information. + * See {@see UseStatements::splitImportUseStatement()} for more details about the array format. + */ + public static function mergeImportUseStatements(array $previousUseStatements, array $currentUseStatement) + { + if (isset($previousUseStatements['name']) === false) { + $previousUseStatements['name'] = $currentUseStatement['name']; + } else { + $previousUseStatements['name'] += $currentUseStatement['name']; + } + if (isset($previousUseStatements['function']) === false) { + $previousUseStatements['function'] = $currentUseStatement['function']; + } else { + $previousUseStatements['function'] += $currentUseStatement['function']; + } + if (isset($previousUseStatements['const']) === false) { + $previousUseStatements['const'] = $currentUseStatement['const']; + } else { + $previousUseStatements['const'] += $currentUseStatement['const']; + } + + return $previousUseStatements; + } +} diff --git a/PHPCSUtils/Utils/Variables.php b/PHPCSUtils/Utils/Variables.php new file mode 100644 index 00000000..1a121d24 --- /dev/null +++ b/PHPCSUtils/Utils/Variables.php @@ -0,0 +1,331 @@ + => + */ + public static $phpReservedVars = [ + '_SERVER' => true, + '_GET' => true, + '_POST' => true, + '_REQUEST' => true, + '_SESSION' => true, + '_ENV' => true, + '_COOKIE' => true, + '_FILES' => true, + 'GLOBALS' => true, + 'http_response_header' => false, + 'argc' => false, + 'argv' => false, + + // Deprecated. + 'php_errormsg' => false, + + // Removed PHP 5.4.0. + 'HTTP_SERVER_VARS' => false, + 'HTTP_GET_VARS' => false, + 'HTTP_POST_VARS' => false, + 'HTTP_SESSION_VARS' => false, + 'HTTP_ENV_VARS' => false, + 'HTTP_COOKIE_VARS' => false, + 'HTTP_POST_FILES' => false, + + // Removed PHP 5.6.0. + 'HTTP_RAW_POST_DATA' => false, + ]; + + /** + * Retrieve the visibility and implementation properties of a class member variable. + * + * Main differences with the PHPCS version: + * - Removed the parse error warning for properties in interfaces. + * This will now throw the same _"$stackPtr is not a class member var"_ runtime exception as + * other non-property variables passed to the method. + * - Defensive coding against incorrect calls to this method. + * - Support PHP 8.0 identifier name tokens in property types, cross-version PHP & PHPCS. + * - Support for the PHP 8.2 `true` type. + * + * @see \PHP_CodeSniffer\Files\File::getMemberProperties() Original source. + * @see \PHPCSUtils\BackCompat\BCFile::getMemberProperties() Cross-version compatible version of the original. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position in the stack of the `T_VARIABLE` token + * to acquire the properties for. + * + * @return array Array with information about the class member variable. + * The format of the return value is: + * ```php + * array( + * 'scope' => string, // Public, private, or protected. + * 'scope_specified' => boolean, // TRUE if the scope was explicitly specified. + * 'is_static' => boolean, // TRUE if the static keyword was found. + * 'is_readonly' => boolean, // TRUE if the readonly keyword was found. + * 'type' => string, // The type of the var (empty if no type specified). + * 'type_token' => integer, // The stack pointer to the start of the type + * // or FALSE if there is no type. + * 'type_end_token' => integer, // The stack pointer to the end of the type + * // or FALSE if there is no type. + * 'nullable_type' => boolean, // TRUE if the type is preceded by the + * // nullability operator. + * ); + * ``` + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a + * `T_VARIABLE` token. + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a + * class member variable. + */ + public static function getMemberProperties(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]) === false || $tokens[$stackPtr]['code'] !== \T_VARIABLE) { + throw new RuntimeException('$stackPtr must be of type T_VARIABLE'); + } + + if (Scopes::isOOProperty($phpcsFile, $stackPtr) === false) { + throw new RuntimeException('$stackPtr is not a class member var'); + } + + $valid = Collections::propertyModifierKeywords() + Tokens::$emptyTokens; + + $scope = 'public'; + $scopeSpecified = false; + $isStatic = false; + $isReadonly = false; + + $startOfStatement = $phpcsFile->findPrevious( + [ + \T_SEMICOLON, + \T_OPEN_CURLY_BRACKET, + \T_CLOSE_CURLY_BRACKET, + \T_ATTRIBUTE_END, + ], + ($stackPtr - 1) + ); + + for ($i = ($startOfStatement + 1); $i < $stackPtr; $i++) { + if (isset($valid[$tokens[$i]['code']]) === false) { + break; + } + + switch ($tokens[$i]['code']) { + case \T_PUBLIC: + $scope = 'public'; + $scopeSpecified = true; + break; + case \T_PRIVATE: + $scope = 'private'; + $scopeSpecified = true; + break; + case \T_PROTECTED: + $scope = 'protected'; + $scopeSpecified = true; + break; + case \T_STATIC: + $isStatic = true; + break; + case \T_READONLY: + $isReadonly = true; + break; + } + } + + $type = ''; + $typeToken = false; + $typeEndToken = false; + $nullableType = false; + $propertyTypeTokens = Collections::propertyTypeTokens(); + + /* + * BC PHPCS < 3.x.x: The union type separator is not (yet) retokenized correctly + * for union types containing the `true` type. + */ + $propertyTypeTokens[\T_BITWISE_OR] = \T_BITWISE_OR; + + if ($i < $stackPtr) { + // We've found a type. + for ($i; $i < $stackPtr; $i++) { + if ($tokens[$i]['code'] === \T_VARIABLE) { + // Hit another variable in a group definition. + break; + } + + if ($tokens[$i]['code'] === \T_NULLABLE) { + $nullableType = true; + } + + if (isset($propertyTypeTokens[$tokens[$i]['code']]) === true) { + $typeEndToken = $i; + if ($typeToken === false) { + $typeToken = $i; + } + + $type .= $tokens[$i]['content']; + } + } + + if ($type !== '' && $nullableType === true) { + $type = '?' . $type; + } + } + + return [ + 'scope' => $scope, + 'scope_specified' => $scopeSpecified, + 'is_static' => $isStatic, + 'is_readonly' => $isReadonly, + 'type' => $type, + 'type_token' => $typeToken, + 'type_end_token' => $typeEndToken, + 'nullable_type' => $nullableType, + ]; + } + + /** + * Verify if a given variable name is the name of a PHP reserved variable. + * + * @see \PHPCSUtils\Utils\Variables::$phpReservedVars List of variables names reserved by PHP. + * + * @since 1.0.0 + * + * @param string $name The full variable name with or without leading dollar sign. + * This allows for passing an array key variable name, such as + * `'_GET'` retrieved from `$GLOBALS['_GET']`. + * > Note: when passing an array key, string quotes are expected + * to have been stripped already. + * Also see: {@see \PHPCSUtils\Utils\TextStrings::stripQuotes()}. + * + * @return bool + */ + public static function isPHPReservedVarName($name) + { + if (\strpos($name, '$') === 0) { + $name = \substr($name, 1); + } + + return (isset(self::$phpReservedVars[$name]) === true); + } + + /** + * Verify if a given variable or array key token points to a PHP superglobal. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position in the stack of a `T_VARIABLE` + * token or of the `T_CONSTANT_ENCAPSED_STRING` + * array key to a variable in `$GLOBALS`. + * + * @return bool `TRUE` if this points to a superglobal; `FALSE` when not. + * > Note: This includes returning `FALSE` when an unsupported token has + * been passed, when a `T_CONSTANT_ENCAPSED_STRING` has been passed which + * is not an array index key; or when it is, but is not an index to the + * `$GLOBALS` variable. + */ + public static function isSuperglobal(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]) === false + || ($tokens[$stackPtr]['code'] !== \T_VARIABLE + && $tokens[$stackPtr]['code'] !== \T_CONSTANT_ENCAPSED_STRING) + ) { + return false; + } + + $content = $tokens[$stackPtr]['content']; + + if ($tokens[$stackPtr]['code'] === \T_CONSTANT_ENCAPSED_STRING) { + $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if (($prev === false || $tokens[$prev]['code'] !== \T_OPEN_SQUARE_BRACKET) + || ($next === false || $tokens[$next]['code'] !== \T_CLOSE_SQUARE_BRACKET) + ) { + // Not a single string array index key. + return false; + } + + $pprev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true); + if ($pprev === false + || $tokens[$pprev]['code'] !== \T_VARIABLE + || $tokens[$pprev]['content'] !== '$GLOBALS' + ) { + // Not accessing the `$GLOBALS` array. + return false; + } + + // Strip quotes. + $content = TextStrings::stripQuotes($content); + } + + return self::isSuperglobalName($content); + } + + /** + * Verify if a given variable name is the name of a PHP superglobal. + * + * @since 1.0.0 + * + * @param string $name The full variable name with or without leading dollar sign. + * This allows for passing an array key variable name, such as + * `'_GET'` retrieved from `$GLOBALS['_GET']`. + * > Note: when passing an array key, string quotes are expected + * to have been stripped already. + * Also see: {@see \PHPCSUtils\Utils\TextStrings::stripQuotes()}. + * + * @return bool + */ + public static function isSuperglobalName($name) + { + if (\strpos($name, '$') === 0) { + $name = \substr($name, 1); + } + + if (isset(self::$phpReservedVars[$name]) === false) { + return false; + } + + return self::$phpReservedVars[$name]; + } +} diff --git a/PHPCSUtils/ruleset.xml b/PHPCSUtils/ruleset.xml new file mode 100644 index 00000000..f4365516 --- /dev/null +++ b/PHPCSUtils/ruleset.xml @@ -0,0 +1,4 @@ + + + Utility methods for external PHPCS standards. + diff --git a/phpcs/CodeSniffer.conf b/phpcs/CodeSniffer.conf index 418a9ce0..4f057139 100644 --- a/phpcs/CodeSniffer.conf +++ b/phpcs/CodeSniffer.conf @@ -1,5 +1,5 @@ '../PHPCompatibility,../MoodleCS/moodle', + 'installed_paths' => '../PHPCSUtils,../PHPCompatibility,../MoodleCS/moodle', ) ?> diff --git a/readme_moodle.txt b/readme_moodle.txt index ce041c6e..434f37be 100644 --- a/readme_moodle.txt +++ b/readme_moodle.txt @@ -47,7 +47,7 @@ Local modifications (only allowed if there is a PR upstream backing it): Instructions to upgrade the PHPCompatibility bundled version: -- Drop a checkout of the PHPCompatibility dir of https://github.com/wimg/PHPCompatibility.git +- Drop a checkout of the PHPCompatibility dir of https://github.com/PHPCompatibility/PHPCompatibility within the "PHPCompatibility" directory of the local_codechecker plugin. Always removing all the previous contents. - Don't delete anything. 100% complete drop. @@ -66,3 +66,22 @@ Local modifications (only allowed if there is a PR upstream backing it): it before the jump to phpcs 3. ===== ===== ===== ===== ===== ===== ===== + +Instructions to upgrade the PHPCSUtils bundled version: + +- Drop a checkout of the PHPCSUtils dir of https://github.com/PHPCSStandards/PHPCSUtils + within the "PHPCSUtils" directory of the local_codechecker plugin. Always + removing all the previous contents. +- Don't delete anything. 100% complete drop. +- Update the details in thirdpartylibs.xml +- Update the details in this readme + +Current checkout: + + 1.0.1 (4fd2e30) + +Local modifications (only allowed if there is a PR upstream backing it): + + - None, right now. + +===== ===== ===== ===== ===== ===== ===== diff --git a/thirdpartylibs.xml b/thirdpartylibs.xml index 43e15d90..c6a57cd2 100644 --- a/thirdpartylibs.xml +++ b/thirdpartylibs.xml @@ -14,6 +14,13 @@ LGPL 3 + + PHPCSUtils + A suite of utility functions for use with PHP_CodeSniffer + 1.0.1 (4fd2e30) + GPL + 3 + MoodleCS Moodle Coding Style