diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 00000000..1621caa9 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,3 @@ +coverage_clover: build/logs/clover.xml +json_path: build/logs/coveralls-upload.json +service_name: travis-ci diff --git a/.gitattributes b/.gitattributes index 30f7d6a7..2ce6c7da 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,9 +5,13 @@ # https://www.reddit.com/r/PHP/comments/2jzp6k/i_dont_need_your_tests_in_my_production # https://blog.madewithlove.be/post/gitattributes/ # +/.coveralls.yml export-ignore /.gitattributes export-ignore /.gitignore export-ignore /.travis.yml export-ignore +/phpcs.xml.dist export-ignore +/phpunit.xml.dist export-ignore +/Tests/ export-ignore # # Auto detect text files and perform LF normalization diff --git a/.gitignore b/.gitignore index d1502b08..de278d07 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ +build/ vendor/ -composer.lock +/composer.lock +/.phpcs.xml +/phpcs.xml +/phpunit.xml +/.phpunit.result.cache diff --git a/.travis.yml b/.travis.yml index 5fc495a2..32a29dc4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ language: php ## Cache composer and apt downloads. cache: + apt: true directories: # Cache directory for older Composer versions. - $HOME/.composer/cache/files @@ -11,22 +12,195 @@ cache: - $HOME/.cache/composer/files php: - - 5.4 - - 7.3 + - 5.5 + - 5.6 + - 7.0 + - 7.1 + - 7.2 + +env: + - PHPCS_VERSION="dev-master" LINT=1 + - PHPCS_VERSION="3.1.0" + - PHPCS_VERSION="2.9.2" + - PHPCS_VERSION="2.6.0" + +# Define the stages used. +# For non-PRs, only the sniff and quicktest stages are run. +# For pull requests and merges, the full script is run (skipping quicktest). +# Note: for pull requests, "develop" is the base branch name. +# See: https://docs.travis-ci.com/user/conditions-v1 +stages: + - name: sniff + - name: quicktest + if: type = push AND branch NOT IN (master, develop) + - name: test + if: branch IN (master, develop) + - name: coverage + if: branch IN (master, develop) jobs: fast_finish: true + include: + #### SNIFF STAGE #### + - stage: sniff + php: 7.3 + env: PHPCS_VERSION="dev-master" + addons: + apt: + packages: + - libxml2-utils + install: skip + script: + # Validate the composer.json file. + # @link https://getcomposer.org/doc/03-cli.md#validate + - composer validate --no-check-all --strict + + # Check the code style of the code base. + - composer travis-checkcs + + # Validate the xml files. + # @link http://xmlsoft.org/xmllint.html + - xmllint --noout --schema ./vendor/squizlabs/php_codesniffer/phpcs.xsd ./PHPCSUtils/ruleset.xml + - xmllint --noout --schema ./vendor/squizlabs/php_codesniffer/phpcs.xsd ./PHPCS23Utils/ruleset.xml + + # Check the code-style consistency of the xml files. + - diff -B ./PHPCSUtils/ruleset.xml <(xmllint --format "./PHPCSUtils/ruleset.xml") + - diff -B ./PHPCS23Utils/ruleset.xml <(xmllint --format "./PHPCS23Utils/ruleset.xml") + + #### QUICK TEST STAGE #### + # This is a much quicker test which only runs the unit tests and linting against the low/high + # supported PHP/PHPCS combinations. + # These are basically the same builds as in the Coverage stage, but then without doing + # the code-coverage. + - stage: quicktest + php: 7.4 + env: PHPCS_VERSION="dev-master" LINT=1 + - stage: quicktest + php: 7.3 + # PHPCS is only compatible with PHP 7.3 as of version 3.3.1/2.9.2. + env: PHPCS_VERSION="2.9.2" + + - stage: quicktest + php: 5.4 + env: PHPCS_VERSION="dev-master" LINT=1 + - stage: quicktest + php: 5.4 + env: PHPCS_VERSION="2.6.0" + + #### TEST STAGE #### + # Additional builds to prevent issues with PHPCS versions incompatible with certain PHP versions. + - stage: test + php: 7.3 + env: PHPCS_VERSION="dev-master" LINT=1 + - php: 7.3 + # PHPCS is only compatible with PHP 7.3 as of version 3.3.1/2.9.2. + env: PHPCS_VERSION="3.3.1" + + - php: 5.4 + env: PHPCS_VERSION="3.1.0" + - php: 5.4 + env: PHPCS_VERSION="2.9.2" + + # PHPCS is only compatible with PHP 7.4 as of version 3.5.0. + - php: 7.4 + env: PHPCS_VERSION="3.5.0" + + # One extra build to verify issues around PHPCS annotations when they weren't fully accounted for yet. + - php: 7.2 + env: PHPCS_VERSION="3.2.0" + + - php: "nightly" + env: PHPCS_VERSION="n/a" LINT=1 + + #### CODE COVERAGE STAGE #### + # N.B.: Coverage is only checked on the lowest and highest stable PHP versions for all PHPCS versions. + # These builds are left out off the "test" stage so as not to duplicate test runs. + # The script used is the default script below, the same as for the `test` stage. + - stage: coverage + php: 7.4 + env: PHPCS_VERSION="dev-master" LINT=1 COVERALLS_VERSION="^2.0" + - php: 7.3 + # PHPCS is only compatible with PHP 7.3 as of version 3.3.1/2.9.2. + env: PHPCS_VERSION="2.9.2" COVERALLS_VERSION="^2.0" + + - php: 5.4 + env: PHPCS_VERSION="dev-master" LINT=1 COVERALLS_VERSION="^1.0" + - php: 5.4 + env: PHPCS_VERSION="2.6.0" COVERALLS_VERSION="^1.0" + + + allow_failures: + # Allow failures for unstable builds. + - php: "nightly" before_install: # Speed up build time by disabling Xdebug when its not needed. - - phpenv config-rm xdebug.ini || echo 'No xdebug config.' + - | + if [[ "$TRAVIS_BUILD_STAGE_NAME" != "Coverage" ]]; then + phpenv config-rm xdebug.ini || echo 'No xdebug config.' + fi + + # On stable PHPCS versions, allow for PHP deprecation notices. + # Unit tests don't need to fail on those for stable releases where those issues won't get fixed anymore. + - | + if [[ "$TRAVIS_BUILD_STAGE_NAME" != "Sniff" && $PHPCS_VERSION != "dev-master" && "$PHPCS_VERSION" != "n/a" ]]; then + echo 'error_reporting = E_ALL & ~E_DEPRECATED' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini + fi + + - export XMLLINT_INDENT=" " + + +install: + # Set up test environment using Composer. + - | + if [[ $PHPCS_VERSION != "n/a" ]]; then + composer require --no-update --no-scripts squizlabs/php_codesniffer:${PHPCS_VERSION} + fi + - | + if [[ "$TRAVIS_BUILD_STAGE_NAME" == "Coverage" ]]; then + composer require --dev --no-update --no-suggest --no-scripts php-coveralls/php-coveralls:${COVERALLS_VERSION} + fi + - | + if [[ $PHPCS_VERSION == "n/a" ]]; then + # Don't install PHPUnit when it's not needed. + composer remove --dev phpunit/phpunit --no-update --no-scripts + elif [[ "$PHPCS_VERSION" < "3.1.0" ]]; then + # PHPCS < 3.1.0 is not compatible with PHPUnit 6.x. + composer require --dev phpunit/phpunit:"^4.0||^5.0" --no-update --no-scripts + elif [[ "$PHPCS_VERSION" < "3.2.3" ]]; then + # PHPCS < 3.2.3 is not compatible with PHPUnit 7.x. + composer require --dev phpunit/phpunit:"^4.0||^5.0||^6.0" --no-update --no-scripts + fi # --prefer-dist will allow for optimal use of the travis caching ability. + # The Composer PHPCS plugin takes care of setting the installed_paths for PHPCS. - composer install --prefer-dist --no-suggest +before_script: + - if [[ "$TRAVIS_BUILD_STAGE_NAME" == "Coverage" ]]; then mkdir -p build/logs; fi + - phpenv rehash + + script: - # Validate the composer.json file on low/high PHP versions. - # @link https://getcomposer.org/doc/03-cli.md#validate - - composer validate --no-check-all --strict + # Lint PHP files against parse errors. + - if [[ "$LINT" == "1" ]]; then composer lint; fi + + # Run the unit tests. + - | + if [[ $PHPCS_VERSION != "n/a" && "$TRAVIS_BUILD_STAGE_NAME" != "Coverage" ]]; then + composer test + elif [[ $PHPCS_VERSION != "n/a" && "$TRAVIS_BUILD_STAGE_NAME" == "Coverage" ]]; then + composer coverage + fi + +after_success: + - | + if [[ "$TRAVIS_BUILD_STAGE_NAME" == "Coverage" && $COVERALLS_VERSION == "^1.0" ]]; then + php vendor/bin/coveralls -v -x build/logs/clover.xml + fi + - | + if [[ "$TRAVIS_BUILD_STAGE_NAME" == "Coverage" && $COVERALLS_VERSION == "^2.0" ]]; then + php vendor/bin/php-coveralls -v -x build/logs/clover.xml + fi diff --git a/PHPCS23Utils/Sniffs/Load/LoadUtilsSniff.php b/PHPCS23Utils/Sniffs/Load/LoadUtilsSniff.php new file mode 100644 index 00000000..c0d18bcc --- /dev/null +++ b/PHPCS23Utils/Sniffs/Load/LoadUtilsSniff.php @@ -0,0 +1,52 @@ + + + Standard which makes the PHPCSUtils utility methods for external PHPCS standards available in PHPCS 2.x. + diff --git a/PHPCSUtils/AbstractSniffs/AbstractArrayDeclarationSniff.php b/PHPCSUtils/AbstractSniffs/AbstractArrayDeclarationSniff.php new file mode 100644 index 00000000..5646e450 --- /dev/null +++ b/PHPCSUtils/AbstractSniffs/AbstractArrayDeclarationSniff.php @@ -0,0 +1,517 @@ + \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 += BCTokens::assignmentTokens(); + $this->acceptedTokens += BCTokens::comparisonTokens(); + $this->acceptedTokens += BCTokens::arithmeticTokens(); + $this->acceptedTokens += BCTokens::operators(); + $this->acceptedTokens += BCTokens::booleanOperators(); + $this->acceptedTokens += BCTokens::castTokens(); + $this->acceptedTokens += BCTokens::bracketTokens(); + $this->acceptedTokens += BCTokens::heredocTokens(); + } + + /** + * Returns an array of tokens this test wants to listen for. + * + * @since 1.0.0 + * + * @codeCoverageIgnore + * + * @return array + */ + public function register() + { + return [ + \T_ARRAY, + \T_OPEN_SHORT_ARRAY, + \T_OPEN_SQUARE_BRACKET, + ]; + } + + /** + * 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 `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. + * + * This contains 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) { + $arrowPtr = Arrays::getDoubleArrowPtr($phpcsFile, $arrayItem['start'], $arrayItem['end']); + + 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. + * + * @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. + */ + public function processOpenClose(File $phpcsFile, $openPtr, $closePtr) + { + } + + /** + * Process the tokens in an array key. + * + * Optional method to be implemented in concrete child classes. + * + * The $startPtr and $endPtr do not discount whitespace or comments, but are all inclusive to + * allow 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. + */ + 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. + * + * @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 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. + */ + public function processNoKey(File $phpcsFile, $startPtr, $itemNr) + { + } + + /** + * Process the double arrow. + * + * Optional method to be implemented in concrete child classes. + * + * @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. + */ + public function processArrow(File $phpcsFile, $arrowPtr, $itemNr) + { + } + + /** + * Process the tokens in an array value. + * + * Optional method to be implemented in concrete child classes. + * + * The $startPtr and $endPtr do not discount whitespace or comments, but are all inclusive to + * allow 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. + */ + public function processValue(File $phpcsFile, $startPtr, $endPtr, $itemNr) + { + } + + /** + * Process the comma after an array item. + * + * Optional method to be implemented in concrete child classes. + * + * @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. + */ + public function processComma(File $phpcsFile, $commaPtr, $itemNr) + { + } + + /** + * Determine what the actual array key would be. + * + * Optional helper function for processsing array keys in the processKey() function. + * + * @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) { + try { + $number = Numbers::getCompleteNumber($phpcsFile, $i); + $content .= $number['content']; + $i = $number['last_token']; + } catch (RuntimeException $e) { + // This must be PHP 3.5.3 with the broken backfill. Let's presume it's a ordinary number. + // If it's not, the sniff will bow out on the following T_STRING anyway if the + // backfill was broken. + $content .= \str_replace('_', '', $this->tokens[$i]['content']); + } + 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 (\preg_match('`(?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. + */ + + $integerKey = false; + + 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; + } + } +} diff --git a/PHPCSUtils/BackCompat/BCFile.php b/PHPCSUtils/BackCompat/BCFile.php new file mode 100644 index 00000000..c347e4e4 --- /dev/null +++ b/PHPCSUtils/BackCompat/BCFile.php @@ -0,0 +1,1464 @@ + + * @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\BackCompat\BCTokens; +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: + * - Array return type declarations were incorrectly tokenized to `T_ARRAY_HINT` + * instead of `T_RETURN_TYPE` in some circumstances prior to PHPCS 2.8.0. + * - `T_NULLABLE` was not available prior to PHPCS 2.8.0 and utility functions did + * not take nullability of types into account. + * - The PHPCS native ignore annotations were not available prior to PHPCS 3.2.0. + * - The way return types were tokenized has changed in PHPCS 3.3.0. + * Previously a lot of them were tokenized as `T_RETURN_HINT`. For namespaced + * classnames this only tokenized the classname, not the whole return type. + * Now tokenization is "normalized" with most tokens being `T_STRING`, including + * array return type declarations. + * - Typed properties were not recognized prior to PHPCS 3.5.0, including the + * `?` nullability token not being converted to `T_NULLABLE`. + * - General PHP cross-version incompatibilities. + * + * 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 Source of these utility methods. + * + * @since 1.0.0 + */ +class BCFile +{ + + /** + * Returns the declaration names for classes, interfaces, traits, and functions. + * + * PHPCS cross-version compatible version of the File::getDeclarationName() method. + * + * Changelog for the PHPCS native function: + * - Introduced in PHPCS 0.0.5. + * - PHPCS 2.8.0: Returns null when passed an anonymous class. Previously, the method + * would throw a "token not of an accepted type" exception. + * - PHPCS 2.9.0: Returns null when passed a PHP closure. Previously, the method + * would throw a "token not of an accepted type" exception. + * - PHPCS 3.0.0: Added support for ES6 class/method syntax. + * - PHPCS 3.0.0: The Exception thrown changed from a `PHP_CodeSniffer_Exception` to + * `\PHP_CodeSniffer\Exceptions\RuntimeException`. + * + * Note: For ES6 classes in combination with PHPCS 2.x, passing a `T_STRING` token to + * this method will be accepted for JS files. + * Note: support for JS ES6 method syntax has not been back-filled for PHPCS < 3.0.0. + * + * @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, or function. + * + * @return string|null The name of the class, interface, trait, 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_TRAIT, or T_INTERFACE. + */ + public static function getDeclarationName(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + $tokenCode = $tokens[$stackPtr]['code']; + + if ($tokenCode === T_ANON_CLASS || $tokenCode === T_CLOSURE) { + return null; + } + + /* + * BC: Work-around JS ES6 classes not being tokenized as T_CLASS in PHPCS < 3.0.0. + */ + if ($phpcsFile->tokenizerType === 'JS' + && $tokenCode === T_STRING + && $tokens[$stackPtr]['content'] === 'class' + ) { + $tokenCode = T_CLASS; + } + + if ($tokenCode !== T_FUNCTION + && $tokenCode !== T_CLASS + && $tokenCode !== T_INTERFACE + && $tokenCode !== T_TRAIT + ) { + throw new RuntimeException('Token type "' . $tokens[$stackPtr]['type'] . '" is not T_FUNCTION, T_CLASS, T_INTERFACE or T_TRAIT'); + } + + 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']; + } + + $content = null; + for ($i = ($stackPtr + 1); $i < $phpcsFile->numTokens; $i++) { + if ($tokens[$i]['code'] === T_STRING) { + /* + * BC: In PHPCS 2.6.0, in case of live coding, the last token in a file will be tokenized + * as T_STRING, but won't have the `content` index set. + */ + if (isset($tokens[$i]['content'])) { + $content = $tokens[$i]['content']; + } + break; + } + } + + return $content; + } + + /** + * Returns the method parameters for the specified function token. + * + * Also supports passing in a USE token for a closure use group. + * + * Each parameter is in the following format: + * + * + * 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. + * '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 nullable. + * '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 an additional array indexs of: + * '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. + * + * PHPCS cross-version compatible version of the File::getMethodParameters() method. + * + * Changelog for the PHPCS native function: + * - Introduced in PHPCS 0.0.5. + * - PHPCS 2.8.0: Now recognises `self` as a valid type declaration. + * - PHPCS 2.8.0: The return array now contains a new "token" index containing the stack pointer + * to the variable. + * - PHPCS 2.8.0: The return array now contains a new "content" index containing the raw content + * of the param definition. + * - PHPCS 2.8.0: Added support for nullable types. + * - The return array now contains a new "nullable_type" index set to true or false + * for each method parameter. + * - PHPCS 2.8.0: Added support for closures. + * - PHPCS 3.0.0: The Exception thrown changed from a `PHP_CodeSniffer_Exception` to + * `\PHP_CodeSniffer\Exceptions\TokenizerException`. + * - PHPCS 3.3.0: The return array now contains a new "type_hint_token" array index. + * - Provides the position in the token stack of the first token in the type declaration. + * - PHPCS 3.3.1: Fixed incompatibility with PHP 7.3. + * - PHPCS 3.5.0: The Exception thrown changed from a `TokenizerException` to + * `\PHP_CodeSniffer\Exceptions\RuntimeException`. + * - PHPCS 3.5.0: Added support for closure USE groups. + * - PHPCS 3.5.0: The return array now contains yet more more information. + * - If a type hint is specified, the position of the last token in the hint will be + * set in a "type_hint_end_token" array index. + * - If a default is specified, the position of the first token in the default value + * will be set in a "default_token" array index. + * - If a default is specified, the position of the equals sign will be set in a + * "default_equal_token" array index. + * - If the param is not the last, the position of the comma will be set in a + * "comma_token" array index. + * - If the param is passed by reference, the position of the reference operator + * will be set in a "reference_token" array index. + * - If the param is variable length, the position of the variadic operator will + * be set in a "variadic_token" array index. + * - PHPCS 3.5.3: Fixed a bug where the "type_hint_end_token" array index for a type hinted + * parameter would bleed through to the next (non-type hinted) parameter. + * + * @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, or T_USE. + */ + public static function getMethodParameters(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if ($tokens[$stackPtr]['code'] !== T_FUNCTION + && $tokens[$stackPtr]['code'] !== T_CLOSURE + && $tokens[$stackPtr]['code'] !== T_USE + ) { + throw new RuntimeException('$stackPtr must be of type T_FUNCTION or T_CLOSURE or T_USE'); + } + + if ($tokens[$stackPtr]['code'] === T_USE) { + $opener = $phpcsFile->findNext(T_OPEN_PARENTHESIS, ($stackPtr + 1)); + if ($opener === false || isset($tokens[$opener]['parenthesis_owner']) === true) { + 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; + $passByReference = false; + $referenceToken = false; + $variableLength = false; + $variadicToken = false; + $typeHint = ''; + $typeHintToken = false; + $typeHintEndToken = false; + $nullableType = false; + + 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); + } + } + + // Changed from checking 'code' to 'type' to allow for T_NULLABLE not existing in PHPCS < 2.8.0. + switch ($tokens[$i]['type']) { + 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_ARRAY_HINT': // PHPCS < 3.3.0. + 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': + // This is a string, so it may be a type hint, 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_NS_SEPARATOR': + // 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': + case 'T_INLINE_THEN': // PHPCS < 2.8.0. + if ($defaultStart === null) { + $nullableType = true; + $typeHint .= $tokens[$i]['content']; + $typeHintEndToken = $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]['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 ($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; + $passByReference = false; + $referenceToken = false; + $variableLength = false; + $variadicToken = false; + $typeHint = ''; + $typeHintToken = false; + $typeHintEndToken = false; + $nullableType = false; + + $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: + * + * 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. + * 'nullable_return_type' => false, // TRUE if the return type is nullable. + * '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. + * - PHPCS 3.0.0: Removed the `is_closure` array index which was always `false` anyway. + * - PHPCS 3.0.0: The Exception thrown changed from a `PHP_CodeSniffer_Exception` to + * `\PHP_CodeSniffer\Exceptions\TokenizerException`. + * - PHPCS 3.3.0: New `return_type`, `return_type_token` and `nullable_return_type` array indexes. + * - The `return_type` index contains the return type of the function or closer, + * or a blank string if not specified. + * - If the return type is nullable, the return type will contain the leading `?`. + * - A `nullable_return_type` array index in the return value will also be set to `true`. + * - If the return type contains namespace information, it will be cleaned of + * whitespace and comments. + * - To access the original return value string, use the main tokens array. + * - PHPCS 3.4.0: New `has_body` array index. + * - `false` if the method has no body (as with abstract and interface methods) + * or `true` otherwise. + * - PHPCS 3.5.0: The Exception thrown changed from a `TokenizerException` to + * `\PHP_CodeSniffer\Exceptions\RuntimeException`. + * + * @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 or a T_CLOSURE token. + */ + public static function getMethodProperties(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if ($tokens[$stackPtr]['code'] !== T_FUNCTION + && $tokens[$stackPtr]['code'] !== T_CLOSURE + ) { + throw new RuntimeException('$stackPtr must be of type T_FUNCTION or T_CLOSURE'); + } + + if ($tokens[$stackPtr]['code'] === T_FUNCTION) { + $valid = [ + T_PUBLIC => T_PUBLIC, + T_PRIVATE => T_PRIVATE, + T_PROTECTED => T_PROTECTED, + T_STATIC => T_STATIC, + T_FINAL => T_FINAL, + T_ABSTRACT => T_ABSTRACT, + T_WHITESPACE => T_WHITESPACE, + T_COMMENT => T_COMMENT, + T_DOC_COMMENT => T_DOC_COMMENT, + ]; + } else { + $valid = [ + T_STATIC => T_STATIC, + T_WHITESPACE => T_WHITESPACE, + T_COMMENT => T_COMMENT, + T_DOC_COMMENT => T_DOC_COMMENT, + ]; + } + + $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; + $nullableReturnType = false; + $hasBody = true; + + if (isset($tokens[$stackPtr]['parenthesis_closer']) === true) { + $scopeOpener = null; + if (isset($tokens[$stackPtr]['scope_opener']) === true) { + $scopeOpener = $tokens[$stackPtr]['scope_opener']; + } + + for ($i = $tokens[$stackPtr]['parenthesis_closer']; $i < $phpcsFile->numTokens; $i++) { + if (($scopeOpener === null && $tokens[$i]['code'] === T_SEMICOLON) + || ($scopeOpener !== null && $i === $scopeOpener) + ) { + // End of function definition. + break; + } + + if ($tokens[$i]['type'] === 'T_NULLABLE' + // Handle nullable tokens in PHPCS < 2.8.0. + || (defined('T_NULLABLE') === false && $tokens[$i]['code'] === T_INLINE_THEN) + ) { + $nullableReturnType = true; + } + + if (isset(Collections::$returnTypeTokens[$tokens[$i]['code']]) === true) { + if ($returnTypeToken === false) { + $returnTypeToken = $i; + } + + $returnType .= $tokens[$i]['content']; + } + } + + $end = $phpcsFile->findNext([T_OPEN_CURLY_BRACKET, T_SEMICOLON], $tokens[$stackPtr]['parenthesis_closer']); + $hasBody = $tokens[$end]['code'] === T_OPEN_CURLY_BRACKET; + } + + if ($returnType !== '' && $nullableReturnType === true) { + $returnType = '?' . $returnType; + } + + return [ + 'scope' => $scope, + 'scope_specified' => $scopeSpecified, + 'return_type' => $returnType, + 'return_type_token' => $returnTypeToken, + 'nullable_return_type' => $nullableReturnType, + 'is_abstract' => $isAbstract, + 'is_final' => $isFinal, + 'is_static' => $isStatic, + 'has_body' => $hasBody, + ]; + } + + /** + * Returns the visibility and implementation properties of a class member var. + * + * The format of the return value is: + * + * + * 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. + * '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 nullable. + * ); + * + * + * PHPCS cross-version compatible version of the File::getMemberProperties() method. + * + * Changelog for the PHPCS native function: + * - Introduced in PHPCS 0.0.5. + * - PHPCS 3.0.0: The Exception thrown changed from a `PHP_CodeSniffer_Exception` to + * `\PHP_CodeSniffer\Exceptions\TokenizerException`. + * - PHPCS 3.4.0: Fixed method params being recognized as properties, PHPCS#2214. + * - PHPCS 3.5.0: New `type`, `type_token`, `type_end_token` and `nullable_type` array indexes. + * - The `type` index contains the type of the member var, or a blank string + * if not specified. + * - If the type is nullable, `type` will contain the leading `?`. + * - If a type is specified, the position of the first token in the type will + * be set in a `type_token` array index. + * - If a type is specified, the position of the last token in the type will + * be set in a `type_end_token` array index. + * - If the type is nullable, a `nullable_type` array index will also be set to TRUE. + * - If the type contains namespace information, it will be cleaned of whitespace + * and comments in the `type` value. + * - PHPCS 3.5.0: The Exception thrown changed from a `TokenizerException` to + * `\PHP_CodeSniffer\Exceptions\RuntimeException`. + * + * @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) + { + $tokens = $phpcsFile->getTokens(); + + if ($tokens[$stackPtr]['code'] !== T_VARIABLE) { + throw new RuntimeException('$stackPtr must be of type T_VARIABLE'); + } + + $conditions = array_keys($tokens[$stackPtr]['conditions']); + $ptr = array_pop($conditions); + if (isset($tokens[$ptr]) === false + || ($tokens[$ptr]['code'] !== T_CLASS + && $tokens[$ptr]['code'] !== T_ANON_CLASS + && $tokens[$ptr]['code'] !== T_TRAIT) + ) { + if (isset($tokens[$ptr]) === true + && $tokens[$ptr]['code'] === T_INTERFACE + ) { + // T_VARIABLEs in interfaces can actually be method arguments + // but they wont be seen as being inside the method because there + // are no scope openers and closers for abstract methods. If it is in + // parentheses, we can be pretty sure it is a method argument. + if (isset($tokens[$stackPtr]['nested_parenthesis']) === false + || empty($tokens[$stackPtr]['nested_parenthesis']) === true + ) { + $error = 'Possible parse error: interfaces may not include member vars'; + $phpcsFile->addWarning($error, $stackPtr, 'Internal.ParseError.InterfaceHasMemberVar'); + return []; + } + } else { + throw new RuntimeException('$stackPtr is not a class member var'); + } + } + + // Make sure it's not a method parameter. + if (empty($tokens[$stackPtr]['nested_parenthesis']) === false) { + $parenthesis = array_keys($tokens[$stackPtr]['nested_parenthesis']); + $deepestOpen = array_pop($parenthesis); + if ($deepestOpen > $ptr + && isset($tokens[$deepestOpen]['parenthesis_owner']) === true + && $tokens[$tokens[$deepestOpen]['parenthesis_owner']]['code'] === T_FUNCTION + ) { + throw new RuntimeException('$stackPtr is not a class member var'); + } + } + + $valid = Collections::$propertyModifierKeywords; + $valid += Tokens::$emptyTokens; + + $scope = 'public'; + $scopeSpecified = false; + $isStatic = false; + + $startOfStatement = $phpcsFile->findPrevious( + [ + T_SEMICOLON, + T_OPEN_CURLY_BRACKET, + T_CLOSE_CURLY_BRACKET, + ], + ($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; + } + } + + $type = ''; + $typeToken = false; + $typeEndToken = false; + $nullableType = false; + + 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]['type'] === 'T_NULLABLE' + // Handle nullable property types in PHPCS < 3.5.0. + || $tokens[$i]['code'] === T_INLINE_THEN + ) { + $nullableType = true; + } + + if (isset(Collections::$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, + 'type' => $type, + 'type_token' => $typeToken, + 'type_end_token' => $typeEndToken, + 'nullable_type' => $nullableType, + ]; + } + + /** + * Returns the implementation properties of a class. + * + * The format of the return value is: + * + * 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. + * - PHPCS 3.0.0: The Exception thrown changed from a `PHP_CodeSniffer_Exception` to + * `\PHP_CodeSniffer\Exceptions\TokenizerException`. + * - PHPCS 3.5.0: The Exception thrown changed from a `TokenizerException` to + * `\PHP_CodeSniffer\Exceptions\RuntimeException`. + * + * @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) + { + $tokens = $phpcsFile->getTokens(); + + if ($tokens[$stackPtr]['code'] !== T_CLASS) { + throw new RuntimeException('$stackPtr must be of type T_CLASS'); + } + + $valid = [ + T_FINAL => T_FINAL, + T_ABSTRACT => T_ABSTRACT, + T_WHITESPACE => T_WHITESPACE, + T_COMMENT => T_COMMENT, + T_DOC_COMMENT => T_DOC_COMMENT, + ]; + + $isAbstract = false; + $isFinal = false; + + for ($i = ($stackPtr - 1); $i > 0; $i--) { + if (isset($valid[$tokens[$i]['code']]) === false) { + break; + } + + switch ($tokens[$i]['code']) { + case T_ABSTRACT: + $isAbstract = true; + break; + + case T_FINAL: + $isFinal = true; + break; + } + } + + return [ + 'is_abstract' => $isAbstract, + 'is_final' => $isFinal, + ]; + } + + /** + * 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. + * - PHPCS 3.1.1: Bug fix: misidentification of reference vs bitwise operator, PHPCS#1604/#1609. + * - An array assignment of a calculated value with a bitwise and operator in it, + * was being misidentified as a reference. + * - A calculated default value for a function parameter with a bitwise and operator + * in it, was being misidentified as a reference. + * - New by reference was not recognized as a reference. + * - References to class properties with `self::`, `parent::`, `static::`, + * `namespace\ClassName::`, `classname::` were not recognized as references. + * + * @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) + { + $tokens = $phpcsFile->getTokens(); + + if ($tokens[$stackPtr]['code'] !== T_BITWISE_AND) { + return false; + } + + $tokenBefore = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + + if ($tokens[$tokenBefore]['code'] === T_FUNCTION) { + // 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(BCTokens::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; + } + + if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) { + $brackets = $tokens[$stackPtr]['nested_parenthesis']; + $lastBracket = array_pop($brackets); + if (isset($tokens[$lastBracket]['parenthesis_owner']) === true) { + $owner = $tokens[$tokens[$lastBracket]['parenthesis_owner']]; + if ($owner['code'] === T_FUNCTION + || $owner['code'] === T_CLOSURE + ) { + $params = self::getMethodParameters($phpcsFile, $tokens[$lastBracket]['parenthesis_owner']); + foreach ($params as $param) { + $varToken = $tokenAfter; + if ($param['variable_length'] === true) { + $varToken = $phpcsFile->findNext( + (Tokens::$emptyTokens + [T_ELLIPSIS]), + ($stackPtr + 1), + null, + true + ); + } + + if ($param['token'] === $varToken + && $param['pass_by_reference'] === true + ) { + // Function parameter declared to be passed by reference. + return true; + } + } + } + } else { + $prev = false; + for ($t = ($tokens[$lastBracket]['parenthesis_opener'] - 1); $t >= 0; $t--) { + if ($tokens[$t]['code'] !== T_WHITESPACE) { + $prev = $t; + break; + } + } + + if ($prev !== false && $tokens[$prev]['code'] === T_USE) { + // Closure use by reference. + return true; + } + } + } + + // Pass by reference in function calls and assign by reference in arrays. + 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[] = T_NS_SEPARATOR; + $skip[] = T_SELF; + $skip[] = T_PARENT; + $skip[] = T_STATIC; + $skip[] = T_STRING; + $skip[] = T_NAMESPACE; + $skip[] = T_DOUBLE_COLON; + + $nextSignificantAfter = $phpcsFile->findNext( + $skip, + ($stackPtr + 1), + null, + true + ); + if ($tokens[$nextSignificantAfter]['code'] === T_VARIABLE) { + return true; + } + } + } + + return false; + } + + /** + * 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. + * - PHPCS 3.3.0: New `$origContent` parameter to optionally return original + * (non tab-replaced) content. + * - PHPCS 3.4.0: - Now throws a `RuntimeException` if the $start param is invalid. + * This stops an infinite loop when the function is passed invalid data. + * - If the $length param is invalid, an empty string will be returned. + * + * @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) + { + $tokens = $phpcsFile->getTokens(); + + if (is_int($start) === false || isset($tokens[$start]) === false) { + throw new RuntimeException('The $start position for getTokensAsString() must exist in the token stack'); + } + + if (is_int($length) === false || $length <= 0) { + return ''; + } + + $str = ''; + $end = ($start + $length); + if ($end > $phpcsFile->numTokens) { + $end = $phpcsFile->numTokens; + } + + for ($i = $start; $i < $end; $i++) { + // If tabs are being converted to spaces by the tokeniser, 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']; + } + } + + return $str; + } + + /** + * 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. + * - PHPCS 2.6.2: New optional `$ignore` parameter to selectively ignore stop points. + * + * @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) + { + $tokens = $phpcsFile->getTokens(); + + $endTokens = Tokens::$blockOpeners; + + $endTokens[T_COLON] = true; + $endTokens[T_COMMA] = true; + $endTokens[T_DOUBLE_ARROW] = true; + $endTokens[T_SEMICOLON] = true; + $endTokens[T_OPEN_TAG] = true; + $endTokens[T_CLOSE_TAG] = true; + $endTokens[T_OPEN_SHORT_ARRAY] = true; + + if ($ignore !== null) { + $ignore = (array) $ignore; + foreach ($ignore as $code) { + unset($endTokens[$code]); + } + } + + $lastNotEmpty = $start; + + for ($i = $start; $i >= 0; $i--) { + if (isset($endTokens[$tokens[$i]['code']]) === true) { + // Found the end of the previous statement. + return $lastNotEmpty; + } + + if (isset($tokens[$i]['scope_opener']) === true + && $i === $tokens[$i]['scope_closer'] + ) { + // Found the end of the previous scope block. + return $lastNotEmpty; + } + + // Skip nested statements. + if (isset($tokens[$i]['bracket_opener']) === true + && $i === $tokens[$i]['bracket_closer'] + ) { + $i = $tokens[$i]['bracket_opener']; + } elseif (isset($tokens[$i]['parenthesis_opener']) === true + && $i === $tokens[$i]['parenthesis_closer'] + ) { + $i = $tokens[$i]['parenthesis_opener']; + } + + if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === false) { + $lastNotEmpty = $i; + } + } + + return 0; + } + + /** + * 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. + * - PHPCS 2.6.2: New optional `$ignore` parameter to selectively ignore stop points. + * - PHPCS 2.7.1: Improved handling of short arrays, PHPCS #1203. + * - PHPCS 3.3.0: Bug fix: end of statement detection when passed a scope opener, PHPCS #1863. + * - PHPCS 3.5.0: Improved handling of group use statements. + * + * @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) + { + $tokens = $phpcsFile->getTokens(); + + $endTokens = [ + T_COLON => true, + T_COMMA => true, + T_DOUBLE_ARROW => true, + T_SEMICOLON => true, + T_CLOSE_PARENTHESIS => true, + T_CLOSE_SQUARE_BRACKET => true, + T_CLOSE_CURLY_BRACKET => true, + T_CLOSE_SHORT_ARRAY => true, + T_OPEN_TAG => true, + T_CLOSE_TAG => true, + ]; + + if ($ignore !== null) { + $ignore = (array) $ignore; + foreach ($ignore as $code) { + unset($endTokens[$code]); + } + } + + $lastNotEmpty = $start; + + for ($i = $start; $i < $phpcsFile->numTokens; $i++) { + if ($i !== $start && isset($endTokens[$tokens[$i]['code']]) === true) { + // Found the end of the statement. + if ($tokens[$i]['code'] === T_CLOSE_PARENTHESIS + || $tokens[$i]['code'] === T_CLOSE_SQUARE_BRACKET + || $tokens[$i]['code'] === T_CLOSE_CURLY_BRACKET + || $tokens[$i]['code'] === T_CLOSE_SHORT_ARRAY + || $tokens[$i]['code'] === T_OPEN_TAG + || $tokens[$i]['code'] === T_CLOSE_TAG + ) { + return $lastNotEmpty; + } + + return $i; + } + + // Skip nested statements. + if (isset($tokens[$i]['scope_closer']) === true + && ($i === $tokens[$i]['scope_opener'] + || $i === $tokens[$i]['scope_condition']) + ) { + if ($i === $start && isset(Tokens::$scopeOpeners[$tokens[$i]['code']]) === true) { + return $tokens[$i]['scope_closer']; + } + + $i = $tokens[$i]['scope_closer']; + } elseif (isset($tokens[$i]['bracket_closer']) === true + && $i === $tokens[$i]['bracket_opener'] + ) { + $i = $tokens[$i]['bracket_closer']; + } elseif (isset($tokens[$i]['parenthesis_closer']) === true + && $i === $tokens[$i]['parenthesis_opener'] + ) { + $i = $tokens[$i]['parenthesis_closer']; + } elseif ($tokens[$i]['code'] === T_OPEN_USE_GROUP) { + $end = $phpcsFile->findNext(T_CLOSE_USE_GROUP, ($i + 1)); + if ($end !== false) { + $i = $end; + } + } + + if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === false) { + $lastNotEmpty = $i; + } + } + + return ($phpcsFile->numTokens - 1); + } + + /** + * 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. + * - This method has received no significant code updates since PHPCS 2.6.0. + * + * @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. + * - This method has received no significant code updates since PHPCS 2.6.0. + * + * @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. + * + * @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) + { + return $phpcsFile->getCondition($stackPtr, $type); + } + + /** + * 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. + * - PHPCS 2.8.0: Now supports anonymous classes. + * - PHPCS 3.1.0: Now supports interfaces extending interfaces (incorrectly, only supporting + * single interface extension). + * - PHPCS 3.3.2: Fixed bug causing bleed through with nested classes, PHPCS#2127. + * + * @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) + { + $tokens = $phpcsFile->getTokens(); + + // Check for the existence of the token. + if (isset($tokens[$stackPtr]) === false) { + return false; + } + + if ($tokens[$stackPtr]['code'] !== T_CLASS + && $tokens[$stackPtr]['code'] !== T_ANON_CLASS + && $tokens[$stackPtr]['code'] !== T_INTERFACE + ) { + return false; + } + + if (isset($tokens[$stackPtr]['scope_opener']) === false) { + return false; + } + + $classOpenerIndex = $tokens[$stackPtr]['scope_opener']; + $extendsIndex = $phpcsFile->findNext(T_EXTENDS, $stackPtr, $classOpenerIndex); + if ($extendsIndex === false) { + return false; + } + + $find = [ + T_NS_SEPARATOR, + T_STRING, + T_WHITESPACE, + ]; + + $end = $phpcsFile->findNext($find, ($extendsIndex + 1), ($classOpenerIndex + 1), true); + $name = $phpcsFile->getTokensAsString(($extendsIndex + 1), ($end - $extendsIndex - 1)); + $name = trim($name); + + if ($name === '') { + return false; + } + + return $name; + } + + /** + * Returns the names of the interfaces that the specified class implements. + * + * PHPCS cross-version compatible version of the File::findImplementedInterfaceNames() method. + * + * Changelog for the PHPCS native function: + * - Introduced in PHPCS 2.7.0. + * - PHPCS 2.8.0: Now supports anonymous classes. + * + * @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. + * + * @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) + { + $tokens = $phpcsFile->getTokens(); + + // Check for the existence of the token. + if (isset($tokens[$stackPtr]) === false) { + return false; + } + + if ($tokens[$stackPtr]['code'] !== T_CLASS + && $tokens[$stackPtr]['code'] !== T_ANON_CLASS + ) { + return false; + } + + if (isset($tokens[$stackPtr]['scope_closer']) === false) { + return false; + } + + $classOpenerIndex = $tokens[$stackPtr]['scope_opener']; + $implementsIndex = $phpcsFile->findNext(T_IMPLEMENTS, $stackPtr, $classOpenerIndex); + if ($implementsIndex === false) { + return false; + } + + $find = [ + T_NS_SEPARATOR, + T_STRING, + T_WHITESPACE, + T_COMMA, + ]; + + $end = $phpcsFile->findNext($find, ($implementsIndex + 1), ($classOpenerIndex + 1), true); + $name = $phpcsFile->getTokensAsString(($implementsIndex + 1), ($end - $implementsIndex - 1)); + $name = trim($name); + + if ($name === '') { + return false; + } else { + $names = explode(',', $name); + $names = array_map('trim', $names); + return $names; + } + } +} diff --git a/PHPCSUtils/BackCompat/BCTokens.php b/PHPCSUtils/BackCompat/BCTokens.php new file mode 100644 index 00000000..d06400ae --- /dev/null +++ b/PHPCSUtils/BackCompat/BCTokens.php @@ -0,0 +1,355 @@ + `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 + */ +class BCTokens +{ + + /** + * Token types that are comments containing PHPCS instructions. + * + * @since 1.0.0 + * + * @var string[] + */ + protected static $phpcsCommentTokensTypes = [ + 'T_PHPCS_ENABLE', + 'T_PHPCS_DISABLE', + 'T_PHPCS_SET', + 'T_PHPCS_IGNORE', + 'T_PHPCS_IGNORE_FILE', + ]; + + /** + * Tokens that open class and object scopes. + * + * @since 1.0.0 + * + * @var array => + */ + protected static $ooScopeTokens = [ + \T_CLASS => \T_CLASS, + \T_ANON_CLASS => \T_ANON_CLASS, + \T_INTERFACE => \T_INTERFACE, + \T_TRAIT => \T_TRAIT, + ]; + + /** + * Tokens that represent text strings. + * + * @since 1.0.0 + * + * @var array => + */ + protected static $textStringTokens = [ + \T_CONSTANT_ENCAPSED_STRING => \T_CONSTANT_ENCAPSED_STRING, + \T_DOUBLE_QUOTED_STRING => \T_DOUBLE_QUOTED_STRING, + \T_INLINE_HTML => \T_INLINE_HTML, + \T_HEREDOC => \T_HEREDOC, + \T_NOWDOC => \T_NOWDOC, + ]; + + /** + * Handle calls to (undeclared) methods for token arrays which haven't received any + * changes since PHPCS 2.6.0. + * + * @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 + */ + public static function __callStatic($name, $args) + { + if (isset(Tokens::${$name})) { + return Tokens::${$name}; + } + + // Default to an empty array. + return []; + } + + /** + * Retrieve the PHPCS assignment tokens array in a cross-version compatible manner. + * + * Changelog for the PHPCS native array: + * - Introduced in PHPCS 0.0.5. + * - PHPCS 2.9.0: The PHP 7.4 `T_COALESCE_EQUAL` token was added to the array. + * The `T_COALESCE_EQUAL` token was introduced in PHPCS 2.8.1. + * - PHPCS 3.2.0: The JS `T_ZSR_EQUAL` token was added to the array. + * The `T_ZSR_EQUAL` token was introduced in PHPCS 2.8.0. + * + * @see \PHP_CodeSniffer\Util\Tokens::$assignmentTokens Original array. + * + * @since 1.0.0 + * + * @return array => Token array. + */ + public static function assignmentTokens() + { + $tokens = Tokens::$assignmentTokens; + + /* + * The `T_COALESCE_EQUAL` token may be available pre-PHPCS 2.8.1 depending on + * the PHP version used to run PHPCS. + */ + if (\defined('T_COALESCE_EQUAL')) { + $tokens[\T_COALESCE_EQUAL] = \T_COALESCE_EQUAL; + } + + if (\defined('T_ZSR_EQUAL')) { + $tokens[\T_ZSR_EQUAL] = \T_ZSR_EQUAL; + } + + return $tokens; + } + + /** + * Retrieve the PHPCS comparison tokens array in a cross-version compatible manner. + * + * Changelog for the PHPCS native array: + * - Introduced in PHPCS 0.5.0. + * - PHPCS 2.9.0: The PHP 7.0 `T_COALESCE` token was added to the array. + * The `T_COALESCE` token was introduced in PHPCS 2.6.1. + * - PHPCS 2.9.0: The PHP 7.0 `T_SPACESHIP` token was added to the array. + * The `T_SPACESHIP` token was introduced in PHPCS 2.5.1. + * + * @see \PHP_CodeSniffer\Util\Tokens::$comparisonTokens Original array. + * + * @since 1.0.0 + * + * @return array => Token array. + */ + public static function comparisonTokens() + { + $tokens = Tokens::$comparisonTokens + [\T_SPACESHIP => \T_SPACESHIP]; + + if (\defined('T_COALESCE')) { + $tokens[\T_COALESCE] = \T_COALESCE; + } + + return $tokens; + } + + /** + * Retrieve the PHPCS arithmetic tokens array in a cross-version compatible manner. + * + * Changelog for the PHPCS native array: + * - Introduced in PHPCS 0.5.0. + * - PHPCS 2.9.0: The PHP 5.6 `T_POW` token was added to the array. + * The `T_POW` token was introduced in PHPCS 2.4.0. + * + * @see \PHP_CodeSniffer\Util\Tokens::$arithmeticTokens Original array. + * + * @since 1.0.0 + * + * @return array => Token array or an empty array for PHPCS versions in + * which the PHPCS native comment tokens did not exist yet. + */ + public static function arithmeticTokens() + { + return Tokens::$arithmeticTokens + [ \T_POW => \T_POW ]; + } + + /** + * Retrieve the PHPCS operator tokens array in a cross-version compatible manner. + * + * Changelog for the PHPCS native array: + * - Introduced in PHPCS 0.0.5. + * - PHPCS 2.6.1: The PHP 7.0 `T_COALESCE` token was backfilled and added to the array. + * - PHPCS 2.8.1: The PHP 7.4 `T_COALESCE_EQUAL` token was backfilled and (incorrectly) + * added to the array. + * - PHPCS 2.9.0: The `T_COALESCE_EQUAL` token was removed from the array. + * + * @see \PHP_CodeSniffer\Util\Tokens::$operators Original array. + * + * @since 1.0.0 + * + * @return array => Token array. + */ + public static function operators() + { + $tokens = Tokens::$operators; + + /* + * The `T_COALESCE` token may be available pre-PHPCS 2.6.1 depending on the PHP version + * used to run PHPCS. + */ + if (\defined('T_COALESCE')) { + $tokens[\T_COALESCE] = \T_COALESCE; + } + + if (\defined('T_COALESCE_EQUAL')) { + unset($tokens[\T_COALESCE_EQUAL]); + } + + return $tokens; + } + + /** + * Retrieve the PHPCS parenthesis openers tokens array in a cross-version compatible manner. + * + * Changelog for the PHPCS native array: + * - Introduced in PHPCS 0.0.5. + * - PHPCS 3.5.0: `T_LIST` and `T_ANON_CLASS` added to the array. + * + * Note: While `T_LIST` and `T_ANON_CLASS` will be included in the return value for this + * method, the associated parentheses will not have the `'parenthesis_owner'` index set + * until PHPCS 3.5.0. + * + * @see \PHP_CodeSniffer\Util\Tokens::$parenthesisOpeners Original array. + * + * @since 1.0.0 + * + * @return array => Token array. + */ + public static function parenthesisOpeners() + { + $tokens = Tokens::$parenthesisOpeners; + $tokens[\T_LIST] = \T_LIST; + $tokens[\T_ANON_CLASS] = \T_ANON_CLASS; + + return $tokens; + } + + /** + * Retrieve the PHPCS comment tokens array in a cross-version compatible manner. + * + * Changelog for the PHPCS native array: + * - Introduced in PHPCS 3.2.3. + * + * @see \PHP_CodeSniffer\Util\Tokens::$phpcsCommentTokens Original array. + * + * @since 1.0.0 + * + * @return array => Token array or an empty array for PHPCS + * versions in which the PHPCS native annotation + * tokens did not exist yet. + */ + public static function phpcsCommentTokens() + { + static $tokenArray = []; + + if (isset(Tokens::$phpcsCommentTokens)) { + return Tokens::$phpcsCommentTokens; + } + + if (\defined('T_PHPCS_IGNORE')) { + // PHPCS 3.2.0 - 3.2.2. + if (empty($tokenArray)) { + foreach (self::$phpcsCommentTokensTypes as $type) { + $tokenArray[\constant($type)] = \constant($type); + } + } + + return $tokenArray; + } + + return []; + } + + /** + * Retrieve the PHPCS text string tokens array in a cross-version compatible manner. + * + * Changelog for the PHPCS native array: + * - Introduced in PHPCS 2.9.0. + * + * @see \PHP_CodeSniffer\Util\Tokens::$textStringTokens Original array. + * + * @since 1.0.0 + * + * @return array => Token array. + */ + public static function textStringTokens() + { + if (isset(Tokens::$textStringTokens)) { + return Tokens::$textStringTokens; + } + + return self::$textStringTokens; + } + + /** + * 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.1.0: `T_SELF` and `T_STATIC` 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[\T_SELF] = \T_SELF; + $tokens[\T_STATIC] = \T_STATIC; + + return $tokens; + } + + /** + * Retrieve the OO scope tokens array in a cross-version compatible manner. + * + * Changelog for the PHPCS native array: + * - Introduced in PHPCS 3.1.0. + * + * @see \PHP_CodeSniffer\Util\Tokens::$ooScopeTokens Original array. + * + * @since 1.0.0 + * + * @return array => Token array. + */ + public static function ooScopeTokens() + { + if (isset(Tokens::$ooScopeTokens)) { + return Tokens::$ooScopeTokens; + } + + return self::$ooScopeTokens; + } +} diff --git a/PHPCSUtils/BackCompat/Helper.php b/PHPCSUtils/BackCompat/Helper.php new file mode 100644 index 00000000..11cfaf0b --- /dev/null +++ b/PHPCSUtils/BackCompat/Helper.php @@ -0,0 +1,183 @@ +phpcs->cli->getCommandLineValues(); + if (isset($config[$key])) { + return $config[$key]; + } + } else { + // PHPCS 3.x. + $config = $phpcsFile->config; + if (isset($config->{$key})) { + return $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 $tabWidth; + } + + return self::DEFAULT_TABWIDTH; + } + + /** + * Check whether the `--ignore-annotations` option has been used. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $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 (\class_exists('\PHP_CodeSniffer\Config') === false) { + // PHPCS 2.x does not support `--ignore-annotations`. + return false; + } + + // PHPCS 3.x. + if (isset($phpcsFile, $phpcsFile->config->annotations)) { + return ! $phpcsFile->config->annotations; + } + + $annotations = \PHP_CodeSniffer\Config::getConfigData('annotations'); + if (isset($annotations)) { + return ! $annotations; + } + + return false; + } +} diff --git a/PHPCSUtils/Fixers/SpacesFixer.php b/PHPCSUtils/Fixers/SpacesFixer.php new file mode 100644 index 00000000..557412a3 --- /dev/null +++ b/PHPCSUtils/Fixers/SpacesFixer.php @@ -0,0 +1,245 @@ +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 (($ptrA + 1) !== $ptrB) { + 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 ($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/TestUtils/UtilityMethodTestCase.php b/PHPCSUtils/TestUtils/UtilityMethodTestCase.php new file mode 100644 index 00000000..4928dcd2 --- /dev/null +++ b/PHPCSUtils/TestUtils/UtilityMethodTestCase.php @@ -0,0 +1,346 @@ +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 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 `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 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. + * + * @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. + * + * @var int + */ + protected static $tabWidth = 4; + + /** + * The {@see \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. + * + * @since 1.0.0 + * + * @beforeClass + * + * @return void + */ + public static function setUpTestFile() + { + parent::setUpBeforeClass(); + + $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) { + self::fail("Test case file missing. Expected case file location: $caseFile"); + } + + $contents = \file_get_contents($caseFile); + + if (\version_compare(Helper::getVersion(), '2.99.99', '>')) { + // PHPCS 3.x. + $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. + } catch (RuntimeException $e) { + // PHPCS 3.0.0 < 3.5.0. + } + } else { + // PHPCS 2.x. + $phpcs = new \PHP_CodeSniffer(null, static::$tabWidth); + self::$phpcsFile = new \PHP_CodeSniffer_File( + $caseFile, + [], + [], + $phpcs + ); + + /* + * Using error silencing to drown out "undefined index" notices for tokenizer + * issues in PHPCS 2.x which won't get fixed anymore anyway. + */ + @self::$phpcsFile->start($contents); + } + + // Fail the test if the case file failed to tokenize. + if (self::$phpcsFile->numTokens === 0) { + self::fail("Tokenizing of the test case file failed for case file: $caseFile"); + } + } + + /** + * Clean up after finished test. + * + * @since 1.0.0 + * + * @afterClass + * + * @return void + */ + public static function resetTestFile() + { + self::$phpcsFile = null; + } + + /** + * Get the token pointer for a target token based on a specific comment found on the line before. + * + * 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 delimiter comment to look for. + * @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 + */ + public function getTargetToken($commentString, $tokenType, $tokenContent = null) + { + $start = (self::$phpcsFile->numTokens - 1); + $comment = self::$phpcsFile->findPrevious( + \T_COMMENT, + $start, + null, + false, + $commentString + ); + + $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) { + $msg = 'Failed to find test target token for comment string: ' . $commentString; + if ($tokenContent !== null) { + $msg .= ' With token content: ' . $tokenContent; + } + + $this->fail($msg); + } + + return $target; + } + + /** + * Helper method to tell PHPUnit to expect a PHPCS Exception in a PHPUnit cross-version + * compatible manner. + * + * @param string $msg The expected exception message. + * @param string $type The 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..caa0a72e --- /dev/null +++ b/PHPCSUtils/Tokens/Collections.php @@ -0,0 +1,348 @@ + => + */ + public 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. + * + * List which is backward-compatible with PHPCS < 3.3.0. + * Should only be used selectively. + * + * @since 1.0.0 + * + * @see \PHPCSUtils\Tokens\Collections::$shortArrayTokensBC Related property containing only tokens used + * for short arrays (cross-version). + * + * @var array => + */ + public 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 + * + * @var array => + */ + public static $classModifierKeywords = [ + \T_FINAL => \T_FINAL, + \T_ABSTRACT => \T_ABSTRACT, + ]; + + /** + * 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 `T_NAMESPACE` on purpose as variables declared + * within a namespace scope are still global and not limited to that namespace. + * + * @since 1.0.0 + * + * @var array => + */ + public static $closedScopes = [ + \T_CLASS => \T_CLASS, + \T_ANON_CLASS => \T_ANON_CLASS, + \T_INTERFACE => \T_INTERFACE, + \T_TRAIT => \T_TRAIT, + \T_FUNCTION => \T_FUNCTION, + \T_CLOSURE => \T_CLOSURE, + ]; + + /** + * Tokens which are used to create lists. + * + * @since 1.0.0 + * + * @see \PHPCSUtils\Tokens\Collections::$shortListTokens Related property containing only tokens used + * for short lists. + * + * @var array => + */ + public 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. + * + * List which is backward-compatible with PHPCS < 3.3.0. + * Should only be used selectively. + * + * @since 1.0.0 + * + * @see \PHPCSUtils\Tokens\Collections::$shortListTokensBC Related property containing only tokens used + * for short lists (cross-version). + * + * @var array => + */ + public 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 + * + * @var array => + */ + public static $namespaceDeclarationClosers = [ + \T_SEMICOLON => \T_SEMICOLON, + \T_OPEN_CURLY_BRACKET => \T_OPEN_CURLY_BRACKET, + \T_CLOSE_TAG => \T_CLOSE_TAG, + ]; + + /** + * OO structures which can use the `extends` keyword. + * + * @since 1.0.0 + * + * @var array => + */ + public 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 + * + * @var array => + */ + public static $OOCanImplement = [ + \T_CLASS => \T_CLASS, + \T_ANON_CLASS => \T_ANON_CLASS, + ]; + + /** + * OO scopes in which constants can be declared. + * + * Note: traits can not declare constants. + * + * @since 1.0.0 + * + * @var array => + */ + public static $OOConstantScopes = [ + \T_CLASS => \T_CLASS, + \T_ANON_CLASS => \T_ANON_CLASS, + \T_INTERFACE => \T_INTERFACE, + ]; + + /** + * OO scopes in which properties can be declared. + * + * Note: interfaces can not declare properties. + * + * @since 1.0.0 + * + * @var array => + */ + public 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 + * + * @var array => + */ + public static $parameterTypeTokens = [ + \T_ARRAY_HINT => \T_ARRAY_HINT, // PHPCS < 3.3.0. + \T_CALLABLE => \T_CALLABLE, + \T_SELF => \T_SELF, + \T_PARENT => \T_PARENT, + \T_STRING => \T_STRING, + \T_NS_SEPARATOR => \T_NS_SEPARATOR, + ]; + + /** + * Modifier keywords which can be used for a property declaration. + * + * @since 1.0.0 + * + * @var array => + */ + public static $propertyModifierKeywords = [ + \T_PUBLIC => \T_PUBLIC, + \T_PRIVATE => \T_PRIVATE, + \T_PROTECTED => \T_PROTECTED, + \T_STATIC => \T_STATIC, + \T_VAR => \T_VAR, + ]; + + /** + * Token types which can be encountered in a property type declaration. + * + * @since 1.0.0 + * + * @var array => + */ + public static $propertyTypeTokens = [ + \T_ARRAY_HINT => \T_ARRAY_HINT, // PHPCS < 3.3.0. + \T_CALLABLE => \T_CALLABLE, + \T_SELF => \T_SELF, + \T_PARENT => \T_PARENT, + \T_STRING => \T_STRING, + \T_NS_SEPARATOR => \T_NS_SEPARATOR, + ]; + + /** + * Token types which can be encountered in a return type declaration. + * + * @since 1.0.0 + * + * @var array => + */ + public static $returnTypeTokens = [ + \T_STRING => \T_STRING, + \T_CALLABLE => \T_CALLABLE, + \T_SELF => \T_SELF, + \T_PARENT => \T_PARENT, + \T_NS_SEPARATOR => \T_NS_SEPARATOR, + \T_RETURN_TYPE => \T_RETURN_TYPE, // PHPCS 2.4.0 < 3.3.0. + \T_ARRAY_HINT => \T_ARRAY_HINT, // PHPCS < 2.8.0. + ]; + + /** + * Tokens which are used for short arrays. + * + * @since 1.0.0 + * + * @see \PHPCSUtils\Tokens\Collections::$arrayTokens Related property containing all tokens used for arrays. + * + * @var array => + */ + public 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. + * + * List which is backward-compatible with PHPCS < 3.3.0. + * Should only be used selectively. + * + * @since 1.0.0 + * + * @see \PHPCSUtils\Tokens\Collections::$arrayTokensBC Related property containing all tokens used for arrays + * (cross-version). + * + * @var array => + */ + public 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. + * + * @since 1.0.0 + * + * @see \PHPCSUtils\Tokens\Collections::$listTokens Related property containing all tokens used for lists. + * + * @var array => + */ + public 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. + * + * List which is backward-compatible with PHPCS < 3.3.0. + * Should only be used selectively. + * + * @since 1.0.0 + * + * @see \PHPCSUtils\Tokens\Collections::$listTokensBC Related property containing all tokens used for lists + * (cross-version). + * + * @var array => + */ + public 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 + * + * @var array => + */ + public static $textStingStartTokens = [ + \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, + ]; +} diff --git a/PHPCSUtils/Utils/Arrays.php b/PHPCSUtils/Utils/Arrays.php new file mode 100644 index 00000000..0e490479 --- /dev/null +++ b/PHPCSUtils/Utils/Arrays.php @@ -0,0 +1,291 @@ + => + */ + private static $doubleArrowTargets = [ + \T_DOUBLE_ARROW => \T_DOUBLE_ARROW, + \T_ARRAY => \T_ARRAY, + \T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY, + ]; + + /** + * 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) + { + $tokens = $phpcsFile->getTokens(); + + // Is this one of the tokens this function handles ? + if (isset($tokens[$stackPtr]) === false + || isset(Collections::$shortArrayTokensBC[$tokens[$stackPtr]['code']]) === false + ) { + return false; + } + + // All known tokenizer bugs are in PHPCS versions before 3.3.0. + $phpcsVersion = Helper::getVersion(); + + /* + * Deal with square brackets which may be incorrectly tokenized short arrays. + */ + if (isset(Collections::$shortArrayTokens[$tokens[$stackPtr]['code']]) === false) { + if (\version_compare($phpcsVersion, '3.3.0', '>=')) { + // These will just be properly tokenized, plain square brackets. No need for further checks. + return false; + } + + $opener = $stackPtr; + if ($tokens[$stackPtr]['code'] === \T_CLOSE_SQUARE_BRACKET) { + $opener = $tokens[$stackPtr]['bracket_opener']; + } + + if (isset($tokens[$opener]['bracket_closer']) === false) { + return false; + } + + $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($opener - 1), null, true); + + if (\version_compare($phpcsVersion, '2.8.0', '>=')) { + /* + * BC: Work around a bug in the tokenizer of PHPCS 2.8.0 - 3.2.3 where a `[` would be + * tokenized as T_OPEN_SQUARE_BRACKET instead of T_OPEN_SHORT_ARRAY if it was + * preceded by a PHP open tag at the very start of the file. + * + * If we have square brackets which are not that specific situation, they are just plain + * square brackets. + * + * @link https://github.com/squizlabs/PHP_CodeSniffer/issues/1971 + */ + if ($prevNonEmpty !== 0 || $tokens[$prevNonEmpty]['code'] !== \T_OPEN_TAG) { + return false; + } + } + + if (\version_compare($phpcsVersion, '2.8.0', '<')) { + /* + * BC: Work around a bug in the tokenizer of PHPCS < 2.8.0 where a `[` would be + * tokenized as T_OPEN_SQUARE_BRACKET instead of T_OPEN_SHORT_ARRAY if it was + * preceded by a close curly of a control structure. + * + * If we have square brackets which are not that specific situation, they are just plain + * square brackets. + * + * @link https://github.com/squizlabs/PHP_CodeSniffer/issues/1284 + */ + if ($tokens[$prevNonEmpty]['code'] !== \T_CLOSE_CURLY_BRACKET + || isset($tokens[$prevNonEmpty]['scope_condition']) === false + ) { + return false; + } + } + } else { + /* + * Deal with short array brackets which may be incorrectly tokenized plain square brackets. + */ + if (\version_compare($phpcsVersion, '2.9.0', '<')) { + $opener = $stackPtr; + if ($tokens[$stackPtr]['code'] === \T_CLOSE_SHORT_ARRAY) { + $opener = $tokens[$stackPtr]['bracket_opener']; + } + + /* + * BC: Work around a bug in the tokenizer of PHPCS < 2.9.0 where array dereferencing + * of short array and string literals would be incorrectly tokenized as short array. + * I.e. the square brackets in `'PHP'[0]` would be tokenized as short array. + * + * @link https://github.com/squizlabs/PHP_CodeSniffer/issues/1381 + */ + $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($opener - 1), null, true); + if ($tokens[$prevNonEmpty]['code'] === \T_CLOSE_SHORT_ARRAY + || $tokens[$prevNonEmpty]['code'] === \T_CONSTANT_ENCAPSED_STRING + ) { + return false; + } + + /* + * BC: Work around a bug in the tokenizer of PHPCS 2.8.0 and 2.8.1 where array dereferencing + * of a variable variable would be incorrectly tokenized as short array. + * + * @link https://github.com/squizlabs/PHP_CodeSniffer/issues/1284 + */ + if (\version_compare($phpcsVersion, '2.8.0', '>=') + && $tokens[$prevNonEmpty]['code'] === \T_CLOSE_CURLY_BRACKET + ) { + $openCurly = $tokens[$prevNonEmpty]['bracket_opener']; + $beforeCurlies = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($openCurly - 1), null, true); + if ($tokens[$beforeCurlies]['code'] === \T_DOLLAR) { + return false; + } + } + } + } + + // In all other circumstances, make sure this isn't a short list instead of a short array. + return (Lists::isShortList($phpcsFile, $stackPtr) === false); + } + + /** + * Find the array opener & 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_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 Array with two keys `opener`, `closer` or false if + * not a (short) array token or if the opener/closer + * could not be determined. + */ + 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::$arrayTokensBC[$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()}. + * + * @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 end of the array item (inclusive). + * + * @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 + ); + } + + $targets = self::$doubleArrowTargets + Collections::$closedScopes; + + $doubleArrow = ($start - 1); + ++$end; + do { + $doubleArrow = $phpcsFile->findNext( + $targets, + ($doubleArrow + 1), + $end + ); + + if ($doubleArrow === false) { + break; + } + + if ($tokens[$doubleArrow]['code'] === \T_DOUBLE_ARROW) { + return $doubleArrow; + } + + // Skip over closed scopes which may contain foreach structures or generators. + if (isset(Collections::$closedScopes[$tokens[$doubleArrow]['code']]) === true + && isset($tokens[$doubleArrow]['scope_closer']) === true + ) { + $doubleArrow = $tokens[$doubleArrow]['scope_closer']; + continue; + } + + // Start of nested long/short array. + break; + } while ($doubleArrow < $end); + + return false; + } +} diff --git a/PHPCSUtils/Utils/Conditions.php b/PHPCSUtils/Utils/Conditions.php new file mode 100644 index 00000000..f998d7c9 --- /dev/null +++ b/PHPCSUtils/Utils/Conditions.php @@ -0,0 +1,155 @@ +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 ($reverse === true) { + \end($conditions); + } else { + \reset($conditions); + } + + return \key($conditions); + } + + if ($reverse === true) { + $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 StackPtr to the condition or false if the token does not have the condition. + */ + public static function getFirstCondition(File $phpcsFile, $stackPtr, $types = []) + { + return self::getCondition($phpcsFile, $stackPtr, $types, false); + } + + /** + * 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 StackPtr to the condition or false if the token does not have the condition. + */ + public static function getLastCondition(File $phpcsFile, $stackPtr, $types = []) + { + return self::getCondition($phpcsFile, $stackPtr, $types, true); + } +} diff --git a/PHPCSUtils/Utils/FunctionDeclarations.php b/PHPCSUtils/Utils/FunctionDeclarations.php new file mode 100644 index 00000000..ddf24ba2 --- /dev/null +++ b/PHPCSUtils/Utils/FunctionDeclarations.php @@ -0,0 +1,722 @@ + => + */ + public static $magicFunctions = [ + '__autoload' => 'autoload', + ]; + + /** + * A list of all PHP magic methods. + * + * The array keys are the method names, the values as well, but without the double underscore. + * + * 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 $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 extension 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. + * + * @codeCoverageIgnore + * + * @see \PHPCSUtils\BackCompat\BCFile::getDeclarationName() Original function. + * @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 function. + * + * @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, T_CLASS, T_TRAIT, or T_INTERFACE. + */ + public static function getName(File $phpcsFile, $stackPtr) + { + return ObjectDeclarations::getName($phpcsFile, $stackPtr); + } + + /** + * Retrieves the visibility and implementation properties of a method. + * + * The format of the return value is: + * + * 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 nullable. + * '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 + * ); + * + * + * 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. + * - New `return_type_end_token` (int|false) array index. + * + * @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 + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a + * T_FUNCTION or a T_CLOSURE token. + */ + public static function getProperties(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]) === false + || ($tokens[$stackPtr]['code'] !== \T_FUNCTION + && $tokens[$stackPtr]['code'] !== \T_CLOSURE) + ) { + throw new RuntimeException('$stackPtr must be of type T_FUNCTION or T_CLOSURE'); + } + + 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; + + if (isset($tokens[$stackPtr]['parenthesis_closer']) === true) { + $scopeOpener = null; + if (isset($tokens[$stackPtr]['scope_opener']) === true) { + $scopeOpener = $tokens[$stackPtr]['scope_opener']; + } + + for ($i = $tokens[$stackPtr]['parenthesis_closer']; $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]['type'] === 'T_NULLABLE' + // Handle nullable tokens in PHPCS < 2.8.0. + || (\defined('T_NULLABLE') === false && $tokens[$i]['code'] === \T_INLINE_THEN) + ) { + $nullableReturnType = true; + } + + if (isset(Collections::$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 USE token for a closure use group. + * + * The returned array will contain the following information for each parameter: + * + * + * 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. + * '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 nullable. + * '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: + * '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. + * + * 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. + * + * @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. + */ + public static function getParameters(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]) === false + || ($tokens[$stackPtr]['code'] !== \T_FUNCTION + && $tokens[$stackPtr]['code'] !== \T_CLOSURE + && $tokens[$stackPtr]['code'] !== \T_USE) + ) { + throw new RuntimeException('$stackPtr must be of type T_FUNCTION or T_CLOSURE or T_USE'); + } + + if ($tokens[$stackPtr]['code'] === \T_USE) { + $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 []; + } + + $closer = $tokens[$opener]['parenthesis_closer']; + + $vars = []; + $currVar = null; + $paramStart = ($opener + 1); + $defaultStart = null; + $equalToken = null; + $paramCount = 0; + $passByReference = false; + $referenceToken = false; + $variableLength = false; + $variadicToken = false; + $typeHint = ''; + $typeHintToken = false; + $typeHintEndToken = false; + $nullableType = false; + + for ($i = $paramStart; $i <= $closer; $i++) { + // Changed from checking 'code' to 'type' to allow for T_NULLABLE not existing in PHPCS < 2.8.0. + switch ($tokens[$i]['type']) { + 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_ARRAY_HINT': // PHPCS < 3.3.0. + case 'T_CALLABLE': + case 'T_SELF': + case 'T_PARENT': + case 'T_STATIC': // Self and parent are valid, static invalid, but was probably intended as type hint. + case 'T_STRING': + case 'T_NS_SEPARATOR': + if ($typeHintToken === false) { + $typeHintToken = $i; + } + + $typeHint .= $tokens[$i]['content']; + $typeHintEndToken = $i; + break; + + case 'T_NULLABLE': + case 'T_INLINE_THEN': // PHPCS < 2.8.0. + $nullableType = true; + $typeHint .= $tokens[$i]['content']; + $typeHintEndToken = $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]['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 ($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; + $passByReference = false; + $referenceToken = false; + $variableLength = false; + $variadicToken = false; + $typeHint = ''; + $typeHintToken = false; + $typeHintEndToken = false; + $nullableType = false; + + $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; + } + } + + 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::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 (Conditions::hasCondition($phpcsFile, $stackPtr, BCTokens::ooScopeTokens()) === 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. + * + * @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::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. + * + * @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 PHP native double underscore method. + * + * @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, BCTokens::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 PHP native double underscore method. + * + * @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. + * + * @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 + * + * {@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.} + * + * @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..043331f0 --- /dev/null +++ b/PHPCSUtils/Utils/GetTokensAsString.php @@ -0,0 +1,256 @@ +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..0e50f091 --- /dev/null +++ b/PHPCSUtils/Utils/Lists.php @@ -0,0 +1,400 @@ +getTokens(); + + // Is this one of the tokens this function handles ? + if (isset($tokens[$stackPtr]) === false + || isset(Collections::$shortListTokensBC[$tokens[$stackPtr]['code']]) === false + ) { + return false; + } + + /* + * BC: Work around a bug in the tokenizer of PHPCS 2.8.0 - 3.2.3 where a `[` would be + * tokenized as T_OPEN_SQUARE_BRACKET instead of T_OPEN_SHORT_ARRAY if it was + * preceded by a PHP open tag at the very start of the file. + * + * In that case, we also know for sure that it is a short list as long as the close + * bracket is followed by an `=` sign. + * + * @link https://github.com/squizlabs/PHP_CodeSniffer/issues/1971 + * + * Also work around a bug in the tokenizer of PHPCS < 2.8.0 where a `[` would be + * tokenized as T_OPEN_SQUARE_BRACKET instead of T_OPEN_SHORT_ARRAY if it was + * preceded by a closing curly belonging to a control structure. + * + * @link https://github.com/squizlabs/PHP_CodeSniffer/issues/1284 + */ + if ($tokens[$stackPtr]['code'] === \T_OPEN_SQUARE_BRACKET + || $tokens[$stackPtr]['code'] === \T_CLOSE_SQUARE_BRACKET + ) { + $opener = $stackPtr; + if ($tokens[$stackPtr]['code'] === \T_CLOSE_SQUARE_BRACKET) { + $opener = $tokens[$stackPtr]['bracket_opener']; + } + + if (isset($tokens[$opener]['bracket_closer']) === false) { + // Definitely not a short list. + return false; + } + + $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($opener - 1), null, true); + if ((($prevNonEmpty === 0 && $tokens[$prevNonEmpty]['code'] === \T_OPEN_TAG) // Bug #1971. + || ($tokens[$prevNonEmpty]['code'] === \T_CLOSE_CURLY_BRACKET + && isset($tokens[$prevNonEmpty]['scope_condition']))) // Bug #1284. + ) { + $closer = $tokens[$opener]['bracket_closer']; + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($closer + 1), null, true); + if ($nextNonEmpty !== false && $tokens[$nextNonEmpty]['code'] === \T_EQUAL) { + return true; + } + } + + return false; + } + + switch ($tokens[$stackPtr]['code']) { + case \T_OPEN_SHORT_ARRAY: + $opener = $stackPtr; + $closer = $tokens[$stackPtr]['bracket_closer']; + break; + + case \T_CLOSE_SHORT_ARRAY: + $opener = $tokens[$stackPtr]['bracket_opener']; + $closer = $stackPtr; + break; + } + + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($closer + 1), null, true); + if ($nextNonEmpty !== false && $tokens[$nextNonEmpty]['code'] === \T_EQUAL) { + return true; + } + + // Check for short list in foreach, i.e. `foreach($array as [$a, $b])`. + $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($opener - 1), null, true); + if ($prevNonEmpty !== false + && ($tokens[$prevNonEmpty]['code'] === \T_AS + || $tokens[$prevNonEmpty]['code'] === \T_DOUBLE_ARROW) + && Parentheses::lastOwnerIn($phpcsFile, $prevNonEmpty, \T_FOREACH) !== false + ) { + return true; + } + + // Maybe this is a short list syntax nested inside another short list syntax ? + $parentOpen = $opener; + do { + $parentOpen = $phpcsFile->findPrevious( + [\T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET], // BC: PHPCS#1971. + ($parentOpen - 1), + null, + false, + null, + true + ); + + if ($parentOpen === false) { + return false; + } + } while (isset($tokens[$parentOpen]['bracket_closer']) === true + && $tokens[$parentOpen]['bracket_closer'] < $opener + ); + + return self::isShortList($phpcsFile, $parentOpen); + } + + /** + * Find the list opener & 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_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 Array with two keys `opener`, `closer` or false if + * not a (short) list token or if the opener/closer + * could not be determined. + */ + 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::$listTokensBC[$tokens[$stackPtr]['code']]) === false + ) { + return false; + } + + switch ($tokens[ $stackPtr ]['code']) { + case \T_LIST: + if (isset($tokens[$stackPtr]['parenthesis_opener'])) { + // PHPCS 3.5.0. + $opener = $tokens[$stackPtr]['parenthesis_opener']; + } else { + // PHPCS < 3.5.0. + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if ($nextNonEmpty !== false + && $tokens[$nextNonEmpty]['code'] === \T_OPEN_PARENTHESIS + ) { + $opener = $nextNonEmpty; + } + } + + if (isset($opener, $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: + * + * + * 0 => array( + * 'raw' => string, // The full content of the variable definition, including + * // whitespace and comments. + * // This may be an empty string when an item is being skipped. + * 'is_empty' => bool, // Whether this is an empty list item, i.e. the + * // second item in `list($a, , $b)`. + * ) + * + * + * Non-empty list items will have the following additional array indexes set: + * + * 'assignment' => string, // The content of the assignment part, cleaned of comments. + * // This could be a nested list. + * 'nested_list' => bool, // Whether this is a nested list. + * '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. + * 'variable' => string|false, // The base variable being assigned to or + * // FALSE in case of a nested list or variable variable. + * // I.e. `$a` in `list($a['key'])`. + * 'assignment_token' => int, // The start pointer for the assignment. + * 'assignment_end_token' => int, // The end pointer for the assignment. + * + * + * + * Assignments with keys will have the following additional array indexes set: + * + * '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+). + * + * @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.'); + } + + $opener = $openClose['opener']; + $closer = $openClose['closer']; + + $tokens = $phpcsFile->getTokens(); + + $vars = []; + $start = null; + $lastNonEmpty = null; + $reference = null; + $list = null; + $lastComma = $opener; + $current = []; + + for ($i = ($opener + 1); $i <= $closer; $i++) { + if (isset(Tokens::$emptyTokens[$tokens[$i]['code']])) { + continue; + } + + switch ($tokens[$i]['code']) { + case \T_DOUBLE_ARROW: + $current['key'] = GetTokensAsString::compact($phpcsFile, $start, $lastNonEmpty, true); + + $current['key_token'] = $start; + $current['key_end_token'] = $lastNonEmpty; + $current['double_arrow_token'] = $i; + + // Partial reset. + $start = null; + $lastNonEmpty = null; + $reference = null; + break; + + case \T_COMMA: + case $tokens[$closer]['code']: + 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; + } + } + + $current['raw'] = \trim(GetTokensAsString::normal($phpcsFile, ($lastComma + 1), ($i - 1))); + + if ($start === null) { + $current['is_empty'] = true; + } else { + $current['is_empty'] = false; + $current['assignment'] = \trim( + GetTokensAsString::compact($phpcsFile, $start, $lastNonEmpty, true) + ); + $current['nested_list'] = isset($list); + + $current['assign_by_reference'] = false; + $current['reference_token'] = false; + if (isset($reference)) { + $current['assign_by_reference'] = true; + $current['reference_token'] = $reference; + } + + $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; + } + + $vars[] = $current; + + // Reset. + $start = null; + $lastNonEmpty = null; + $reference = null; + $list = null; + $lastComma = $i; + $current = []; + + 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; + } + } + + return $vars; + } +} diff --git a/PHPCSUtils/Utils/Namespaces.php b/PHPCSUtils/Utils/Namespaces.php new file mode 100644 index 00000000..253dbdc6 --- /dev/null +++ b/PHPCSUtils/Utils/Namespaces.php @@ -0,0 +1,352 @@ +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_OPEN_CURLY_BRACKET) + ) { + return 'declaration'; + } + + if ($start !== $stackPtr + && $tokens[$next]['code'] === \T_NS_SEPARATOR + ) { + 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. + * + * @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 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 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; + } + + $tokens = $phpcsFile->getTokens(); + $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. + * + * Note: 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, they are not + * **within** a namespace. + * + * 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 int|false Token pointer to the applicable namespace keyword or + * false if it couldn't be determined or 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. + */ + + // 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, + ]; + + 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 + /* + * BC: Work around a bug where curlies for variable variables received an incorrect + * and irrelevant scope condition in PHPCS < 3.3.0. + * {@link https://github.com/squizlabs/PHP_CodeSniffer/issues/1882} + */ + && self::isDeclaration($phpcsFile, $tokens[$prev]['scope_condition']) === true + ) { + 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) { + $prev = $tokens[$prev]['scope_opener']; + } + + 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 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) { + return false; + } + + return $prev; + } + } while (true); + + return false; + } + + /** + * 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 empty string if it couldn't be determined + * or 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/Numbers.php b/PHPCSUtils/Utils/Numbers.php new file mode 100644 index 00000000..a9acdc3b --- /dev/null +++ b/PHPCSUtils/Utils/Numbers.php @@ -0,0 +1,480 @@ +[0-9]+) + | + (?P([0-9]*\.(?P>LNUM)|(?P>LNUM)\.[0-9]*)) + ) + [e][+-]?(?P>LNUM) + ) + | + (?P>DNUM) + | + (?:0|[1-9][0-9]*) + )$ + `ixD'; + + /** + * Regex to determine is a T_STRING following a T_[DL]NUMBER is part of a numeric literal sequence. + * + * PHP cross-version compat for PHP 7.4 numeric literals with underscore separators. + * + * @since 1.0.0 + * + * @var string + */ + const REGEX_NUMLIT_STRING = '`^((? true, + ]; + + /** + * Valid tokens which could be part of a numeric literal sequence in PHP < 7.4. + * + * @since 1.0.0 + * + * @var array + */ + private static $numericLiteralAcceptedTokens = [ + \T_LNUMBER => true, + \T_DNUMBER => true, + \T_STRING => true, + ]; + + /** + * Helper function to deal with numeric literals, potentially with underscore separators. + * + * PHP < 7.4 does not tokenize numeric literals containing underscores correctly. + * As of PHPCS 3.5.3, PHPCS contains a back-fill, but this backfill was buggy in the initial + * implementation. A fix for this broken backfill is included in PHPCS 3.5.4. + * + * Either way, this function provides a backfill for all PHPCS/PHP combinations where + * PHP 7.4 numbers with underscore separators are tokenized incorrectly - with the + * exception of PHPCS 3.5.3 as the buggyness of the original backfill implementation makes + * it impossible to provide reliable results. + * + * @link https://github.com/squizlabs/PHP_CodeSniffer/issues/2546 + * @link https://github.com/squizlabs/PHP_CodeSniffer/pull/2771 + * + * @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 the following information about the number: + * - 'orig_content' string The (potentially concatenated) original content of the tokens; + * - 'content' string The (potentially concatenated) 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; + * This will be the same as the original stackPtr if it is not + * a PHP 7.4 number with underscores. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token is not of type + * T_LNUMBER or T_DNUMBER. + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If this function is called in combination + * with an unsupported PHPCS version. + */ + public static function getCompleteNumber(File $phpcsFile, $stackPtr) + { + + static $php74, $phpcsVersion, $phpcsWithBackfill; + + $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' + ); + } + + if (isset($php74, $phpcsVersion, $phpcsWithBackfill) === false) { + $php74 = \version_compare(\PHP_VERSION_ID, '70399', '>'); + $phpcsVersion = Helper::getVersion(); + $maxUnsupported = \max(\array_keys(self::$unsupportedPHPCSVersions)); + $phpcsWithBackfill = \version_compare($phpcsVersion, $maxUnsupported, '>'); + } + + /* + * Bow out for PHPCS version(s) with broken tokenization of PHP 7.4 numeric literals with + * separators, including for PHP 7.4, as the backfill kicks in for PHP 7.4 while it shouldn't. + * + * @link https://github.com/squizlabs/PHP_CodeSniffer/issues/2546 + */ + if (isset(self::$unsupportedPHPCSVersions[$phpcsVersion]) === true) { + throw new RuntimeException('The ' . __METHOD__ . '() method does not support PHPCS ' . $phpcsVersion); + } + + $content = $tokens[$stackPtr]['content']; + $result = [ + 'orig_content' => $content, + 'content' => \str_replace('_', '', $content), + 'code' => $tokens[$stackPtr]['code'], + 'type' => $tokens[$stackPtr]['type'], + 'decimal' => self::getDecimalValue($content), + 'last_token' => $stackPtr, + ]; + + // When things are already correctly tokenized, there's not much to do. + if ($php74 === true + || $phpcsWithBackfill === true + || isset($tokens[($stackPtr + 1)]) === false + || $tokens[($stackPtr + 1)]['code'] !== \T_STRING + || $tokens[($stackPtr + 1)]['content'][0] !== '_' + ) { + return $result; + } + + $hex = false; + if (\strpos($content, '0x') === 0) { + $hex = true; + } + + $lastChar = \substr($content, -1); + if (\preg_match('`[0-9]`', $lastChar) !== 1) { + if ($hex === false || \preg_match('`[A-F]`i', $lastChar) !== 1) { + // Last character not valid for numeric literal sequence with underscores. + // No need to look any further. + return $result; + } + } + + /* + * OK, so this could potentially be a PHP 7.4 number with an underscore separator with PHPCS + * being run on PHP < 7.4. + */ + + $regex = self::REGEX_NUMLIT_STRING; + if ($hex === true) { + $regex = self::REGEX_HEX_NUMLIT_STRING; + } + + $next = $stackPtr; + $lastToken = $stackPtr; + + while (isset($tokens[++$next], self::$numericLiteralAcceptedTokens[$tokens[$next]['code']]) === true) { + if ($tokens[$next]['code'] === \T_STRING + && \preg_match($regex, $tokens[$next]['content'], $matches) !== 1 + ) { + break; + } + + $content .= $tokens[$next]['content']; + $lastToken = $next; + $lastChar = \substr(\strtolower($content), -1); + + // Support floats. + if ($lastChar === 'e' + && isset($tokens[($next + 1)], $tokens[($next + 2)]) === true + && ($tokens[($next + 1)]['code'] === \T_MINUS + || $tokens[($next + 1)]['code'] === \T_PLUS) + && $tokens[($next + 2)]['code'] === \T_LNUMBER + ) { + $content .= $tokens[($next + 1)]['content']; + $content .= $tokens[($next + 2)]['content']; + $next += 2; + $lastToken = $next; + } + + // Don't look any further if the last char is not valid before a separator. + if (\preg_match('`[0-9]`', $lastChar) !== 1) { + if ($hex === false || \preg_match('`[a-f]`i', $lastChar) !== 1) { + break; + } + } + } + + // OK, so we now have `content` including potential underscores. Let's strip them out. + $result['orig_content'] = $content; + $result['content'] = \str_replace('_', '', $content); + $result['decimal'] = self::getDecimalValue($result['content']); + $result['last_token'] = $lastToken; + + // Determine actual token type. + $type = $result['type']; + if ($type === 'T_LNUMBER') { + if ($hex === false + && (\strpos($result['content'], '.') !== false + || \stripos($result['content'], 'e') !== false) + ) { + $type = 'T_DNUMBER'; + } elseif (($result['decimal'] + 0) > \PHP_INT_MAX) { + $type = 'T_DNUMBER'; + } + } + + $result['code'] = \constant($type); + $result['type'] = $type; + + return $result; + } + + /** + * Get the decimal number value of a numeric string. + * + * Takes PHP 7.4 numeric literal separators in numbers into account. + * + * @since 1.0.0 + * + * @param string $string Arbitrary token content string. + * + * @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($string) + { + if (\is_string($string) === false || $string === '') { + 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.} + */ + $string = \str_replace('_', '', $string); + + if (self::isDecimalInt($string) === true) { + return $string; + } + + if (self::isHexidecimalInt($string) === true) { + return (string) \hexdec($string); + } + + if (self::isBinaryInt($string) === true) { + return (string) \bindec($string); + } + + if (self::isOctalInt($string) === true) { + return (string) \octdec($string); + } + + if (self::isFloat($string) === true) { + return $string; + } + + 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 $string Arbitrary string. + * + * @return bool + */ + public static function isDecimalInt($string) + { + if (\is_string($string) === false || $string === '') { + return false; + } + + // Remove potential PHP 7.4 numeric literal separators. + $string = \str_replace('_', '', $string); + + return (\preg_match(self::REGEX_DECIMAL_INT, $string) === 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 $string Arbitrary string. + * + * @return bool + */ + public static function isHexidecimalInt($string) + { + if (\is_string($string) === false || $string === '') { + return false; + } + + // Remove potential PHP 7.4 numeric literal separators. + $string = \str_replace('_', '', $string); + + return (\preg_match(self::REGEX_HEX_INT, $string) === 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 $string Arbitrary string. + * + * @return bool + */ + public static function isBinaryInt($string) + { + if (\is_string($string) === false || $string === '') { + return false; + } + + // Remove potential PHP 7.4 numeric literal separators. + $string = \str_replace('_', '', $string); + + return (\preg_match(self::REGEX_BINARY_INT, $string) === 1); + } + + /** + * Verify whether the contents of an arbitrary string represents an octal integer. + * + * Takes PHP 7.4 numeric literal separators in numbers into account. + * + * @since 1.0.0 + * + * @param string $string Arbitrary string. + * + * @return bool + */ + public static function isOctalInt($string) + { + if (\is_string($string) === false || $string === '') { + return false; + } + + // Remove potential PHP 7.4 numeric literal separators. + $string = \str_replace('_', '', $string); + + return (\preg_match(self::REGEX_OCTAL_INT, $string) === 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 $string Arbitrary string. + * + * @return bool + */ + public static function isFloat($string) + { + if (\is_string($string) === false || $string === '') { + return false; + } + + // Remove potential PHP 7.4 numeric literal separators. + $string = \str_replace('_', '', $string); + + return (\preg_match(self::REGEX_FLOAT, $string) === 1); + } +} diff --git a/PHPCSUtils/Utils/ObjectDeclarations.php b/PHPCSUtils/Utils/ObjectDeclarations.php new file mode 100644 index 00000000..45373b09 --- /dev/null +++ b/PHPCSUtils/Utils/ObjectDeclarations.php @@ -0,0 +1,361 @@ +getTokens(); + + if (isset($tokens[$stackPtr]) === false + || ($tokens[$stackPtr]['code'] === \T_ANON_CLASS || $tokens[$stackPtr]['code'] === \T_CLOSURE) + ) { + return null; + } + + $tokenCode = $tokens[$stackPtr]['code']; + + /* + * BC: Work-around JS ES6 classes not being tokenized as T_CLASS in PHPCS < 3.0.0. + */ + if ($phpcsFile->tokenizerType === 'JS' + && $tokenCode === \T_STRING + && $tokens[$stackPtr]['content'] === 'class' + ) { + $tokenCode = \T_CLASS; + } + + if ($tokenCode !== \T_FUNCTION + && $tokenCode !== \T_CLASS + && $tokenCode !== \T_INTERFACE + && $tokenCode !== \T_TRAIT + ) { + throw new RuntimeException( + 'Token type "' . $tokens[$stackPtr]['type'] . '" is not T_FUNCTION, T_CLASS, T_INTERFACE or T_TRAIT' + ); + } + + 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; + + $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) { + $content = null; + + /* + * BC: In PHPCS 2.6.0, in case of live coding, the last token in a file will be tokenized + * as T_STRING, but won't have the `content` index set. + */ + if (isset($tokens[$nameStart]['content'])) { + $content = $tokens[$nameStart]['content']; + } + + return $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. + * + * The format of the return value is: + * + * array( + * 'is_abstract' => false, // true if the abstract keyword was found. + * 'is_final' => false, // true if the final keyword was found. + * ); + * + * + * Main differences with the PHPCS version: + * - Bugs fixed: + * - Handling of PHPCS annotations. + * - Handling of unorthodox docblock placement. + * - A class cannot both be abstract as well as final, so this utility should not allow for that. + * - Defensive coding against incorrect calls to this method. + * + * @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 + * + * @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, + 'is_final' => 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; + break 2; + + case \T_FINAL: + $properties['is_final'] = true; + break 2; + } + } + + 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. + * - Improved handling of parse errors. + * - The returned name will be clean of superfluous whitespace and/or comments. + * + * @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 implements. + * + * Main differences with the PHPCS version: + * - Bugs fixed: + * - Handling of PHPCS annotations. + * - Handling of comments. + * - Improved handling of parse errors. + * - The returned name(s) will be clean of superfluous whitespace and/or comments. + * + * @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. + * + * @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, $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 = [ + \T_NS_SEPARATOR, + \T_STRING, + ]; + $find += 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..29ad3226 --- /dev/null +++ b/PHPCSUtils/Utils/Operators.php @@ -0,0 +1,264 @@ + => + */ + private static $extraUnaryIndicators = [ + \T_STRING_CONCAT => true, + \T_RETURN => 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, + ]; + + /** + * Determine if the passed token is a reference operator. + * + * Main differences with the PHPCS version: + * - Defensive coding against incorrect calls to this method. + * - Improved handling of select tokenizer errors involving short lists/short arrays. + * + * @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 ($tokens[$tokenBefore]['code'] === \T_FUNCTION) { + // 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(BCTokens::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::lastOwnerIn($phpcsFile, $stackPtr, [\T_FUNCTION, \T_CLOSURE]); + if ($lastOwner !== false) { + $params = FunctionDeclarations::getParameters($phpcsFile, $lastOwner); + foreach ($params as $param) { + if ($param['pass_by_reference'] === true) { + // Function parameter declared to be passed by reference. + return true; + } + } + } elseif (isset($tokens[$lastOpener]['parenthesis_owner']) === false) { + $prev = false; + for ($t = ($lastOpener - 1); $t >= 0; $t--) { + if ($tokens[$t]['code'] !== \T_WHITESPACE) { + $prev = $t; + break; + } + } + + if ($prev !== false && $tokens[$prev]['code'] === \T_USE) { + // Closure use by reference. + return true; + } + } + } + + // Pass by reference in function calls and assign by reference in arrays. + if ($tokens[$tokenBefore]['code'] === \T_OPEN_PARENTHESIS + || $tokens[$tokenBefore]['code'] === \T_COMMA + || $tokens[$tokenBefore]['code'] === \T_OPEN_SHORT_ARRAY + || $tokens[$tokenBefore]['code'] === \T_OPEN_SQUARE_BRACKET // PHPCS 2.8.0 < 3.3.0. + ) { + if ($tokens[$tokenAfter]['code'] === \T_VARIABLE) { + return true; + } else { + $skip = Tokens::$emptyTokens; + $skip[] = \T_NS_SEPARATOR; + $skip[] = \T_SELF; + $skip[] = \T_PARENT; + $skip[] = \T_STATIC; + $skip[] = \T_STRING; + $skip[] = \T_NAMESPACE; + $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 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(BCTokens::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(BCTokens::operators()[$tokens[$prev]['code']]) === true + || isset(BCTokens::comparisonTokens()[$tokens[$prev]['code']]) === true + || isset(Tokens::$booleanOperators[$tokens[$prev]['code']]) === true + || isset(BCTokens::assignmentTokens()[$tokens[$prev]['code']]) === true + || isset(Tokens::$castTokens[$tokens[$prev]['code']]) === true + || isset(self::$extraUnaryIndicators[$tokens[$prev]['code']]) === true + ) { + return true; + } + + /* + * BC for PHPCS < 3.1.0 in which the PHP 5.5 T_YIELD token was not yet backfilled. + * Note: not accounting for T_YIELD_FROM as that would be a parse error anyway. + */ + if ($tokens[$prev]['code'] === \T_STRING && $tokens[$prev]['content'] === 'yield') { + 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_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..4a6a706c --- /dev/null +++ b/PHPCSUtils/Utils/Orthography.php @@ -0,0 +1,121 @@ + 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 + */ +class Orthography +{ + + /** + * Characters which are considered terminal points for a sentence. + * + * @link https://www.thepunctuationguide.com/terminal-points.html + * + * @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 $string 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. + * + * @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($string) + { + $string = \ltrim($string); + return (\preg_match('`^[\p{Lu}\p{Lt}\p{Lo}]`u', $string) > 0); + } + + /** + * Check if the first character of an arbitrary text string is a lowercase letter. + * + * @since 1.0.0 + * + * @param string $string 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. + * + * @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($string) + { + $string = \ltrim($string); + return (\preg_match('`^\p{Ll}`u', $string) > 0); + } + + /** + * Check if the last character of an arbitrary text string is a valid punctuation character. + * + * @since 1.0.0 + * + * @param string $string 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. + * @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($string, $allowedChars = self::TERMINAL_POINTS) + { + static $encoding; + + if (isset($encoding) === false) { + $encoding = Helper::getConfigData('encoding'); + } + + $string = \rtrim($string); + if (\function_exists('iconv_substr') === true) { + $lastChar = \iconv_substr($string, -1, 1, $encoding); + } else { + $lastChar = \substr($string, -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..240905de --- /dev/null +++ b/PHPCSUtils/Utils/Parentheses.php @@ -0,0 +1,411 @@ +getTokens(); + + if (isset($tokens[$stackPtr]['parenthesis_owner'])) { + return $tokens[$stackPtr]['parenthesis_owner']; + } + + /* + * `T_LIST` and `T_ANON_CLASS` only became parentheses owners in PHPCS 3.5.0. + * + * {@internal 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 (\version_compare(Helper::getVersion(), '3.5.0', '>=') === true) { + return false; + } + + 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 + && ($tokens[$prevNonEmpty]['code'] === \T_LIST + || $tokens[$prevNonEmpty]['code'] === \T_ANON_CLASS + // Work-around: anon classes were, in certain circumstances, tokenized as T_CLASS prior to PHPCS 3.4.0. + || $tokens[$prevNonEmpty]['code'] === \T_CLASS) + ) { + 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; + + /* + * Work around tokenizer bug where anon classes were, in certain circumstances, tokenized + * as `T_CLASS` prior to PHPCS 3.4.0. + * As `T_CLASS` is normally not an parenthesis owner, we can safely add it to the array + * without doing a version check. + */ + if (\in_array(\T_ANON_CLASS, $validOwners, true)) { + $validOwners[] = \T_CLASS; + } + + 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 position of the opener to the first (outer) set of parentheses an arbitrary + * token is wrapped in, where the parentheses owner is within the set of valid owners. + * + * If no `$validOwners` are specified, the opener to the first set of parentheses surrounding + * the token 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 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 position of the closer to the first (outer) set of parentheses an arbitrary + * token is wrapped in, where the parentheses owner is within the set of valid owners. + * + * If no `$validOwners` are specified, the closer to the first set of parentheses surrounding + * the token 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 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 position of the parentheses owner to the first (outer) set of parentheses an + * arbitrary token is wrapped in, where the parentheses owner is within the set of valid owners. + * + * If no `$validOwners` are specified, the owner to the first set of parentheses surrounding + * the token will be returned or false if the first set of parentheses does not have an owner. + * + * @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 position of the opener to the last (inner) set of parentheses an arbitrary + * token is wrapped in, where the parentheses owner is within the set of valid owners. + * + * If no `$validOwners` are specified, the opener to the last set of parentheses surrounding + * the token 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 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 position of the closer to the last (inner) set of parentheses an arbitrary + * token is wrapped in, where the parentheses owner is within the set of valid owners. + * + * If no `$validOwners` are specified, the closer to the last set of parentheses surrounding + * the token 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 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 position of the parentheses owner to the last (inner) set of parentheses an + * arbitrary token is wrapped in where the parentheses owner is within the set of valid owners. + * + * If no `$validOwners` are specified, the owner to the last set of parentheses surrounding + * the token will be returned or false if the last set of parentheses does not have an owner. + * + * @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 a 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 a 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..2c0d5a1c --- /dev/null +++ b/PHPCSUtils/Utils/PassedParameters.php @@ -0,0 +1,326 @@ + => + */ + private static $allowedConstructs = [ + \T_STRING => true, + \T_VARIABLE => true, + \T_SELF => true, + \T_STATIC => true, + \T_ARRAY => true, + \T_OPEN_SHORT_ARRAY => true, + \T_ISSET => true, + \T_UNSET => true, + // BC for various short array tokenizer issues. See the Arrays class for more details. + \T_OPEN_SQUARE_BRACKET => true, + ]; + + /** + * Tokens which are considered stop point, either because they are the end + * of the parameter (comma) or because we need to skip over them. + * + * @since 1.0.0 + * + * @var array => + */ + 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, + ]; + + /** + * Checks if any parameters have been passed. + * + * - If passed a T_STRING 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 + * unreliable. + * - If passed a T_SELF or T_STATIC stack pointer, it will accept it as a + * 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. + * - 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 the T_STRING, T_VARIABLE, T_ARRAY, + * T_OPEN_SHORT_ARRAY, T_ISSET, or T_UNSET token. + * + * @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) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr], self::$allowedConstructs[$tokens[$stackPtr]['code']]) === false) { + throw new RuntimeException( + 'The hasParameters() method expects a function call, array, isset or unset token to be passed.' + ); + } + + if ($tokens[$stackPtr]['code'] === \T_SELF || $tokens[$stackPtr]['code'] === \T_STATIC) { + $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 (($tokens[$stackPtr]['code'] === \T_OPEN_SHORT_ARRAY + || $tokens[$stackPtr]['code'] === \T_OPEN_SQUARE_BRACKET) + && 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 ($tokens[$stackPtr]['code'] === \T_OPEN_SHORT_ARRAY + || $tokens[$stackPtr]['code'] === \T_OPEN_SQUARE_BRACKET + ) { + 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; + } + + $closeParenthesis = $tokens[$next]['parenthesis_closer']; + $nextNextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($next + 1), ($closeParenthesis + 1), true); + + if ($nextNextNonEmpty === $closeParenthesis) { + // No parameters. + return false; + } + + return true; + } + + /** + * Get information on all parameters passed. + * + * See {@see PHPCSUtils\Utils\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 the T_STRING, T_VARIABLE, T_ARRAY, + * T_OPEN_SHORT_ARRAY, T_ISSET, or T_UNSET token. + * + * @return array Returns a multi-dimentional array with the "start" token pointer, "end" token + * pointer, "raw" parameter value and "clean" (only code, no comments) parameter + * value for all parameters. The array starts at index 1. + * If no parameters are found, will return an empty array. + * + * @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) + { + if (self::hasParameters($phpcsFile, $stackPtr) === false) { + return []; + } + + // 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 ($tokens[$stackPtr]['code'] === \T_OPEN_SHORT_ARRAY + || $tokens[$stackPtr]['code'] === \T_OPEN_SQUARE_BRACKET + ) { + $opener = $stackPtr; + $closer = $tokens[$stackPtr]['bracket_closer']; + } else { + $opener = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + $closer = $tokens[$opener]['parenthesis_closer']; + } + + $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; + } + + if ($tokens[$nextComma]['code'] !== \T_COMMA + && $tokens[$nextComma]['code'] !== $tokens[$closer]['code'] + ) { + // Just in case. + continue; + } + + // Ok, we've reached the end of the parameter. + $paramEnd = ($nextComma - 1); + $parameters[$cnt]['start'] = $paramStart; + $parameters[$cnt]['end'] = $paramEnd; + $parameters[$cnt]['raw'] = \trim(GetTokensAsString::normal($phpcsFile, $paramStart, $paramEnd)); + $parameters[$cnt]['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) { + break; + } + + // Prepare for the next parameter. + $paramStart = ($nextComma + 1); + $cnt++; + } + + return $parameters; + } + + /** + * Get information on a specific parameter passed. + * + * See {@see PHPCSUtils\Utils\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 the T_STRING, T_VARIABLE, T_ARRAY, + * T_OPEN_SHORT_ARRAY, T_ISSET or T_UNSET token. + * @param int $paramOffset The 1-based index position of the parameter to retrieve. + * + * @return array|false Returns an array with the "start" token pointer, "end" token pointer, + * "raw" parameter value and "clean" (only code, no comments) parameter + * value for the parameter at the specified offset. + * Or FALSE if the specified parameter is not found. + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the token passed is not one of the + * accepted types or doesn't exist. + */ + public static function getParameter(File $phpcsFile, $stackPtr, $paramOffset) + { + $parameters = self::getParameters($phpcsFile, $stackPtr); + + if (isset($parameters[$paramOffset]) === false) { + return false; + } + + return $parameters[$paramOffset]; + } + + /** + * Count the number of parameters which have been passed. + * + * See {@see PHPCSUtils\Utils\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 the T_STRING, T_VARIABLE, T_ARRAY, + * T_OPEN_SHORT_ARRAY, T_ISSET or T_UNSET 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)); + } +} diff --git a/PHPCSUtils/Utils/Scopes.php b/PHPCSUtils/Utils/Scopes.php new file mode 100644 index 00000000..486fbe18 --- /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 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 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, BCTokens::ooScopeTokens()) !== false) { + return true; + } + + return false; + } +} diff --git a/PHPCSUtils/Utils/TextStrings.php b/PHPCSUtils/Utils/TextStrings.php new file mode 100644 index 00000000..e89a268d --- /dev/null +++ b/PHPCSUtils/Utils/TextStrings.php @@ -0,0 +1,115 @@ +getTokens(); + + // Must be the start of a text string token. + if (isset($tokens[$stackPtr], Collections::$textStingStartTokens[$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 ($tokens[$stackPtr]['code'] === \T_CONSTANT_ENCAPSED_STRING + || $tokens[$stackPtr]['code'] === \T_DOUBLE_QUOTED_STRING + ) { + $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'); + } + } + + switch ($tokens[$stackPtr]['code']) { + case \T_START_HEREDOC: + $stripQuotes = false; + $targetType = \T_HEREDOC; + $current = ($stackPtr + 1); + break; + + case \T_START_NOWDOC: + $stripQuotes = false; + $targetType = \T_NOWDOC; + $current = ($stackPtr + 1); + break; + + default: + $targetType = $tokens[$stackPtr]['code']; + $current = $stackPtr; + break; + } + + $string = ''; + do { + $string .= $tokens[$current]['content']; + ++$current; + } while (isset($tokens[$current]) && $tokens[$current]['code'] === $targetType); + + if ($stripQuotes === true) { + return self::stripQuotes($string); + } + + return $string; + } + + /** + * Strip text delimiter quotes from an arbitrary string. + * + * Intended for use with the "contents" of a T_CONSTANT_ENCAPSED_STRING / T_DOUBLE_QUOTED_STRING. + * + * Prevents stripping mis-matched quotes. + * Prevents stripping quotes from the textual content of the string. + * + * @since 1.0.0 + * + * @param string $string The raw string. + * + * @return string String without quotes around it. + */ + public static function stripQuotes($string) + { + return \preg_replace('`^([\'"])(.*)\1$`Ds', '$2', $string); + } +} diff --git a/PHPCSUtils/Utils/UseStatements.php b/PHPCSUtils/Utils/UseStatements.php new file mode 100644 index 00000000..81d29731 --- /dev/null +++ b/PHPCSUtils/Utils/UseStatements.php @@ -0,0 +1,347 @@ +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 ''; + } + + $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 = BCTokens::ooScopeTokens(); + // Only classes and traits 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 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 this category. + * + * For example, for this function group use statement: + * `use function Vendor\Package\{LevelA\Name as Alias, LevelB\Another_Name}` + * the return value would look like this: + * `[ + * 'name' => [], + * 'function' => [ + * 'Alias' => 'Vendor\Package\LevelA\Name', + * 'Another_Name' => 'Vendor\Package\LevelB\Another_Name', + * ], + * 'const' => [], + * ]` + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified position is not a + * T_USE token or not 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; + } + + $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']; + + /* + * BC: Work round a tokenizer bug related to a parse error. + * + * If `function` or `const` is used as the alias, the semi-colon after it would + * be tokenized as T_STRING. + * For `function` this was fixed in PHPCS 2.8.0. For `const` the issue still exists + * in PHPCS 3.5.2. + * + * Along the same lines, the `}` T_CLOSE_USE_GROUP would also be tokenized as T_STRING. + */ + if ($tokenCode === \T_STRING) { + if ($tokens[$i]['content'] === ';') { + $tokenCode = \T_SEMICOLON; + } elseif ($tokens[$i]['content'] === '}') { + $tokenCode = \T_CLOSE_USE_GROUP; + } + } + + 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_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; + + case \T_FUNCTION: + case \T_CONST: + /* + * BC: Work around tokenizer bug in PHPCS < 3.4.1. + * + * `function`/`const` in `use function`/`use const` tokenized as T_FUNCTION/T_CONST + * instead of T_STRING when there is a comment between the keywords. + * + * @link https://github.com/squizlabs/PHP_CodeSniffer/issues/2431 + */ + if ($start === true && $fixedType === false) { + $type = \strtolower($tokens[$i]['content']); + $start = false; + if ($useGroup === false) { + $fixedType = true; + } + + break; + } + + $start = false; + + if ($hasAlias === false) { + $name .= $tokens[$i]['content']; + } + + $alias = $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) { + $name .= $tokens[$i]['content']; + } + + $alias = $tokens[$i]['content']; + break; + } + } + + return $statements; + } +} diff --git a/PHPCSUtils/Utils/Variables.php b/PHPCSUtils/Utils/Variables.php new file mode 100644 index 00000000..b240f0b4 --- /dev/null +++ b/PHPCSUtils/Utils/Variables.php @@ -0,0 +1,312 @@ + => + */ + 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 var. + * + * The format of the return value is: + * + * + * 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. + * '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 nullable. + * ); + * + * + * 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. + * + * @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 + * + * @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) + { + $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; + + $startOfStatement = $phpcsFile->findPrevious( + [ + \T_SEMICOLON, + \T_OPEN_CURLY_BRACKET, + \T_CLOSE_CURLY_BRACKET, + ], + ($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; + } + } + + $type = ''; + $typeToken = false; + $typeEndToken = false; + $nullableType = false; + + 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]['type'] === 'T_NULLABLE' + // Handle nullable types in PHPCS < 3.5.0 and for PHP-4 style `var` properties in PHPCS < 3.5.4. + || $tokens[$i]['code'] === \T_INLINE_THEN + ) { + $nullableType = true; + } + + if (isset(Collections::$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, + '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. + * + * @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. + * + * @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; or when an unsupported + * token has been passed; or when a T_CONSTANT_ENCAPSED_STRING is not an array + * index key; or when it is, but 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. + * + * @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/Tests/AbstractSniffs/AbstractArrayDeclaration/AbstractArrayDeclarationSniffTest.inc b/Tests/AbstractSniffs/AbstractArrayDeclaration/AbstractArrayDeclarationSniffTest.inc new file mode 100644 index 00000000..6c9e353e --- /dev/null +++ b/Tests/AbstractSniffs/AbstractArrayDeclaration/AbstractArrayDeclarationSniffTest.inc @@ -0,0 +1,28 @@ + 'a', + 2 => 'b', + 3 => 'c', + 4 => 'd', +); + +/* testMultiLineShortArrayMixedKeysNoKeys */ +$array = [ + 12 => 'numeric key', + 'value', + 'string' => 'string key', +]; + +/* testShortCircuit */ +$array = [1, 'a' => 2, ]; + +/* testShortList */ +[$a, $b] = $array; diff --git a/Tests/AbstractSniffs/AbstractArrayDeclaration/AbstractArrayDeclarationSniffTest.php b/Tests/AbstractSniffs/AbstractArrayDeclaration/AbstractArrayDeclarationSniffTest.php new file mode 100644 index 00000000..274fc59f --- /dev/null +++ b/Tests/AbstractSniffs/AbstractArrayDeclaration/AbstractArrayDeclarationSniffTest.php @@ -0,0 +1,612 @@ +getTargetToken( + '/* testShortList */', + [\T_ARRAY, \T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET] + ); + + $mockObj = $this->getMockBuilder('\PHPCSUtils\AbstractSniffs\AbstractArrayDeclarationSniff') + ->setMethods($this->methodsToMock) + ->getMockForAbstractClass(); + + $mockObj->expects($this->never()) + ->method('processOpenClose'); + + $mockObj->expects($this->never()) + ->method('processKey'); + + $mockObj->expects($this->never()) + ->method('processNoKey'); + + $mockObj->expects($this->never()) + ->method('processArrow'); + + $mockObj->expects($this->never()) + ->method('processValue'); + + $mockObj->expects($this->never()) + ->method('processComma'); + + $mockObj->process(self::$phpcsFile, $target); + } + + /** + * Test that the abstract sniff correctly bows out after the processOpenClose() method + * when presented with an empty array. + * + * @return void + */ + public function testEmptyArray() + { + $target = $this->getTargetToken( + '/* testEmptyArray */', + [\T_ARRAY, \T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET] + ); + + $mockObj = $this->getMockBuilder('\PHPCSUtils\AbstractSniffs\AbstractArrayDeclarationSniff') + ->setMethods($this->methodsToMock) + ->getMockForAbstractClass(); + + $mockObj->expects($this->once()) + ->method('processOpenClose'); + + $mockObj->expects($this->never()) + ->method('processKey'); + + $mockObj->expects($this->never()) + ->method('processNoKey'); + + $mockObj->expects($this->never()) + ->method('processArrow'); + + $mockObj->expects($this->never()) + ->method('processValue'); + + $mockObj->expects($this->never()) + ->method('processComma'); + + $mockObj->process(self::$phpcsFile, $target); + } + + /** + * Test all features of the abstract sniff when presented with a single line short array + * without array keys and without trailing comma after the last array item. + * + * @return void + */ + public function testSingleLineShortArrayNoKeysNoTrailingComma() + { + $target = $this->getTargetToken( + '/* testSingleLineShortArrayNoKeysNoTrailingComma */', + [\T_ARRAY, \T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET] + ); + + $mockObj = $this->getMockBuilder('\PHPCSUtils\AbstractSniffs\AbstractArrayDeclarationSniff') + ->setMethods($this->methodsToMock) + ->getMockForAbstractClass(); + + $mockObj->expects($this->once()) + ->method('processOpenClose') + ->with( + $this->identicalTo(self::$phpcsFile), + $this->equalTo($target), + $this->equalTo($target + 5) + ); + + $mockObj->expects($this->exactly(2)) + ->method('processNoKey') + ->withConsecutive( + [$this->identicalTo(self::$phpcsFile), $this->equalTo($target + 1), $this->equalTo(1)], + [$this->identicalTo(self::$phpcsFile), $this->equalTo($target + 3), $this->equalTo(2)] + ); + + $mockObj->expects($this->exactly(2)) + ->method('processValue') + ->withConsecutive( + [ + $this->identicalTo(self::$phpcsFile), + $this->equalTo($target + 1), + $this->equalTo($target + 1), + $this->equalTo(1), + ], + [ + $this->identicalTo(self::$phpcsFile), + $this->equalTo($target + 3), + $this->equalTo($target + 4), + $this->equalTo(2), + ] + ); + + $mockObj->expects($this->once()) + ->method('processComma') + ->with( + $this->identicalTo(self::$phpcsFile), + $this->equalTo($target + 2), + $this->equalTo(1) + ); + + $mockObj->process(self::$phpcsFile, $target); + + // Note: these methods are deprecated in PHPUnit 8.x and removed in PHPUnit 9.x + + // Verify that the properties have been correctly set. + $this->assertAttributeSame($target, 'stackPtr', $mockObj); + $this->assertAttributeSame($target, 'arrayOpener', $mockObj); + $this->assertAttributeSame(($target + 5), 'arrayCloser', $mockObj); + $this->assertAttributeSame(2, 'itemCount', $mockObj); + $this->assertAttributeSame(true, 'singleLine', $mockObj); + } + + /** + * Test all features of the abstract sniff when presented with a mutli line long array + * with array keys, double arrows and with a trailing comma after the last array item. + * + * @return void + */ + public function testMultiLineLongArrayKeysTrailingComma() + { + $target = $this->getTargetToken( + '/* testMultiLineLongArrayKeysTrailingComma */', + [\T_ARRAY, \T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET] + ); + + $mockObj = $this->getMockBuilder('\PHPCSUtils\AbstractSniffs\AbstractArrayDeclarationSniff') + ->setMethods($this->methodsToMock) + ->getMockForAbstractClass(); + + $mockObj->expects($this->once()) + ->method('processOpenClose') + ->with( + $this->identicalTo(self::$phpcsFile), + $this->equalTo($target + 1), + $this->equalTo($target + 35) + ); + + $mockObj->expects($this->exactly(3)) + ->method('processKey') + ->withConsecutive( + [ + $this->identicalTo(self::$phpcsFile), + $this->equalTo($target + 2), + $this->equalTo($target + 5), + $this->equalTo(1), + ], + [ + $this->identicalTo(self::$phpcsFile), + $this->equalTo($target + 10), + $this->equalTo($target + 13), + $this->equalTo(2), + ], + [ + $this->identicalTo(self::$phpcsFile), + $this->equalTo($target + 18), + $this->equalTo($target + 21), + $this->equalTo(3), + ] + ); + + $mockObj->expects($this->exactly(3)) + ->method('processArrow') + ->withConsecutive( + [$this->identicalTo(self::$phpcsFile), $this->equalTo($target + 6), $this->equalTo(1)], + [$this->identicalTo(self::$phpcsFile), $this->equalTo($target + 14), $this->equalTo(2)], + [$this->identicalTo(self::$phpcsFile), $this->equalTo($target + 22), $this->equalTo(3)] + ); + + $mockObj->expects($this->exactly(3)) + ->method('processValue') + ->withConsecutive( + [ + $this->identicalTo(self::$phpcsFile), + $this->equalTo($target + 7), + $this->equalTo($target + 8), + $this->equalTo(1), + ], + [ + $this->identicalTo(self::$phpcsFile), + $this->equalTo($target + 15), + $this->equalTo($target + 16), + $this->equalTo(2), + ], + [ + $this->identicalTo(self::$phpcsFile), + $this->equalTo($target + 23), + $this->equalTo($target + 24), + $this->equalTo(3), + ] + ) + ->will($this->onConsecutiveCalls(null, null, true)); // Testing short-circuiting the loop. + + $mockObj->expects($this->exactly(2)) + ->method('processComma') + ->withConsecutive( + [$this->identicalTo(self::$phpcsFile), $this->equalTo($target + 9), $this->equalTo(1)], + [$this->identicalTo(self::$phpcsFile), $this->equalTo($target + 17), $this->equalTo(2)] + ); + + $mockObj->process(self::$phpcsFile, $target); + + // Note: these methods are deprecated in PHPUnit 8.x and removed in PHPUnit 9.x + + // Verify that the properties have been correctly set. + $this->assertAttributeSame($target, 'stackPtr', $mockObj); + $this->assertAttributeSame(($target + 1), 'arrayOpener', $mockObj); + $this->assertAttributeSame(($target + 35), 'arrayCloser', $mockObj); + $this->assertAttributeSame(4, 'itemCount', $mockObj); + $this->assertAttributeSame(false, 'singleLine', $mockObj); + } + + /** + * Test all features of the abstract sniff when presented with a multi line short array with + * a mix of items with and without array keys and with a trailing comma after the last array item. + * + * @return void + */ + public function testMultiLineShortArrayMixedKeysNoKeys() + { + $target = $this->getTargetToken( + '/* testMultiLineShortArrayMixedKeysNoKeys */', + [\T_ARRAY, \T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET] + ); + + $mockObj = $this->getMockBuilder('\PHPCSUtils\AbstractSniffs\AbstractArrayDeclarationSniff') + ->setMethods($this->methodsToMock) + ->getMockForAbstractClass(); + + $mockObj->expects($this->once()) + ->method('processOpenClose') + ->with( + $this->identicalTo(self::$phpcsFile), + $this->equalTo($target), + $this->equalTo($target + 22) + ); + + $mockObj->expects($this->exactly(2)) + ->method('processKey') + ->withConsecutive( + [ + $this->identicalTo(self::$phpcsFile), + $this->equalTo($target + 1), + $this->equalTo($target + 4), + $this->equalTo(1), + ], + [ + $this->identicalTo(self::$phpcsFile), + $this->equalTo($target + 13), + $this->equalTo($target + 16), + $this->equalTo(3), + ] + ); + + $mockObj->expects($this->once()) + ->method('processNoKey') + ->withConsecutive( + [$this->identicalTo(self::$phpcsFile), $this->equalTo($target + 9), $this->equalTo(2)] + ); + + $mockObj->expects($this->exactly(2)) + ->method('processArrow') + ->withConsecutive( + [$this->identicalTo(self::$phpcsFile), $this->equalTo($target + 5), $this->equalTo(1)], + [$this->identicalTo(self::$phpcsFile), $this->equalTo($target + 17), $this->equalTo(3)] + ); + + $mockObj->expects($this->exactly(3)) + ->method('processValue') + ->withConsecutive( + [ + $this->identicalTo(self::$phpcsFile), + $this->equalTo($target + 6), + $this->equalTo($target + 7), + $this->equalTo(1), + ], + [ + $this->identicalTo(self::$phpcsFile), + $this->equalTo($target + 9), + $this->equalTo($target + 11), + $this->equalTo(2), + ], + [ + $this->identicalTo(self::$phpcsFile), + $this->equalTo($target + 18), + $this->equalTo($target + 19), + $this->equalTo(3), + ] + ); + + $mockObj->expects($this->exactly(3)) + ->method('processComma') + ->withConsecutive( + [$this->identicalTo(self::$phpcsFile), $this->equalTo($target + 8), $this->equalTo(1)], + [$this->identicalTo(self::$phpcsFile), $this->equalTo($target + 12), $this->equalTo(2)], + [$this->identicalTo(self::$phpcsFile), $this->equalTo($target + 20), $this->equalTo(3)] + ); + + $mockObj->process(self::$phpcsFile, $target); + + // Note: these methods are deprecated in PHPUnit 8.x and removed in PHPUnit 9.x + + // Verify that the properties have been correctly set. + $this->assertAttributeSame($target, 'stackPtr', $mockObj); + $this->assertAttributeSame($target, 'arrayOpener', $mockObj); + $this->assertAttributeSame(($target + 22), 'arrayCloser', $mockObj); + $this->assertAttributeSame(3, 'itemCount', $mockObj); + $this->assertAttributeSame(false, 'singleLine', $mockObj); + } + + /** + * Test short-circuiting the sniff on the call to processOpenClose(). + * + * @return void + */ + public function testShortCircuitOnProcessOpenClose() + { + $target = $this->getTargetToken( + '/* testShortCircuit */', + [\T_ARRAY, \T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET] + ); + + $mockObj = $this->getMockBuilder('\PHPCSUtils\AbstractSniffs\AbstractArrayDeclarationSniff') + ->setMethods($this->methodsToMock) + ->getMockForAbstractClass(); + + $mockObj->expects($this->once()) + ->method('processOpenClose') + ->willReturn(true); + + $mockObj->expects($this->never()) + ->method('processKey'); + + $mockObj->expects($this->never()) + ->method('processNoKey'); + + $mockObj->expects($this->never()) + ->method('processArrow'); + + $mockObj->expects($this->never()) + ->method('processValue'); + + $mockObj->expects($this->never()) + ->method('processComma'); + + $mockObj->process(self::$phpcsFile, $target); + } + + /** + * Test short-circuiting the sniff on the call to processKey(). + * + * @return void + */ + public function testShortCircuitOnProcessKey() + { + $target = $this->getTargetToken( + '/* testShortCircuit */', + [\T_ARRAY, \T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET] + ); + + $mockObj = $this->getMockBuilder('\PHPCSUtils\AbstractSniffs\AbstractArrayDeclarationSniff') + ->setMethods($this->methodsToMock) + ->getMockForAbstractClass(); + + $mockObj->expects($this->once()) + ->method('processOpenClose'); + + $mockObj->expects($this->once()) + ->method('processKey') + ->willReturn(true); + + $mockObj->expects($this->once()) + ->method('processNoKey'); + + $mockObj->expects($this->never()) + ->method('processArrow'); + + $mockObj->expects($this->once()) + ->method('processValue'); + + $mockObj->expects($this->once()) + ->method('processComma'); + + $mockObj->process(self::$phpcsFile, $target); + } + + /** + * Test short-circuiting the sniff on the call to processNoKey(). + * + * @return void + */ + public function testShortCircuitOnProcessNoKey() + { + $target = $this->getTargetToken( + '/* testShortCircuit */', + [\T_ARRAY, \T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET] + ); + + $mockObj = $this->getMockBuilder('\PHPCSUtils\AbstractSniffs\AbstractArrayDeclarationSniff') + ->setMethods($this->methodsToMock) + ->getMockForAbstractClass(); + + $mockObj->expects($this->once()) + ->method('processOpenClose'); + + $mockObj->expects($this->never()) + ->method('processKey'); + + $mockObj->expects($this->once()) + ->method('processNoKey') + ->willReturn(true); + + $mockObj->expects($this->never()) + ->method('processArrow'); + + $mockObj->expects($this->never()) + ->method('processValue'); + + $mockObj->expects($this->never()) + ->method('processComma'); + + $mockObj->process(self::$phpcsFile, $target); + } + + /** + * Test short-circuiting the sniff on the call to processArrow(). + * + * @return void + */ + public function testShortCircuitOnProcessArrow() + { + $target = $this->getTargetToken( + '/* testShortCircuit */', + [\T_ARRAY, \T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET] + ); + + $mockObj = $this->getMockBuilder('\PHPCSUtils\AbstractSniffs\AbstractArrayDeclarationSniff') + ->setMethods($this->methodsToMock) + ->getMockForAbstractClass(); + + $mockObj->expects($this->once()) + ->method('processOpenClose'); + + $mockObj->expects($this->once()) + ->method('processKey'); + + $mockObj->expects($this->once()) + ->method('processNoKey'); + + $mockObj->expects($this->once()) + ->method('processArrow') + ->willReturn(true); + + $mockObj->expects($this->once()) + ->method('processValue'); + + $mockObj->expects($this->once()) + ->method('processComma'); + + $mockObj->process(self::$phpcsFile, $target); + } + + /** + * Test short-circuiting the sniff on the call to processValue(). + * + * @return void + */ + public function testShortCircuitOnProcessValue() + { + $target = $this->getTargetToken( + '/* testShortCircuit */', + [\T_ARRAY, \T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET] + ); + + $mockObj = $this->getMockBuilder('\PHPCSUtils\AbstractSniffs\AbstractArrayDeclarationSniff') + ->setMethods($this->methodsToMock) + ->getMockForAbstractClass(); + + $mockObj->expects($this->once()) + ->method('processOpenClose'); + + $mockObj->expects($this->never()) + ->method('processKey'); + + $mockObj->expects($this->once()) + ->method('processNoKey'); + + $mockObj->expects($this->never()) + ->method('processArrow'); + + $mockObj->expects($this->once()) + ->method('processValue') + ->willReturn(true); + + $mockObj->expects($this->never()) + ->method('processComma'); + + $mockObj->process(self::$phpcsFile, $target); + } + + /** + * Test short-circuiting the sniff on the call to processComma(). + * + * @return void + */ + public function testShortCircuitOnProcessComma() + { + $target = $this->getTargetToken( + '/* testShortCircuit */', + [\T_ARRAY, \T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET] + ); + + $mockObj = $this->getMockBuilder('\PHPCSUtils\AbstractSniffs\AbstractArrayDeclarationSniff') + ->setMethods($this->methodsToMock) + ->getMockForAbstractClass(); + + $mockObj->expects($this->once()) + ->method('processOpenClose'); + + $mockObj->expects($this->never()) + ->method('processKey'); + + $mockObj->expects($this->once()) + ->method('processNoKey'); + + $mockObj->expects($this->never()) + ->method('processArrow'); + + $mockObj->expects($this->once()) + ->method('processValue'); + + $mockObj->expects($this->once()) + ->method('processComma') + ->willReturn(true); + + $mockObj->process(self::$phpcsFile, $target); + } +} diff --git a/Tests/AbstractSniffs/AbstractArrayDeclaration/ArrayDeclarationSniffTestDouble.php b/Tests/AbstractSniffs/AbstractArrayDeclaration/ArrayDeclarationSniffTestDouble.php new file mode 100644 index 00000000..8d080703 --- /dev/null +++ b/Tests/AbstractSniffs/AbstractArrayDeclaration/ArrayDeclarationSniffTestDouble.php @@ -0,0 +1,42 @@ + 'excluded', + MY_CONSTANT => 'excluded', + PHP_INT_MAX => 'excluded', + str_replace('.', '', '1.1') => 'excluded', + self::CONSTANT => 'excluded', + $obj->get_key() => 'excluded', + $obj->prop => 'excluded', + "my $var text" => 'excluded', + << 'excluded', + $var['key']{1} => 'excluded', +]; + +/* testAllEmptyString */ +$emptyStringKey = array( + '' => 'empty', + null => 'null', + (string) false => 'false', +); + +/* testAllZero */ +$everythingZero = [ + '0', + 0 => 'a', + 0.0 => 'b', + '0' => 'c', + 0b0 => 'd', + 0x0 => 'e', + 00 => 'f', + false => 'g', + 0.4 => 'h', + -0.8 => 'i', + 0e0 => 'j', + 0_0 => 'k', + -1 + 1 => 'l', + 3 * 0 => 'm', + 00.00 => 'n', + (int) 'nothing' => 'o', + 15 > 200 => 'p', + "0" => 'q', + 0. => 'r', + .0 => 's', + (true) ? 0 : 1 => 't', + ! true => 'u', +]; + +/* testAllOne */ +$everythingOne = [ + '0', + '1', + 1 => 'a', + 1.1 => 'b', + '1' => 'c', + 0b1 => 'd', + 0x1 => 'e', + 01 => 'f', + true => 'g', + 1.2 /*comment*/ => 'h', + 1e0 => 'i', + 0_1 => 'j', + -1 + 2 => 'k', + 3 * 0.5 => 'l', + 01.00 => 'm', + (int) '1 penny' => 'n', + 15 < 200 => 'o', + "1" => 'p', + 1. => 'q', + 001. => 'r', + (true) ? 1 : 0 => 's', + ! false => 't', + (string) true => 'u', +]; + +/* testAllEleven */ +$everythingEleven = [ + 11 => 'a', + 11.0 => 'b', + '11' => 'c', + 0b1011 => 'd', + 0Xb => 'e', + 013 => 'f', + 11.8 => 'g', + 1.1e1 => 'h', + 1_1 => 'i', + 0_13 => 'j', + -1 + 12 => 'k', + 22 / /*comment*/ 2 => 'l', + 0011.0011 => 'm', + (int) '11 lane' => 'n', + "11" => 'o', + 11. => 'p', + 35 % 12 => 'q', +]; + +/* testAllStringAbc */ +$textualStringKeyVariations = [ + 'abc' => 1, + 'ab' . 'c' => 4, + << 5, + <<< 'NOW' +abc +NOW + => 6, + "abc" => 7, +]; diff --git a/Tests/AbstractSniffs/AbstractArrayDeclaration/GetActualArrayKeyTest.php b/Tests/AbstractSniffs/AbstractArrayDeclaration/GetActualArrayKeyTest.php new file mode 100644 index 00000000..d552d34b --- /dev/null +++ b/Tests/AbstractSniffs/AbstractArrayDeclaration/GetActualArrayKeyTest.php @@ -0,0 +1,109 @@ +tokens = self::$phpcsFile->getTokens(); + + $stackPtr = $this->getTargetToken($testMarker, [\T_ARRAY, \T_OPEN_SHORT_ARRAY]); + $arrayItems = PassedParameters::getParameters(self::$phpcsFile, $stackPtr); + + foreach ($arrayItems as $itemNr => $arrayItem) { + if ($itemNr < $expectedFrom) { + continue; + } + + $arrowPtr = Arrays::getDoubleArrowPtr(self::$phpcsFile, $arrayItem['start'], $arrayItem['end']); + if ($arrowPtr !== false) { + $result = $testObj->getActualArrayKey(self::$phpcsFile, $arrayItem['start'], ($arrowPtr - 1)); + $this->assertSame( + $expected, + $result, + 'Failed: actual key ' . $result . ' is not the same as the expected key ' . $expected + . ' for item number ' . $itemNr + ); + } + } + } + + /** + * Data provider. + * + * @see testGetActualArrayKey() For the array format. + * + * @return array + */ + public function dataGetActualArrayKey() + { + return [ + 'unsupported-key-types' => [ + '/* testAllVoid */', + null, + 0, + ], + 'keys-all-empty-string' => [ + '/* testAllEmptyString */', + '', + 0, + ], + 'keys-all-integer-zero' => [ + '/* testAllZero */', + 0, + 0, + ], + 'keys-all-integer-one' => [ + '/* testAllOne */', + 1, + 1, + ], + 'keys-all-integer-eleven' => [ + '/* testAllEleven */', + 11, + 0, + ], + 'keys-all-string-abc' => [ + '/* testAllStringAbc */', + 'abc', + 0, + ], + ]; + } +} diff --git a/Tests/BackCompat/BCFile/FindEndOfStatementTest.inc b/Tests/BackCompat/BCFile/FindEndOfStatementTest.inc new file mode 100644 index 00000000..f8763c6a --- /dev/null +++ b/Tests/BackCompat/BCFile/FindEndOfStatementTest.inc @@ -0,0 +1,31 @@ + + * @author Juliette Reinders Folmer + * + * With documentation contributions from: + * @author Phil Davis + + * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHPCSUtils\Tests\BackCompat\BCFile; + +use PHPCSUtils\BackCompat\BCFile; +use PHPCSUtils\TestUtils\UtilityMethodTestCase; + +/** + * Tests for the \PHPCSUtils\BackCompat\BCFile::findEndOfStatement method. + * + * @covers \PHPCSUtils\BackCompat\BCFile::findEndOfStatement + * + * @since 1.0.0 + */ +class FindEndOfStatementTest extends UtilityMethodTestCase +{ + + /** + * Test a simple assignment. + * + * @return void + */ + public function testSimpleAssignment() + { + $start = (self::$phpcsFile->findNext(T_COMMENT, 0, null, false, '/* testSimpleAssignment */') + 2); + $found = BCFile::findEndOfStatement(self::$phpcsFile, $start); + + $tokens = self::$phpcsFile->getTokens(); + $this->assertSame($tokens[($start + 5)], $tokens[$found]); + } + + /** + * Test a direct call to a control structure. + * + * @return void + */ + public function testControlStructure() + { + $start = (self::$phpcsFile->findNext(T_COMMENT, 0, null, false, '/* testControlStructure */') + 2); + $found = BCFile::findEndOfStatement(self::$phpcsFile, $start); + + $tokens = self::$phpcsFile->getTokens(); + $this->assertSame($tokens[($start + 6)], $tokens[$found]); + } + + /** + * Test the assignment of a closure. + * + * @return void + */ + public function testClosureAssignment() + { + $start = (self::$phpcsFile->findNext(T_COMMENT, 0, null, false, '/* testClosureAssignment */') + 2); + $found = BCFile::findEndOfStatement(self::$phpcsFile, $start); + + $tokens = self::$phpcsFile->getTokens(); + $this->assertSame($tokens[($start + 13)], $tokens[$found]); + } + + /** + * Test using a heredoc in a function argument. + * + * @return void + */ + public function testHeredocFunctionArg() + { + // Find the end of the function. + $start = (self::$phpcsFile->findNext(T_COMMENT, 0, null, false, '/* testHeredocFunctionArg */') + 2); + $found = BCFile::findEndOfStatement(self::$phpcsFile, $start); + + $tokens = self::$phpcsFile->getTokens(); + $this->assertSame($tokens[($start + 10)], $tokens[$found]); + + // Find the end of the heredoc. + $start += 2; + $found = BCFile::findEndOfStatement(self::$phpcsFile, $start); + + $tokens = self::$phpcsFile->getTokens(); + $this->assertSame($tokens[($start + 4)], $tokens[$found]); + + // Find the end of the last arg. + $start = ($found + 2); + $found = BCFile::findEndOfStatement(self::$phpcsFile, $start); + + $tokens = self::$phpcsFile->getTokens(); + $this->assertSame($tokens[$start], $tokens[$found]); + } + + /** + * Test parts of a switch statement. + * + * @return void + */ + public function testSwitch() + { + // Find the end of the switch. + $start = (self::$phpcsFile->findNext(T_COMMENT, 0, null, false, '/* testSwitch */') + 2); + $found = BCFile::findEndOfStatement(self::$phpcsFile, $start); + + $tokens = self::$phpcsFile->getTokens(); + $this->assertSame($tokens[($start + 28)], $tokens[$found]); + + // Find the end of the case. + $start += 9; + $found = BCFile::findEndOfStatement(self::$phpcsFile, $start); + + $tokens = self::$phpcsFile->getTokens(); + $this->assertSame($tokens[($start + 8)], $tokens[$found]); + + // Find the end of default case. + $start += 11; + $found = BCFile::findEndOfStatement(self::$phpcsFile, $start); + + $tokens = self::$phpcsFile->getTokens(); + $this->assertSame($tokens[($start + 6)], $tokens[$found]); + } + + /** + * Test statements that are array values. + * + * @return void + */ + public function testStatementAsArrayValue() + { + // Test short array syntax. + $start = (self::$phpcsFile->findNext(T_COMMENT, 0, null, false, '/* testStatementAsArrayValue */') + 7); + $found = BCFile::findEndOfStatement(self::$phpcsFile, $start); + + $tokens = self::$phpcsFile->getTokens(); + $this->assertSame($tokens[($start + 2)], $tokens[$found]); + + // Test long array syntax. + $start += 12; + $found = BCFile::findEndOfStatement(self::$phpcsFile, $start); + + $tokens = self::$phpcsFile->getTokens(); + $this->assertSame($tokens[($start + 2)], $tokens[$found]); + + // Test same statement outside of array. + $start += 10; + $found = BCFile::findEndOfStatement(self::$phpcsFile, $start); + + $tokens = self::$phpcsFile->getTokens(); + $this->assertSame($tokens[($start + 3)], $tokens[$found]); + } + + /** + * Test a use group. + * + * @return void + */ + public function testUseGroup() + { + $start = (self::$phpcsFile->findNext(T_COMMENT, 0, null, false, '/* testUseGroup */') + 2); + $found = BCFile::findEndOfStatement(self::$phpcsFile, $start); + + $tokens = self::$phpcsFile->getTokens(); + $this->assertSame($tokens[($start + 23)], $tokens[$found]); + } +} diff --git a/Tests/BackCompat/BCFile/FindExtendedClassNameTest.inc b/Tests/BackCompat/BCFile/FindExtendedClassNameTest.inc new file mode 100644 index 00000000..dbe9ee38 --- /dev/null +++ b/Tests/BackCompat/BCFile/FindExtendedClassNameTest.inc @@ -0,0 +1,51 @@ + + * @author Juliette Reinders Folmer + * @author Martin Hujer + * + * With documentation contributions from: + * @author George Mponos + * @author Phil Davis + * + * @copyright 2016-2019 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHPCSUtils\Tests\BackCompat\BCFile; + +use PHPCSUtils\TestUtils\UtilityMethodTestCase; + +/** + * Tests for the \PHPCSUtils\BackCompat\BCFile::findExtendedClassName method. + * + * @covers \PHPCSUtils\BackCompat\BCFile::findExtendedClassName + * + * @group objectdeclarations + * + * @since 1.0.0 + */ +class FindExtendedClassNameTest extends UtilityMethodTestCase +{ + + /** + * The fully qualified name of the class being tested. + * + * This allows for the same unit tests to be run for both the BCFile functions + * as well as for the related PHPCSUtils functions. + * + * @var string + */ + const TEST_CLASS = '\PHPCSUtils\BackCompat\BCFile'; + + /** + * Test getting a `false` result when a non-existent token is passed. + * + * @return void + */ + public function testNonExistentToken() + { + $testClass = static::TEST_CLASS; + + $result = $testClass::findExtendedClassName(self::$phpcsFile, 100000); + $this->assertFalse($result); + } + + /** + * Test getting a `false` result when a token other than one of the supported tokens is passed. + * + * @return void + */ + public function testNotAClass() + { + $testClass = static::TEST_CLASS; + + $token = $this->getTargetToken('/* testNotAClass */', [T_FUNCTION]); + $result = $testClass::findExtendedClassName(self::$phpcsFile, $token); + $this->assertFalse($result); + } + + /** + * Test retrieving the name of the class being extended by another class + * (or interface). + * + * @dataProvider dataExtendedClass + * + * @param string $identifier Comment which precedes the test case. + * @param bool $expected Expected function output. + * + * @return void + */ + public function testFindExtendedClassName($identifier, $expected) + { + $testClass = static::TEST_CLASS; + + $OOToken = $this->getTargetToken($identifier, [T_CLASS, T_ANON_CLASS, T_INTERFACE]); + $result = $testClass::findExtendedClassName(self::$phpcsFile, $OOToken); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testFindExtendedClassName() + * + * @return array + */ + public function dataExtendedClass() + { + return [ + [ + '/* testExtendedClass */', + 'testFECNClass', + ], + [ + '/* testNamespacedClass */', + '\PHP_CodeSniffer\Tests\Core\File\testFECNClass', + ], + [ + '/* testNonExtendedClass */', + false, + ], + [ + '/* testInterface */', + false, + ], + [ + '/* testInterfaceThatExtendsInterface */', + 'testFECNInterface', + ], + [ + '/* testInterfaceThatExtendsFQCNInterface */', + '\PHP_CodeSniffer\Tests\Core\File\testFECNInterface', + ], + [ + '/* testNestedExtendedClass */', + false, + ], + [ + '/* testNestedExtendedAnonClass */', + 'testFECNAnonClass', + ], + [ + '/* testClassThatExtendsAndImplements */', + 'testFECNClass', + ], + [ + '/* testClassThatImplementsAndExtends */', + 'testFECNClass', + ], + [ + '/* testExtendedAnonClass */', + 'testFECNExtendedAnonClass', + ], + [ + '/* testInterfaceMultiExtends */', + '\Package\FooInterface', + ], + [ + '/* testMissingExtendsName */', + false, + ], + [ + '/* testParseError */', + false, + ], + ]; + } +} diff --git a/Tests/BackCompat/BCFile/FindImplementedInterfaceNamesTest.inc b/Tests/BackCompat/BCFile/FindImplementedInterfaceNamesTest.inc new file mode 100644 index 00000000..4243fa3b --- /dev/null +++ b/Tests/BackCompat/BCFile/FindImplementedInterfaceNamesTest.inc @@ -0,0 +1,37 @@ + + * @author Juliette Reinders Folmer + * + * With documentation contributions from: + * @author George Mponos + * @author Phil Davis + * + * @copyright 2016-2019 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHPCSUtils\Tests\BackCompat\BCFile; + +use PHPCSUtils\TestUtils\UtilityMethodTestCase; + +/** + * Tests for the \PHPCSUtils\BackCompat\BCFile::findImplementedInterfaceNames method. + * + * @covers \PHPCSUtils\BackCompat\BCFile::findImplementedInterfaceNames + * + * @group objectdeclarations + * + * @since 1.0.0 + */ +class FindImplementedInterfaceNamesTest extends UtilityMethodTestCase +{ + + /** + * The fully qualified name of the class being tested. + * + * This allows for the same unit tests to be run for both the BCFile functions + * as well as for the related PHPCSUtils functions. + * + * @var string + */ + const TEST_CLASS = '\PHPCSUtils\BackCompat\BCFile'; + + /** + * Test getting a `false` result when a non-existent token is passed. + * + * @return void + */ + public function testNonExistentToken() + { + $testClass = static::TEST_CLASS; + + $result = $testClass::findImplementedInterfaceNames(self::$phpcsFile, 100000); + $this->assertFalse($result); + } + + /** + * Test getting a `false` result when a token other than one of the supported tokens is passed. + * + * @return void + */ + public function testNotAClass() + { + $testClass = static::TEST_CLASS; + + $token = $this->getTargetToken('/* testNotAClass */', [T_FUNCTION]); + $result = $testClass::findImplementedInterfaceNames(self::$phpcsFile, $token); + $this->assertFalse($result); + } + + /** + * Test retrieving the name(s) of the interfaces being implemented by a class. + * + * @dataProvider dataImplementedInterface + * + * @param string $identifier Comment which precedes the test case. + * @param bool $expected Expected function output. + * + * @return void + */ + public function testFindImplementedInterfaceNames($identifier, $expected) + { + $testClass = static::TEST_CLASS; + + $OOToken = $this->getTargetToken($identifier, [T_CLASS, T_ANON_CLASS, T_INTERFACE]); + $result = $testClass::findImplementedInterfaceNames(self::$phpcsFile, $OOToken); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testFindImplementedInterfaceNames() + * + * @return array + */ + public function dataImplementedInterface() + { + return [ + [ + '/* testImplementedClass */', + ['testFIINInterface'], + ], + [ + '/* testMultiImplementedClass */', + [ + 'testFIINInterface', + 'testFIINInterface2', + ], + ], + [ + '/* testNamespacedClass */', + ['\PHP_CodeSniffer\Tests\Core\File\testFIINInterface'], + ], + [ + '/* testNonImplementedClass */', + false, + ], + [ + '/* testInterface */', + false, + ], + [ + '/* testClassThatExtendsAndImplements */', + [ + 'InterfaceA', + '\NameSpaced\Cat\InterfaceB', + ], + ], + [ + '/* testClassThatImplementsAndExtends */', + [ + '\InterfaceA', + 'InterfaceB', + ], + ], + [ + '/* testAnonClassImplements */', + ['testFIINInterface'], + ], + [ + '/* testMissingImplementsName */', + false, + ], + [ + '/* testParseError */', + false, + ], + ]; + } +} diff --git a/Tests/BackCompat/BCFile/GetClassPropertiesTest.inc b/Tests/BackCompat/BCFile/GetClassPropertiesTest.inc new file mode 100644 index 00000000..9753d3ae --- /dev/null +++ b/Tests/BackCompat/BCFile/GetClassPropertiesTest.inc @@ -0,0 +1,31 @@ +expectPhpcsException('$stackPtr must be of type T_CLASS'); + + $testClass = static::TEST_CLASS; + $interface = $this->getTargetToken($testMarker, $tokenType); + $testClass::getClassProperties(self::$phpcsFile, $interface); + } + + /** + * Data provider. + * + * @see testNotAClassException() For the array format. + * + * @return array + */ + public function dataNotAClassException() + { + return [ + 'interface' => [ + '/* testNotAClass */', + \T_INTERFACE, + ], + 'anon-class' => [ + '/* testAnonClass */', + \T_ANON_CLASS, + ], + ]; + } + + /** + * Test retrieving the properties for a class declaration. + * + * @dataProvider dataGetClassProperties + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected Expected function output. + * + * @return void + */ + public function testGetClassProperties($testMarker, $expected) + { + $testClass = static::TEST_CLASS; + + $class = $this->getTargetToken($testMarker, \T_CLASS); + $result = $testClass::getClassProperties(self::$phpcsFile, $class); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testGetClassProperties() For the array format. + * + * @return array + */ + public function dataGetClassProperties() + { + return [ + 'no-properties' => [ + '/* testClassWithoutProperties */', + [ + 'is_abstract' => false, + 'is_final' => false, + ], + ], + 'abstract' => [ + '/* testAbstractClass */', + [ + 'is_abstract' => true, + 'is_final' => false, + ], + ], + 'final' => [ + '/* testFinalClass */', + [ + 'is_abstract' => false, + 'is_final' => true, + ], + ], + 'comments-and-new-lines' => [ + '/* testWithCommentsAndNewLines */', + [ + 'is_abstract' => true, + 'is_final' => false, + ], + ], + 'no-properties-with-docblock' => [ + '/* testWithDocblockWithoutProperties */', + [ + 'is_abstract' => false, + 'is_final' => false, + ], + ], + ]; + } +} diff --git a/Tests/BackCompat/BCFile/GetConditionTest.inc b/Tests/BackCompat/BCFile/GetConditionTest.inc new file mode 100644 index 00000000..e7684daa --- /dev/null +++ b/Tests/BackCompat/BCFile/GetConditionTest.inc @@ -0,0 +1,91 @@ + $v) { + + /* condition 11-2: try */ + try { + --$k; + + /* condition 11-3: catch */ + } catch (Exception $e) { + /* testInException */ + echo 'oh darn'; + /* condition 11-4: finally */ + } finally { + return true; + } + } + + $a++; + } + break; + + /* condition 8b: default */ + default: + /* testInDefault */ + $return = 'nada'; + return $return; + } + } + } + } + } + } + } +} diff --git a/Tests/BackCompat/BCFile/GetConditionTest.php b/Tests/BackCompat/BCFile/GetConditionTest.php new file mode 100644 index 00000000..9dfa77c9 --- /dev/null +++ b/Tests/BackCompat/BCFile/GetConditionTest.php @@ -0,0 +1,441 @@ + => + */ + protected static $testTargets = [ + \T_VARIABLE => '/* testSeriouslyNestedMethod */', + \T_RETURN => '/* testDeepestNested */', + \T_ECHO => '/* testInException */', + \T_CONSTANT_ENCAPSED_STRING => '/* testInDefault */', + ]; + + /** + * List of all the condition markers in the test case file. + * + * @var string[] + */ + protected $conditionMarkers = [ + '/* condition 0: namespace */', + '/* condition 1: if */', + '/* condition 2: function */', + '/* condition 3-1: if */', + '/* condition 3-2: else */', + '/* condition 4: if */', + '/* condition 5: nested class */', + '/* condition 6: class method */', + '/* condition 7: switch */', + '/* condition 8a: case */', + '/* condition 9: while */', + '/* condition 10-1: if */', + '/* condition 11-1: nested anonymous class */', + '/* condition 12: nested anonymous class method */', + '/* condition 13: closure */', + '/* condition 10-2: elseif */', + '/* condition 10-3: foreach */', + '/* condition 11-2: try */', + '/* condition 11-3: catch */', + '/* condition 11-4: finally */', + '/* condition 8b: default */', + ]; + + /** + * Base array with all the scope opening tokens. + * + * This array is merged with expected result arrays for various unit tests + * to make sure all possible conditions are tested. + * + * This array should be kept in sync with the Tokens::$scopeOpeners array. + * This array isn't auto-generated based on the array in Tokens as for these + * tests we want to have access to the token constant names, not just their values. + * + * @var array => + */ + protected $conditionDefaults = [ + 'T_CLASS' => false, + 'T_ANON_CLASS' => false, + 'T_INTERFACE' => false, + 'T_TRAIT' => false, + 'T_NAMESPACE' => false, + 'T_FUNCTION' => false, + 'T_CLOSURE' => false, + 'T_IF' => false, + 'T_SWITCH' => false, + 'T_CASE' => false, + 'T_DECLARE' => false, + 'T_DEFAULT' => false, + 'T_WHILE' => false, + 'T_ELSE' => false, + 'T_ELSEIF' => false, + 'T_FOR' => false, + 'T_FOREACH' => false, + 'T_DO' => false, + 'T_TRY' => false, + 'T_CATCH' => false, + 'T_FINALLY' => false, + 'T_PROPERTY' => false, + 'T_OBJECT' => false, + 'T_USE' => false, + ]; + + /** + * Cache for the test token stack pointers. + * + * @var array => + */ + protected static $testTokens = []; + + /** + * Cache for the marker token stack pointers. + * + * @var array => + */ + protected static $markerTokens = []; + + /** + * OO scope tokens array. + * + * @var => + */ + protected $ooScopeTokens = []; + + /** + * Set up the token position caches for the tests. + * + * Retrieves the test tokens and marker token stack pointer positions + * only once and caches them as they won't change between the tests anyway. + * + * @before + * + * @return void + */ + protected function setUpCaches() + { + if (empty(self::$testTokens) === true) { + foreach (self::$testTargets as $targetToken => $marker) { + self::$testTokens[$marker] = $this->getTargetToken($marker, $targetToken); + } + } + + if (empty(self::$markerTokens) === true) { + foreach ($this->conditionMarkers as $marker) { + self::$markerTokens[$marker] = $this->getTargetToken($marker, Tokens::$scopeOpeners); + } + } + + $this->ooScopeTokens = BCTokens::ooScopeTokens(); + } + + /** + * Test passing a non-existent token pointer. + * + * @return void + */ + public function testNonExistentToken() + { + $testClass = static::TEST_CLASS; + + $result = $testClass::getCondition(self::$phpcsFile, 100000, $this->ooScopeTokens); + $this->assertFalse($result); + + $result = $testClass::hasCondition(self::$phpcsFile, 100000, \T_IF); + $this->assertFalse($result); + } + + /** + * Test passing a non conditional token. + * + * @return void + */ + public function testNonConditionalToken() + { + $testClass = static::TEST_CLASS; + $stackPtr = $this->getTargetToken('/* testStartPoint */', \T_STRING); + + $result = $testClass::getCondition(self::$phpcsFile, $stackPtr, \T_IF); + $this->assertFalse($result); + + $result = $testClass::hasCondition(self::$phpcsFile, $stackPtr, $this->ooScopeTokens); + $this->assertFalse($result); + } + + /** + * Test retrieving a specific condition from a tokens "conditions" array. + * + * @dataProvider dataGetCondition + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expectedResults Array with the condition token type to search for as key + * and the marker for the expected stack pointer result as a value. + * + * @return void + */ + public function testGetCondition($testMarker, $expectedResults) + { + $testClass = static::TEST_CLASS; + $stackPtr = self::$testTokens[$testMarker]; + + // Add expected results for all test markers not listed in the data provider. + $expectedResults += $this->conditionDefaults; + + foreach ($expectedResults as $conditionType => $expected) { + if ($expected !== false) { + $expected = self::$markerTokens[$expected]; + } + + $result = $testClass::getCondition(self::$phpcsFile, $stackPtr, \constant($conditionType)); + $this->assertSame( + $expected, + $result, + "Assertion failed for test marker '{$testMarker}' with condition {$conditionType}" + ); + } + } + + /** + * Data provider. + * + * Only the conditions which are expected to be *found* need to be listed here. + * All other potential conditions will automatically also be tested and will expect + * `false` as a result. + * + * @see testGetCondition() For the array format. + * + * @return array + */ + public static function dataGetCondition() + { + return [ + 'testSeriouslyNestedMethod' => [ + '/* testSeriouslyNestedMethod */', + [ + 'T_CLASS' => '/* condition 5: nested class */', + 'T_NAMESPACE' => '/* condition 0: namespace */', + 'T_FUNCTION' => '/* condition 2: function */', + 'T_IF' => '/* condition 1: if */', + 'T_ELSE' => '/* condition 3-2: else */', + ], + ], + 'testDeepestNested' => [ + '/* testDeepestNested */', + [ + 'T_CLASS' => '/* condition 5: nested class */', + 'T_ANON_CLASS' => '/* condition 11-1: nested anonymous class */', + 'T_NAMESPACE' => '/* condition 0: namespace */', + 'T_FUNCTION' => '/* condition 2: function */', + 'T_CLOSURE' => '/* condition 13: closure */', + 'T_IF' => '/* condition 1: if */', + 'T_SWITCH' => '/* condition 7: switch */', + 'T_CASE' => '/* condition 8a: case */', + 'T_WHILE' => '/* condition 9: while */', + 'T_ELSE' => '/* condition 3-2: else */', + ], + ], + 'testInException' => [ + '/* testInException */', + [ + 'T_CLASS' => '/* condition 5: nested class */', + 'T_NAMESPACE' => '/* condition 0: namespace */', + 'T_FUNCTION' => '/* condition 2: function */', + 'T_IF' => '/* condition 1: if */', + 'T_SWITCH' => '/* condition 7: switch */', + 'T_CASE' => '/* condition 8a: case */', + 'T_WHILE' => '/* condition 9: while */', + 'T_ELSE' => '/* condition 3-2: else */', + 'T_FOREACH' => '/* condition 10-3: foreach */', + 'T_CATCH' => '/* condition 11-3: catch */', + ], + ], + 'testInDefault' => [ + '/* testInDefault */', + [ + 'T_CLASS' => '/* condition 5: nested class */', + 'T_NAMESPACE' => '/* condition 0: namespace */', + 'T_FUNCTION' => '/* condition 2: function */', + 'T_IF' => '/* condition 1: if */', + 'T_SWITCH' => '/* condition 7: switch */', + 'T_DEFAULT' => '/* condition 8b: default */', + 'T_ELSE' => '/* condition 3-2: else */', + ], + ], + ]; + } + + /** + * Test whether a token has a condition of a certain type. + * + * @dataProvider dataHasCondition + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expectedResults Array with the condition token type to search for as key + * and the expected result as a value. + * + * @return void + */ + public function testHasCondition($testMarker, $expectedResults) + { + $testClass = static::TEST_CLASS; + $stackPtr = self::$testTokens[$testMarker]; + + // Add expected results for all test markers not listed in the data provider. + $expectedResults += $this->conditionDefaults; + + foreach ($expectedResults as $conditionType => $expected) { + $result = $testClass::hasCondition(self::$phpcsFile, $stackPtr, \constant($conditionType)); + $this->assertSame( + $expected, + $result, + "Assertion failed for test marker '{$testMarker}' with condition {$conditionType}" + ); + } + } + + /** + * Data Provider. + * + * Only list the "true" conditions in the $results array. + * All other potential conditions will automatically also be tested + * and will expect "false" as a result. + * + * @see testHasCondition() For the array format. + * + * @return array + */ + public static function dataHasCondition() + { + return [ + 'testSeriouslyNestedMethod' => [ + '/* testSeriouslyNestedMethod */', + [ + 'T_CLASS' => true, + 'T_NAMESPACE' => true, + 'T_FUNCTION' => true, + 'T_IF' => true, + 'T_ELSE' => true, + ], + ], + 'testDeepestNested' => [ + '/* testDeepestNested */', + [ + 'T_CLASS' => true, + 'T_ANON_CLASS' => true, + 'T_NAMESPACE' => true, + 'T_FUNCTION' => true, + 'T_CLOSURE' => true, + 'T_IF' => true, + 'T_SWITCH' => true, + 'T_CASE' => true, + 'T_WHILE' => true, + 'T_ELSE' => true, + ], + ], + 'testInException' => [ + '/* testInException */', + [ + 'T_CLASS' => true, + 'T_NAMESPACE' => true, + 'T_FUNCTION' => true, + 'T_IF' => true, + 'T_SWITCH' => true, + 'T_CASE' => true, + 'T_WHILE' => true, + 'T_ELSE' => true, + 'T_FOREACH' => true, + 'T_CATCH' => true, + ], + ], + 'testInDefault' => [ + '/* testInDefault */', + [ + 'T_CLASS' => true, + 'T_NAMESPACE' => true, + 'T_FUNCTION' => true, + 'T_IF' => true, + 'T_SWITCH' => true, + 'T_DEFAULT' => true, + 'T_ELSE' => true, + ], + ], + ]; + } + + /** + * Test whether a token has a condition of a certain type, with multiple allowed possibilities. + * + * @return void + */ + public function testHasConditionMultipleTypes() + { + $testClass = static::TEST_CLASS; + $stackPtr = self::$testTokens['/* testInException */']; + + $result = $testClass::hasCondition(self::$phpcsFile, $stackPtr, [\T_TRY, \T_FINALLY]); + $this->assertFalse( + $result, + 'Failed asserting that "testInException" does not have a "try" nor a "finally" condition' + ); + + $result = $testClass::hasCondition(self::$phpcsFile, $stackPtr, [\T_TRY, \T_CATCH, \T_FINALLY]); + $this->assertTrue( + $result, + 'Failed asserting that "testInException" has a "try", "catch" or "finally" condition' + ); + + $stackPtr = self::$testTokens['/* testSeriouslyNestedMethod */']; + + $result = $testClass::hasCondition(self::$phpcsFile, $stackPtr, [\T_ANON_CLASS, \T_CLOSURE]); + $this->assertFalse( + $result, + 'Failed asserting that "testSeriouslyNestedMethod" does not have an anonymous class nor a closure condition' + ); + + $result = $testClass::hasCondition(self::$phpcsFile, $stackPtr, $this->ooScopeTokens); + $this->assertTrue( + $result, + 'Failed asserting that "testSeriouslyNestedMethod" has an OO Scope token condition' + ); + } +} diff --git a/Tests/BackCompat/BCFile/GetDeclarationNameJSTest.js b/Tests/BackCompat/BCFile/GetDeclarationNameJSTest.js new file mode 100644 index 00000000..ee0a76a4 --- /dev/null +++ b/Tests/BackCompat/BCFile/GetDeclarationNameJSTest.js @@ -0,0 +1,23 @@ +/* testInvalidTokenPassed */ +print something; + +var object = +{ + /* testClosure */ + propertyName: function () {} +} + +/* testFunction */ +function functionName() {} + +/* testClass */ +class ClassName +{ + /* testMethod */ + methodName() { + return false; + } +} + +/* testFunctionUnicode */ +function π() {} diff --git a/Tests/BackCompat/BCFile/GetDeclarationNameJSTest.php b/Tests/BackCompat/BCFile/GetDeclarationNameJSTest.php new file mode 100644 index 00000000..bf97018f --- /dev/null +++ b/Tests/BackCompat/BCFile/GetDeclarationNameJSTest.php @@ -0,0 +1,146 @@ +expectPhpcsException('Token type "T_STRING" is not T_FUNCTION, T_CLASS, T_INTERFACE or T_TRAIT'); + + $target = $this->getTargetToken('/* testInvalidTokenPassed */', \T_STRING); + BCFile::getDeclarationName(self::$phpcsFile, $target); + } + + /** + * Test receiving "null" when passed an anonymous construct or in case of a parse error. + * + * @dataProvider dataGetDeclarationNameNull + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string $targetType Token type of the token to get as stackPtr. + * + * @return void + */ + public function testGetDeclarationNameNull($testMarker, $targetType) + { + $target = $this->getTargetToken($testMarker, $targetType); + $result = BCFile::getDeclarationName(self::$phpcsFile, $target); + $this->assertNull($result); + } + + /** + * Data provider. + * + * @see GetDeclarationNameTest::testGetDeclarationNameNull() + * + * @return array + */ + public function dataGetDeclarationNameNull() + { + return [ + 'closure' => [ + '/* testClosure */', + \T_CLOSURE, + ], + ]; + } + + /** + * Test retrieving the name of a function or OO structure. + * + * @dataProvider dataGetDeclarationName + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $expected Expected function output. + * @param int|string $targetType Token type of the token to get as stackPtr. + * + * @return void + */ + public function testGetDeclarationName($testMarker, $expected, $targetType = null) + { + if (isset($targetType) === false) { + $targetType = [\T_CLASS, \T_INTERFACE, \T_TRAIT, \T_FUNCTION]; + } + + $target = $this->getTargetToken($testMarker, $targetType); + $result = BCFile::getDeclarationName(self::$phpcsFile, $target); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see GetDeclarationNameTest::testGetDeclarationName() + * + * @return array + */ + public function dataGetDeclarationName() + { + return [ + 'function' => [ + '/* testFunction */', + 'functionName', + ], + 'class' => [ + '/* testClass */', + 'ClassName', + [\T_CLASS, \T_STRING], + ], + 'function-unicode-name' => [ + '/* testFunctionUnicode */', + 'π', + ], + ]; + } + + /** + * Test retrieving the name of JS ES6 class method. + * + * @return void + */ + public function testGetDeclarationNameES6Method() + { + if (\version_compare(Helper::getVersion(), '3.0.0', '<') === true) { + $this->markTestSkipped('Support for JS ES6 method has not been backfilled for PHPCS 2.x (yet)'); + } + + $target = $this->getTargetToken('/* testMethod */', [\T_CLASS, \T_INTERFACE, \T_TRAIT, \T_FUNCTION]); + $result = BCFile::getDeclarationName(self::$phpcsFile, $target); + $this->assertSame('methodName', $result); + } +} diff --git a/Tests/BackCompat/BCFile/GetDeclarationNameTest.inc b/Tests/BackCompat/BCFile/GetDeclarationNameTest.inc new file mode 100644 index 00000000..b7ebb056 --- /dev/null +++ b/Tests/BackCompat/BCFile/GetDeclarationNameTest.inc @@ -0,0 +1,65 @@ +expectPhpcsException('Token type "T_STRING" is not T_FUNCTION, T_CLASS, T_INTERFACE or T_TRAIT'); + + $target = $this->getTargetToken('/* testInvalidTokenPassed */', \T_STRING); + BCFile::getDeclarationName(self::$phpcsFile, $target); + } + + /** + * Test receiving "null" when passed an anonymous construct or in case of a parse error. + * + * @dataProvider dataGetDeclarationNameNull + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string $targetType Token type of the token to get as stackPtr. + * + * @return void + */ + public function testGetDeclarationNameNull($testMarker, $targetType) + { + $target = $this->getTargetToken($testMarker, $targetType); + $result = BCFile::getDeclarationName(self::$phpcsFile, $target); + $this->assertNull($result); + } + + /** + * Data provider. + * + * @see testGetDeclarationNameNull() For the array format. + * + * @return array + */ + public function dataGetDeclarationNameNull() + { + return [ + 'closure' => [ + '/* testClosure */', + \T_CLOSURE, + ], + 'anon-class-with-parentheses' => [ + '/* testAnonClassWithParens */', + \T_ANON_CLASS, + ], + 'anon-class-with-parentheses-2' => [ + '/* testAnonClassWithParens2 */', + \T_ANON_CLASS, + ], + 'anon-class-without-parentheses' => [ + '/* testAnonClassWithoutParens */', + \T_ANON_CLASS, + ], + 'anon-class-extends-without-parentheses' => [ + '/* testAnonClassExtendsWithoutParens */', + \T_ANON_CLASS, + ], + + /* + * Note: this particular test *will* throw tokenizer "undefined offset" notices on PHPCS 2.6.0, + * but the test will pass. + */ + 'live-coding' => [ + '/* testLiveCoding */', + \T_FUNCTION, + ], + ]; + } + + /** + * Test retrieving the name of a function or OO structure. + * + * @dataProvider dataGetDeclarationName + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $expected Expected function output. + * @param int|string $targetType Token type of the token to get as stackPtr. + * + * @return void + */ + public function testGetDeclarationName($testMarker, $expected, $targetType = null) + { + if (isset($targetType) === false) { + $targetType = [\T_CLASS, \T_INTERFACE, \T_TRAIT, \T_FUNCTION]; + } + + $target = $this->getTargetToken($testMarker, $targetType); + $result = BCFile::getDeclarationName(self::$phpcsFile, $target); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testGetDeclarationName() For the array format. + * + * @return array + */ + public function dataGetDeclarationName() + { + return [ + 'function' => [ + '/* testFunction */', + 'functionName', + ], + 'class' => [ + '/* testClass */', + 'ClassName', + ], + 'method' => [ + '/* testMethod */', + 'methodName', + ], + 'abstract-method' => [ + '/* testAbstractMethod */', + 'abstractMethodName', + ], + 'extended-class' => [ + '/* testExtendedClass */', + 'ExtendedClass', + ], + 'interface' => [ + '/* testInterface */', + 'InterfaceName', + ], + 'trait' => [ + '/* testTrait */', + 'TraitName', + ], + 'function-name-ends-with-number' => [ + '/* testFunctionEndingWithNumber */', + 'ValidNameEndingWithNumber5', + ], + 'class-with-numbers-in-name' => [ + '/* testClassWithNumber */', + 'ClassWith1Number', + ], + 'interface-with-numbers-in-name' => [ + '/* testInterfaceWithNumbers */', + 'InterfaceWith12345Numbers', + ], + 'class-with-comments-and-new-lines' => [ + '/* testClassWithCommentsAndNewLines */', + 'ClassWithCommentsAndNewLines', + ], + ]; + } +} diff --git a/Tests/BackCompat/BCFile/GetMemberPropertiesTest.inc b/Tests/BackCompat/BCFile/GetMemberPropertiesTest.inc new file mode 100644 index 00000000..b50f8a1a --- /dev/null +++ b/Tests/BackCompat/BCFile/GetMemberPropertiesTest.inc @@ -0,0 +1,184 @@ + 'a', 'b' => 'b' ), + /* testGroupPrivate 3 */ + $varQ = 'string', + /* testGroupPrivate 4 */ + $varR = 123, + /* testGroupPrivate 5 */ + $varS = ONE / self::THREE, + /* testGroupPrivate 6 */ + $varT = [ + 'a' => 'a', + 'b' => 'b' + ], + /* testGroupPrivate 7 */ + $varU = __DIR__ . "/base"; + + + /* testMethodParam */ + public function methodName($param) { + /* testImportedGlobal */ + global $importedGlobal = true; + + /* testLocalVariable */ + $localVariable = true; + } + + /* testPropertyAfterMethod */ + private static $varV = true; + + /* testMessyNullableType */ + public /* comment + */ ? //comment + array $foo = []; + + /* testNamespaceType */ + public \MyNamespace\MyClass $foo; + + /* testNullableNamespaceType 1 */ + private ?ClassName $nullableClassType; + + /* testNullableNamespaceType 2 */ + protected ?Folder\ClassName $nullableClassType2; + + /* testMultilineNamespaceType */ + public \MyNamespace /** comment *\/ comment */ + \MyClass /* comment */ + \Foo $foo; + +} + +interface Base +{ + /* testInterfaceProperty */ + protected $anonymous; +} + +/* testGlobalVariable */ +$globalVariable = true; + +/* testNotAVariable */ +return; + +$a = ( $foo == $bar ? new stdClass() : + new class() { + /* testNestedProperty 1 */ + public $var = true; + + /* testNestedMethodParam 1 */ + public function something($var = false) {} + } +); + +function_call( 'param', new class { + /* testNestedProperty 2 */ + public $year = 2017; + + /* testNestedMethodParam 2 */ + public function __construct( $open, $post_id ) {} +}, 10, 2 ); diff --git a/Tests/BackCompat/BCFile/GetMemberPropertiesTest.php b/Tests/BackCompat/BCFile/GetMemberPropertiesTest.php new file mode 100644 index 00000000..707afc7e --- /dev/null +++ b/Tests/BackCompat/BCFile/GetMemberPropertiesTest.php @@ -0,0 +1,637 @@ + + * @author Greg Sherwood + * + * With documentation contributions from: + * @author Phil Davis + * + * @copyright 2017-2019 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHPCSUtils\Tests\BackCompat\BCFile; + +use PHPCSUtils\TestUtils\UtilityMethodTestCase; + +/** + * Tests for the \PHPCSUtils\BackCompat\BCFile::getMemberProperties method. + * + * @covers \PHPCSUtils\BackCompat\BCFile::getMemberProperties + * + * @group variables + * + * @since 1.0.0 + */ +class GetMemberPropertiesTest extends UtilityMethodTestCase +{ + + /** + * The fully qualified name of the class being tested. + * + * This allows for the same unit tests to be run for both the BCFile functions + * as well as for the related PHPCSUtils functions. + * + * @var string + */ + const TEST_CLASS = '\PHPCSUtils\BackCompat\BCFile'; + + /** + * Test the getMemberProperties() method. + * + * @dataProvider dataGetMemberProperties + * + * @param string $identifier Comment which precedes the test case. + * @param bool $expected Expected function output. + * + * @return void + */ + public function testGetMemberProperties($identifier, $expected) + { + $testClass = static::TEST_CLASS; + + $variable = $this->getTargetToken($identifier, T_VARIABLE); + $result = $testClass::getMemberProperties(self::$phpcsFile, $variable); + + if (isset($expected['type_token']) && $expected['type_token'] !== false) { + $expected['type_token'] += $variable; + } + if (isset($expected['type_end_token']) && $expected['type_end_token'] !== false) { + $expected['type_end_token'] += $variable; + } + + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testGetMemberProperties() + * + * @return array + */ + public function dataGetMemberProperties() + { + return [ + [ + '/* testVar */', + [ + 'scope' => 'public', + 'scope_specified' => false, + 'is_static' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testVarType */', + [ + 'scope' => 'public', + 'scope_specified' => false, + 'is_static' => false, + 'type' => '?int', + 'type_token' => -2, // Offset from the T_VARIABLE token. + 'type_end_token' => -2, // Offset from the T_VARIABLE token. + 'nullable_type' => true, + ], + ], + [ + '/* testPublic */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testPublicType */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'type' => 'string', + 'type_token' => -2, // Offset from the T_VARIABLE token. + 'type_end_token' => -2, // Offset from the T_VARIABLE token. + 'nullable_type' => false, + ], + ], + [ + '/* testProtected */', + [ + 'scope' => 'protected', + 'scope_specified' => true, + 'is_static' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testProtectedType */', + [ + 'scope' => 'protected', + 'scope_specified' => true, + 'is_static' => false, + 'type' => 'bool', + 'type_token' => -2, // Offset from the T_VARIABLE token. + 'type_end_token' => -2, // Offset from the T_VARIABLE token. + 'nullable_type' => false, + ], + ], + [ + '/* testPrivate */', + [ + 'scope' => 'private', + 'scope_specified' => true, + 'is_static' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testPrivateType */', + [ + 'scope' => 'private', + 'scope_specified' => true, + 'is_static' => false, + 'type' => 'array', + 'type_token' => -2, // Offset from the T_VARIABLE token. + 'type_end_token' => -2, // Offset from the T_VARIABLE token. + 'nullable_type' => false, + ], + ], + [ + '/* testStatic */', + [ + 'scope' => 'public', + 'scope_specified' => false, + 'is_static' => true, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testStaticType */', + [ + 'scope' => 'public', + 'scope_specified' => false, + 'is_static' => true, + 'type' => '?string', + 'type_token' => -2, // Offset from the T_VARIABLE token. + 'type_end_token' => -2, // Offset from the T_VARIABLE token. + 'nullable_type' => true, + ], + ], + [ + '/* testStaticVar */', + [ + 'scope' => 'public', + 'scope_specified' => false, + 'is_static' => true, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testVarStatic */', + [ + 'scope' => 'public', + 'scope_specified' => false, + 'is_static' => true, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testPublicStatic */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => true, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testProtectedStatic */', + [ + 'scope' => 'protected', + 'scope_specified' => true, + 'is_static' => true, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testPrivateStatic */', + [ + 'scope' => 'private', + 'scope_specified' => true, + 'is_static' => true, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testNoPrefix */', + [ + 'scope' => 'public', + 'scope_specified' => false, + 'is_static' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testPublicStaticWithDocblock */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => true, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testProtectedStaticWithDocblock */', + [ + 'scope' => 'protected', + 'scope_specified' => true, + 'is_static' => true, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testPrivateStaticWithDocblock */', + [ + 'scope' => 'private', + 'scope_specified' => true, + 'is_static' => true, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testGroupType 1 */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'type' => 'float', + 'type_token' => -6, // Offset from the T_VARIABLE token. + 'type_end_token' => -6, // Offset from the T_VARIABLE token. + 'nullable_type' => false, + ], + ], + [ + '/* testGroupType 2 */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'type' => 'float', + 'type_token' => -13, // Offset from the T_VARIABLE token. + 'type_end_token' => -13, // Offset from the T_VARIABLE token. + 'nullable_type' => false, + ], + ], + [ + '/* testGroupNullableType 1 */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => true, + 'type' => '?string', + 'type_token' => -6, // Offset from the T_VARIABLE token. + 'type_end_token' => -6, // Offset from the T_VARIABLE token. + 'nullable_type' => true, + ], + ], + [ + '/* testGroupNullableType 2 */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => true, + 'type' => '?string', + 'type_token' => -17, // Offset from the T_VARIABLE token. + 'type_end_token' => -17, // Offset from the T_VARIABLE token. + 'nullable_type' => true, + ], + ], + [ + '/* testGroupProtectedStatic 1 */', + [ + 'scope' => 'protected', + 'scope_specified' => true, + 'is_static' => true, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testGroupProtectedStatic 2 */', + [ + 'scope' => 'protected', + 'scope_specified' => true, + 'is_static' => true, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testGroupProtectedStatic 3 */', + [ + 'scope' => 'protected', + 'scope_specified' => true, + 'is_static' => true, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testGroupPrivate 1 */', + [ + 'scope' => 'private', + 'scope_specified' => true, + 'is_static' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testGroupPrivate 2 */', + [ + 'scope' => 'private', + 'scope_specified' => true, + 'is_static' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testGroupPrivate 3 */', + [ + 'scope' => 'private', + 'scope_specified' => true, + 'is_static' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testGroupPrivate 4 */', + [ + 'scope' => 'private', + 'scope_specified' => true, + 'is_static' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testGroupPrivate 5 */', + [ + 'scope' => 'private', + 'scope_specified' => true, + 'is_static' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testGroupPrivate 6 */', + [ + 'scope' => 'private', + 'scope_specified' => true, + 'is_static' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testGroupPrivate 7 */', + [ + 'scope' => 'private', + 'scope_specified' => true, + 'is_static' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testMessyNullableType */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'type' => '?array', + 'type_token' => -2, // Offset from the T_VARIABLE token. + 'type_end_token' => -2, // Offset from the T_VARIABLE token. + 'nullable_type' => true, + ], + ], + [ + '/* testNamespaceType */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'type' => '\MyNamespace\MyClass', + 'type_token' => -5, // Offset from the T_VARIABLE token. + 'type_end_token' => -2, // Offset from the T_VARIABLE token. + 'nullable_type' => false, + ], + ], + [ + '/* testNullableNamespaceType 1 */', + [ + 'scope' => 'private', + 'scope_specified' => true, + 'is_static' => false, + 'type' => '?ClassName', + 'type_token' => -2, // Offset from the T_VARIABLE token. + 'type_end_token' => -2, // Offset from the T_VARIABLE token. + 'nullable_type' => true, + ], + ], + [ + '/* testNullableNamespaceType 2 */', + [ + 'scope' => 'protected', + 'scope_specified' => true, + 'is_static' => false, + 'type' => '?Folder\ClassName', + 'type_token' => -4, // Offset from the T_VARIABLE token. + 'type_end_token' => -2, // Offset from the T_VARIABLE token. + 'nullable_type' => true, + ], + ], + [ + '/* testMultilineNamespaceType */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'type' => '\MyNamespace\MyClass\Foo', + 'type_token' => -18, // Offset from the T_VARIABLE token. + 'type_end_token' => -2, // Offset from the T_VARIABLE token. + 'nullable_type' => false, + ], + ], + [ + '/* testPropertyAfterMethod */', + [ + 'scope' => 'private', + 'scope_specified' => true, + 'is_static' => true, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testInterfaceProperty */', + [], + ], + [ + '/* testNestedProperty 1 */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + [ + '/* testNestedProperty 2 */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'type' => '', + 'type_token' => false, + 'type_end_token' => false, + 'nullable_type' => false, + ], + ], + ]; + } + + /** + * Test receiving an expected exception when a non property is passed. + * + * @dataProvider dataNotClassProperty + * + * @param string $identifier Comment which precedes the test case. + * + * @return void + */ + public function testNotClassPropertyException($identifier) + { + $this->expectPhpcsException('$stackPtr is not a class member var'); + + $testClass = static::TEST_CLASS; + + $variable = $this->getTargetToken($identifier, T_VARIABLE); + $testClass::getMemberProperties(self::$phpcsFile, $variable); + } + + /** + * Data provider. + * + * @see testNotClassPropertyException() + * + * @return array + */ + public function dataNotClassProperty() + { + return [ + ['/* testMethodParam */'], + ['/* testImportedGlobal */'], + ['/* testLocalVariable */'], + ['/* testGlobalVariable */'], + ['/* testNestedMethodParam 1 */'], + ['/* testNestedMethodParam 2 */'], + ]; + } + + /** + * Test receiving an expected exception when a non variable is passed. + * + * @return void + */ + public function testNotAVariableException() + { + $this->expectPhpcsException('$stackPtr must be of type T_VARIABLE'); + + $testClass = static::TEST_CLASS; + + $next = $this->getTargetToken('/* testNotAVariable */', T_RETURN); + $testClass::getMemberProperties(self::$phpcsFile, $next); + } +} diff --git a/Tests/BackCompat/BCFile/GetMethodParametersParseError1Test.inc b/Tests/BackCompat/BCFile/GetMethodParametersParseError1Test.inc new file mode 100644 index 00000000..465679eb --- /dev/null +++ b/Tests/BackCompat/BCFile/GetMethodParametersParseError1Test.inc @@ -0,0 +1,4 @@ +getTargetToken('/* testParseError */', [\T_FUNCTION, \T_CLOSURE]); + $result = BCFile::getMethodParameters(self::$phpcsFile, $target); + + $this->assertSame([], $result); + } +} diff --git a/Tests/BackCompat/BCFile/GetMethodParametersParseError2Test.inc b/Tests/BackCompat/BCFile/GetMethodParametersParseError2Test.inc new file mode 100644 index 00000000..667cb5ed --- /dev/null +++ b/Tests/BackCompat/BCFile/GetMethodParametersParseError2Test.inc @@ -0,0 +1,4 @@ +getTargetToken('/* testParseError */', [\T_FUNCTION, \T_CLOSURE]); + $result = BCFile::getMethodParameters(self::$phpcsFile, $target); + + $this->assertSame([], $result); + } +} diff --git a/Tests/BackCompat/BCFile/GetMethodParametersTest.inc b/Tests/BackCompat/BCFile/GetMethodParametersTest.inc new file mode 100644 index 00000000..e539d5be --- /dev/null +++ b/Tests/BackCompat/BCFile/GetMethodParametersTest.inc @@ -0,0 +1,120 @@ + + * @author Juliette Reinders Folmer + * + * With documentation contributions from: + * @author Diogo Oliveira de Melo + * @author George Mponos + * + * @copyright 2010-2019 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHPCSUtils\Tests\BackCompat\BCFile; + +use PHPCSUtils\BackCompat\BCFile; +use PHPCSUtils\TestUtils\UtilityMethodTestCase; + +/** + * Tests for the \PHPCSUtils\BackCompat\BCFile::getMethodParameters method. + * + * @covers \PHPCSUtils\BackCompat\BCFile::getMethodParameters + * + * @group functiondeclarations + * + * @since 1.0.0 + */ +class GetMethodParametersTest extends UtilityMethodTestCase +{ + + /** + * Test receiving an expected exception when a non function/use token is passed. + * + * @return void + */ + public function testUnexpectedTokenException() + { + $this->expectPhpcsException('$stackPtr must be of type T_FUNCTION or T_CLOSURE or T_USE'); + + $next = $this->getTargetToken('/* testNotAFunction */', [T_INTERFACE]); + BCFile::getMethodParameters(self::$phpcsFile, $next); + } + + /** + * Test receiving an expected exception when a non-closure use token is passed. + * + * @dataProvider dataInvalidUse + * + * @param string $identifier The comment which preceeds the test. + * + * @return void + */ + public function testInvalidUse($identifier) + { + $this->expectPhpcsException('$stackPtr was not a valid T_USE'); + + $use = $this->getTargetToken($identifier, [T_USE]); + BCFile::getMethodParameters(self::$phpcsFile, $use); + } + + /** + * Data Provider. + * + * @see testInvalidUse() For the array format. + * + * @return array + */ + public function dataInvalidUse() + { + return [ + 'ImportUse' => ['/* testImportUse */'], + 'ImportGroupUse' => ['/* testImportGroupUse */'], + 'TraitUse' => ['/* testTraitUse */'], + 'InvalidUse' => ['/* testInvalidUse */'], + ]; + } + + /** + * Test receiving an empty array when there are no parameters. + * + * @dataProvider dataNoParams + * + * @param string $commentString The comment which preceeds the test. + * @param array $targetTokenType Optional. The token type to search for after $commentString. + * Defaults to the function/closure tokens. + * + * @return void + */ + public function testNoParams($commentString, $targetTokenType = [T_FUNCTION, T_CLOSURE]) + { + $target = $this->getTargetToken($commentString, $targetTokenType); + $result = BCFile::getMethodParameters(self::$phpcsFile, $target); + + $this->assertSame([], $result); + } + + /** + * Data Provider. + * + * @see testNoParams() For the array format. + * + * @return array + */ + public function dataNoParams() + { + return [ + 'FunctionNoParams' => ['/* testFunctionNoParams */'], + 'ClosureNoParams' => ['/* testClosureNoParams */'], + 'ClosureUseNoParams' => ['/* testClosureUseNoParams */', T_USE], + ]; + } + + /** + * Verify pass-by-reference parsing. + * + * @return void + */ + public function testPassByReference() + { + $expected = []; + $expected[0] = [ + 'token' => 5, // Offset from the T_FUNCTION token. + 'name' => '$var', + 'content' => '&$var', + 'pass_by_reference' => true, + 'reference_token' => 4, // Offset from the T_FUNCTION token. + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '', + 'type_hint_token' => false, + 'type_hint_end_token' => false, + 'nullable_type' => false, + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Verify array hint parsing. + * + * @return void + */ + public function testArrayHint() + { + $expected = []; + $expected[0] = [ + 'token' => 6, // Offset from the T_FUNCTION token. + 'name' => '$var', + 'content' => 'array $var', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => 'array', + 'type_hint_token' => 4, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 4, // Offset from the T_FUNCTION token. + 'nullable_type' => false, + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Verify type hint parsing. + * + * @return void + */ + public function testTypeHint() + { + $expected = []; + $expected[0] = [ + 'token' => 6, // Offset from the T_FUNCTION token. + 'name' => '$var1', + 'content' => 'foo $var1', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => 'foo', + 'type_hint_token' => 4, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 4, // Offset from the T_FUNCTION token. + 'nullable_type' => false, + 'comma_token' => 7, // Offset from the T_FUNCTION token. + ]; + + $expected[1] = [ + 'token' => 11, // Offset from the T_FUNCTION token. + 'name' => '$var2', + 'content' => 'bar $var2', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => 'bar', + 'type_hint_token' => 9, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 9, // Offset from the T_FUNCTION token. + 'nullable_type' => false, + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Verify self type hint parsing. + * + * @return void + */ + public function testSelfTypeHint() + { + $expected = []; + $expected[0] = [ + 'token' => 6, // Offset from the T_FUNCTION token. + 'name' => '$var', + 'content' => 'self $var', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => 'self', + 'type_hint_token' => 4, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 4, // Offset from the T_FUNCTION token. + 'nullable_type' => false, + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Verify nullable type hint parsing. + * + * @return void + */ + public function testNullableTypeHint() + { + $expected = []; + $expected[0] = [ + 'token' => 7, // Offset from the T_FUNCTION token. + 'name' => '$var1', + 'content' => '?int $var1', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '?int', + 'type_hint_token' => 5, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 5, // Offset from the T_FUNCTION token. + 'nullable_type' => true, + 'comma_token' => 8, // Offset from the T_FUNCTION token. + ]; + + $expected[1] = [ + 'token' => 14, // Offset from the T_FUNCTION token. + 'name' => '$var2', + 'content' => '?\bar $var2', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '?\bar', + 'type_hint_token' => 11, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 12, // Offset from the T_FUNCTION token. + 'nullable_type' => true, + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Verify variable. + * + * @return void + */ + public function testVariable() + { + $expected = []; + $expected[0] = [ + 'token' => 4, // Offset from the T_FUNCTION token. + 'name' => '$var', + 'content' => '$var', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '', + 'type_hint_token' => false, + 'type_hint_end_token' => false, + 'nullable_type' => false, + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Verify default value parsing with a single function param. + * + * @return void + */ + public function testSingleDefaultValue() + { + $expected = []; + $expected[0] = [ + 'token' => 4, // Offset from the T_FUNCTION token. + 'name' => '$var1', + 'content' => '$var1=self::CONSTANT', + 'default' => 'self::CONSTANT', + 'default_token' => 6, // Offset from the T_FUNCTION token. + 'default_equal_token' => 5, // Offset from the T_FUNCTION token. + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '', + 'type_hint_token' => false, + 'type_hint_end_token' => false, + 'nullable_type' => false, + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Verify default value parsing. + * + * @return void + */ + public function testDefaultValues() + { + $expected = []; + $expected[0] = [ + 'token' => 4, // Offset from the T_FUNCTION token. + 'name' => '$var1', + 'content' => '$var1=1', + 'default' => '1', + 'default_token' => 6, // Offset from the T_FUNCTION token. + 'default_equal_token' => 5, // Offset from the T_FUNCTION token. + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '', + 'type_hint_token' => false, + 'type_hint_end_token' => false, + 'nullable_type' => false, + 'comma_token' => 7, // Offset from the T_FUNCTION token. + ]; + $expected[1] = [ + 'token' => 9, // Offset from the T_FUNCTION token. + 'name' => '$var2', + 'content' => "\$var2='value'", + 'default' => "'value'", + 'default_token' => 11, // Offset from the T_FUNCTION token. + 'default_equal_token' => 10, // Offset from the T_FUNCTION token. + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '', + 'type_hint_token' => false, + 'type_hint_end_token' => false, + 'nullable_type' => false, + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Verify "bitwise and" in default value !== pass-by-reference. + * + * @return void + */ + public function testBitwiseAndConstantExpressionDefaultValue() + { + $expected = []; + $expected[0] = [ + 'token' => 4, // Offset from the T_FUNCTION token. + 'name' => '$a', + 'content' => '$a = 10 & 20', + 'default' => '10 & 20', + 'default_token' => 8, // Offset from the T_FUNCTION token. + 'default_equal_token' => 6, // Offset from the T_FUNCTION token. + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '', + 'type_hint_token' => false, + 'type_hint_end_token' => false, + 'nullable_type' => false, + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Verify default value parsing with array values. + * + * @return void + */ + public function testArrayDefaultValues() + { + $expected = []; + $expected[0] = [ + 'token' => 4, // Offset from the T_FUNCTION token. + 'name' => '$var1', + 'content' => '$var1 = []', + 'default' => '[]', + 'default_token' => 8, // Offset from the T_FUNCTION token. + 'default_equal_token' => 6, // Offset from the T_FUNCTION token. + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '', + 'type_hint_token' => false, + 'type_hint_end_token' => false, + 'nullable_type' => false, + 'comma_token' => 10, // Offset from the T_FUNCTION token. + ]; + $expected[1] = [ + 'token' => 12, // Offset from the T_FUNCTION token. + 'name' => '$var2', + 'content' => '$var2 = array(1, 2, 3)', + 'default' => 'array(1, 2, 3)', + 'default_token' => 16, // Offset from the T_FUNCTION token. + 'default_equal_token' => 14, // Offset from the T_FUNCTION token. + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '', + 'type_hint_token' => false, + 'type_hint_end_token' => false, + 'nullable_type' => false, + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Verify having a T_STRING constant as a default value for the second parameter. + * + * @return void + */ + public function testConstantDefaultValueSecondParam() + { + $expected = []; + $expected[0] = [ + 'token' => 4, // Offset from the T_FUNCTION token. + 'name' => '$var1', + 'content' => '$var1', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '', + 'type_hint_token' => false, + 'type_hint_end_token' => false, + 'nullable_type' => false, + 'comma_token' => 5, // Offset from the T_FUNCTION token. + ]; + $expected[1] = [ + 'token' => 7, // Offset from the T_FUNCTION token. + 'name' => '$var2', + 'content' => '$var2 = M_PI', + 'default' => 'M_PI', + 'default_token' => 11, // Offset from the T_FUNCTION token. + 'default_equal_token' => 9, // Offset from the T_FUNCTION token. + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '', + 'type_hint_token' => false, + 'type_hint_end_token' => false, + 'nullable_type' => false, + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Verify distinquishing between a nullable type and a ternary within a default expression. + * + * @return void + */ + public function testScalarTernaryExpressionInDefault() + { + $expected = []; + $expected[0] = [ + 'token' => 5, // Offset from the T_FUNCTION token. + 'name' => '$a', + 'content' => '$a = FOO ? \'bar\' : 10', + 'default' => 'FOO ? \'bar\' : 10', + 'default_token' => 9, // Offset from the T_FUNCTION token. + 'default_equal_token' => 7, // Offset from the T_FUNCTION token. + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '', + 'type_hint_token' => false, + 'type_hint_end_token' => false, + 'nullable_type' => false, + 'comma_token' => 18, // Offset from the T_FUNCTION token. + ]; + $expected[1] = [ + 'token' => 24, // Offset from the T_FUNCTION token. + 'name' => '$b', + 'content' => '? bool $b', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '?bool', + 'type_hint_token' => 22, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 22, // Offset from the T_FUNCTION token. + 'nullable_type' => true, + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Verify a variadic parameter being recognized correctly. + * + * @return void + */ + public function testVariadicFunction() + { + $expected = []; + $expected[0] = [ + 'token' => 9, // Offset from the T_FUNCTION token. + 'name' => '$a', + 'content' => 'int ... $a', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => true, + 'variadic_token' => 7, // Offset from the T_FUNCTION token. + 'type_hint' => 'int', + 'type_hint_token' => 5, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 5, // Offset from the T_FUNCTION token. + 'nullable_type' => false, + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Verify a variadic parameter passed by reference being recognized correctly. + * + * @return void + */ + public function testVariadicByRefFunction() + { + $expected = []; + $expected[0] = [ + 'token' => 7, // Offset from the T_FUNCTION token. + 'name' => '$a', + 'content' => '&...$a', + 'pass_by_reference' => true, + 'reference_token' => 5, // Offset from the T_FUNCTION token. + 'variable_length' => true, + 'variadic_token' => 6, // Offset from the T_FUNCTION token. + 'type_hint' => '', + 'type_hint_token' => false, + 'type_hint_end_token' => false, + 'nullable_type' => false, + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Verify handling of a variadic parameter with a class based type declaration. + * + * @return void + */ + public function testVariadicFunctionClassType() + { + $expected = []; + $expected[0] = [ + 'token' => 4, // Offset from the T_FUNCTION token. + 'name' => '$unit', + 'content' => '$unit', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '', + 'type_hint_token' => false, + 'type_hint_end_token' => false, + 'nullable_type' => false, + 'comma_token' => 5, // Offset from the T_FUNCTION token. + ]; + $expected[1] = [ + 'token' => 10, // Offset from the T_FUNCTION token. + 'name' => '$intervals', + 'content' => 'DateInterval ...$intervals', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => true, + 'variadic_token' => 9, + 'type_hint' => 'DateInterval', + 'type_hint_token' => 7, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 7, // Offset from the T_FUNCTION token. + 'nullable_type' => false, + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Verify distinquishing between a nullable type and a ternary within a default expression. + * + * @return void + */ + public function testNameSpacedTypeDeclaration() + { + $expected = []; + $expected[0] = [ + 'token' => 12, // Offset from the T_FUNCTION token. + 'name' => '$a', + 'content' => '\Package\Sub\ClassName $a', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '\Package\Sub\ClassName', + 'type_hint_token' => 5, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 10, // Offset from the T_FUNCTION token. + 'nullable_type' => false, + 'comma_token' => 13, // Offset from the T_FUNCTION token. + ]; + $expected[1] = [ + 'token' => 20, // Offset from the T_FUNCTION token. + 'name' => '$b', + 'content' => '?Sub\AnotherClass $b', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '?Sub\AnotherClass', + 'type_hint_token' => 16, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 18, // Offset from the T_FUNCTION token. + 'nullable_type' => true, + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Verify correctly recognizing all type declarations supported by PHP. + * + * @return void + */ + public function testWithAllTypes() + { + $expected = []; + $expected[0] = [ + 'token' => 9, // Offset from the T_FUNCTION token. + 'name' => '$a', + 'content' => '?ClassName $a', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '?ClassName', + 'type_hint_token' => 7, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 7, // Offset from the T_FUNCTION token. + 'nullable_type' => true, + 'comma_token' => 10, // Offset from the T_FUNCTION token. + ]; + $expected[1] = [ + 'token' => 15, // Offset from the T_FUNCTION token. + 'name' => '$b', + 'content' => 'self $b', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => 'self', + 'type_hint_token' => 13, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 13, // Offset from the T_FUNCTION token. + 'nullable_type' => false, + 'comma_token' => 16, // Offset from the T_FUNCTION token. + ]; + $expected[2] = [ + 'token' => 21, // Offset from the T_FUNCTION token. + 'name' => '$c', + 'content' => 'parent $c', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => 'parent', + 'type_hint_token' => 19, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 19, // Offset from the T_FUNCTION token. + 'nullable_type' => false, + 'comma_token' => 22, // Offset from the T_FUNCTION token. + ]; + $expected[3] = [ + 'token' => 27, // Offset from the T_FUNCTION token. + 'name' => '$d', + 'content' => 'object $d', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => 'object', + 'type_hint_token' => 25, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 25, // Offset from the T_FUNCTION token. + 'nullable_type' => false, + 'comma_token' => 28, // Offset from the T_FUNCTION token. + ]; + $expected[4] = [ + 'token' => 34, // Offset from the T_FUNCTION token. + 'name' => '$e', + 'content' => '?int $e', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '?int', + 'type_hint_token' => 32, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 32, // Offset from the T_FUNCTION token. + 'nullable_type' => true, + 'comma_token' => 35, // Offset from the T_FUNCTION token. + ]; + $expected[5] = [ + 'token' => 41, // Offset from the T_FUNCTION token. + 'name' => '$f', + 'content' => 'string &$f', + 'pass_by_reference' => true, + 'reference_token' => 40, // Offset from the T_FUNCTION token. + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => 'string', + 'type_hint_token' => 38, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 38, // Offset from the T_FUNCTION token. + 'nullable_type' => false, + 'comma_token' => 42, // Offset from the T_FUNCTION token. + ]; + $expected[6] = [ + 'token' => 47, // Offset from the T_FUNCTION token. + 'name' => '$g', + 'content' => 'iterable $g', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => 'iterable', + 'type_hint_token' => 45, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 45, // Offset from the T_FUNCTION token. + 'nullable_type' => false, + 'comma_token' => 48, // Offset from the T_FUNCTION token. + ]; + $expected[7] = [ + 'token' => 53, // Offset from the T_FUNCTION token. + 'name' => '$h', + 'content' => 'bool $h = true', + 'default' => 'true', + 'default_token' => 57, // Offset from the T_FUNCTION token. + 'default_equal_token' => 55, // Offset from the T_FUNCTION token. + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => 'bool', + 'type_hint_token' => 51, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 51, // Offset from the T_FUNCTION token. + 'nullable_type' => false, + 'comma_token' => 58, // Offset from the T_FUNCTION token. + ]; + $expected[8] = [ + 'token' => 63, // Offset from the T_FUNCTION token. + 'name' => '$i', + 'content' => 'callable $i = \'is_null\'', + 'default' => "'is_null'", + 'default_token' => 67, // Offset from the T_FUNCTION token. + 'default_equal_token' => 65, // Offset from the T_FUNCTION token. + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => 'callable', + 'type_hint_token' => 61, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 61, // Offset from the T_FUNCTION token. + 'nullable_type' => false, + 'comma_token' => 68, // Offset from the T_FUNCTION token. + ]; + $expected[9] = [ + 'token' => 73, // Offset from the T_FUNCTION token. + 'name' => '$j', + 'content' => 'float $j = 1.1', + 'default' => '1.1', + 'default_token' => 77, // Offset from the T_FUNCTION token. + 'default_equal_token' => 75, // Offset from the T_FUNCTION token. + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => 'float', + 'type_hint_token' => 71, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 71, // Offset from the T_FUNCTION token. + 'nullable_type' => false, + 'comma_token' => 78, // Offset from the T_FUNCTION token. + ]; + $expected[10] = [ + 'token' => 84, // Offset from the T_FUNCTION token. + 'name' => '$k', + 'content' => 'array ...$k', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => true, + 'variadic_token' => 83, // Offset from the T_FUNCTION token. + 'type_hint' => 'array', + 'type_hint_token' => 81, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 81, // Offset from the T_FUNCTION token. + 'nullable_type' => false, + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Verify handling of a declaration interlaced with whitespace and comments. + * + * @return void + */ + public function testMessyDeclaration() + { + $expected = []; + $expected[0] = [ + 'token' => 25, // Offset from the T_FUNCTION token. + 'name' => '$a', + 'content' => '// comment + ?\MyNS /* comment */ + \ SubCat // phpcs:ignore Standard.Cat.Sniff -- for reasons. + \ MyClass $a', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '?\MyNS\SubCat\MyClass', + 'type_hint_token' => 9, + 'type_hint_end_token' => 23, + 'nullable_type' => true, + 'comma_token' => 26, // Offset from the T_FUNCTION token. + ]; + $expected[1] = [ + 'token' => 29, // Offset from the T_FUNCTION token. + 'name' => '$b', + 'content' => "\$b /* test */ = /* test */ 'default' /* test*/", + 'default' => "'default' /* test*/", + 'default_token' => 37, // Offset from the T_FUNCTION token. + 'default_equal_token' => 33, // Offset from the T_FUNCTION token. + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '', + 'type_hint_token' => false, + 'type_hint_end_token' => false, + 'nullable_type' => false, + 'comma_token' => 40, // Offset from the T_FUNCTION token. + ]; + $expected[2] = [ + 'token' => 62, // Offset from the T_FUNCTION token. + 'name' => '$c', + 'content' => '// phpcs:ignore Stnd.Cat.Sniff -- For reasons. + ? /*comment*/ + bool // phpcs:disable Stnd.Cat.Sniff -- For reasons. + & /*test*/ ... /* phpcs:ignore */ $c', + 'pass_by_reference' => true, + 'reference_token' => 54, // Offset from the T_FUNCTION token. + 'variable_length' => true, + 'variadic_token' => 58, // Offset from the T_FUNCTION token. + 'type_hint' => '?bool', + 'type_hint_token' => 50, // Offset from the T_FUNCTION token. + 'type_hint_end_token' => 50, // Offset from the T_FUNCTION token. + 'nullable_type' => true, + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Verify handling of a closure. + * + * @return void + */ + public function testClosure() + { + $expected = []; + $expected[0] = [ + 'token' => 3, // Offset from the T_FUNCTION token. + 'name' => '$a', + 'content' => '$a = \'test\'', + 'default' => "'test'", + 'default_token' => 7, // Offset from the T_FUNCTION token. + 'default_equal_token' => 5, // Offset from the T_FUNCTION token. + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '', + 'type_hint_token' => false, + 'type_hint_end_token' => false, + 'nullable_type' => false, + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Verify handling of a closure T_USE token correctly. + * + * @return void + */ + public function testClosureUse() + { + $expected = []; + $expected[0] = [ + 'token' => 3, // Offset from the T_USE token. + 'name' => '$foo', + 'content' => '$foo', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '', + 'type_hint_token' => false, + 'type_hint_end_token' => false, + 'nullable_type' => false, + 'comma_token' => 4, // Offset from the T_USE token. + ]; + $expected[1] = [ + 'token' => 6, // Offset from the T_USE token. + 'name' => '$bar', + 'content' => '$bar', + 'pass_by_reference' => false, + 'reference_token' => false, + 'variable_length' => false, + 'variadic_token' => false, + 'type_hint' => '', + 'type_hint_token' => false, + 'type_hint_end_token' => false, + 'nullable_type' => false, + 'comma_token' => false, + ]; + + $this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected, [T_USE]); + } + + /** + * Test helper. + * + * @param string $commentString The comment which preceeds the test. + * @param array $expected The expected function output. + * @param array $targetType Optional. The token type to search for after $commentString. + * Defaults to the function/closure tokens. + * + * @return void + */ + protected function getMethodParametersTestHelper($commentString, $expected, $targetType = [T_FUNCTION, T_CLOSURE]) + { + $target = $this->getTargetToken($commentString, $targetType); + $found = BCFile::getMethodParameters(self::$phpcsFile, $target); + + foreach ($expected as $key => $param) { + $expected[$key]['token'] += $target; + + if ($param['reference_token'] !== false) { + $expected[$key]['reference_token'] += $target; + } + if ($param['variadic_token'] !== false) { + $expected[$key]['variadic_token'] += $target; + } + if ($param['type_hint_token'] !== false) { + $expected[$key]['type_hint_token'] += $target; + } + if ($param['type_hint_end_token'] !== false) { + $expected[$key]['type_hint_end_token'] += $target; + } + if ($param['comma_token'] !== false) { + $expected[$key]['comma_token'] += $target; + } + if (isset($param['default_token'])) { + $expected[$key]['default_token'] += $target; + } + if (isset($param['default_equal_token'])) { + $expected[$key]['default_equal_token'] += $target; + } + } + + $this->assertSame($expected, $found); + } +} diff --git a/Tests/BackCompat/BCFile/GetMethodPropertiesTest.inc b/Tests/BackCompat/BCFile/GetMethodPropertiesTest.inc new file mode 100644 index 00000000..2139fb23 --- /dev/null +++ b/Tests/BackCompat/BCFile/GetMethodPropertiesTest.inc @@ -0,0 +1,70 @@ + + * @author Chris Wilkinson + * @author Juliette Reinders Folmer + * + * @copyright 2018-2019 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHPCSUtils\Tests\BackCompat\BCFile; + +use PHPCSUtils\BackCompat\BCFile; +use PHPCSUtils\TestUtils\UtilityMethodTestCase; + +/** + * Tests for the \PHPCSUtils\BackCompat\BCFile::getMethodProperties method. + * + * @covers \PHPCSUtils\BackCompat\BCFile::getMethodProperties + * + * @group functiondeclarations + * + * @since 1.0.0 + */ +class GetMethodPropertiesTest extends UtilityMethodTestCase +{ + + /** + * Test receiving an expected exception when a non function token is passed. + * + * @return void + */ + public function testNotAFunctionException() + { + $this->expectPhpcsException('$stackPtr must be of type T_FUNCTION or T_CLOSURE'); + + $next = $this->getTargetToken('/* testNotAFunction */', T_RETURN); + BCFile::getMethodProperties(self::$phpcsFile, $next); + } + + /** + * Test a basic function. + * + * @return void + */ + public function testBasicFunction() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => '', + 'return_type_token' => false, + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Test a function with a return type. + * + * @return void + */ + public function testReturnFunction() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'array', + 'return_type_token' => 11, // Offset from the T_FUNCTION token. + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Test a closure used as a function argument. + * + * @return void + */ + public function testNestedClosure() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'int', + 'return_type_token' => 8, // Offset from the T_FUNCTION token. + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Test a basic method. + * + * @return void + */ + public function testBasicMethod() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => '', + 'return_type_token' => false, + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Test a private static method. + * + * @return void + */ + public function testPrivateStaticMethod() + { + $expected = [ + 'scope' => 'private', + 'scope_specified' => true, + 'return_type' => '', + 'return_type_token' => false, + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => true, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Test a basic final method. + * + * @return void + */ + public function testFinalMethod() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => true, + 'return_type' => '', + 'return_type_token' => false, + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => true, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Test a protected method with a return type. + * + * @return void + */ + public function testProtectedReturnMethod() + { + $expected = [ + 'scope' => 'protected', + 'scope_specified' => true, + 'return_type' => 'int', + 'return_type_token' => 8, // Offset from the T_FUNCTION token. + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Test a public method with a return type. + * + * @return void + */ + public function testPublicReturnMethod() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => true, + 'return_type' => 'array', + 'return_type_token' => 7, // Offset from the T_FUNCTION token. + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Test a public method with a nullable return type. + * + * @return void + */ + public function testNullableReturnMethod() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => true, + 'return_type' => '?array', + 'return_type_token' => 8, // Offset from the T_FUNCTION token. + 'nullable_return_type' => true, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Test a public method with a nullable return type. + * + * @return void + */ + public function testMessyNullableReturnMethod() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => true, + 'return_type' => '?array', + 'return_type_token' => 18, // Offset from the T_FUNCTION token. + 'nullable_return_type' => true, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Test a method with a namespaced return type. + * + * @return void + */ + public function testReturnNamespace() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => '\MyNamespace\MyClass', + 'return_type_token' => 7, // Offset from the T_FUNCTION token. + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Test a method with a messy namespaces return type. + * + * @return void + */ + public function testReturnMultilineNamespace() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => '\MyNamespace\MyClass\Foo', + 'return_type_token' => 7, // Offset from the T_FUNCTION token. + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Test a basic abstract method. + * + * @return void + */ + public function testAbstractMethod() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => '', + 'return_type_token' => false, + 'nullable_return_type' => false, + 'is_abstract' => true, + 'is_final' => false, + 'is_static' => false, + 'has_body' => false, + ]; + + $this->getMethodPropertiesTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Test an abstract method with a return type. + * + * @return void + */ + public function testAbstractReturnMethod() + { + $expected = [ + 'scope' => 'protected', + 'scope_specified' => true, + 'return_type' => 'bool', + 'return_type_token' => 7, // Offset from the T_FUNCTION token. + 'nullable_return_type' => false, + 'is_abstract' => true, + 'is_final' => false, + 'is_static' => false, + 'has_body' => false, + ]; + + $this->getMethodPropertiesTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Test a basic interface method. + * + * @return void + */ + public function testInterfaceMethod() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => '', + 'return_type_token' => false, + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => false, + ]; + + $this->getMethodPropertiesTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Test for incorrect tokenization of array return type declarations in PHPCS < 2.8.0. + * + * @link https://github.com/squizlabs/PHP_CodeSniffer/pull/1264 + * + * @return void + */ + public function testPhpcsIssue1264() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'array', + 'return_type_token' => 8, // Offset from the T_FUNCTION token. + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Test helper. + * + * @param string $commentString The comment which preceeds the test. + * @param array $expected The expected function output. + * + * @return void + */ + protected function getMethodPropertiesTestHelper($commentString, $expected) + { + $function = $this->getTargetToken($commentString, [T_FUNCTION, T_CLOSURE]); + $found = BCFile::getMethodProperties(self::$phpcsFile, $function); + + if ($expected['return_type_token'] !== false) { + $expected['return_type_token'] += $function; + } + + $this->assertSame($expected, $found); + } +} diff --git a/Tests/BackCompat/BCFile/GetTokensAsStringTest.inc b/Tests/BackCompat/BCFile/GetTokensAsStringTest.inc new file mode 100644 index 00000000..f0293e1b --- /dev/null +++ b/Tests/BackCompat/BCFile/GetTokensAsStringTest.inc @@ -0,0 +1,17 @@ +expectPhpcsException('The $start position for getTokensAsString() must exist in the token stack'); + + BCFile::getTokensAsString(self::$phpcsFile, 100000, 10); + } + + /** + * Test passing a non integer `$start`, like the result of a failed $phpcsFile->findNext(). + * + * @return void + */ + public function testNonIntegerStart() + { + $this->expectPhpcsException('The $start position for getTokensAsString() must exist in the token stack'); + + BCFile::getTokensAsString(self::$phpcsFile, false, 10); + } + + /** + * Test passing a non integer `$length`. + * + * @return void + */ + public function testNonIntegerLength() + { + $result = BCFile::getTokensAsString(self::$phpcsFile, 10, false); + $this->assertSame('', $result); + + $result = BCFile::getTokensAsString(self::$phpcsFile, 10, 1.5); + $this->assertSame('', $result); + } + + /** + * Test passing a zero or negative `$length`. + * + * @return void + */ + public function testLengthEqualToOrLessThanZero() + { + $result = BCFile::getTokensAsString(self::$phpcsFile, 10, -10); + $this->assertSame('', $result); + + $result = BCFile::getTokensAsString(self::$phpcsFile, 10, 0); + $this->assertSame('', $result); + } + + /** + * Test passing a `$length` beyond the end of the file. + * + * @return void + */ + public function testLengthBeyondEndOfFile() + { + $semicolon = $this->getTargetToken('/* testEndOfFile */', \T_SEMICOLON); + $result = BCFile::getTokensAsString(self::$phpcsFile, $semicolon, 20); + $this->assertSame('; +', $result); + } + + /** + * Test getting a token set as a string. + * + * @dataProvider dataGetTokensAsString() + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string|array $startTokenType The type of token(s) to look for for the start of the string. + * @param int $length Token length to get. + * @param string $expected The expected function return value. + * + * @return void + */ + public function testGetTokensAsString($testMarker, $startTokenType, $length, $expected) + { + $start = $this->getTargetToken($testMarker, $startTokenType); + $result = BCFile::getTokensAsString(self::$phpcsFile, $start, $length); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testGetTokensAsString() For the array format. + * + * @return array + */ + public function dataGetTokensAsString() + { + return [ + 'length-0' => [ + '/* testNamespace */', + \T_NAMESPACE, + 0, + '', + ], + 'length-1' => [ + '/* testNamespace */', + \T_NAMESPACE, + 1, + 'namespace', + ], + 'length-2' => [ + '/* testNamespace */', + \T_NAMESPACE, + 2, + 'namespace ', + ], + 'length-3' => [ + '/* testNamespace */', + \T_NAMESPACE, + 3, + 'namespace Foo', + ], + 'length-4' => [ + '/* testNamespace */', + \T_NAMESPACE, + 4, + 'namespace Foo\\', + ], + 'length-5' => [ + '/* testNamespace */', + \T_NAMESPACE, + 5, + 'namespace Foo\Bar', + ], + 'length-6' => [ + '/* testNamespace */', + \T_NAMESPACE, + 6, + 'namespace Foo\Bar\\', + ], + 'length-7' => [ + '/* testNamespace */', + \T_NAMESPACE, + 7, + 'namespace Foo\Bar\Baz', + ], + 'length-8' => [ + '/* testNamespace */', + \T_NAMESPACE, + 8, + 'namespace Foo\Bar\Baz;', + ], + 'length-9' => [ + '/* testNamespace */', + \T_NAMESPACE, + 9, + 'namespace Foo\Bar\Baz; +', + ], + 'use-with-comments' => [ + '/* testUseWithComments */', + \T_USE, + 17, + 'use Foo /*comment*/ \ Bar + // phpcs:ignore Stnd.Cat.Sniff -- For reasons. + \ Bah;', + ], + 'echo-with-tabs' => [ + '/* testEchoWithTabs */', + \T_ECHO, + 13, + 'echo \'foo\', + \'bar\' , + \'baz\';', + ], + 'end-of-file' => [ + '/* testEndOfFile */', + \T_ECHO, + 4, + 'echo $foo;', + ], + ]; + } + + /** + * Test getting a token set as a string with the original, non tab-replaced content. + * + * @dataProvider dataGetOrigContent() + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string|array $startTokenType The type of token(s) to look for for the start of the string. + * @param int $length Token length to get. + * @param string $expected The expected function return value. + * + * @return void + */ + public function testGetOrigContent($testMarker, $startTokenType, $length, $expected) + { + $start = $this->getTargetToken($testMarker, $startTokenType); + $result = BCFile::getTokensAsString(self::$phpcsFile, $start, $length, true); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testGetOrigContent() For the array format. + * + * @return array + */ + public function dataGetOrigContent() + { + return [ + 'use-with-comments' => [ + '/* testUseWithComments */', + \T_USE, + 17, + 'use Foo /*comment*/ \ Bar + // phpcs:ignore Stnd.Cat.Sniff -- For reasons. + \ Bah;', + ], + 'echo-with-tabs' => [ + '/* testEchoWithTabs */', + \T_ECHO, + 13, + 'echo \'foo\', + \'bar\' , + \'baz\';', + ], + 'end-of-file' => [ + '/* testEndOfFile */', + \T_ECHO, + 4, + 'echo $foo;', + ], + ]; + } +} diff --git a/Tests/BackCompat/BCFile/IsReferenceTest.inc b/Tests/BackCompat/BCFile/IsReferenceTest.inc new file mode 100644 index 00000000..1c95480e --- /dev/null +++ b/Tests/BackCompat/BCFile/IsReferenceTest.inc @@ -0,0 +1,164 @@ + $first, 'b' => $something & $somethingElse ]; + +/* testBitwiseAndF */ +$a = array( 'a' => $first, 'b' => $something & \MyClass::$somethingElse ); + +/* testBitwiseAndG */ +$a = $something & $somethingElse; + +/* testBitwiseAndH */ +function myFunction($a = 10 & 20) {} + +/* testBitwiseAndI */ +$closure = function ($a = MY_CONSTANT & parent::OTHER_CONSTANT) {}; + +/* testFunctionReturnByReference */ +function &myFunction() {} + +/* testFunctionPassByReferenceA */ +function myFunction( &$a ) {} + +/* testFunctionPassByReferenceB */ +function myFunction( $a, &$b ) {} + +/* testFunctionPassByReferenceC */ +$closure = function ( &$a ) {}; + +/* testFunctionPassByReferenceD */ +$closure = function ( $a, &$b ) {}; + +/* testFunctionPassByReferenceE */ +function myFunction(array &$one) {} + +/* testFunctionPassByReferenceF */ +$closure = function (\MyClass &$one) {}; + +/* testFunctionPassByReferenceG */ +$closure = function ($param, &...$moreParams) {}; + +/* testForeachValueByReference */ +foreach( $array as $key => &$value ) {} + +/* testForeachKeyByReference */ +foreach( $array as &$key => $value ) {} + +/* testArrayValueByReferenceA */ +$a = [ 'a' => &$something ]; + +/* testArrayValueByReferenceB */ +$a = [ 'a' => $something, 'b' => &$somethingElse ]; + +/* testArrayValueByReferenceC */ +$a = [ &$something ]; + +/* testArrayValueByReferenceD */ +$a = [ $something, &$somethingElse ]; + +/* testArrayValueByReferenceE */ +$a = array( 'a' => &$something ); + +/* testArrayValueByReferenceF */ +$a = array( 'a' => $something, 'b' => &$somethingElse ); + +/* testArrayValueByReferenceG */ +$a = array( &$something ); + +/* testArrayValueByReferenceH */ +$a = array( $something, &$somethingElse ); + +/* testAssignByReferenceA */ +$b = &$something; + +/* testAssignByReferenceB */ +$b =& $something; + +/* testAssignByReferenceC */ +$b .= &$something; + +/* testAssignByReferenceD */ +$myValue = &$obj->getValue(); + +/* testAssignByReferenceE */ +$collection = &collector(); + +/* testAssignByReferenceF */ +$collection ??= &collector(); + +/* testShortListAssignByReferenceNoKeyA */ +[ + &$a, + /* testShortListAssignByReferenceNoKeyB */ + &$b, + /* testNestedShortListAssignByReferenceNoKey */ + [$c, &$d] +] = $array; + +/* testLongListAssignByReferenceNoKeyA */ +list($a, &$b, list(/* testLongListAssignByReferenceNoKeyB */ &$c, /* testLongListAssignByReferenceNoKeyC */ &$d)) = $array; + +[ + /* testNestedShortListAssignByReferenceWithKeyA */ + 'a' => [&$a, $b], + /* testNestedShortListAssignByReferenceWithKeyB */ + 'b' => [$c, &$d] +] = $array; + + +/* testLongListAssignByReferenceWithKeyA */ +list(get_key()[1] => &$e) = [1, 2, 3]; + +/* testPassByReferenceA */ +functionCall(&$something, $somethingElse); + +/* testPassByReferenceB */ +functionCall($something, &$somethingElse); + +/* testPassByReferenceC */ +functionCall($something, &$this->somethingElse); + +/* testPassByReferenceD */ +functionCall($something, &self::$somethingElse); + +/* testPassByReferenceE */ +functionCall($something, &parent::$somethingElse); + +/* testPassByReferenceF */ +functionCall($something, &static::$somethingElse); + +/* testPassByReferenceG */ +functionCall($something, &SomeClass::$somethingElse); + +/* testPassByReferenceH */ +functionCall(&\SomeClass::$somethingElse); + +/* testPassByReferenceI */ +functionCall($something, &\SomeNS\SomeClass::$somethingElse); + +/* testPassByReferenceJ */ +functionCall($something, &namespace\SomeClass::$somethingElse); + +/* testNewByReferenceA */ +$foobar2 = &new Foobar(); + +/* testNewByReferenceB */ +functionCall( $something , &new Foobar() ); + +/* testUseByReference */ +$closure = function() use (&$var){}; diff --git a/Tests/BackCompat/BCFile/IsReferenceTest.php b/Tests/BackCompat/BCFile/IsReferenceTest.php new file mode 100644 index 00000000..67a28d57 --- /dev/null +++ b/Tests/BackCompat/BCFile/IsReferenceTest.php @@ -0,0 +1,313 @@ + + * @author Greg Sherwood + * + * With documentation contributions from: + * @author Phil Davis + * + * @copyright 2017-2019 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHPCSUtils\Tests\BackCompat\BCFile; + +use PHPCSUtils\TestUtils\UtilityMethodTestCase; + +/** + * Tests for the \PHPCSUtils\BackCompat\BCFile::isReference method. + * + * @covers \PHPCSUtils\BackCompat\BCFile::isReference + * + * @group operators + * + * @since 1.0.0 + */ +class IsReferenceTest extends UtilityMethodTestCase +{ + + /** + * The fully qualified name of the class being tested. + * + * This allows for the same unit tests to be run for both the BCFile functions + * as well as for the related PHPCSUtils functions. + * + * @var string + */ + const TEST_CLASS = '\PHPCSUtils\BackCompat\BCFile'; + + /** + * Test that false is returned when a non-"bitwise and" token is passed. + * + * @return void + */ + public function testNotBitwiseAndToken() + { + $testClass = static::TEST_CLASS; + + $target = $this->getTargetToken('/* testBitwiseAndA */', T_STRING); + $this->assertFalse($testClass::isReference(self::$phpcsFile, $target)); + } + + /** + * Test correctly identifying that whether a "bitwise and" token is a reference or not. + * + * @dataProvider dataIsReference + * + * @param string $identifier Comment which precedes the test case. + * @param bool $expected Expected function output. + * + * @return void + */ + public function testIsReference($identifier, $expected) + { + $testClass = static::TEST_CLASS; + + $bitwiseAnd = $this->getTargetToken($identifier, T_BITWISE_AND); + $result = $testClass::isReference(self::$phpcsFile, $bitwiseAnd); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testIsReference() + * + * @return array + */ + public function dataIsReference() + { + return [ + [ + '/* testBitwiseAndA */', + false, + ], + [ + '/* testBitwiseAndB */', + false, + ], + [ + '/* testBitwiseAndC */', + false, + ], + [ + '/* testBitwiseAndD */', + false, + ], + [ + '/* testBitwiseAndE */', + false, + ], + [ + '/* testBitwiseAndF */', + false, + ], + [ + '/* testBitwiseAndG */', + false, + ], + [ + '/* testBitwiseAndH */', + false, + ], + [ + '/* testBitwiseAndI */', + false, + ], + [ + '/* testFunctionReturnByReference */', + true, + ], + [ + '/* testFunctionPassByReferenceA */', + true, + ], + [ + '/* testFunctionPassByReferenceB */', + true, + ], + [ + '/* testFunctionPassByReferenceC */', + true, + ], + [ + '/* testFunctionPassByReferenceD */', + true, + ], + [ + '/* testFunctionPassByReferenceE */', + true, + ], + [ + '/* testFunctionPassByReferenceF */', + true, + ], + [ + '/* testFunctionPassByReferenceG */', + true, + ], + [ + '/* testForeachValueByReference */', + true, + ], + [ + '/* testForeachKeyByReference */', + true, + ], + [ + '/* testArrayValueByReferenceA */', + true, + ], + [ + '/* testArrayValueByReferenceB */', + true, + ], + [ + '/* testArrayValueByReferenceC */', + true, + ], + [ + '/* testArrayValueByReferenceD */', + true, + ], + [ + '/* testArrayValueByReferenceE */', + true, + ], + [ + '/* testArrayValueByReferenceF */', + true, + ], + [ + '/* testArrayValueByReferenceG */', + true, + ], + [ + '/* testArrayValueByReferenceH */', + true, + ], + [ + '/* testAssignByReferenceA */', + true, + ], + [ + '/* testAssignByReferenceB */', + true, + ], + [ + '/* testAssignByReferenceC */', + true, + ], + [ + '/* testAssignByReferenceD */', + true, + ], + [ + '/* testAssignByReferenceE */', + true, + ], + [ + '/* testAssignByReferenceF */', + true, + ], + [ + '/* testShortListAssignByReferenceNoKeyA */', + true, + ], + [ + '/* testShortListAssignByReferenceNoKeyB */', + true, + ], + [ + '/* testNestedShortListAssignByReferenceNoKey */', + true, + ], + [ + '/* testLongListAssignByReferenceNoKeyA */', + true, + ], + [ + '/* testLongListAssignByReferenceNoKeyB */', + true, + ], + [ + '/* testLongListAssignByReferenceNoKeyC */', + true, + ], + [ + '/* testNestedShortListAssignByReferenceWithKeyA */', + true, + ], + [ + '/* testNestedShortListAssignByReferenceWithKeyB */', + true, + ], + [ + '/* testLongListAssignByReferenceWithKeyA */', + true, + ], + [ + '/* testPassByReferenceA */', + true, + ], + [ + '/* testPassByReferenceB */', + true, + ], + [ + '/* testPassByReferenceC */', + true, + ], + [ + '/* testPassByReferenceD */', + true, + ], + [ + '/* testPassByReferenceE */', + true, + ], + [ + '/* testPassByReferenceF */', + true, + ], + [ + '/* testPassByReferenceG */', + true, + ], + [ + '/* testPassByReferenceH */', + true, + ], + [ + '/* testPassByReferenceI */', + true, + ], + [ + '/* testPassByReferenceJ */', + true, + ], + [ + '/* testNewByReferenceA */', + true, + ], + [ + '/* testNewByReferenceB */', + true, + ], + [ + '/* testUseByReference */', + true, + ], + ]; + } +} diff --git a/Tests/BackCompat/BCTokens/ArithmeticTokensTest.php b/Tests/BackCompat/BCTokens/ArithmeticTokensTest.php new file mode 100644 index 00000000..94360f03 --- /dev/null +++ b/Tests/BackCompat/BCTokens/ArithmeticTokensTest.php @@ -0,0 +1,46 @@ + \T_PLUS, + \T_MINUS => \T_MINUS, + \T_MULTIPLY => \T_MULTIPLY, + \T_DIVIDE => \T_DIVIDE, + \T_MODULUS => \T_MODULUS, + \T_POW => \T_POW, + ]; + + $this->assertSame($expected, BCTokens::arithmeticTokens()); + } +} diff --git a/Tests/BackCompat/BCTokens/AssignmentTokensTest.php b/Tests/BackCompat/BCTokens/AssignmentTokensTest.php new file mode 100644 index 00000000..88f2a38f --- /dev/null +++ b/Tests/BackCompat/BCTokens/AssignmentTokensTest.php @@ -0,0 +1,66 @@ + \T_EQUAL, + \T_AND_EQUAL => \T_AND_EQUAL, + \T_OR_EQUAL => \T_OR_EQUAL, + \T_CONCAT_EQUAL => \T_CONCAT_EQUAL, + \T_DIV_EQUAL => \T_DIV_EQUAL, + \T_MINUS_EQUAL => \T_MINUS_EQUAL, + \T_POW_EQUAL => \T_POW_EQUAL, + \T_MOD_EQUAL => \T_MOD_EQUAL, + \T_MUL_EQUAL => \T_MUL_EQUAL, + \T_PLUS_EQUAL => \T_PLUS_EQUAL, + \T_XOR_EQUAL => \T_XOR_EQUAL, + \T_DOUBLE_ARROW => \T_DOUBLE_ARROW, + \T_SL_EQUAL => \T_SL_EQUAL, + \T_SR_EQUAL => \T_SR_EQUAL, + ]; + + if (\version_compare($version, '2.8.1', '>=') === true + || \version_compare(\PHP_VERSION_ID, '70399', '>=') === true + ) { + $expected[\T_COALESCE_EQUAL] = \T_COALESCE_EQUAL; + } + + if (\version_compare($version, '2.8.0', '>=') === true) { + $expected[\T_ZSR_EQUAL] = \T_ZSR_EQUAL; + } + + $this->assertSame($expected, BCTokens::assignmentTokens()); + } +} diff --git a/Tests/BackCompat/BCTokens/ComparisonTokensTest.php b/Tests/BackCompat/BCTokens/ComparisonTokensTest.php new file mode 100644 index 00000000..f8a6edf5 --- /dev/null +++ b/Tests/BackCompat/BCTokens/ComparisonTokensTest.php @@ -0,0 +1,57 @@ + \T_IS_EQUAL, + \T_IS_IDENTICAL => \T_IS_IDENTICAL, + \T_IS_NOT_EQUAL => \T_IS_NOT_EQUAL, + \T_IS_NOT_IDENTICAL => \T_IS_NOT_IDENTICAL, + \T_LESS_THAN => \T_LESS_THAN, + \T_GREATER_THAN => \T_GREATER_THAN, + \T_IS_SMALLER_OR_EQUAL => \T_IS_SMALLER_OR_EQUAL, + \T_IS_GREATER_OR_EQUAL => \T_IS_GREATER_OR_EQUAL, + \T_SPACESHIP => \T_SPACESHIP, + ]; + + if (\version_compare($version, '2.6.1', '>=') === true + || \version_compare(\PHP_VERSION_ID, '60999', '>=') === true + ) { + $expected[\T_COALESCE] = \T_COALESCE; + } + + $this->assertSame($expected, BCTokens::comparisonTokens()); + } +} diff --git a/Tests/BackCompat/BCTokens/EmptyTokensTest.php b/Tests/BackCompat/BCTokens/EmptyTokensTest.php new file mode 100644 index 00000000..2f915613 --- /dev/null +++ b/Tests/BackCompat/BCTokens/EmptyTokensTest.php @@ -0,0 +1,112 @@ + => + */ + protected $commentTokens = [ + \T_COMMENT => \T_COMMENT, + \T_DOC_COMMENT => \T_DOC_COMMENT, + \T_DOC_COMMENT_STAR => \T_DOC_COMMENT_STAR, + \T_DOC_COMMENT_WHITESPACE => \T_DOC_COMMENT_WHITESPACE, + \T_DOC_COMMENT_TAG => \T_DOC_COMMENT_TAG, + \T_DOC_COMMENT_OPEN_TAG => \T_DOC_COMMENT_OPEN_TAG, + \T_DOC_COMMENT_CLOSE_TAG => \T_DOC_COMMENT_CLOSE_TAG, + \T_DOC_COMMENT_STRING => \T_DOC_COMMENT_STRING, + ]; + + /** + * Token types that are comments containing PHPCS instructions. + * + * @var array => + */ + protected $phpcsCommentTokens = [ + 'PHPCS_T_PHPCS_ENABLE' => 'PHPCS_T_PHPCS_ENABLE', + 'PHPCS_T_PHPCS_DISABLE' => 'PHPCS_T_PHPCS_DISABLE', + 'PHPCS_T_PHPCS_SET' => 'PHPCS_T_PHPCS_SET', + 'PHPCS_T_PHPCS_IGNORE' => 'PHPCS_T_PHPCS_IGNORE', + 'PHPCS_T_PHPCS_IGNORE_FILE' => 'PHPCS_T_PHPCS_IGNORE_FILE', + ]; + + /** + * Test the Tokens::emptyTokens() method. + * + * @covers \PHPCSUtils\BackCompat\BCTokens::__callStatic + * + * @return void + */ + public function testEmptyTokens() + { + $version = Helper::getVersion(); + $expected = [\T_WHITESPACE => \T_WHITESPACE] + $this->commentTokens; + + if (\version_compare($version, '3.2.0', '>=') === true) { + $expected += $this->phpcsCommentTokens; + } + + $this->assertSame($expected, BCTokens::emptyTokens()); + } + + /** + * Test the Tokens::commentTokens() method. + * + * @covers \PHPCSUtils\BackCompat\BCTokens::__callStatic + * + * @return void + */ + public function testCommentTokens() + { + $version = Helper::getVersion(); + $expected = $this->commentTokens; + + if (\version_compare($version, '3.2.0', '>=') === true) { + $expected += $this->phpcsCommentTokens; + } + + $this->assertSame($expected, BCTokens::commentTokens()); + } + + /** + * Test the Tokens::phpcsCommentTokens() method. + * + * @covers \PHPCSUtils\BackCompat\BCTokens::phpcsCommentTokens + * + * @return void + */ + public function testPhpcsCommentTokens() + { + $version = Helper::getVersion(); + $expected = []; + + if (\version_compare($version, '3.2.0', '>=') === true) { + $expected = $this->phpcsCommentTokens; + } + + $this->assertSame($expected, BCTokens::phpcsCommentTokens()); + } +} diff --git a/Tests/BackCompat/BCTokens/FunctionNameTokensTest.php b/Tests/BackCompat/BCTokens/FunctionNameTokensTest.php new file mode 100644 index 00000000..16bc277a --- /dev/null +++ b/Tests/BackCompat/BCTokens/FunctionNameTokensTest.php @@ -0,0 +1,52 @@ + \T_STRING, + \T_EVAL => \T_EVAL, + \T_EXIT => \T_EXIT, + \T_INCLUDE => \T_INCLUDE, + \T_INCLUDE_ONCE => \T_INCLUDE_ONCE, + \T_REQUIRE => \T_REQUIRE, + \T_REQUIRE_ONCE => \T_REQUIRE_ONCE, + \T_ISSET => \T_ISSET, + \T_UNSET => \T_UNSET, + \T_EMPTY => \T_EMPTY, + \T_SELF => \T_SELF, + \T_STATIC => \T_STATIC, + ]; + + $this->assertSame($expected, BCTokens::functionNameTokens()); + } +} diff --git a/Tests/BackCompat/BCTokens/OoScopeTokensTest.php b/Tests/BackCompat/BCTokens/OoScopeTokensTest.php new file mode 100644 index 00000000..9db86945 --- /dev/null +++ b/Tests/BackCompat/BCTokens/OoScopeTokensTest.php @@ -0,0 +1,44 @@ + \T_CLASS, + \T_ANON_CLASS => \T_ANON_CLASS, + \T_INTERFACE => \T_INTERFACE, + \T_TRAIT => \T_TRAIT, + ]; + + $this->assertSame($expected, BCTokens::ooScopeTokens()); + } +} diff --git a/Tests/BackCompat/BCTokens/OperatorsTest.php b/Tests/BackCompat/BCTokens/OperatorsTest.php new file mode 100644 index 00000000..b4a2c00d --- /dev/null +++ b/Tests/BackCompat/BCTokens/OperatorsTest.php @@ -0,0 +1,65 @@ + \T_MINUS, + \T_PLUS => \T_PLUS, + \T_MULTIPLY => \T_MULTIPLY, + \T_DIVIDE => \T_DIVIDE, + \T_MODULUS => \T_MODULUS, + \T_POW => \T_POW, + \T_SPACESHIP => \T_SPACESHIP, + \T_BITWISE_AND => \T_BITWISE_AND, + \T_BITWISE_OR => \T_BITWISE_OR, + \T_BITWISE_XOR => \T_BITWISE_XOR, + \T_SL => \T_SL, + \T_SR => \T_SR, + ]; + + if (\version_compare($version, '2.6.1', '>=') === true + || \version_compare(\PHP_VERSION_ID, '60999', '>=') === true + ) { + $expected[\T_COALESCE] = \T_COALESCE; + } + + \asort($expected); + + $result = BCTokens::operators(); + \asort($result); + + $this->assertSame($expected, $result); + } +} diff --git a/Tests/BackCompat/BCTokens/ParenthesisOpenersTest.php b/Tests/BackCompat/BCTokens/ParenthesisOpenersTest.php new file mode 100644 index 00000000..a2e8cd11 --- /dev/null +++ b/Tests/BackCompat/BCTokens/ParenthesisOpenersTest.php @@ -0,0 +1,58 @@ + \T_ARRAY, + \T_LIST => \T_LIST, + \T_FUNCTION => \T_FUNCTION, + \T_CLOSURE => \T_CLOSURE, + \T_ANON_CLASS => \T_ANON_CLASS, + \T_WHILE => \T_WHILE, + \T_FOR => \T_FOR, + \T_FOREACH => \T_FOREACH, + \T_SWITCH => \T_SWITCH, + \T_IF => \T_IF, + \T_ELSEIF => \T_ELSEIF, + \T_CATCH => \T_CATCH, + \T_DECLARE => \T_DECLARE, + ]; + + \asort($expected); + + $result = BCTokens::parenthesisOpeners(); + \asort($result); + + $this->assertSame($expected, $result); + } +} diff --git a/Tests/BackCompat/BCTokens/TextStringTokensTest.php b/Tests/BackCompat/BCTokens/TextStringTokensTest.php new file mode 100644 index 00000000..24093f28 --- /dev/null +++ b/Tests/BackCompat/BCTokens/TextStringTokensTest.php @@ -0,0 +1,45 @@ + \T_CONSTANT_ENCAPSED_STRING, + \T_DOUBLE_QUOTED_STRING => \T_DOUBLE_QUOTED_STRING, + \T_INLINE_HTML => \T_INLINE_HTML, + \T_HEREDOC => \T_HEREDOC, + \T_NOWDOC => \T_NOWDOC, + ]; + + $this->assertSame($expected, BCTokens::textStringTokens()); + } +} diff --git a/Tests/BackCompat/BCTokens/UnchangedTokenArraysTest.php b/Tests/BackCompat/BCTokens/UnchangedTokenArraysTest.php new file mode 100644 index 00000000..ea2941eb --- /dev/null +++ b/Tests/BackCompat/BCTokens/UnchangedTokenArraysTest.php @@ -0,0 +1,245 @@ + => + */ + protected $equalityTokens = [ + \T_IS_EQUAL => \T_IS_EQUAL, + \T_IS_NOT_EQUAL => \T_IS_NOT_EQUAL, + \T_IS_IDENTICAL => \T_IS_IDENTICAL, + \T_IS_NOT_IDENTICAL => \T_IS_NOT_IDENTICAL, + \T_IS_SMALLER_OR_EQUAL => \T_IS_SMALLER_OR_EQUAL, + \T_IS_GREATER_OR_EQUAL => \T_IS_GREATER_OR_EQUAL, + ]; + + /** + * Tokens that perform boolean operations. + * + * @var array => + */ + protected $booleanOperators = [ + \T_BOOLEAN_AND => \T_BOOLEAN_AND, + \T_BOOLEAN_OR => \T_BOOLEAN_OR, + \T_LOGICAL_AND => \T_LOGICAL_AND, + \T_LOGICAL_OR => \T_LOGICAL_OR, + \T_LOGICAL_XOR => \T_LOGICAL_XOR, + ]; + + /** + * Tokens that represent casting. + * + * @var array => + */ + protected $castTokens = [ + \T_INT_CAST => \T_INT_CAST, + \T_STRING_CAST => \T_STRING_CAST, + \T_DOUBLE_CAST => \T_DOUBLE_CAST, + \T_ARRAY_CAST => \T_ARRAY_CAST, + \T_BOOL_CAST => \T_BOOL_CAST, + \T_OBJECT_CAST => \T_OBJECT_CAST, + \T_UNSET_CAST => \T_UNSET_CAST, + \T_BINARY_CAST => \T_BINARY_CAST, + ]; + + /** + * Tokens that are allowed to open scopes. + * + * @var array => + */ + protected $scopeOpeners = [ + \T_CLASS => \T_CLASS, + \T_ANON_CLASS => \T_ANON_CLASS, + \T_INTERFACE => \T_INTERFACE, + \T_TRAIT => \T_TRAIT, + \T_NAMESPACE => \T_NAMESPACE, + \T_FUNCTION => \T_FUNCTION, + \T_CLOSURE => \T_CLOSURE, + \T_IF => \T_IF, + \T_SWITCH => \T_SWITCH, + \T_CASE => \T_CASE, + \T_DECLARE => \T_DECLARE, + \T_DEFAULT => \T_DEFAULT, + \T_WHILE => \T_WHILE, + \T_ELSE => \T_ELSE, + \T_ELSEIF => \T_ELSEIF, + \T_FOR => \T_FOR, + \T_FOREACH => \T_FOREACH, + \T_DO => \T_DO, + \T_TRY => \T_TRY, + \T_CATCH => \T_CATCH, + \T_FINALLY => \T_FINALLY, + \T_PROPERTY => \T_PROPERTY, + \T_OBJECT => \T_OBJECT, + \T_USE => \T_USE, + ]; + + /** + * Tokens that represent scope modifiers. + * + * @var array => + */ + protected $scopeModifiers = [ + \T_PRIVATE => \T_PRIVATE, + \T_PUBLIC => \T_PUBLIC, + \T_PROTECTED => \T_PROTECTED, + ]; + + /** + * Tokens that can prefix a method name + * + * @var array => + */ + protected $methodPrefixes = [ + \T_PRIVATE => \T_PRIVATE, + \T_PUBLIC => \T_PUBLIC, + \T_PROTECTED => \T_PROTECTED, + \T_ABSTRACT => \T_ABSTRACT, + \T_STATIC => \T_STATIC, + \T_FINAL => \T_FINAL, + ]; + + /** + * Tokens that open code blocks. + * + * @var array => + */ + protected $blockOpeners = [ + \T_OPEN_CURLY_BRACKET => \T_OPEN_CURLY_BRACKET, + \T_OPEN_SQUARE_BRACKET => \T_OPEN_SQUARE_BRACKET, + \T_OPEN_PARENTHESIS => \T_OPEN_PARENTHESIS, + \T_OBJECT => \T_OBJECT, + ]; + + /** + * Tokens that represent strings. + * + * @var array => + */ + protected $stringTokens = [ + \T_CONSTANT_ENCAPSED_STRING => \T_CONSTANT_ENCAPSED_STRING, + \T_DOUBLE_QUOTED_STRING => \T_DOUBLE_QUOTED_STRING, + ]; + + /** + * Tokens that represent brackets and parenthesis. + * + * @var array => + */ + protected $bracketTokens = [ + \T_OPEN_CURLY_BRACKET => \T_OPEN_CURLY_BRACKET, + \T_CLOSE_CURLY_BRACKET => \T_CLOSE_CURLY_BRACKET, + \T_OPEN_SQUARE_BRACKET => \T_OPEN_SQUARE_BRACKET, + \T_CLOSE_SQUARE_BRACKET => \T_CLOSE_SQUARE_BRACKET, + \T_OPEN_PARENTHESIS => \T_OPEN_PARENTHESIS, + \T_CLOSE_PARENTHESIS => \T_CLOSE_PARENTHESIS, + ]; + + /** + * Tokens that include files. + * + * @var array => + */ + protected $includeTokens = [ + \T_REQUIRE_ONCE => \T_REQUIRE_ONCE, + \T_REQUIRE => \T_REQUIRE, + \T_INCLUDE_ONCE => \T_INCLUDE_ONCE, + \T_INCLUDE => \T_INCLUDE, + ]; + + /** + * Tokens that make up a heredoc string. + * + * @var array => + */ + protected $heredocTokens = [ + \T_START_HEREDOC => \T_START_HEREDOC, + \T_END_HEREDOC => \T_END_HEREDOC, + \T_HEREDOC => \T_HEREDOC, + \T_START_NOWDOC => \T_START_NOWDOC, + \T_END_NOWDOC => \T_END_NOWDOC, + \T_NOWDOC => \T_NOWDOC, + ]; + + /** + * Test the method. + * + * @dataProvider dataUnchangedTokenArrays + * + * @param string $name The token array name. + * @param array $expected The token array content. + * + * @return void + */ + public function testUnchangedTokenArrays($name, $expected) + { + $this->assertSame($expected, BCTokens::$name()); + } + + /** + * Data provider. + * + * @see testUnchangedTokenArrays() For the array format. + * + * @return array + */ + public function dataUnchangedTokenArrays() + { + $phpunitProp = [ + 'backupGlobals' => true, + 'backupGlobalsBlacklist' => true, + 'backupStaticAttributes' => true, + 'backupStaticAttributesBlacklist' => true, + 'runTestInSeparateProcess' => true, + 'preserveGlobalState' => true, + ]; + + $data = []; + $tokenArrays = \get_object_vars($this); + foreach ($tokenArrays as $name => $expected) { + if (isset($phpunitProp[$name])) { + continue; + } + + $data[$name] = [$name, $expected]; + } + + return $data; + } + + /** + * Test calling a token property method for a token array which doesn't exist. + * + * @return void + */ + public function testUndeclaredTokenArray() + { + $this->assertSame([], BCTokens::notATokenArray()); + } +} diff --git a/Tests/BackCompat/Helper/ConfigDataTest.php b/Tests/BackCompat/Helper/ConfigDataTest.php new file mode 100644 index 00000000..09474246 --- /dev/null +++ b/Tests/BackCompat/Helper/ConfigDataTest.php @@ -0,0 +1,48 @@ +assertTrue($return); + + $result = Helper::getConfigData('arbitrary_name'); + $this->assertSame($expected, $result); + + // Reset the value after the test. + $return = Helper::setConfigData('arbitrary_name', $original, true); + } +} diff --git a/Tests/BackCompat/Helper/GetCommandLineDataTest.inc b/Tests/BackCompat/Helper/GetCommandLineDataTest.inc new file mode 100644 index 00000000..3e0f3b14 --- /dev/null +++ b/Tests/BackCompat/Helper/GetCommandLineDataTest.inc @@ -0,0 +1,3 @@ +assertSame($expected, $result); + } + + /** + * Test the getCommandLineData() method when requesting an unknown setting. + * + * @covers \PHPCSUtils\BackCompat\Helper::getCommandLineData + * + * @return void + */ + public function testGetCommandLineDataNull() + { + $result = Helper::getCommandLineData(self::$phpcsFile, 'foobar'); + $this->assertNull($result); + } + + /** + * Test the getTabWidth() method. + * + * @covers \PHPCSUtils\BackCompat\Helper::getTabWidth + * + * @return void + */ + public function testGetTabWidth() + { + $result = Helper::getTabWidth(self::$phpcsFile); + $this->assertSame(4, $result, 'Failed retrieving the default tab width'); + + if (\version_compare(Helper::getVersion(), '2.99.99', '>') === true) { + // PHPCS 3.x. + self::$phpcsFile->config->tabWidth = 2; + } else { + // PHPCS 2.x. + self::$phpcsFile->phpcs->cli->setCommandLineValues(['--tab-width=2']); + } + + $result = Helper::getTabWidth(self::$phpcsFile); + $this->assertSame(2, $result, 'Failed retrieving the custom set tab width'); + + // Restore defaults before moving to the next test. + if (\version_compare(Helper::getVersion(), '2.99.99', '>') === true) { + self::$phpcsFile->config->restoreDefaults(); + } else { + self::$phpcsFile->phpcs->cli->setCommandLineValues(['--tab-width=4']); + } + } + + /** + * Test the ignoreAnnotations() method. + * + * @covers \PHPCSUtils\BackCompat\Helper::ignoreAnnotations + * + * @return void + */ + public function testIgnoreAnnotationsV2() + { + if (\version_compare(Helper::getVersion(), '2.99.99', '>') === true) { + $this->markTestSkipped('Test only applicable to PHPCS 2.x'); + } + + $this->assertFalse(Helper::ignoreAnnotations()); + } + + /** + * Test the ignoreAnnotations() method. + * + * @covers \PHPCSUtils\BackCompat\Helper::ignoreAnnotations + * + * @return void + */ + public function testIgnoreAnnotationsV3Default() + { + if (\version_compare(Helper::getVersion(), '2.99.99', '<=') === true) { + $this->markTestSkipped('Test only applicable to PHPCS 3.x'); + } + + $result = Helper::ignoreAnnotations(); + $this->assertFalse($result, 'Failed default ignoreAnnotations test without passing $phpcsFile'); + + $result = Helper::ignoreAnnotations(self::$phpcsFile); + $this->assertFalse($result, 'Failed default ignoreAnnotations test while passing $phpcsFile'); + + // Restore defaults before moving to the next test. + self::$phpcsFile->config->restoreDefaults(); + } + + /** + * Test the ignoreAnnotations() method. + * + * @covers \PHPCSUtils\BackCompat\Helper::ignoreAnnotations + * + * @return void + */ + public function testIgnoreAnnotationsV3SetViaMethod() + { + if (\version_compare(Helper::getVersion(), '2.99.99', '<=') === true) { + $this->markTestSkipped('Test only applicable to PHPCS 3.x'); + } + + Helper::setConfigData('annotations', false, true); + + $result = Helper::ignoreAnnotations(); + $this->assertTrue($result); + + // Restore defaults before moving to the next test. + Helper::setConfigData('annotations', true, true); + } + + /** + * Test the ignoreAnnotations() method. + * + * @covers \PHPCSUtils\BackCompat\Helper::ignoreAnnotations + * + * @return void + */ + public function testIgnoreAnnotationsV3SetViaProperty() + { + if (\version_compare(Helper::getVersion(), '2.99.99', '<=') === true) { + $this->markTestSkipped('Test only applicable to PHPCS 3.x'); + } + + self::$phpcsFile->config->annotations = false; + + $result = Helper::ignoreAnnotations(self::$phpcsFile); + $this->assertTrue($result); + + // Restore defaults before moving to the next test. + self::$phpcsFile->config->restoreDefaults(); + } +} diff --git a/Tests/BackCompat/Helper/GetVersionTest.php b/Tests/BackCompat/Helper/GetVersionTest.php new file mode 100644 index 00000000..8f5c8c51 --- /dev/null +++ b/Tests/BackCompat/Helper/GetVersionTest.php @@ -0,0 +1,61 @@ +markTestSkipped('The test for the Helper::getVersion() method will only run' + . ' if the PHPCS_VERSION environment variable is set, such as during a Travis CI build' + . ' or when this variable has been set in the PHPUnit configuration file.'); + + return; + } + + $result = Helper::getVersion(); + + if ($expected === 'dev-master') { + $this->assertTrue(\version_compare(self::DEVMASTER, $result, '<=')); + } else { + $this->assertSame($expected, $result); + } + } +} diff --git a/Tests/Fixers/SpacesFixer/SpacesFixerExceptionsTest.inc b/Tests/Fixers/SpacesFixer/SpacesFixerExceptionsTest.inc new file mode 100644 index 00000000..a0177f5c --- /dev/null +++ b/Tests/Fixers/SpacesFixer/SpacesFixerExceptionsTest.inc @@ -0,0 +1,7 @@ +expectPhpcsException('The $stackPtr and the $secondPtr token must exist and not be whitespace'); + + SpacesFixer::checkAndFix(self::$phpcsFile, 10000, 10, 0, 'Dummy'); + } + + /** + * Test passing a non-existent token pointer for the second token. + * + * @return void + */ + public function testNonExistentSecondToken() + { + $this->expectPhpcsException('The $stackPtr and the $secondPtr token must exist and not be whitespace'); + + SpacesFixer::checkAndFix(self::$phpcsFile, 10, 10000, 0, 'Dummy'); + } + + /** + * Test passing whitespace for the stackPtr token. + * + * @return void + */ + public function testFirstTokenWhitespace() + { + $this->expectPhpcsException('The $stackPtr and the $secondPtr token must exist and not be whitespace'); + + $stackPtr = $this->getTargetToken('/* testPassingWhitespace1 */', \T_WHITESPACE); + SpacesFixer::checkAndFix(self::$phpcsFile, $stackPtr, 10, 0, 'Dummy'); + } + + /** + * Test passing whitespace for the second token. + * + * @return void + */ + public function testSecondTokenWhitespace() + { + $this->expectPhpcsException('The $stackPtr and the $secondPtr token must exist and not be whitespace'); + + $secondPtr = $this->getTargetToken('/* testPassingWhitespace2 */', \T_WHITESPACE); + SpacesFixer::checkAndFix(self::$phpcsFile, 10, $secondPtr, 0, 'Dummy'); + } + + /** + * Test passing non-adjacent tokens. + * + * @return void + */ + public function testNonAdjacentTokens() + { + $this->expectPhpcsException( + 'The $stackPtr and the $secondPtr token must be adjacent tokens separated only' + . ' by whitespace and/or comments' + ); + + $stackPtr = $this->getTargetToken('/* testPassingTokensWithSomethingBetween */', \T_ECHO); + $secondPtr = $this->getTargetToken('/* testPassingTokensWithSomethingBetween */', \T_STRING_CONCAT); + SpacesFixer::checkAndFix(self::$phpcsFile, $stackPtr, $secondPtr, 0, 'Dummy'); + } + + /** + * Test passing non-adjacent tokens in reverse order. + * + * @return void + */ + public function testNonAdjacentTokensReverseOrder() + { + $this->expectPhpcsException( + 'The $stackPtr and the $secondPtr token must be adjacent tokens separated only' + . ' by whitespace and/or comments' + ); + + $stackPtr = $this->getTargetToken('/* testPassingTokensWithSomethingBetween */', \T_ECHO); + $secondPtr = $this->getTargetToken('/* testPassingTokensWithSomethingBetween */', \T_STRING_CONCAT); + SpacesFixer::checkAndFix(self::$phpcsFile, $secondPtr, $stackPtr, 0, 'Dummy'); + } + + /** + * Test passing an negative integer value for spaces. + * + * @return void + */ + public function testInvalidExpectedSpacesNegativeValue() + { + $this->expectPhpcsException('The $expectedSpaces setting should be either "newline", 0 or a positive integer'); + + $stackPtr = $this->getTargetToken('/* testPassingWhitespace1 */', \T_ECHO); + $secondPtr = $this->getTargetToken('/* testPassingWhitespace1 */', \T_CONSTANT_ENCAPSED_STRING); + SpacesFixer::checkAndFix(self::$phpcsFile, $stackPtr, $secondPtr, -10, 'Dummy'); + } + + /** + * Test passing an value of a type which is not accepted for spaces. + * + * @return void + */ + public function testInvalidExpectedSpacesUnexpectedType() + { + $this->expectPhpcsException('The $expectedSpaces setting should be either "newline", 0 or a positive integer'); + + $stackPtr = $this->getTargetToken('/* testPassingWhitespace1 */', \T_ECHO); + $secondPtr = $this->getTargetToken('/* testPassingWhitespace1 */', \T_CONSTANT_ENCAPSED_STRING); + SpacesFixer::checkAndFix(self::$phpcsFile, $stackPtr, $secondPtr, false, 'Dummy'); + } + + /** + * Test passing a non-decimal string value for spaces. + * + * @return void + */ + public function testInvalidExpectedSpacesNonDecimalString() + { + $this->expectPhpcsException('The $expectedSpaces setting should be either "newline", 0 or a positive integer'); + + $stackPtr = $this->getTargetToken('/* testPassingWhitespace1 */', \T_ECHO); + $secondPtr = $this->getTargetToken('/* testPassingWhitespace1 */', \T_CONSTANT_ENCAPSED_STRING); + SpacesFixer::checkAndFix(self::$phpcsFile, $stackPtr, $secondPtr, ' ', 'Dummy'); + } +} diff --git a/Tests/Fixers/SpacesFixer/SpacesFixerNewlineTest.inc.fixed b/Tests/Fixers/SpacesFixer/SpacesFixerNewlineTest.inc.fixed new file mode 100644 index 00000000..a8fb0c66 --- /dev/null +++ b/Tests/Fixers/SpacesFixer/SpacesFixerNewlineTest.inc.fixed @@ -0,0 +1,38 @@ +getTargetToken($testMarker, \T_ARRAY); + $secondPtr = $this->getTargetToken($testMarker, \T_OPEN_PARENTHESIS); + + /* + * Note: passing $stackPtr and $secondPtr in reverse order to make sure that case is + * covered by a test as well. + */ + SpacesFixer::checkAndFix( + self::$phpcsFile, + $secondPtr, + $stackPtr, + static::SPACES, + static::MSG, + static::CODE, + 'error', + 0, + static::METRIC + ); + + $result = \array_merge(self::$phpcsFile->getErrors(), self::$phpcsFile->getWarnings()); + + // Expect no errors. + $this->assertCount(0, $result, 'Failed to assert that no violations were found'); + + // Check that the metric is recorded correctly. + $metrics = self::$phpcsFile->getMetrics(); + $this->assertGreaterThanOrEqual( + 1, + $metrics[static::METRIC]['values'][$expected['found']], + 'Failed recorded metric check' + ); + } + + /** + * Data Provider. + * + * @see testCheckAndFixNoError() For the array format. + * + * @return array + */ + public function dataCheckAndFixNoError() + { + $data = []; + $baseData = $this->getAllData(); + + foreach ($this->compliantCases as $caseName) { + if (isset($baseData[$caseName])) { + $data[$caseName] = $baseData[$caseName]; + } + } + + return $data; + } + + /** + * Test that violations are correctly reported. + * + * @dataProvider dataCheckAndFix + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected Expected error details. + * @param string $type The message type to test: 'error' or 'warning'. + * + * @return void + */ + public function testCheckAndFix($testMarker, $expected, $type) + { + $stackPtr = $this->getTargetToken($testMarker, \T_ARRAY); + $secondPtr = $this->getTargetToken($testMarker, \T_OPEN_PARENTHESIS); + + SpacesFixer::checkAndFix( + self::$phpcsFile, + $stackPtr, + $secondPtr, + static::SPACES, + static::MSG, + static::CODE, + $type, + 0, + static::METRIC + ); + + if ($type === 'error') { + $result = self::$phpcsFile->getErrors(); + } else { + $result = self::$phpcsFile->getWarnings(); + } + + $tokens = self::$phpcsFile->getTokens(); + + if (isset($result[$tokens[$stackPtr]['line']][$tokens[$stackPtr]['column']]) === false) { + $this->fail('Expected 1 violation. None found.'); + } + + $messages = $result[$tokens[$stackPtr]['line']][$tokens[$stackPtr]['column']]; + + // Expect one violation. + $this->assertCount(1, $messages, 'Expected 1 violation, found: ' . \count($messages)); + + /* + * Test the violation details. + */ + + $expectedMessage = \sprintf(static::MSG, static::MSG_REPLACEMENT_1, $expected['found']); + $this->assertSame($expectedMessage, $messages[0]['message'], 'Message comparison failed'); + + // PHPCS 2.x places `unknownSniff.` before the actual error code for utility tests with a dummy error code. + $errorCodeResult = \str_replace('unknownSniff.', '', $messages[0]['source']); + $this->assertSame(static::CODE, $errorCodeResult, 'Error code comparison failed'); + + $this->assertSame($expected['fixable'], $messages[0]['fixable'], 'Fixability comparison failed'); + + // Check that the metric is recorded correctly. + $metrics = self::$phpcsFile->getMetrics(); + $this->assertGreaterThanOrEqual( + 1, + $metrics[static::METRIC]['values'][$expected['found']], + 'Failed recorded metric check' + ); + } + + /** + * Data Provider. + * + * @see testCheckAndFix() For the array format. + * + * @return array + */ + public function dataCheckAndFix() + { + $data = $this->getAllData(); + + foreach ($this->compliantCases as $caseName) { + unset($data[$caseName]); + } + + return $data; + } + + /** + * Test that the fixes are correctly made. + * + * @return void + */ + public function testFixesMade() + { + self::$phpcsFile->fixer->startFile(self::$phpcsFile); + self::$phpcsFile->fixer->enabled = true; + + $data = $this->getAllData(); + foreach ($data as $dataset) { + $stackPtr = $this->getTargetToken($dataset[0], \T_ARRAY); + $secondPtr = $this->getTargetToken($dataset[0], \T_OPEN_PARENTHESIS); + + SpacesFixer::checkAndFix( + self::$phpcsFile, + $stackPtr, + $secondPtr, + static::SPACES, + static::MSG, + static::CODE, + $dataset[1], + 0 + ); + } + + $fixedFile = __DIR__ . static::$fixedFile; + $expected = \file_get_contents($fixedFile); + $result = self::$phpcsFile->fixer->getContents(); + + $this->assertSame( + $expected, + $result, + \sprintf( + 'Fixed version of %s does not match expected version in %s', + \basename(static::$caseFile), + \basename($fixedFile) + ) + ); + } + + /** + * Helper function holding the base data for the data providers. + * + * @return array + */ + protected function getAllData() + { + return [ + 'no-space' => [ + '/* testNoSpace */', + [ + 'found' => 'no spaces', + 'fixable' => true, + ], + 'error', + ], + 'one-space' => [ + '/* testOneSpace */', + [ + 'found' => '1 space', + 'fixable' => true, + ], + 'error', + ], + 'two-spaces' => [ + '/* testTwoSpaces */', + [ + 'found' => '2 spaces', + 'fixable' => true, + ], + 'error', + ], + 'multiple-spaces' => [ + '/* testMultipleSpaces */', + [ + 'found' => '13 spaces', + 'fixable' => true, + ], + 'warning', + ], + 'newline-and-trailing-spaces' => [ + '/* testNewlineAndTrailingSpaces */', + [ + 'found' => 'a new line', + 'fixable' => true, + ], + 'error', + ], + 'multiple-newlines-and-spaces' => [ + '/* testMultipleNewlinesAndSpaces */', + [ + 'found' => 'multiple new lines', + 'fixable' => true, + ], + 'error', + ], + 'comment-no-space' => [ + '/* testCommentNoSpace */', + [ + 'found' => 'non-whitespace tokens', + 'fixable' => false, + ], + 'warning', + ], + 'comment-and-space' => [ + '/* testCommentAndSpaces */', + [ + 'found' => '1 space', + 'fixable' => false, + ], + 'error', + ], + 'comment-and-new line' => [ + '/* testCommentAndNewline */', + [ + 'found' => 'a new line', + 'fixable' => false, + ], + 'error', + ], + ]; + } +} diff --git a/Tests/Fixers/SpacesFixer/SpacesFixerOneSpaceTest.inc.fixed b/Tests/Fixers/SpacesFixer/SpacesFixerOneSpaceTest.inc.fixed new file mode 100644 index 00000000..d820504e --- /dev/null +++ b/Tests/Fixers/SpacesFixer/SpacesFixerOneSpaceTest.inc.fixed @@ -0,0 +1,29 @@ +getTargetToken($testMarker, \T_COMMENT); + $secondPtr = $this->getTargetToken($testMarker, \T_LNUMBER, '3'); + + SpacesFixer::checkAndFix( + self::$phpcsFile, + $stackPtr, + $secondPtr, + self::SPACES, + self::MSG, + self::CODE, + $type, + 8 + ); + + if ($type === 'error') { + $result = self::$phpcsFile->getErrors(); + } else { + $result = self::$phpcsFile->getWarnings(); + } + + $tokens = self::$phpcsFile->getTokens(); + + if (isset($result[$tokens[$stackPtr]['line']][$tokens[$stackPtr]['column']]) === false) { + $this->fail('Expected 1 violation. None found.'); + } + + $messages = $result[$tokens[$stackPtr]['line']][$tokens[$stackPtr]['column']]; + + // Expect one violation. + $this->assertCount(1, $messages, 'Expected 1 violation, found: ' . \count($messages)); + + /* + * Test the violation details. + */ + + $expectedMessage = \sprintf(self::MSG, self::MSG_REPLACEMENT_1, $expected['found']); + $this->assertSame($expectedMessage, $messages[0]['message'], 'Message comparison failed'); + + // PHPCS 2.x places `unknownSniff.` before the actual error code for utility tests with a dummy error code. + $errorCodeResult = \str_replace('unknownSniff.', '', $messages[0]['source']); + $this->assertSame(self::CODE, $errorCodeResult, 'Error code comparison failed'); + + $this->assertSame($expected['fixable'], $messages[0]['fixable'], 'Fixability comparison failed'); + + // Additional test checking changed severity. + $this->assertSame(8, $messages[0]['severity'], 'Severity comparison failed'); + + // Check that no metric is recorded. + $metrics = self::$phpcsFile->getMetrics(); + $this->assertFalse( + isset($metrics[static::METRIC]['values'][$expected['found']]), + 'Failed recorded metric check' + ); + } + + /** + * Test that the fixes are correctly made. + * + * @return void + */ + public function testFixesMade() + { + self::$phpcsFile->fixer->startFile(self::$phpcsFile); + self::$phpcsFile->fixer->enabled = true; + + $data = $this->dataCheckAndFix(); + foreach ($data as $dataset) { + $stackPtr = $this->getTargetToken($dataset[0], \T_COMMENT); + $secondPtr = $this->getTargetToken($dataset[0], \T_LNUMBER, '3'); + + SpacesFixer::checkAndFix( + self::$phpcsFile, + $stackPtr, + $secondPtr, + self::SPACES, + self::MSG, + self::CODE, + $dataset[1] + ); + } + + $fixedFile = __DIR__ . '/TrailingCommentHandlingTest.inc.fixed'; + $expected = \file_get_contents($fixedFile); + $result = self::$phpcsFile->fixer->getContents(); + + $this->assertSame( + $expected, + $result, + \sprintf( + 'Fixed version of %s does not match expected version in %s', + \basename(self::$caseFile), + \basename($fixedFile) + ) + ); + } + + /** + * Data Provider. + * + * @see testCheckAndFix() For the array format. + * + * @return array + */ + public function dataCheckAndFix() + { + return [ + 'trailing-comment-not-fixable' => [ + '/* testTrailingOpenCommentAsPtrA */', + [ + 'found' => 'a new line', + 'fixable' => false, + ], + 'error', + ], + 'trailing-comment-fixable' => [ + '/* testTrailingClosedCommentAsPtrA */', + [ + 'found' => 'a new line', + 'fixable' => true, + ], + 'error', + ], + ]; + } +} diff --git a/Tests/TestUtils/UtilityMethodTestCase/FailedToTokenizeTest.inc b/Tests/TestUtils/UtilityMethodTestCase/FailedToTokenizeTest.inc new file mode 100644 index 00000000..0bd34aa1 --- /dev/null +++ b/Tests/TestUtils/UtilityMethodTestCase/FailedToTokenizeTest.inc @@ -0,0 +1,26 @@ + 50 deep. + +if ($a) { if ($a) { if ($a) { if ($a) { if ($a) { // 5 +if ($a) { if ($a) { if ($a) { if ($a) { if ($a) { // 10 +if ($a) { if ($a) { if ($a) { if ($a) { if ($a) { // 15 +if ($a) { if ($a) { if ($a) { if ($a) { if ($a) { // 20 +if ($a) { if ($a) { if ($a) { if ($a) { if ($a) { // 25 +if ($a) { if ($a) { if ($a) { if ($a) { if ($a) { // 30 +if ($a) { if ($a) { if ($a) { if ($a) { if ($a) { // 35 +if ($a) { if ($a) { if ($a) { if ($a) { if ($a) { // 40 +if ($a) { if ($a) { if ($a) { if ($a) { if ($a) { // 45 +if ($a) { if ($a) { if ($a) { if ($a) { if ($a) { // 50 +if ($a) { if ($a) { if ($a) { if ($a) { if ($a) { // 55 +}}}}} +}}}}} +}}}}} +}}}}} +}}}}} +}}}}} +}}}}} +}}}}} +}}}}} +}}}}} +}}}}} diff --git a/Tests/TestUtils/UtilityMethodTestCase/FailedToTokenizeTest.php b/Tests/TestUtils/UtilityMethodTestCase/FailedToTokenizeTest.php new file mode 100644 index 00000000..ad8a6de1 --- /dev/null +++ b/Tests/TestUtils/UtilityMethodTestCase/FailedToTokenizeTest.php @@ -0,0 +1,64 @@ +expectException($exception); + $this->expectExceptionMessage($msg); + } else { + // PHPUnit 4. + $this->setExpectedException($exception, $msg); + } + + parent::setUpTestFile(); + } +} diff --git a/Tests/TestUtils/UtilityMethodTestCase/MissingCaseFileTest.php b/Tests/TestUtils/UtilityMethodTestCase/MissingCaseFileTest.php new file mode 100644 index 00000000..ca946fba --- /dev/null +++ b/Tests/TestUtils/UtilityMethodTestCase/MissingCaseFileTest.php @@ -0,0 +1,64 @@ +expectException($exception); + $this->expectExceptionMessage($msg); + } else { + // PHPUnit 4. + $this->setExpectedException($exception, $msg); + } + + parent::setUpTestFile(); + } +} diff --git a/Tests/TestUtils/UtilityMethodTestCase/UtilityMethodTestCaseTest.inc b/Tests/TestUtils/UtilityMethodTestCase/UtilityMethodTestCaseTest.inc new file mode 100644 index 00000000..361ee307 --- /dev/null +++ b/Tests/TestUtils/UtilityMethodTestCase/UtilityMethodTestCaseTest.inc @@ -0,0 +1,13 @@ +method( false ); + +/* testFindingTargetWithContent */ +echo $a->method( 'foo' ), $b->otherMethod( 'bar' ); + +/* testNotFindingTarget */ +echo 'not found'; + +/* testDelimiter */ +echo 'foo'; diff --git a/Tests/TestUtils/UtilityMethodTestCase/UtilityMethodTestCaseTest.php b/Tests/TestUtils/UtilityMethodTestCase/UtilityMethodTestCaseTest.php new file mode 100644 index 00000000..8e83bc78 --- /dev/null +++ b/Tests/TestUtils/UtilityMethodTestCase/UtilityMethodTestCaseTest.php @@ -0,0 +1,210 @@ +assertInstanceOf('PHP_CodeSniffer\Files\File', self::$phpcsFile); + $this->assertSame(57, self::$phpcsFile->numTokens); + + $tokens = self::$phpcsFile->getTokens(); + if (\method_exists($this, 'assertIsArray')) { + // PHPUnit 7+. + $this->assertIsArray($tokens); + } else { + // PHPUnit 4/5/6. + $this->assertInternalType('array', $tokens); + } + } + + /** + * Test the getTargetToken() method. + * + * @dataProvider dataGetTargetToken + * + * @param int|false $expected Expected function output. + * @param string $commentString The delimiter comment to look for. + * @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 void + */ + public function testGetTargetToken($expected, $commentString, $tokenType, $tokenContent = null) + { + if (isset($tokenContent)) { + $result = $this->getTargetToken($commentString, $tokenType, $tokenContent); + } else { + $result = $this->getTargetToken($commentString, $tokenType); + } + + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testGetTargetToken() For the array format. + * + * @return array + */ + public function dataGetTargetToken() + { + return [ + 'single-token-type' => [ + 6, + '/* testFindingTarget */', + \T_VARIABLE, + ], + 'multi-token-type-1' => [ + 6, + '/* testFindingTarget */', + [\T_VARIABLE, \T_FALSE], + ], + 'multi-token-type-2' => [ + 11, + '/* testFindingTarget */', + [\T_FALSE, \T_LNUMBER], + ], + 'content-method' => [ + 23, + '/* testFindingTargetWithContent */', + \T_STRING, + 'method', + ], + 'content-otherMethod' => [ + 33, + '/* testFindingTargetWithContent */', + \T_STRING, + 'otherMethod', + ], + 'content-$a' => [ + 21, + '/* testFindingTargetWithContent */', + \T_VARIABLE, + '$a', + ], + 'content-$b' => [ + 31, + '/* testFindingTargetWithContent */', + \T_VARIABLE, + '$b', + ], + 'content-foo' => [ + 26, + '/* testFindingTargetWithContent */', + [\T_CONSTANT_ENCAPSED_STRING, \T_DOUBLE_QUOTED_STRING], + "'foo'", + ], + 'content-bar' => [ + 36, + '/* testFindingTargetWithContent */', + [\T_CONSTANT_ENCAPSED_STRING, \T_DOUBLE_QUOTED_STRING], + "'bar'", + ], + ]; + } + + /** + * Test the behaviour of the getTargetToken() method when the target is not found. + * + * @return void + */ + public function testGetTargetTokenNotFound() + { + $msg = 'Failed to find test target token for comment string: '; + $exception = 'PHPUnit\Framework\AssertionFailedError'; + if (\class_exists('PHPUnit_Framework_AssertionFailedError')) { + // PHPUnit < 6. + $exception = 'PHPUnit_Framework_AssertionFailedError'; + } + + if (\method_exists($this, 'expectException')) { + // PHPUnit 5+. + $this->expectException($exception); + $this->expectExceptionMessage($msg); + } else { + // PHPUnit 4. + $this->setExpectedException($exception, $msg); + } + + $this->getTargetToken('/* testNotFindingTarget */', [\T_VARIABLE], '$a'); + } + + /** + * Test that the helper method to handle cross-version testing of exceptions in PHPUnit + * works correctly. + * + * @return void + */ + public function testExpectPhpcsRuntimeException() + { + $this->expectPhpcsException('testing-1-2-3'); + throw new RuntimeException('testing-1-2-3'); + } + + /** + * Test that the helper method to handle cross-version testing of exceptions in PHPUnit + * works correctly. + * + * @return void + */ + public function testExpectPhpcsTokenizerException() + { + $this->expectPhpcsException('testing-1-2-3', 'tokenizer'); + throw new TokenizerException('testing-1-2-3'); + } + + /** + * Test that the class is correct reset. + * + * @return void + */ + public function testTearDown() + { + parent::resetTestFile(); + $this->assertNull(self::$phpcsFile); + } +} diff --git a/Tests/Utils/Arrays/GetDoubleArrowPtrTest.inc b/Tests/Utils/Arrays/GetDoubleArrowPtrTest.inc new file mode 100644 index 00000000..e2dd831f --- /dev/null +++ b/Tests/Utils/Arrays/GetDoubleArrowPtrTest.inc @@ -0,0 +1,74 @@ + 'arrow numeric index', + + /* testArrowStringIndex */ + 'foo' => 'arrow string index', + + /* testArrowMultiTokenIndex */ + 'concat' . 'index' => 'arrow multi token index', + + /* testNoArrowValueShortArray */ + [ + 'value only' => 'arrow belongs to value', + ], + + /* testNoArrowValueLongArray */ + array( + 'value only' => 'arrow belongs to value', + ), + + /* testNoArrowValueNestedArrays */ + array( + [ + array( + ['key' => 'arrow belongs to nested array'], + ), + ], + ), + + /* testNoArrowValueClosure */ + function() { + echo 'closure as value arrow belongs to value'; + return array( $a => $b ); + }, + + /* testArrowValueShortArray */ + 'index and value short array' => [ + 'index and value' => '', + ], + + /* testArrowValueLongArray */ + 'index and value long array' => array( + 'index and value' => '', + ), + + /* testArrowValueClosure */ + 'index and value closure' => function() { + echo 'closure as value arrow belongs to value'; + return array( $a => $b ); + }, + + /* testNoArrowValueAnonClassForeach */ + new class($iterable) { + public function __construct($iterable) { + $return = 0; + foreach ($iterable as $key => $value) { + $return = $key * $value; + } + return $return; + } + }, + + /* testNoArrowValueClosureYieldWithKey */ + function() { yield 'k' => $x }, + + /* testArrowKeyClosureYieldWithKey */ + function() { yield 'k' => $x }() => 'value', +]; diff --git a/Tests/Utils/Arrays/GetDoubleArrowPtrTest.php b/Tests/Utils/Arrays/GetDoubleArrowPtrTest.php new file mode 100644 index 00000000..b0c4de3d --- /dev/null +++ b/Tests/Utils/Arrays/GetDoubleArrowPtrTest.php @@ -0,0 +1,201 @@ + => + */ + private static $parameters = []; + + /** + * Set up the parameters cache for the tests. + * + * Retrieves the parameters array only once and caches it as it won't change + * between the tests anyway. + * + * @before + * + * @return void + */ + protected function setUpCache() + { + if (empty(self::$parameters) === true) { + $target = $this->getTargetToken('/* testGetDoubleArrowPtr */', [\T_OPEN_SHORT_ARRAY]); + $parameters = PassedParameters::getParameters(self::$phpcsFile, $target); + + foreach ($parameters as $index => $values) { + \preg_match('`^(/\* test[^*]+ \*/)`', $values['raw'], $matches); + if (empty($matches[1]) === false) { + self::$parameters[$matches[1]] = $values; + } + } + } + } + + /** + * Test receiving an expected exception when an invalid start position is passed. + * + * @return void + */ + public function testInvalidStartPositionException() + { + $this->expectPhpcsException( + 'Invalid start and/or end position passed to getDoubleArrowPtr(). Received: $start -10, $end 10' + ); + + Arrays::getDoubleArrowPtr(self::$phpcsFile, -10, 10); + } + + /** + * Test receiving an expected exception when an invalid end position is passed. + * + * @return void + */ + public function testInvalidEndPositionException() + { + $this->expectPhpcsException( + 'Invalid start and/or end position passed to getDoubleArrowPtr(). Received: $start 0, $end 100000' + ); + + Arrays::getDoubleArrowPtr(self::$phpcsFile, 0, 100000); + } + + /** + * Test receiving an expected exception when the start position is after the end position. + * + * @return void + */ + public function testInvalidStartEndPositionException() + { + $this->expectPhpcsException( + 'Invalid start and/or end position passed to getDoubleArrowPtr(). Received: $start 10, $end 5' + ); + + Arrays::getDoubleArrowPtr(self::$phpcsFile, 10, 5); + } + + /** + * Test retrieving the position of the double arrow for an array parameter. + * + * @dataProvider dataGetDoubleArrowPtr + * + * @param string $testMarker The comment which is part of the target array item in the test file. + * @param array $expected The expected function call result. + * + * @return void + */ + public function testGetDoubleArrowPtr($testMarker, $expected) + { + if (isset(self::$parameters[$testMarker]) === false) { + $this->fail('Test case not found for ' . $testMarker); + } + + $start = self::$parameters[$testMarker]['start']; + $end = self::$parameters[$testMarker]['end']; + + // Change double arrow position from offset to exact position. + if ($expected !== false) { + $expected += $start; + } + + $result = Arrays::getDoubleArrowPtr(self::$phpcsFile, $start, $end); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * The double arrow positions are provided as offsets from the $start stackPtr. + * + * @see testGetDoubleArrowPtr() + * + * @return array + */ + public function dataGetDoubleArrowPtr() + { + return [ + 'test-no-arrow' => [ + '/* testValueNoArrow */', + false, + ], + 'test-arrow-numeric-index' => [ + '/* testArrowNumericIndex */', + 8, + ], + 'test-arrow-string-index' => [ + '/* testArrowStringIndex */', + 8, + ], + 'test-arrow-multi-token-index' => [ + '/* testArrowMultiTokenIndex */', + 12, + ], + 'test-no-arrow-value-short-array' => [ + '/* testNoArrowValueShortArray */', + false, + ], + 'test-no-arrow-value-long-array' => [ + '/* testNoArrowValueLongArray */', + false, + ], + 'test-no-arrow-value-nested-arrays' => [ + '/* testNoArrowValueNestedArrays */', + false, + ], + 'test-no-arrow-value-closure' => [ + '/* testNoArrowValueClosure */', + false, + ], + 'test-arrow-value-short-array' => [ + '/* testArrowValueShortArray */', + 8, + ], + 'test-arrow-value-long-array' => [ + '/* testArrowValueLongArray */', + 8, + ], + 'test-arrow-value-closure' => [ + '/* testArrowValueClosure */', + 8, + ], + 'test-no-arrow-value-anon-class-with-foreach' => [ + '/* testNoArrowValueAnonClassForeach */', + false, + ], + 'test-no-arrow-value-closure-with-keyed-yield' => [ + '/* testNoArrowValueClosureYieldWithKey */', + false, + ], + 'test-arrow-key-closure-with-keyed-yield' => [ + '/* testArrowKeyClosureYieldWithKey */', + 24, + ], + ]; + } +} diff --git a/Tests/Utils/Arrays/GetOpenCloseTest.inc b/Tests/Utils/Arrays/GetOpenCloseTest.inc new file mode 100644 index 00000000..95666d9f --- /dev/null +++ b/Tests/Utils/Arrays/GetOpenCloseTest.inc @@ -0,0 +1,25 @@ + $bar] = $array; + +/* testArrayAccess */ +echo $array['index']; + +/* testLongArray */ +$array = array($a, /* testNestedLongArray */ array ( $b )); + +/* testShortArray */ +$array = [$a, /* testNestedShortArray */ [$b]]; + +/* testArrayWithCommentsAndAnnotations */ +$array = array // Comment. + ( + 0 => $a, + /* phpcs:ignore Stnd.Cat.Sniff -- For reasons. */ + 1 => $b, + ); + +/* testParseError */ +// Intentional parse error. This has to be the last test in the file. +array( $a diff --git a/Tests/Utils/Arrays/GetOpenCloseTest.php b/Tests/Utils/Arrays/GetOpenCloseTest.php new file mode 100644 index 00000000..6c1981e8 --- /dev/null +++ b/Tests/Utils/Arrays/GetOpenCloseTest.php @@ -0,0 +1,169 @@ +assertFalse(Arrays::getOpenClose(self::$phpcsFile, 100000)); + } + + /** + * Test that false is returned when a non-(short) array token is passed. + * + * @dataProvider dataNotArrayToken + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @return void + */ + public function testNotArrayToken($testMarker) + { + $target = $this->getTargetToken($testMarker, [\T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET]); + $this->assertFalse(Arrays::getOpenClose(self::$phpcsFile, $target)); + } + + /** + * Data provider. + * + * @see testNotArrayToken() For the array format. + * + * @return array + */ + public function dataNotArrayToken() + { + return [ + 'short-list' => ['/* testShortList */'], + 'array-access-square-bracket' => ['/* testArrayAccess */'], + ]; + } + + /** + * Test retrieving the open/close tokens for an array. + * + * @dataProvider dataGetOpenClose + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string|array $targetToken The token type(s) to look for. + * @param array|false $expected The expected function return value. + * + * @return void + */ + public function testGetOpenClose($testMarker, $targetToken, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, $targetToken); + + // Convert offsets to absolute positions. + if (isset($expected['opener'], $expected['closer'])) { + $expected['opener'] += $stackPtr; + $expected['closer'] += $stackPtr; + } + + $result = Arrays::getOpenClose(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * The opener/closer positions are provided as offsets from the target stackPtr. + * + * @see testGetOpenClose() For the array format. + * + * @return array + */ + public function dataGetOpenClose() + { + return [ + 'long-array' => [ + '/* testLongArray */', + \T_ARRAY, + [ + 'opener' => 1, + 'closer' => 14, + ], + ], + 'long-array-nested' => [ + '/* testNestedLongArray */', + \T_ARRAY, + [ + 'opener' => 2, + 'closer' => 6, + ], + ], + 'short-array' => [ + '/* testShortArray */', + \T_OPEN_SHORT_ARRAY, + [ + 'opener' => 0, + 'closer' => 9, + ], + ], + 'short-array-nested' => [ + '/* testNestedShortArray */', + \T_OPEN_SHORT_ARRAY, + [ + 'opener' => 0, + 'closer' => 2, + ], + ], + 'long-array-with-comments-and-annotations' => [ + '/* testArrayWithCommentsAndAnnotations */', + \T_ARRAY, + [ + 'opener' => 4, + 'closer' => 26, + ], + ], + 'parse-error' => [ + '/* testParseError */', + \T_ARRAY, + false, + ], + ]; + } + + /** + * Test retrieving the open/close tokens for a nested array, skipping the short array check. + * + * @return void + */ + public function testGetOpenCloseThirdParam() + { + $stackPtr = $this->getTargetToken('/* testNestedShortArray */', \T_OPEN_SHORT_ARRAY); + $expected = [ + 'opener' => $stackPtr, + 'closer' => ($stackPtr + 2), + ]; + + $result = Arrays::getOpenClose(self::$phpcsFile, $stackPtr, true); + $this->assertSame($expected, $result); + } +} diff --git a/Tests/Utils/Arrays/IsShortArrayTest.inc b/Tests/Utils/Arrays/IsShortArrayTest.inc new file mode 100644 index 00000000..f900264c --- /dev/null +++ b/Tests/Utils/Arrays/IsShortArrayTest.inc @@ -0,0 +1,68 @@ + $x1, "y" => $y1], + /* testNestedShortArrayWithKeys_2 */ + 1 => ["x" => $x2, "y" => $y2], + /* testNestedShortArrayWithKeys_3 */ + 'key' => ["x" => $x3, "y" => $y3], +]; + +/* testNestedAnonClassWithTraitUseAs */ +// Parse error, but not our concern, it is short array syntax. +array_map(function($a) { + return new class() { + use MyTrait { + MyTrait::functionName as []; + } + }; +}, $array); + +/* testParseError */ +// Parse error, but not our concern, it is short array syntax. +use Something as [$a]; + +/* testLiveCoding */ +// This has to be the last test in the file. +[$a, /* testLiveCodingNested */ [$b] diff --git a/Tests/Utils/Arrays/IsShortArrayTest.php b/Tests/Utils/Arrays/IsShortArrayTest.php new file mode 100644 index 00000000..c8a7ea37 --- /dev/null +++ b/Tests/Utils/Arrays/IsShortArrayTest.php @@ -0,0 +1,199 @@ +assertFalse(Arrays::isShortArray(self::$phpcsFile, 100000)); + } + + /** + * Test that false is returned when a non-short array token is passed. + * + * @dataProvider dataNotShortArrayToken + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string|array $targetToken The token type(s) to look for. + * + * @return void + */ + public function testNotShortArrayToken($testMarker, $targetToken) + { + $target = $this->getTargetToken($testMarker, $targetToken); + $this->assertFalse(Arrays::isShortArray(self::$phpcsFile, $target)); + } + + /** + * Data provider. + * + * @see testNotShortArrayToken() For the array format. + * + * @return array + */ + public function dataNotShortArrayToken() + { + return [ + 'long-array' => [ + '/* testLongArray */', + \T_ARRAY, + ], + 'array-assignment-no-key' => [ + '/* testArrayAssignmentEmpty */', + \T_OPEN_SQUARE_BRACKET, + ], + 'array-assignment-string-key' => [ + '/* testArrayAssignmentStringKey */', + \T_OPEN_SQUARE_BRACKET, + ], + 'array-assignment-int-key' => [ + '/* testArrayAssignmentIntKey */', + \T_OPEN_SQUARE_BRACKET, + ], + 'array-assignment-var-key' => [ + '/* testArrayAssignmentVarKey */', + \T_OPEN_SQUARE_BRACKET, + ], + 'array-access-string-key' => [ + '/* testArrayAccessStringKey */', + \T_CLOSE_SQUARE_BRACKET, + ], + 'array-access-int-key-1' => [ + '/* testArrayAccessIntKey1 */', + \T_OPEN_SQUARE_BRACKET, + ], + 'array-access-int-key-2' => [ + '/* testArrayAccessIntKey2 */', + \T_OPEN_SQUARE_BRACKET, + ], + 'array-access-function-call' => [ + '/* testArrayAccessFunctionCall */', + \T_OPEN_SQUARE_BRACKET, + ], + 'array-access-constant' => [ + '/* testArrayAccessConstant */', + \T_OPEN_SQUARE_BRACKET, + ], + 'live-coding' => [ + '/* testLiveCoding */', + \T_OPEN_SQUARE_BRACKET, + ], + ]; + } + + /** + * Test whether a T_OPEN_SHORT_ARRAY token is a short array. + * + * @dataProvider dataIsShortArray + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param bool $expected The expected boolean return value. + * @param int|string|array $targetToken The token type(s) to test. Defaults to T_OPEN_SHORT_ARRAY. + * + * @return void + */ + public function testIsShortArray($testMarker, $expected, $targetToken = \T_OPEN_SHORT_ARRAY) + { + $stackPtr = $this->getTargetToken($testMarker, $targetToken); + $result = Arrays::isShortArray(self::$phpcsFile, $stackPtr); + + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testIsShortArray() For the array format. + * + * @return array + */ + public function dataIsShortArray() + { + return [ + 'short-array-not-nested' => [ + '/* testNonNestedShortArray */', + true, + ], + 'comparison-no-assignment' => [ + '/* testInComparison */', + true, + ], + 'comparison-no-assignment-nested' => [ + '/* testNestedInComparison */', + true, + ], + 'short-array-in-foreach' => [ + '/* testShortArrayInForeach */', + true, + ], + 'short-list-in-foreach' => [ + '/* testShortListInForeach */', + false, + ], + 'chained-assignment-short-list' => [ + '/* testMultiAssignShortlist */', + false, + ], + 'chained-assignment-short-array' => [ + '/* testMultiAssignShortArray */', + true, + \T_CLOSE_SHORT_ARRAY, + ], + 'short-array-with-nesting-and-keys' => [ + '/* testShortArrayWithNestingAndKeys */', + true, + ], + 'short-array-nested-with-keys-1' => [ + '/* testNestedShortArrayWithKeys_1 */', + true, + ], + 'short-array-nested-with-keys-2' => [ + '/* testNestedShortArrayWithKeys_2 */', + true, + ], + 'short-array-nested-with-keys-3' => [ + '/* testNestedShortArrayWithKeys_3 */', + true, + ], + 'parse-error-anon-class-trait-use-as' => [ + '/* testNestedAnonClassWithTraitUseAs */', + true, + ], + 'parse-error-use-as' => [ + '/* testParseError */', + true, + ], + 'parse-error-live-coding' => [ + '/* testLiveCodingNested */', + true, + ], + ]; + } +} diff --git a/Tests/Utils/Arrays/IsShortArrayTokenizerBC1Test.php b/Tests/Utils/Arrays/IsShortArrayTokenizerBC1Test.php new file mode 100644 index 00000000..32b25289 --- /dev/null +++ b/Tests/Utils/Arrays/IsShortArrayTokenizerBC1Test.php @@ -0,0 +1,129 @@ +getTargetToken($testMarker, [\T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET]); + $result = Arrays::isShortArray(self::$phpcsFile, $stackPtr); + + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testIsShortArray() For the array format. + * + * @return array + */ + public function dataIsShortArray() + { + return [ + 'issue-1971-list-first-in-file' => [ + '/* testTokenizerIssue1971PHPCSlt330gt271A */', + false, + ], + 'issue-1971-list-first-in-file-nested' => [ + '/* testTokenizerIssue1971PHPCSlt330gt271B */', + false, + ], + 'issue-1381-array-dereferencing-1-array' => [ + '/* testTokenizerIssue1381PHPCSlt290A1 */', + true, + ], + 'issue-1381-array-dereferencing-1-deref' => [ + '/* testTokenizerIssue1381PHPCSlt290A2 */', + false, + ], + 'issue-1381-array-dereferencing-2' => [ + '/* testTokenizerIssue1381PHPCSlt290B */', + false, + ], + 'issue-1381-array-dereferencing-3' => [ + '/* testTokenizerIssue1381PHPCSlt290C */', + false, + ], + 'issue-1381-array-dereferencing-4' => [ + '/* testTokenizerIssue1381PHPCSlt290D1 */', + false, + ], + 'issue-1381-array-dereferencing-4-deref-deref' => [ + '/* testTokenizerIssue1381PHPCSlt290D2 */', + false, + ], + 'issue-1284-short-list-directly-after-close-curly-control-structure' => [ + '/* testTokenizerIssue1284PHPCSlt280A */', + false, + ], + 'issue-1284-short-array-directly-after-close-curly-control-structure' => [ + '/* testTokenizerIssue1284PHPCSlt280B */', + true, + ], + 'issue-1284-array-access-variable-variable' => [ + '/* testTokenizerIssue1284PHPCSlt290C */', + false, + ], + 'issue-1284-array-access-variable-property' => [ + '/* testTokenizerIssue1284PHPCSlt280D */', + false, + ], + ]; + } +} diff --git a/Tests/Utils/Arrays/IsShortArrayTokenizerBC2Test.php b/Tests/Utils/Arrays/IsShortArrayTokenizerBC2Test.php new file mode 100644 index 00000000..907c6937 --- /dev/null +++ b/Tests/Utils/Arrays/IsShortArrayTokenizerBC2Test.php @@ -0,0 +1,90 @@ +getTargetToken($testMarker, [\T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET]); + $result = Arrays::isShortArray(self::$phpcsFile, $stackPtr); + + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testIsShortArray() For the array format. + * + * @return array + */ + public function dataIsShortArray() + { + return [ + // Make sure the utility method does not throw false positives for short array at start of file. + 'issue-1971-short-array-first-in-file' => [ + '/* testTokenizerIssue1971PHPCSlt330gt271C */', + true, + ], + 'issue-1971-short-array-first-in-file-nested' => [ + '/* testTokenizerIssue1971PHPCSlt330gt271D */', + true, + ], + ]; + } +} diff --git a/Tests/Utils/Conditions/GetConditionTest.php b/Tests/Utils/Conditions/GetConditionTest.php new file mode 100644 index 00000000..ebf8ffd1 --- /dev/null +++ b/Tests/Utils/Conditions/GetConditionTest.php @@ -0,0 +1,292 @@ +conditionDefaults; + + foreach ($expectedResults as $conditionType => $expected) { + if ($expected !== false) { + $expected = self::$markerTokens[$expected]; + } + + $result = Conditions::getCondition(self::$phpcsFile, $stackPtr, \constant($conditionType), true); + $this->assertSame( + $expected, + $result, + "Assertion failed for test marker '{$testMarker}' with condition {$conditionType} (reversed)" + ); + } + } + + /** + * Data provider. + * + * Only the conditions which are expected to be *found* need to be listed here. + * All other potential conditions will automatically also be tested and will expect + * `false` as a result. + * + * @see testGetConditionReversed() For the array format. + * + * @return array + */ + public static function dataGetConditionReversed() + { + $data = self::dataGetCondition(); + + // Set up the data for the reversed results. + $data['testSeriouslyNestedMethod'][1]['T_IF'] = '/* condition 4: if */'; + + $data['testDeepestNested'][1]['T_FUNCTION'] = '/* condition 12: nested anonymous class method */'; + $data['testDeepestNested'][1]['T_IF'] = '/* condition 10-1: if */'; + + $data['testInException'][1]['T_FUNCTION'] = '/* condition 6: class method */'; + $data['testInException'][1]['T_IF'] = '/* condition 4: if */'; + + $data['testInDefault'][1]['T_FUNCTION'] = '/* condition 6: class method */'; + $data['testInDefault'][1]['T_IF'] = '/* condition 4: if */'; + + return $data; + } + + /** + * Test retrieving a specific condition from a token's "conditions" array, + * with multiple allowed possibilities. + * + * @return void + */ + public function testGetConditionMultipleTypes() + { + $stackPtr = self::$testTokens['/* testInException */']; + + $result = Conditions::getCondition(self::$phpcsFile, $stackPtr, [\T_DO, \T_FOR]); + $this->assertFalse( + $result, + 'Failed asserting that "testInException" does not have a "do" nor a "for" condition' + ); + + $result = Conditions::getCondition(self::$phpcsFile, $stackPtr, [\T_DO, \T_FOR, \T_FOREACH]); + $this->assertSame( + self::$markerTokens['/* condition 10-3: foreach */'], + $result, + 'Failed asserting that "testInException" has a condition based on the types "do", "for" and "foreach"' + ); + + $stackPtr = self::$testTokens['/* testDeepestNested */']; + + $result = Conditions::getCondition(self::$phpcsFile, $stackPtr, [\T_INTERFACE, \T_TRAIT]); + $this->assertFalse( + $result, + 'Failed asserting that "testDeepestNested" does not have an interface nor a trait condition' + ); + + $result = Conditions::getCondition(self::$phpcsFile, $stackPtr, $this->ooScopeTokens); + $this->assertSame( + self::$markerTokens['/* condition 5: nested class */'], + $result, + 'Failed asserting that "testDeepestNested" has a class condition based on the OO Scope token types' + ); + } + + /** + * Test passing a non conditional token to getFirstCondition()/getLastCondition(). + * + * @return void + */ + public function testNonConditionalTokenGetFirstLast() + { + $stackPtr = $this->getTargetToken('/* testStartPoint */', \T_STRING); + + $result = Conditions::getFirstCondition(self::$phpcsFile, $stackPtr); + $this->assertFalse($result, 'Failed asserting that getFirstCondition() on non conditional token returns false'); + + $result = Conditions::getLastCondition(self::$phpcsFile, $stackPtr); + $this->assertFalse($result, 'Failed asserting that getLastCondition() on non conditional token returns false'); + } + + /** + * Test retrieving the first condition token pointer, in general and of specific types. + * + * @dataProvider dataGetFirstCondition + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @return void + */ + public function testGetFirstCondition($testMarker) + { + $stackPtr = self::$testTokens[$testMarker]; + + $result = Conditions::getFirstCondition(self::$phpcsFile, $stackPtr); + $this->assertSame(self::$markerTokens['/* condition 0: namespace */'], $result); + + $result = Conditions::getFirstCondition(self::$phpcsFile, $stackPtr, \T_IF); + $this->assertSame(self::$markerTokens['/* condition 1: if */'], $result); + + $result = Conditions::getFirstCondition(self::$phpcsFile, $stackPtr, $this->ooScopeTokens); + $this->assertSame(self::$markerTokens['/* condition 5: nested class */'], $result); + + $result = Conditions::getFirstCondition(self::$phpcsFile, $stackPtr, [\T_ELSEIF]); + $this->assertFalse($result); + } + + /** + * Data provider. Pass the markers for the test tokens on. + * + * @see testGetFirstCondition() For the array format. + * + * @return array + */ + public function dataGetFirstCondition() + { + $data = []; + foreach (self::$testTargets as $marker) { + $data[\trim($marker, '/* ')] = [$marker]; + } + + return $data; + } + + /** + * Test retrieving the last condition token pointer, in general and of specific types. + * + * @dataProvider dataGetLastCondition + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected The marker for the pointers to the expected condition + * results for the pre-set tests. + * + * @return void + */ + public function testGetLastCondition($testMarker, $expected) + { + $stackPtr = self::$testTokens[$testMarker]; + + $result = Conditions::getLastCondition(self::$phpcsFile, $stackPtr); + $this->assertSame(self::$markerTokens[$expected['no type']], $result); + + $result = Conditions::getLastCondition(self::$phpcsFile, $stackPtr, \T_IF); + $this->assertSame(self::$markerTokens[$expected['T_IF']], $result); + + $result = Conditions::getLastCondition(self::$phpcsFile, $stackPtr, $this->ooScopeTokens); + $this->assertSame(self::$markerTokens[$expected['OO tokens']], $result); + + $result = Conditions::getLastCondition(self::$phpcsFile, $stackPtr, [\T_FINALLY]); + $this->assertFalse($result); + } + + /** + * Data provider. + * + * @see testGetLastCondition() For the array format. + * + * @return array + */ + public function dataGetLastCondition() + { + return [ + 'testSeriouslyNestedMethod' => [ + '/* testSeriouslyNestedMethod */', + [ + 'no type' => '/* condition 5: nested class */', + 'T_IF' => '/* condition 4: if */', + 'OO tokens' => '/* condition 5: nested class */', + ], + ], + 'testDeepestNested' => [ + '/* testDeepestNested */', + [ + 'no type' => '/* condition 13: closure */', + 'T_IF' => '/* condition 10-1: if */', + 'OO tokens' => '/* condition 11-1: nested anonymous class */', + ], + ], + 'testInException' => [ + '/* testInException */', + [ + 'no type' => '/* condition 11-3: catch */', + 'T_IF' => '/* condition 4: if */', + 'OO tokens' => '/* condition 5: nested class */', + ], + ], + 'testInDefault' => [ + '/* testInDefault */', + [ + 'no type' => '/* condition 8b: default */', + 'T_IF' => '/* condition 4: if */', + 'OO tokens' => '/* condition 5: nested class */', + ], + ], + ]; + } +} diff --git a/Tests/Utils/FunctionDeclarations/GetParametersDiffTest.inc b/Tests/Utils/FunctionDeclarations/GetParametersDiffTest.inc new file mode 100644 index 00000000..0b8c0af4 --- /dev/null +++ b/Tests/Utils/FunctionDeclarations/GetParametersDiffTest.inc @@ -0,0 +1,3 @@ +expectPhpcsException('$stackPtr must be of type T_FUNCTION or T_CLOSURE or T_USE'); + + FunctionDeclarations::getParameters(self::$phpcsFile, 10000); + } +} diff --git a/Tests/Utils/FunctionDeclarations/GetParametersParseError1Test.php b/Tests/Utils/FunctionDeclarations/GetParametersParseError1Test.php new file mode 100644 index 00000000..e77c489a --- /dev/null +++ b/Tests/Utils/FunctionDeclarations/GetParametersParseError1Test.php @@ -0,0 +1,62 @@ +getTargetToken('/* testParseError */', [\T_FUNCTION, \T_CLOSURE]); + $result = FunctionDeclarations::getParameters(self::$phpcsFile, $target); + + $this->assertSame([], $result); + } +} diff --git a/Tests/Utils/FunctionDeclarations/GetParametersParseError2Test.php b/Tests/Utils/FunctionDeclarations/GetParametersParseError2Test.php new file mode 100644 index 00000000..7d856e90 --- /dev/null +++ b/Tests/Utils/FunctionDeclarations/GetParametersParseError2Test.php @@ -0,0 +1,62 @@ +getTargetToken('/* testParseError */', [\T_FUNCTION, \T_CLOSURE]); + $result = FunctionDeclarations::getParameters(self::$phpcsFile, $target); + + $this->assertSame([], $result); + } +} diff --git a/Tests/Utils/FunctionDeclarations/GetParametersTest.php b/Tests/Utils/FunctionDeclarations/GetParametersTest.php new file mode 100644 index 00000000..d930296a --- /dev/null +++ b/Tests/Utils/FunctionDeclarations/GetParametersTest.php @@ -0,0 +1,142 @@ +expectPhpcsException('$stackPtr must be of type T_FUNCTION or T_CLOSURE or T_USE'); + + $next = $this->getTargetToken('/* testNotAFunction */', [\T_INTERFACE]); + FunctionDeclarations::getParameters(self::$phpcsFile, $next); + } + + /** + * Test receiving an expected exception when a non-closure use token is passed. + * + * @dataProvider dataInvalidUse + * + * @param string $identifier The comment which preceeds the test. + * + * @return void + */ + public function testInvalidUse($identifier) + { + $this->expectPhpcsException('$stackPtr was not a valid closure T_USE'); + + $use = $this->getTargetToken($identifier, [\T_USE]); + FunctionDeclarations::getParameters(self::$phpcsFile, $use); + } + + /** + * Test receiving an empty array when there are no parameters. + * + * @dataProvider dataNoParams + * + * @param string $commentString The comment which preceeds the test. + * @param array $targetTokenType Optional. The token type to search for after $commentString. + * Defaults to the function/closure tokens. + * + * @return void + */ + public function testNoParams($commentString, $targetTokenType = [\T_FUNCTION, \T_CLOSURE]) + { + $target = $this->getTargetToken($commentString, $targetTokenType); + $result = FunctionDeclarations::getParameters(self::$phpcsFile, $target); + + $this->assertSame([], $result); + } + + /** + * Test helper. + * + * @param string $commentString The comment which preceeds the test. + * @param array $expected The expected function output. + * @param array $targetType Optional. The token type to search for after $commentString. + * Defaults to the function/closure tokens. + * + * @return void + */ + protected function getMethodParametersTestHelper($commentString, $expected, $targetType = [\T_FUNCTION, \T_CLOSURE]) + { + $target = $this->getTargetToken($commentString, $targetType); + $found = FunctionDeclarations::getParameters(self::$phpcsFile, $target); + + foreach ($expected as $key => $param) { + $expected[$key]['token'] += $target; + + if ($param['reference_token'] !== false) { + $expected[$key]['reference_token'] += $target; + } + if ($param['variadic_token'] !== false) { + $expected[$key]['variadic_token'] += $target; + } + if ($param['type_hint_token'] !== false) { + $expected[$key]['type_hint_token'] += $target; + } + if ($param['type_hint_end_token'] !== false) { + $expected[$key]['type_hint_end_token'] += $target; + } + if ($param['comma_token'] !== false) { + $expected[$key]['comma_token'] += $target; + } + if (isset($param['default_token'])) { + $expected[$key]['default_token'] += $target; + } + if (isset($param['default_equal_token'])) { + $expected[$key]['default_equal_token'] += $target; + } + } + + $this->assertSame($expected, $found); + } +} diff --git a/Tests/Utils/FunctionDeclarations/GetPropertiesDiffTest.inc b/Tests/Utils/FunctionDeclarations/GetPropertiesDiffTest.inc new file mode 100644 index 00000000..34138d72 --- /dev/null +++ b/Tests/Utils/FunctionDeclarations/GetPropertiesDiffTest.inc @@ -0,0 +1,24 @@ +expectPhpcsException('$stackPtr must be of type T_FUNCTION or T_CLOSURE'); + + FunctionDeclarations::getProperties(self::$phpcsFile, 10000); + } + + /** + * Test handling of the PHPCS 3.2.0+ annotations between the keywords. + * + * @return void + */ + public function testMessyPhpcsAnnotationsMethod() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => true, + 'return_type' => '', + 'return_type_token' => false, + 'return_type_end_token' => false, + 'nullable_return_type' => false, + 'is_abstract' => true, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getPropertiesTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Test handling of the PHPCS 3.2.0+ annotations between the keywords with a static closure. + * + * @return void + */ + public function testMessyPhpcsAnnotationsStaticClosure() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => '', + 'return_type_token' => false, + 'return_type_end_token' => false, + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => true, + 'has_body' => true, + ]; + + $this->getPropertiesTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Test that the new "return_type_end_token" index is set correctly. + * + * @return void + */ + public function testReturnTypeEndTokenIndex() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => '?\MyNamespace\MyClass\Foo', + 'return_type_token' => 8, // Offset from the T_FUNCTION token. + 'return_type_end_token' => 20, // Offset from the T_FUNCTION token. + 'nullable_return_type' => true, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getPropertiesTestHelper('/* ' . __FUNCTION__ . ' */', $expected); + } + + /** + * Test helper. + * + * @param string $commentString The comment which preceeds the test. + * @param array $expected The expected function output. + * + * @return void + */ + protected function getPropertiesTestHelper($commentString, $expected) + { + $function = $this->getTargetToken($commentString, [\T_FUNCTION, \T_CLOSURE]); + $found = FunctionDeclarations::getProperties(self::$phpcsFile, $function); + + if ($expected['return_type_token'] !== false) { + $expected['return_type_token'] += $function; + } + if ($expected['return_type_end_token'] !== false) { + $expected['return_type_end_token'] += $function; + } + + $this->assertSame($expected, $found); + } +} diff --git a/Tests/Utils/FunctionDeclarations/GetPropertiesTest.php b/Tests/Utils/FunctionDeclarations/GetPropertiesTest.php new file mode 100644 index 00000000..d8e48c12 --- /dev/null +++ b/Tests/Utils/FunctionDeclarations/GetPropertiesTest.php @@ -0,0 +1,91 @@ +expectPhpcsException('$stackPtr must be of type T_FUNCTION or T_CLOSURE'); + + $next = $this->getTargetToken('/* testNotAFunction */', \T_RETURN); + FunctionDeclarations::getProperties(self::$phpcsFile, $next); + } + + /** + * Test helper. + * + * @param string $commentString The comment which preceeds the test. + * @param array $expected The expected function output. + * + * @return void + */ + protected function getMethodPropertiesTestHelper($commentString, $expected) + { + $function = $this->getTargetToken($commentString, [\T_FUNCTION, \T_CLOSURE]); + $found = FunctionDeclarations::getProperties(self::$phpcsFile, $function); + + if ($expected['return_type_token'] !== false) { + $expected['return_type_token'] += $function; + } + + /* + * Test the new `return_type_end_token` key which is not in the original datasets. + */ + $this->assertArrayHasKey('return_type_end_token', $found); + $this->assertTrue($found['return_type_end_token'] === false || \is_int($found['return_type_end_token'])); + + // Remove the array key before doing the compare against the original dataset. + unset($found['return_type_end_token']); + + $this->assertSame($expected, $found); + } +} diff --git a/Tests/Utils/FunctionDeclarations/IsMagicFunctionNameTest.php b/Tests/Utils/FunctionDeclarations/IsMagicFunctionNameTest.php new file mode 100644 index 00000000..34ee0e4a --- /dev/null +++ b/Tests/Utils/FunctionDeclarations/IsMagicFunctionNameTest.php @@ -0,0 +1,88 @@ +assertTrue(FunctionDeclarations::isMagicFunctionName($name)); + } + + /** + * Data provider. + * + * @see testIsMagicFunctionName() For the array format. + * + * @return array + */ + public function dataIsMagicFunctionName() + { + return [ + 'lowercase' => ['__autoload'], + 'uppercase' => ['__AUTOLOAD'], + 'mixedcase' => ['__AutoLoad'], + ]; + } + + /** + * Test non-magic function names. + * + * @dataProvider dataIsNotMagicFunctionName + * + * @param string $name The function name to test. + * + * @return void + */ + public function testIsNotMagicFunctionName($name) + { + $this->assertFalse(FunctionDeclarations::isMagicFunctionName($name)); + } + + /** + * Data provider. + * + * @see testIsNotMagicFunctionName() For the array format. + * + * @return array + */ + public function dataIsNotMagicFunctionName() + { + return [ + 'no_underscore' => ['noDoubleUnderscore'], + 'single_underscore' => ['_autoload'], + 'triple_underscore' => ['___autoload'], + 'not_magic_function_name' => ['__notAutoload'], + ]; + } +} diff --git a/Tests/Utils/FunctionDeclarations/IsMagicMethodNameTest.php b/Tests/Utils/FunctionDeclarations/IsMagicMethodNameTest.php new file mode 100644 index 00000000..07b126c5 --- /dev/null +++ b/Tests/Utils/FunctionDeclarations/IsMagicMethodNameTest.php @@ -0,0 +1,154 @@ +assertTrue(FunctionDeclarations::isMagicMethodName($name)); + } + + /** + * Test valid PHP magic method names. + * + * @dataProvider dataIsMagicMethodName + * @covers \PHPCSUtils\Utils\FunctionDeclarations::isSpecialMethodName + * + * @param string $name The function name to test. + * + * @return void + */ + public function testIsSpecialMethodName($name) + { + $this->assertTrue(FunctionDeclarations::isSpecialMethodName($name)); + } + + /** + * Data provider. + * + * @see testIsMagicMethodName() For the array format. + * @see testIsSpecialMethodName() For the array format. + * + * @return array + */ + public function dataIsMagicMethodName() + { + return [ + // Normal case. + 'construct-defined-case' => ['__construct'], + 'destruct-defined-case' => ['__destruct'], + 'call-defined-case' => ['__call'], + 'callStatic-defined-case' => ['__callStatic'], + 'get-defined-case' => ['__get'], + 'set-defined-case' => ['__set'], + 'isset-defined-case' => ['__isset'], + 'unset-defined-case' => ['__unset'], + 'sleep-defined-case' => ['__sleep'], + 'wakeup-defined-case' => ['__wakeup'], + 'toString-defined-case' => ['__toString'], + 'set_state-defined-case' => ['__set_state'], + 'clone-defined-case' => ['__clone'], + 'invoke-defined-case' => ['__invoke'], + 'debugInfo-defined-case' => ['__debugInfo'], + 'serialize-defined-case' => ['__serialize'], + 'unserialize-defined-case' => ['__unserialize'], + + // Uppercase et al. + 'construct-changed-case' => ['__CONSTRUCT'], + 'destruct-changed-case' => ['__Destruct'], + 'call-changed-case' => ['__Call'], + 'callStatic-changed-case' => ['__callstatic'], + 'get-changed-case' => ['__GET'], + 'set-changed-case' => ['__SeT'], + 'isset-changed-case' => ['__isSet'], + 'unset-changed-case' => ['__unSet'], + 'sleep-changed-case' => ['__SleeP'], + 'wakeup-changed-case' => ['__wakeUp'], + 'toString-changed-case' => ['__TOString'], + 'set_state-changed-case' => ['__Set_State'], + 'clone-changed-case' => ['__CLONE'], + 'invoke-changed-case' => ['__Invoke'], + 'debugInfo-changed-case' => ['__Debuginfo'], + 'serialize-changed-case' => ['__SERIALIZE'], + 'unserialize-changed-case' => ['__unSerialize'], + ]; + } + + /** + * Test non-magic method names. + * + * @dataProvider dataIsNotMagicMethodName + * @covers \PHPCSUtils\Utils\FunctionDeclarations::isMagicMethodName + * + * @param string $name The function name to test. + * + * @return void + */ + public function testIsNotMagicMethodName($name) + { + $this->assertFalse(FunctionDeclarations::isMagicMethodName($name)); + } + + /** + * Test non-magic method names. + * + * @dataProvider dataIsNotMagicMethodName + * @covers \PHPCSUtils\Utils\FunctionDeclarations::isSpecialMethodName + * + * @param string $name The function name to test. + * + * @return void + */ + public function testIsNotSpecialMethodName($name) + { + $this->assertFalse(FunctionDeclarations::isSpecialMethodName($name)); + } + + /** + * Data provider. + * + * @see testIsNotMagicMethodName() For the array format. + * @see testIsNotSpecialMethodName() For the array format. + * + * @return array + */ + public function dataIsNotMagicMethodName() + { + return [ + 'no_underscore' => ['construct'], + 'single_underscore' => ['_destruct'], + 'triple_underscore' => ['___call'], + 'not_magic_method_name' => ['__myFunction'], + ]; + } +} diff --git a/Tests/Utils/FunctionDeclarations/IsPHPDoubleUnderscoreMethodNameTest.php b/Tests/Utils/FunctionDeclarations/IsPHPDoubleUnderscoreMethodNameTest.php new file mode 100644 index 00000000..f29d2238 --- /dev/null +++ b/Tests/Utils/FunctionDeclarations/IsPHPDoubleUnderscoreMethodNameTest.php @@ -0,0 +1,144 @@ +assertTrue(FunctionDeclarations::isPHPDoubleUnderscoreMethodName($name)); + } + + /** + * Test valid PHP native double underscore method names. + * + * @dataProvider dataIsPHPDoubleUnderscoreMethodName + * @covers \PHPCSUtils\Utils\FunctionDeclarations::isSpecialMethodName + * + * @param string $name The function name to test. + * + * @return void + */ + public function testIsSpecialMethodName($name) + { + $this->assertTrue(FunctionDeclarations::isSpecialMethodName($name)); + } + + /** + * Data provider. + * + * @see testIsPHPDoubleUnderscoreMethodName() For the array format. + * @see testIsSpecialMethodName() For the array format. + * + * @return array + */ + public function dataIsPHPDoubleUnderscoreMethodName() + { + return [ + // Normal case. + 'doRequest-defined-case' => ['__doRequest'], + 'getCookies-defined-case' => ['__getCookies'], + 'getFunctions-defined-case' => ['__getFunctions'], + 'getLastRequest-defined-case' => ['__getLastRequest'], + 'getLastRequestHeaders-defined-case' => ['__getLastRequestHeaders'], + 'getLastResponse-defined-case' => ['__getLastResponse'], + 'getLastResponseHeaders-defined-case' => ['__getLastResponseHeaders'], + 'getTypes-defined-case' => ['__getTypes'], + 'setCookie-defined-case' => ['__setCookie'], + 'setLocation-defined-case' => ['__setLocation'], + 'setSoapHeaders-defined-case' => ['__setSoapHeaders'], + 'soapCall-defined-case' => ['__soapCall'], + + // Uppercase et al. + 'doRequest-changed-case' => ['__DOREQUEST'], + 'getCookies-changed-case' => ['__getcookies'], + 'getFunctions-changed-case' => ['__Getfunctions'], + 'getLastRequest-changed-case' => ['__GETLASTREQUEST'], + 'getLastRequestHeaders-changed-case' => ['__getlastrequestheaders'], + 'getLastResponse-changed-case' => ['__GetlastResponse'], + 'getLastResponseHeaders-changed-case' => ['__GETLASTRESPONSEHEADERS'], + 'getTypes-changed-case' => ['__GetTypes'], + 'setCookie-changed-case' => ['__SETCookie'], + 'setLocation-changed-case' => ['__sETlOCATION'], + 'setSoapHeaders-changed-case' => ['__SetSOAPHeaders'], + 'soapCall-changed-case' => ['__SOAPCall'], + ]; + } + + /** + * Test function names which are not valid PHP native double underscore methods. + * + * @dataProvider dataIsNotPHPDoubleUnderscoreMethodName + * @covers \PHPCSUtils\Utils\FunctionDeclarations::isPHPDoubleUnderscoreMethodName + * + * @param string $name The function name to test. + * + * @return void + */ + public function testIsNotPHPDoubleUnderscoreMethodName($name) + { + $this->assertFalse(FunctionDeclarations::isPHPDoubleUnderscoreMethodName($name)); + } + + /** + * Test function names which are not valid PHP native double underscore methods. + * + * @dataProvider dataIsNotPHPDoubleUnderscoreMethodName + * @covers \PHPCSUtils\Utils\FunctionDeclarations::isSpecialMethodName + * + * @param string $name The function name to test. + * + * @return void + */ + public function testIsNotSpecialMethodName($name) + { + $this->assertFalse(FunctionDeclarations::isSpecialMethodName($name)); + } + + /** + * Data provider. + * + * @see testIsNotPHPDoubleUnderscoreMethodName() For the array format. + * @see testIsNotSpecialMethodName() For the array format. + * + * @return array + */ + public function dataIsNotPHPDoubleUnderscoreMethodName() + { + return [ + 'no_underscore' => ['getLastResponseHeaders'], + 'single_underscore' => ['_setLocation'], + 'triple_underscore' => ['___getCookies'], + 'not_magic_function_name' => ['__getFirstRequestHeader'], + ]; + } +} diff --git a/Tests/Utils/FunctionDeclarations/SpecialFunctionsTest.inc b/Tests/Utils/FunctionDeclarations/SpecialFunctionsTest.inc new file mode 100644 index 00000000..374c28da --- /dev/null +++ b/Tests/Utils/FunctionDeclarations/SpecialFunctionsTest.inc @@ -0,0 +1,96 @@ +assertFalse($result, 'isMagicFunction() did not return false'); + + $result = FunctionDeclarations::isMagicMethod(self::$phpcsFile, 10000); + $this->assertFalse($result, 'isMagicMethod() did not return false'); + + $result = FunctionDeclarations::isPHPDoubleUnderscoreMethod(self::$phpcsFile, 10000); + $this->assertFalse($result, 'isPHPDoubleUnderscoreMethod() did not return false'); + + $result = FunctionDeclarations::isSpecialMethod(self::$phpcsFile, 10000); + $this->assertFalse($result, 'isSpecialMethod() did not return false'); + } + + /** + * Test that the special function methods return false when passed a non-function token. + * + * @covers \PHPCSUtils\Utils\FunctionDeclarations::isMagicFunction + * @covers \PHPCSUtils\Utils\FunctionDeclarations::isMagicMethod + * @covers \PHPCSUtils\Utils\FunctionDeclarations::isPHPDoubleUnderscoreMethod + * @covers \PHPCSUtils\Utils\FunctionDeclarations::isSpecialMethod + * + * @return void + */ + public function testNotAFunctionToken() + { + $stackPtr = $this->getTargetToken('/* testNotAFunction */', \T_ECHO); + + $result = FunctionDeclarations::isMagicFunction(self::$phpcsFile, $stackPtr); + $this->assertFalse($result, 'isMagicFunction() did not return false'); + + $result = FunctionDeclarations::isMagicMethod(self::$phpcsFile, $stackPtr); + $this->assertFalse($result, 'isMagicMethod() did not return false'); + + $result = FunctionDeclarations::isPHPDoubleUnderscoreMethod(self::$phpcsFile, $stackPtr); + $this->assertFalse($result, 'isPHPDoubleUnderscoreMethod() did not return false'); + + $result = FunctionDeclarations::isSpecialMethod(self::$phpcsFile, $stackPtr); + $this->assertFalse($result, 'isSpecialMethod() did not return false'); + } + + /** + * Test correctly detecting magic functions. + * + * @dataProvider dataItsAKindOfMagic + * @covers \PHPCSUtils\Utils\FunctionDeclarations::isMagicFunction + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected The expected return values for the various functions. + * + * @return void + */ + public function testIsMagicFunction($testMarker, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, \T_FUNCTION); + $result = FunctionDeclarations::isMagicFunction(self::$phpcsFile, $stackPtr); + $this->assertSame($expected['function'], $result); + } + + /** + * Test correctly detecting magic methods. + * + * @dataProvider dataItsAKindOfMagic + * @covers \PHPCSUtils\Utils\FunctionDeclarations::isMagicMethod + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected The expected return values for the various functions. + * + * @return void + */ + public function testIsMagicMethod($testMarker, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, \T_FUNCTION); + $result = FunctionDeclarations::isMagicMethod(self::$phpcsFile, $stackPtr); + $this->assertSame($expected['method'], $result); + } + + /** + * Test correctly detecting PHP native double underscore methods. + * + * @dataProvider dataItsAKindOfMagic + * @covers \PHPCSUtils\Utils\FunctionDeclarations::isPHPDoubleUnderscoreMethod + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected The expected return values for the various functions. + * + * @return void + */ + public function testIsPHPDoubleUnderscoreMethod($testMarker, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, \T_FUNCTION); + $result = FunctionDeclarations::isPHPDoubleUnderscoreMethod(self::$phpcsFile, $stackPtr); + $this->assertSame($expected['double'], $result); + } + + /** + * Test correctly detecting magic methods and double underscore methods. + * + * @dataProvider dataItsAKindOfMagic + * @covers \PHPCSUtils\Utils\FunctionDeclarations::isSpecialMethod + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected The expected return values for the various functions. + * + * @return void + */ + public function testIsSpecialMethod($testMarker, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, \T_FUNCTION); + $result = FunctionDeclarations::isSpecialMethod(self::$phpcsFile, $stackPtr); + $this->assertSame($expected['special'], $result); + } + + /** + * Data provider. + * + * @see testIsMagicFunction() For the array format. + * @see testIsMagicMethod() For the array format. + * @see testIsPHPDoubleUnderscoreMethod() For the array format. + * @see testIsSpecialMethod() For the array format. + * + * @return array + */ + public function dataItsAKindOfMagic() + { + return [ + 'MagicMethodInClass' => [ + '/* testMagicMethodInClass */', + [ + 'function' => false, + 'method' => true, + 'double' => false, + 'special' => true, + ], + ], + 'MagicMethodInClassUppercase' => [ + '/* testMagicMethodInClassUppercase */', + [ + 'function' => false, + 'method' => true, + 'double' => false, + 'special' => true, + ], + ], + 'MagicMethodInClassMixedCase' => [ + '/* testMagicMethodInClassMixedCase */', + [ + 'function' => false, + 'method' => true, + 'double' => false, + 'special' => true, + ], + ], + 'MagicFunctionInClassNotGlobal' => [ + '/* testMagicFunctionInClassNotGlobal */', + [ + 'function' => false, + 'method' => false, + 'double' => false, + 'special' => false, + ], + ], + 'MethodInClassNotMagicName' => [ + '/* testMethodInClassNotMagicName */', + [ + 'function' => false, + 'method' => false, + 'double' => false, + 'special' => false, + ], + ], + 'MagicMethodNotInClass' => [ + '/* testMagicMethodNotInClass */', + [ + 'function' => false, + 'method' => false, + 'double' => false, + 'special' => false, + ], + ], + 'MagicFunction' => [ + '/* testMagicFunction */', + [ + 'function' => true, + 'method' => false, + 'double' => false, + 'special' => false, + ], + ], + 'MagicFunctionInConditionMixedCase' => [ + '/* testMagicFunctionInConditionMixedCase */', + [ + 'function' => true, + 'method' => false, + 'double' => false, + 'special' => false, + ], + ], + 'FunctionNotMagicName' => [ + '/* testFunctionNotMagicName */', + [ + 'function' => false, + 'method' => false, + 'double' => false, + 'special' => false, + ], + ], + 'MagicMethodInAnonClass' => [ + '/* testMagicMethodInAnonClass */', + [ + 'function' => false, + 'method' => true, + 'double' => false, + 'special' => true, + ], + ], + 'MagicMethodInAnonClassUppercase' => [ + '/* testMagicMethodInAnonClassUppercase */', + [ + 'function' => false, + 'method' => true, + 'double' => false, + 'special' => true, + ], + ], + 'MagicFunctionInAnonClassNotGlobal' => [ + '/* testMagicFunctionInAnonClassNotGlobal */', + [ + 'function' => false, + 'method' => false, + 'double' => false, + 'special' => false, + ], + ], + 'MethodInAnonClassNotMagicName' => [ + '/* testMethodInAnonClassNotMagicName */', + [ + 'function' => false, + 'method' => false, + 'double' => false, + 'special' => false, + ], + ], + 'DoubleUnderscoreMethodInClass' => [ + '/* testDoubleUnderscoreMethodInClass */', + [ + 'function' => false, + 'method' => false, + 'double' => true, + 'special' => true, + ], + ], + 'DoubleUnderscoreMethodInClassMixedcase' => [ + '/* testDoubleUnderscoreMethodInClassMixedcase */', + [ + 'function' => false, + 'method' => false, + 'double' => true, + 'special' => true, + ], + ], + + 'DoubleUnderscoreMethodInClassNotExtended' => [ + '/* testDoubleUnderscoreMethodInClassNotExtended */', + [ + 'function' => false, + 'method' => false, + 'double' => false, + 'special' => false, + ], + ], + 'DoubleUnderscoreMethodNotInClass' => [ + '/* testDoubleUnderscoreMethodNotInClass */', + [ + 'function' => false, + 'method' => false, + 'double' => false, + 'special' => false, + ], + ], + 'MagicMethodInTrait' => [ + '/* testMagicMethodInTrait */', + [ + 'function' => false, + 'method' => true, + 'double' => false, + 'special' => true, + ], + ], + + 'DoubleUnderscoreMethodInTrait' => [ + '/* testDoubleUnderscoreMethodInTrait */', + [ + 'function' => false, + 'method' => false, + 'double' => true, + 'special' => true, + ], + ], + 'MagicFunctionInTraitNotGloba' => [ + '/* testMagicFunctionInTraitNotGlobal */', + [ + 'function' => false, + 'method' => false, + 'double' => false, + 'special' => false, + ], + ], + 'MethodInTraitNotMagicName' => [ + '/* testMethodInTraitNotMagicName */', + [ + 'function' => false, + 'method' => false, + 'double' => false, + 'special' => false, + ], + ], + 'MagicMethodInInterface' => [ + '/* testMagicMethodInInterface */', + [ + 'function' => false, + 'method' => true, + 'double' => false, + 'special' => true, + ], + ], + + 'DoubleUnderscoreMethodInInterface' => [ + '/* testDoubleUnderscoreMethodInInterface */', + [ + 'function' => false, + 'method' => false, + 'double' => true, + 'special' => true, + ], + ], + 'MagicFunctionInInterfaceNotGlobal' => [ + '/* testMagicFunctionInInterfaceNotGlobal */', + [ + 'function' => false, + 'method' => false, + 'double' => false, + 'special' => false, + ], + ], + 'MethodInInterfaceNotMagicName' => [ + '/* testMethodInInterfaceNotMagicName */', + [ + 'function' => false, + 'method' => false, + 'double' => false, + 'special' => false, + ], + ], + ]; + } +} diff --git a/Tests/Utils/GetTokensAsString/GetTokensAsStringTest.php b/Tests/Utils/GetTokensAsString/GetTokensAsStringTest.php new file mode 100644 index 00000000..5dde8be1 --- /dev/null +++ b/Tests/Utils/GetTokensAsString/GetTokensAsStringTest.php @@ -0,0 +1,309 @@ +expectPhpcsException('The $start position for GetTokensAsString methods must exist in the token stack'); + + GetTokensAsString::normal(self::$phpcsFile, 100000, 100010); + } + + /** + * Test passing a non integer `$start`, like the result of a failed $phpcsFile->findNext(). + * + * @return void + */ + public function testNonIntegerStart() + { + $this->expectPhpcsException('The $start position for GetTokensAsString methods must exist in the token stack'); + + GetTokensAsString::noEmpties(self::$phpcsFile, false, 10); + } + + /** + * Test passing a non integer `$end`, like the result of a failed $phpcsFile->findNext(). + * + * @return void + */ + public function testNonIntegerEnd() + { + $result = GetTokensAsString::tabReplaced(self::$phpcsFile, 10, false); + $this->assertSame('', $result); + + $result = GetTokensAsString::origContent(self::$phpcsFile, 10, 11.5); + $this->assertSame('', $result); + } + + /** + * Test passing a token pointer to $end which is less than $start. + * + * @return void + */ + public function testEndBeforeStart() + { + $result = GetTokensAsString::noComments(self::$phpcsFile, 10, 5); + $this->assertSame('', $result); + } + + /** + * Test passing a `$end` beyond the end of the file. + * + * @return void + */ + public function testLengthBeyondEndOfFile() + { + $semicolon = $this->getTargetToken('/* testEndOfFile */', \T_SEMICOLON); + $result = GetTokensAsString::origContent(self::$phpcsFile, $semicolon, 1000); + $this->assertSame('; +', $result); + } + + /** + * Test getting a token set as a string. + * + * @dataProvider dataGetTokensAsString() + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string|array $startTokenType The type of token(s) to look for for the start of the string. + * @param array $expected The expected function's return values. + * + * @return void + */ + public function testNormal($testMarker, $startTokenType, $expected) + { + $start = $this->getTargetToken($testMarker, $startTokenType); + $end = $this->getTargetToken($testMarker, \T_SEMICOLON); + + $result = GetTokensAsString::normal(self::$phpcsFile, $start, $end); + $this->assertSame($expected['normal'], $result); + } + + /** + * Test getting a token set as a string with the original content. + * + * @dataProvider dataGetTokensAsString() + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string|array $startTokenType The type of token(s) to look for for the start of the string. + * @param array $expected The expected function's return values. + * + * @return void + */ + public function testOrigContent($testMarker, $startTokenType, $expected) + { + $start = $this->getTargetToken($testMarker, $startTokenType); + $end = $this->getTargetToken($testMarker, \T_SEMICOLON); + + $result = GetTokensAsString::origContent(self::$phpcsFile, $start, $end); + $this->assertSame($expected['orig'], $result); + } + + /** + * Test getting a token set as a string without comments. + * + * @dataProvider dataGetTokensAsString() + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string|array $startTokenType The type of token(s) to look for for the start of the string. + * @param array $expected The expected function's return values. + * + * @return void + */ + public function testNoComments($testMarker, $startTokenType, $expected) + { + $start = $this->getTargetToken($testMarker, $startTokenType); + $end = $this->getTargetToken($testMarker, \T_SEMICOLON); + + $result = GetTokensAsString::noComments(self::$phpcsFile, $start, $end); + $this->assertSame($expected['no_comments'], $result); + } + + /** + * Test getting a token set as a string without comments or whitespace. + * + * @dataProvider dataGetTokensAsString() + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string|array $startTokenType The type of token(s) to look for for the start of the string. + * @param array $expected The expected function's return values. + * + * @return void + */ + public function testNoEmpties($testMarker, $startTokenType, $expected) + { + $start = $this->getTargetToken($testMarker, $startTokenType); + $end = $this->getTargetToken($testMarker, \T_SEMICOLON); + + $result = GetTokensAsString::noEmpties(self::$phpcsFile, $start, $end); + $this->assertSame($expected['no_empties'], $result); + } + + /** + * Test getting a token set as a string with compacted whitespace. + * + * @dataProvider dataGetTokensAsString() + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string|array $startTokenType The type of token(s) to look for for the start of the string. + * @param array $expected The expected function's return values. + * + * @return void + */ + public function testCompact($testMarker, $startTokenType, $expected) + { + $start = $this->getTargetToken($testMarker, $startTokenType); + $end = $this->getTargetToken($testMarker, \T_SEMICOLON); + + $result = GetTokensAsString::compact(self::$phpcsFile, $start, $end); + $this->assertSame($expected['compact'], $result); + } + + /** + * Test getting a token set as a string without comments and with compacted whitespace. + * + * @dataProvider dataGetTokensAsString() + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string|array $startTokenType The type of token(s) to look for for the start of the string. + * @param array $expected The expected function's return values. + * + * @return void + */ + public function testCompactNoComments($testMarker, $startTokenType, $expected) + { + $start = $this->getTargetToken($testMarker, $startTokenType); + $end = $this->getTargetToken($testMarker, \T_SEMICOLON); + + $result = GetTokensAsString::compact(self::$phpcsFile, $start, $end, true); + $this->assertSame($expected['compact_nc'], $result); + } + + /** + * Data provider. + * + * @see testNormal() For the array format. + * @see testOrigContent() For the array format. + * @see testNoComments() For the array format. + * @see testNoEmpties() For the array format. + * @see testCompact() For the array format. + * @see testCompactNoComments() For the array format. + * + * @return array + */ + public function dataGetTokensAsString() + { + return [ + 'namespace' => [ + 'marker' => '/* testNamespace */', + 'type' => \T_NAMESPACE, + 'expected' => [ + 'normal' => 'namespace Foo\Bar\Baz;', + 'orig' => 'namespace Foo\Bar\Baz;', + 'no_comments' => 'namespace Foo\Bar\Baz;', + 'no_empties' => 'namespaceFoo\Bar\Baz;', + 'compact' => 'namespace Foo\Bar\Baz;', + 'compact_nc' => 'namespace Foo\Bar\Baz;', + ], + ], + 'use-with-comments' => [ + 'marker' => '/* testUseWithComments */', + 'type' => \T_STRING, + 'expected' => [ + 'normal' => 'Foo /*comment*/ \ Bar + // phpcs:ignore Stnd.Cat.Sniff -- For reasons. + \ Bah;', + 'orig' => 'Foo /*comment*/ \ Bar + // phpcs:ignore Stnd.Cat.Sniff -- For reasons. + \ Bah;', + 'no_comments' => 'Foo \ Bar + \ Bah;', + 'no_empties' => 'Foo\Bar\Bah;', + 'compact' => 'Foo /*comment*/ \ Bar // phpcs:ignore Stnd.Cat.Sniff -- For reasons. + \ Bah;', + 'compact_nc' => 'Foo \ Bar \ Bah;', + ], + ], + 'echo-with-tabs' => [ + 'marker' => '/* testEchoWithTabs */', + 'type' => \T_ECHO, + 'expected' => [ + 'normal' => 'echo \'foo\', + \'bar\' , + \'baz\';', + 'orig' => 'echo \'foo\', + \'bar\' , + \'baz\';', + 'no_comments' => 'echo \'foo\', + \'bar\' , + \'baz\';', + 'no_empties' => 'echo\'foo\',\'bar\',\'baz\';', + 'compact' => 'echo \'foo\', \'bar\' , \'baz\';', + 'compact_nc' => 'echo \'foo\', \'bar\' , \'baz\';', + ], + ], + 'end-of-file' => [ + 'marker' => '/* testEndOfFile */', + 'type' => \T_ECHO, + 'expected' => [ + 'normal' => 'echo $foo;', + 'orig' => 'echo $foo;', + 'no_comments' => 'echo $foo;', + 'no_empties' => 'echo$foo;', + 'compact' => 'echo $foo;', + 'compact_nc' => 'echo $foo;', + ], + ], + ]; + } +} diff --git a/Tests/Utils/Lists/GetAssignmentsTest.inc b/Tests/Utils/Lists/GetAssignmentsTest.inc new file mode 100644 index 00000000..da8cfbfa --- /dev/null +++ b/Tests/Utils/Lists/GetAssignmentsTest.inc @@ -0,0 +1,95 @@ +propA, $this->propB] = $array; + +/* testShortListInForeachKeyedWithRef */ +foreach ($data as ['id' => & $id, 'name' => $name]) {} + +/* testNestedLongList */ +list($a, list($b, $c)) = array(1, array(2, 3)); + +/* testLongListWithKeys */ +list('name' => $a, 'id' => $b, 'field' => $c) = ['name' => 1, 'id' => 2, 'field' => 3]; + +/* testLongListWithEmptyEntries */ +list( , $a, , $b,, $c, ,) = [1, 2, 3, 4, 5, 6, 7, 8]; + +/* testLongListMultiLineKeyedWithTrailingComma */ +class Foo { + function bar() { + list( + "name" => $this->name, + "colour" => $this->colour, + "age" => $this->age, + "cuteness" => $this->cuteness, + ) = $attributes; + } +} + +/* testShortListWithKeysNestedLists */ +['a' => [&$a, $b], 'b' => [$c, &$d]] = $array; + +/* testLongListWithArrayVars */ +list($a[], $a[0], $a[]) = [1, 2, 3]; + +/* testShortListMultiLineWithVariableKeys */ +[ + 'a' . 'b' => $a, + ($a * 2) + => $b->prop->prop /* comment */ ['index'], + CONSTANT & /*comment*/ OTHER => $c, + (string) &$c => &$d["D"], + get_key()[1] => $e, + $prop['index'] => $f->prop['index'], + $obj->fieldname => ${$g}, + $simple => &$h, +] = $array; + +/* testLongListWithCloseParensInKey */ +list(get_key()[1] => &$e) = [1, 2, 3]; + +/* testLongListVariableVar */ +list( ${$drink}, $foo->{$bar['baz']} ) = $infoArray; + +/* testLongListKeyedNestedLists */ +list( + 'a' => + list("x" => $x1, "y" => $y1), + 'b' => + list("x" => $x2, "y" => $y2) +) = $points; + +/* testLongListMixedKeyedUnkeyed */ +// Parse error, but not our concern. +list($unkeyed, "key" => $keyed) = $array; + +/* testShortListWithEmptiesAndKey */ +// Empty elements are not allowed where keys are specified. Parse error, but not our concern. +[,,,, "key" => $keyed] = $array; + + +/* testLiveCoding */ +// Intentional parse error. This has to be the last test in the file. +list( diff --git a/Tests/Utils/Lists/GetAssignmentsTest.php b/Tests/Utils/Lists/GetAssignmentsTest.php new file mode 100644 index 00000000..830b98b3 --- /dev/null +++ b/Tests/Utils/Lists/GetAssignmentsTest.php @@ -0,0 +1,833 @@ +expectPhpcsException('The Lists::getAssignments() method expects a long/short list token.'); + + Lists::getAssignments(self::$phpcsFile, 100000); + } + + /** + * Test that false is returned when a non-(short) list token is passed. + * + * @dataProvider dataNotListToken + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string|array $targetToken The token type(s) to look for. + * + * @return void + */ + public function testNotListToken($testMarker, $targetToken) + { + $this->expectPhpcsException('The Lists::getAssignments() method expects a long/short list token.'); + + $target = $this->getTargetToken($testMarker, $targetToken); + Lists::getAssignments(self::$phpcsFile, $target); + } + + /** + * Data provider. + * + * @see testNotListToken() For the array format. + * + * @return array + */ + public function dataNotListToken() + { + return [ + 'not-a-list' => [ + '/* testNotAList */', + \T_OPEN_SHORT_ARRAY, + ], + 'live-coding' => [ + '/* testLiveCoding */', + \T_LIST, + ], + ]; + } + + /** + * Test retrieving the details of the variable assignments for a list. + * + * @dataProvider dataGetAssignments + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string|array $targetToken The token type(s) to look for. + * @param array|false $expected The expected function return value. + * + * @return void + */ + public function testGetAssignments($testMarker, $targetToken, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, $targetToken); + + // Convert offsets to absolute positions. + foreach ($expected as $index => $subset) { + if (isset($subset['key_token'])) { + $expected[$index]['key_token'] += $stackPtr; + } + if (isset($subset['key_end_token'])) { + $expected[$index]['key_end_token'] += $stackPtr; + } + if (isset($subset['double_arrow_token'])) { + $expected[$index]['double_arrow_token'] += $stackPtr; + } + if (isset($subset['reference_token']) && $subset['reference_token'] !== false) { + $expected[$index]['reference_token'] += $stackPtr; + } + if (isset($subset['assignment_token'])) { + $expected[$index]['assignment_token'] += $stackPtr; + } + if (isset($subset['assignment_end_token'])) { + $expected[$index]['assignment_end_token'] += $stackPtr; + } + } + + $result = Lists::getAssignments(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * Token positions are provided as offsets from the target stackPtr. + * + * @see testGetAssignments() For the array format. + * + * @return array + */ + public function dataGetAssignments() + { + return [ + 'long-list-empty' => [ + '/* testEmptyLongList */', + \T_LIST, + [], + ], + 'short-list-empty' => [ + '/* testEmptyShortList */', + \T_OPEN_SHORT_ARRAY, + [], + ], + 'long-list-all-empty' => [ + '/* testLongListOnlyEmpties */', + \T_LIST, + [ + 0 => [ + 'raw' => '', + 'is_empty' => true, + ], + 1 => [ + 'raw' => '/* comment */', + 'is_empty' => true, + ], + 2 => [ + 'raw' => '', + 'is_empty' => true, + ], + 3 => [ + 'raw' => '', + 'is_empty' => true, + ], + ], + ], + 'short-list-all-empty-with-comment' => [ + '/* testShortListOnlyEmpties */', + \T_OPEN_SHORT_ARRAY, + [], + ], + 'long-list-basic' => [ + '/* testSimpleLongList */', + \T_LIST, + [ + 0 => [ + 'raw' => '$id', + 'is_empty' => false, + 'assignment' => '$id', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$id', + 'assignment_token' => 2, + 'assignment_end_token' => 2, + ], + 1 => [ + 'raw' => '$name', + 'is_empty' => false, + 'assignment' => '$name', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$name', + 'assignment_token' => 5, + 'assignment_end_token' => 5, + ], + ], + ], + 'short-list-basic' => [ + '/* testSimpleShortList */', + \T_OPEN_SHORT_ARRAY, + [ + 0 => [ + 'raw' => '$this->propA', + 'is_empty' => false, + 'assignment' => '$this->propA', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$this', + 'assignment_token' => 1, + 'assignment_end_token' => 3, + ], + 1 => [ + 'raw' => '$this->propB', + 'is_empty' => false, + 'assignment' => '$this->propB', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$this', + 'assignment_token' => 6, + 'assignment_end_token' => 8, + ], + ], + ], + 'short-list-in-foreach-keyed-with-ref' => [ + '/* testShortListInForeachKeyedWithRef */', + \T_OPEN_SHORT_ARRAY, + [ + 0 => [ + 'key' => "'id'", + 'key_token' => 1, + 'key_end_token' => 1, + 'double_arrow_token' => 3, + 'raw' => '\'id\' => & $id', + 'is_empty' => false, + 'assignment' => '$id', + 'nested_list' => false, + 'assign_by_reference' => true, + 'reference_token' => 5, + 'variable' => '$id', + 'assignment_token' => 7, + 'assignment_end_token' => 7, + ], + 1 => [ + 'key' => "'name'", + 'key_token' => 10, + 'key_end_token' => 10, + 'double_arrow_token' => 12, + 'raw' => '\'name\' => $name', + 'is_empty' => false, + 'assignment' => '$name', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$name', + 'assignment_token' => 14, + 'assignment_end_token' => 14, + ], + ], + ], + 'long-list-nested' => [ + '/* testNestedLongList */', + \T_LIST, + [ + 0 => [ + 'raw' => '$a', + 'is_empty' => false, + 'assignment' => '$a', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$a', + 'assignment_token' => 2, + 'assignment_end_token' => 2, + ], + 1 => [ + 'raw' => 'list($b, $c)', + 'is_empty' => false, + 'assignment' => 'list($b, $c)', + 'nested_list' => true, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => false, + 'assignment_token' => 5, + 'assignment_end_token' => 11, + ], + ], + ], + 'long-list-with-keys' => [ + '/* testLongListWithKeys */', + \T_LIST, + [ + 0 => [ + 'key' => "'name'", + 'key_token' => 2, + 'key_end_token' => 2, + 'double_arrow_token' => 4, + 'raw' => '\'name\' => $a', + 'is_empty' => false, + 'assignment' => '$a', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$a', + 'assignment_token' => 6, + 'assignment_end_token' => 6, + ], + 1 => [ + 'key' => "'id'", + 'key_token' => 9, + 'key_end_token' => 9, + 'double_arrow_token' => 11, + 'raw' => '\'id\' => $b', + 'is_empty' => false, + 'assignment' => '$b', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$b', + 'assignment_token' => 13, + 'assignment_end_token' => 13, + ], + 2 => [ + 'key' => "'field'", + 'key_token' => 16, + 'key_end_token' => 16, + 'double_arrow_token' => 18, + 'raw' => '\'field\' => $c', + 'is_empty' => false, + 'assignment' => '$c', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$c', + 'assignment_token' => 20, + 'assignment_end_token' => 20, + ], + ], + ], + 'long-list-with-empties' => [ + '/* testLongListWithEmptyEntries */', + \T_LIST, + [ + 0 => [ + 'raw' => '', + 'is_empty' => true, + ], + 1 => [ + 'raw' => '$a', + 'is_empty' => false, + 'assignment' => '$a', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$a', + 'assignment_token' => 5, + 'assignment_end_token' => 5, + ], + 2 => [ + 'raw' => '', + 'is_empty' => true, + ], + 3 => [ + 'raw' => '$b', + 'is_empty' => false, + 'assignment' => '$b', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$b', + 'assignment_token' => 10, + 'assignment_end_token' => 10, + ], + 4 => [ + 'raw' => '', + 'is_empty' => true, + ], + 5 => [ + 'raw' => '$c', + 'is_empty' => false, + 'assignment' => '$c', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$c', + 'assignment_token' => 14, + 'assignment_end_token' => 14, + ], + 6 => [ + 'raw' => '', + 'is_empty' => true, + ], + 7 => [ + 'raw' => '', + 'is_empty' => true, + ], + ], + ], + 'long-list-multi-line-keyed' => [ + '/* testLongListMultiLineKeyedWithTrailingComma */', + \T_LIST, + [ + 0 => [ + 'key' => '"name"', + 'key_token' => 4, + 'key_end_token' => 4, + 'double_arrow_token' => 6, + 'raw' => '"name" => $this->name', + 'is_empty' => false, + 'assignment' => '$this->name', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$this', + 'assignment_token' => 8, + 'assignment_end_token' => 10, + ], + 1 => [ + 'key' => '"colour"', + 'key_token' => 14, + 'key_end_token' => 14, + 'double_arrow_token' => 16, + 'raw' => '"colour" => $this->colour', + 'is_empty' => false, + 'assignment' => '$this->colour', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$this', + 'assignment_token' => 18, + 'assignment_end_token' => 20, + ], + 2 => [ + 'key' => '"age"', + 'key_token' => 24, + 'key_end_token' => 24, + 'double_arrow_token' => 26, + 'raw' => '"age" => $this->age', + 'is_empty' => false, + 'assignment' => '$this->age', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$this', + 'assignment_token' => 28, + 'assignment_end_token' => 30, + ], + 3 => [ + 'key' => '"cuteness"', + 'key_token' => 34, + 'key_end_token' => 34, + 'double_arrow_token' => 36, + 'raw' => '"cuteness" => $this->cuteness', + 'is_empty' => false, + 'assignment' => '$this->cuteness', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$this', + 'assignment_token' => 38, + 'assignment_end_token' => 40, + ], + 4 => [ + 'raw' => '', + 'is_empty' => true, + ], + ], + ], + 'short-list-with-keys-nested-lists' => [ + '/* testShortListWithKeysNestedLists */', + [\T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET], + [ + 0 => [ + 'key' => "'a'", + 'key_token' => 1, + 'key_end_token' => 1, + 'double_arrow_token' => 3, + 'raw' => '\'a\' => [&$a, $b]', + 'is_empty' => false, + 'assignment' => '[&$a, $b]', + 'nested_list' => true, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => false, + 'assignment_token' => 5, + 'assignment_end_token' => 11, + ], + 1 => [ + 'key' => "'b'", + 'key_token' => 14, + 'key_end_token' => 14, + 'double_arrow_token' => 16, + 'raw' => '\'b\' => [$c, &$d]', + 'is_empty' => false, + 'assignment' => '[$c, &$d]', + 'nested_list' => true, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => false, + 'assignment_token' => 18, + 'assignment_end_token' => 24, + ], + ], + ], + 'long-list-with-array-vars' => [ + '/* testLongListWithArrayVars */', + \T_LIST, + [ + 0 => [ + 'raw' => '$a[]', + 'is_empty' => false, + 'assignment' => '$a[]', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$a', + 'assignment_token' => 2, + 'assignment_end_token' => 4, + ], + 1 => [ + 'raw' => '$a[0]', + 'is_empty' => false, + 'assignment' => '$a[0]', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$a', + 'assignment_token' => 7, + 'assignment_end_token' => 10, + ], + 2 => [ + 'raw' => '$a[]', + 'is_empty' => false, + 'assignment' => '$a[]', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$a', + 'assignment_token' => 13, + 'assignment_end_token' => 15, + ], + ], + ], + 'short-list-multi-line-with-variable-keys' => [ + '/* testShortListMultiLineWithVariableKeys */', + \T_OPEN_SHORT_ARRAY, + [ + 0 => [ + 'key' => "'a' . 'b'", + 'key_token' => 3, + 'key_end_token' => 7, + 'double_arrow_token' => 9, + 'raw' => '\'a\' . \'b\' => $a', + 'is_empty' => false, + 'assignment' => '$a', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$a', + 'assignment_token' => 11, + 'assignment_end_token' => 11, + ], + 1 => [ + 'key' => '($a * 2)', + 'key_token' => 15, + 'key_end_token' => 21, + 'double_arrow_token' => 24, + 'raw' => '($a * 2) + => $b->prop->prop /* comment */ [\'index\']', + 'is_empty' => false, + 'assignment' => '$b->prop->prop [\'index\']', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$b', + 'assignment_token' => 26, + 'assignment_end_token' => 36, + ], + 2 => [ + 'key' => 'CONSTANT & OTHER', + 'key_token' => 40, + 'key_end_token' => 46, + 'double_arrow_token' => 48, + 'raw' => 'CONSTANT & /*comment*/ OTHER => $c', + 'is_empty' => false, + 'assignment' => '$c', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$c', + 'assignment_token' => 50, + 'assignment_end_token' => 50, + ], + 3 => [ + 'key' => '(string) &$c', + 'key_token' => 54, + 'key_end_token' => 57, + 'double_arrow_token' => 59, + 'raw' => '(string) &$c => &$d["D"]', + 'is_empty' => false, + 'assignment' => '$d["D"]', + 'nested_list' => false, + 'assign_by_reference' => true, + 'reference_token' => 61, + 'variable' => '$d', + 'assignment_token' => 62, + 'assignment_end_token' => 65, + ], + 4 => [ + 'key' => 'get_key()[1]', + 'key_token' => 69, + 'key_end_token' => 74, + 'double_arrow_token' => 76, + 'raw' => 'get_key()[1] => $e', + 'is_empty' => false, + 'assignment' => '$e', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$e', + 'assignment_token' => 78, + 'assignment_end_token' => 78, + ], + 5 => [ + 'key' => '$prop[\'index\']', + 'key_token' => 82, + 'key_end_token' => 85, + 'double_arrow_token' => 87, + 'raw' => '$prop[\'index\'] => $f->prop[\'index\']', + 'is_empty' => false, + 'assignment' => '$f->prop[\'index\']', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$f', + 'assignment_token' => 89, + 'assignment_end_token' => 94, + ], + 6 => [ + 'key' => '$obj->fieldname', + 'key_token' => 98, + 'key_end_token' => 100, + 'double_arrow_token' => 102, + 'raw' => '$obj->fieldname => ${$g}', + 'is_empty' => false, + 'assignment' => '${$g}', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => false, + 'assignment_token' => 104, + 'assignment_end_token' => 107, + ], + 7 => [ + 'key' => '$simple', + 'key_token' => 111, + 'key_end_token' => 111, + 'double_arrow_token' => 113, + 'raw' => '$simple => &$h', + 'is_empty' => false, + 'assignment' => '$h', + 'nested_list' => false, + 'assign_by_reference' => true, + 'reference_token' => 115, + 'variable' => '$h', + 'assignment_token' => 116, + 'assignment_end_token' => 116, + ], + 8 => [ + 'raw' => '', + 'is_empty' => true, + ], + ], + ], + 'long-list-with-close-parens-in-key' => [ + '/* testLongListWithCloseParensInKey */', + \T_LIST, + [ + 0 => [ + 'key' => 'get_key()[1]', + 'key_token' => 2, + 'key_end_token' => 7, + 'double_arrow_token' => 9, + 'raw' => 'get_key()[1] => &$e', + 'is_empty' => false, + 'assignment' => '$e', + 'nested_list' => false, + 'assign_by_reference' => true, + 'reference_token' => 11, + 'variable' => '$e', + 'assignment_token' => 12, + 'assignment_end_token' => 12, + ], + ], + ], + 'long-list-variable-vars' => [ + '/* testLongListVariableVar */', + \T_LIST, + [ + 0 => [ + 'raw' => '${$drink}', + 'is_empty' => false, + 'assignment' => '${$drink}', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => false, + 'assignment_token' => 3, + 'assignment_end_token' => 6, + ], + 1 => [ + 'raw' => '$foo->{$bar[\'baz\']}', + 'is_empty' => false, + 'assignment' => '$foo->{$bar[\'baz\']}', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$foo', + 'assignment_token' => 9, + 'assignment_end_token' => 16, + ], + ], + ], + 'long-list-keyed-with-nested-lists' => [ + '/* testLongListKeyedNestedLists */', + \T_LIST, + [ + 0 => [ + 'key' => "'a'", + 'key_token' => 4, + 'key_end_token' => 4, + 'double_arrow_token' => 6, + 'raw' => '\'a\' => + list("x" => $x1, "y" => $y1)', + 'is_empty' => false, + 'assignment' => 'list("x" => $x1, "y" => $y1)', + 'nested_list' => true, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => false, + 'assignment_token' => 9, + 'assignment_end_token' => 23, + ], + 1 => [ + 'key' => "'b'", + 'key_token' => 27, + 'key_end_token' => 27, + 'double_arrow_token' => 29, + 'raw' => '\'b\' => + list("x" => $x2, "y" => $y2)', + 'is_empty' => false, + 'assignment' => 'list("x" => $x2, "y" => $y2)', + 'nested_list' => true, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => false, + 'assignment_token' => 32, + 'assignment_end_token' => 46, + ], + ], + ], + 'parse-error-long-list-mixed-keyed-unkeyed' => [ + '/* testLongListMixedKeyedUnkeyed */', + \T_LIST, + [ + 0 => [ + 'raw' => '$unkeyed', + 'is_empty' => false, + 'assignment' => '$unkeyed', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$unkeyed', + 'assignment_token' => 2, + 'assignment_end_token' => 2, + ], + 1 => [ + 'key' => '"key"', + 'key_token' => 5, + 'key_end_token' => 5, + 'double_arrow_token' => 7, + 'raw' => '"key" => $keyed', + 'is_empty' => false, + 'assignment' => '$keyed', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$keyed', + 'assignment_token' => 9, + 'assignment_end_token' => 9, + ], + ], + ], + 'parse-error-short-list-empties-and-key' => [ + '/* testShortListWithEmptiesAndKey */', + \T_OPEN_SHORT_ARRAY, + [ + 0 => [ + 'raw' => '', + 'is_empty' => true, + ], + 1 => [ + 'raw' => '', + 'is_empty' => true, + ], + 2 => [ + 'raw' => '', + 'is_empty' => true, + ], + 3 => [ + 'raw' => '', + 'is_empty' => true, + ], + 4 => [ + 'key' => '"key"', + 'key_token' => 6, + 'key_end_token' => 6, + 'double_arrow_token' => 8, + 'raw' => '"key" => $keyed', + 'is_empty' => false, + 'assignment' => '$keyed', + 'nested_list' => false, + 'assign_by_reference' => false, + 'reference_token' => false, + 'variable' => '$keyed', + 'assignment_token' => 10, + 'assignment_end_token' => 10, + ], + ], + ], + ]; + } +} diff --git a/Tests/Utils/Lists/GetOpenCloseTest.inc b/Tests/Utils/Lists/GetOpenCloseTest.inc new file mode 100644 index 00000000..c6774e38 --- /dev/null +++ b/Tests/Utils/Lists/GetOpenCloseTest.inc @@ -0,0 +1,25 @@ + 'bar']; + +/* testArrayAccess */ +echo $array['index']; + +/* testLongList */ +list($a, /* testNestedLongList */ list ( $b )) = $array; + +/* testShortList */ +[$a, /* testNestedShortList */ [$b]] = $array; + +/* testListWithCommentsAndAnnotations */ +list // Comment. + /* phpcs:ignore Stnd.Cat.Sniff -- For reasons. */ + ( + $a, + $b, + ) = $array; + +/* testParseError */ +// Intentional parse error. This has to be the last test in the file. +list( $a diff --git a/Tests/Utils/Lists/GetOpenCloseTest.php b/Tests/Utils/Lists/GetOpenCloseTest.php new file mode 100644 index 00000000..f2d0c0cb --- /dev/null +++ b/Tests/Utils/Lists/GetOpenCloseTest.php @@ -0,0 +1,169 @@ +assertFalse(Lists::getOpenClose(self::$phpcsFile, 100000)); + } + + /** + * Test that false is returned when a non-(short) list token is passed. + * + * @dataProvider dataNotListToken + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @return void + */ + public function testNotListToken($testMarker) + { + $target = $this->getTargetToken($testMarker, [\T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET]); + $this->assertFalse(Lists::getOpenClose(self::$phpcsFile, $target)); + } + + /** + * Data provider. + * + * @see testNotListToken() For the array format. + * + * @return array + */ + public function dataNotListToken() + { + return [ + 'short-array' => ['/* testShortArray */'], + 'array-access-square-bracket' => ['/* testArrayAccess */'], + ]; + } + + /** + * Test retrieving the open/close tokens for a list. + * + * @dataProvider dataGetOpenClose + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string|array $targetToken The token type(s) to look for. + * @param array|false $expected The expected function return value. + * + * @return void + */ + public function testGetOpenClose($testMarker, $targetToken, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, $targetToken); + + // Convert offsets to absolute positions. + if (isset($expected['opener'], $expected['closer'])) { + $expected['opener'] += $stackPtr; + $expected['closer'] += $stackPtr; + } + + $result = Lists::getOpenClose(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * The opener/closer positions are provided as offsets from the target stackPtr. + * + * @see testGetOpenClose() For the array format. + * + * @return array + */ + public function dataGetOpenClose() + { + return [ + 'long-list' => [ + '/* testLongList */', + \T_LIST, + [ + 'opener' => 1, + 'closer' => 14, + ], + ], + 'long-list-nested' => [ + '/* testNestedLongList */', + \T_LIST, + [ + 'opener' => 2, + 'closer' => 6, + ], + ], + 'short-list' => [ + '/* testShortList */', + \T_OPEN_SHORT_ARRAY, + [ + 'opener' => 0, + 'closer' => 9, + ], + ], + 'short-list-nested' => [ + '/* testNestedShortList */', + \T_OPEN_SHORT_ARRAY, + [ + 'opener' => 0, + 'closer' => 2, + ], + ], + 'long-list-with-comments-and-annotations' => [ + '/* testListWithCommentsAndAnnotations */', + \T_LIST, + [ + 'opener' => 7, + 'closer' => 18, + ], + ], + 'parse-error' => [ + '/* testParseError */', + \T_LIST, + false, + ], + ]; + } + + /** + * Test retrieving the open/close tokens for a nested list, skipping the short list check. + * + * @return void + */ + public function testGetOpenCloseThirdParam() + { + $stackPtr = $this->getTargetToken('/* testNestedShortList */', \T_OPEN_SHORT_ARRAY); + $expected = [ + 'opener' => $stackPtr, + 'closer' => ($stackPtr + 2), + ]; + + $result = Lists::getOpenClose(self::$phpcsFile, $stackPtr, true); + $this->assertSame($expected, $result); + } +} diff --git a/Tests/Utils/Lists/IsShortListTest.inc b/Tests/Utils/Lists/IsShortListTest.inc new file mode 100644 index 00000000..9c41956e --- /dev/null +++ b/Tests/Utils/Lists/IsShortListTest.inc @@ -0,0 +1,87 @@ + [$id, $name, $info]) {} + +foreach ($array as [$a, /* testShortListInForeachNested */ [$b, $c]]) {} + +/* testMultiAssignShortlist */ +$foo = [$baz, $bat] = [$a, $b]; + +/* testShortListWithKeys */ +["id" => $id1, "name" => $name1] = $data[0]; + +/* testShortListInForeachWithKeysDetectOnCloseBracket */ +foreach ($data as ["id" => $id, "name" => $name]) {} + +echo 'just here to prevent the below test running into a tokenizer issue tested separately'; + +// Invalid as empty lists are not allowed, but it is short list syntax. +[$x, /* testNestedShortListEmpty */ [], $y] = $a; + +[$x, [ $y, /* testDeeplyNestedShortList */ [$z]], $q] = $a; + +/* testShortListWithNestingAndKeys */ +[ + /* testNestedShortListWithKeys_1 */ + ["x" => $x1, "y" => $y1], + /* testNestedShortListWithKeys_2 */ + ["x" => $x2, "y" => $y2], + /* testNestedShortListWithKeys_3 */ + ["x" => $x3, "y" => $y3], +] = $points; + +/* testShortListWithoutVars */ +// Invalid list as it doesn't contain variables, but it is short list syntax. +[42] = [1]; + +/* testShortListNestedLongList */ +// Invalid list as mixing short list syntax with list() is not allowed, but it is short list syntax. +[list($a, $b), list($c, $d)] = [[1, 2], [3, 4]]; + +/* testNestedAnonClassWithTraitUseAs */ +// Parse error, but not short list syntax. +array_map(function($a) { + return new class() { + use MyTrait { + MyTrait::functionName as []; + } + }; +}, $array); + +/* testParseError */ +// Parse error, but not short list syntax. +use Something as [$a]; + +/* testLiveCoding */ +// Intentional parse error. This has to be the last test in the file. +[$a, [$b] diff --git a/Tests/Utils/Lists/IsShortListTest.php b/Tests/Utils/Lists/IsShortListTest.php new file mode 100644 index 00000000..8e86d78b --- /dev/null +++ b/Tests/Utils/Lists/IsShortListTest.php @@ -0,0 +1,208 @@ +assertFalse(Lists::isShortList(self::$phpcsFile, 100000)); + } + + /** + * Test that false is returned when a non-short array token is passed. + * + * @dataProvider dataNotShortArrayToken + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string|array $targetToken The token type(s) to look for. + * + * @return void + */ + public function testNotShortArrayToken($testMarker, $targetToken) + { + $target = $this->getTargetToken($testMarker, $targetToken); + $this->assertFalse(Lists::isShortList(self::$phpcsFile, $target)); + } + + /** + * Data provider. + * + * @see testNotShortArrayToken() For the array format. + * + * @return array + */ + public function dataNotShortArrayToken() + { + return [ + 'long-list' => [ + '/* testLongList */', + \T_LIST, + ], + 'array-assignment' => [ + '/* testArrayAssignment */', + \T_CLOSE_SQUARE_BRACKET, + ], + 'live-coding' => [ + '/* testLiveCoding */', + \T_OPEN_SQUARE_BRACKET, + ], + ]; + } + + /** + * Test whether a T_OPEN_SHORT_ARRAY token is a short list. + * + * @dataProvider dataIsShortList + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param bool $expected The expected boolean return value. + * @param int|string|array $targetToken The token type(s) to test. Defaults to T_OPEN_SHORT_ARRAY. + * + * @return void + */ + public function testIsShortList($testMarker, $expected, $targetToken = \T_OPEN_SHORT_ARRAY) + { + $stackPtr = $this->getTargetToken($testMarker, $targetToken); + $result = Lists::isShortList(self::$phpcsFile, $stackPtr); + + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testIsShortList() For the array format. + * + * @return array + */ + public function dataIsShortList() + { + return [ + 'short-array-not-nested' => [ + '/* testNonNestedShortArray */', + false, + ], + 'comparison-no-assignment' => [ + '/* testNoAssignment */', + false, + ], + 'comparison-no-assignment-nested' => [ + '/* testNestedNoAssignment */', + false, + ], + 'short-array-in-foreach' => [ + '/* testShortArrayInForeach */', + false, + ], + 'short-list' => [ + '/* testShortList */', + true, + ], + 'short-list-detect-on-close-bracket' => [ + '/* testShortListDetectOnCloseBracket */', + true, + \T_CLOSE_SHORT_ARRAY, + ], + 'short-list-with-nesting' => [ + '/* testShortListWithNesting */', + true, + ], + 'short-list-nested' => [ + '/* testNestedShortList */', + true, + ], + 'short-list-in-foreach' => [ + '/* testShortListInForeach */', + true, + ], + 'short-list-in-foreach-with-key' => [ + '/* testShortListInForeachWithKey */', + true, + ], + 'short-list-in-foreach-nested' => [ + '/* testShortListInForeachNested */', + true, + ], + 'short-list-chained-assignment' => [ + '/* testMultiAssignShortlist */', + true, + ], + 'short-list-with-keys' => [ + '/* testShortListWithKeys */', + true, + ], + 'short-list-in-foreach-with-keys-detect-on-close-bracket' => [ + '/* testShortListInForeachWithKeysDetectOnCloseBracket */', + true, + \T_CLOSE_SHORT_ARRAY, + ], + 'short-list-nested-empty' => [ + '/* testNestedShortListEmpty */', + true, + ], + 'short-list-deeply-nested' => [ + '/* testDeeplyNestedShortList */', + true, + ], + 'short-list-with-nesting-and-keys' => [ + '/* testShortListWithNestingAndKeys */', + true, + ], + 'short-list-nested-with-keys-1' => [ + '/* testNestedShortListWithKeys_1 */', + true, + ], + 'short-list-nested-with-keys-2' => [ + '/* testNestedShortListWithKeys_2 */', + true, + ], + 'short-list-nested-with-keys-3' => [ + '/* testNestedShortListWithKeys_3 */', + true, + ], + 'short-list-without-vars' => [ + '/* testShortListWithoutVars */', + true, + ], + 'short-list-nested-long-list' => [ + '/* testShortListNestedLongList */', + true, + ], + 'parse-error-anon-class-trait-use-as' => [ + '/* testNestedAnonClassWithTraitUseAs */', + false, + ], + 'parse-error-use-as' => [ + '/* testParseError */', + false, + ], + ]; + } +} diff --git a/Tests/Utils/Lists/IsShortListTokenizerBC1Test.inc b/Tests/Utils/Lists/IsShortListTokenizerBC1Test.inc new file mode 100644 index 00000000..2f8a7dbe --- /dev/null +++ b/Tests/Utils/Lists/IsShortListTokenizerBC1Test.inc @@ -0,0 +1,31 @@ +addedCustomFunctions['nonce']; + +/* testTokenizerIssue1381PHPCSlt290D1 */ +echo $this->deprecated_functions[ $function_name ]/* testTokenizerIssue1381PHPCSlt290D2 */['version']; + +/* testTokenizerIssue1284PHPCSlt280A */ +if ($foo) {} +[$a, $b] = $c; + +/* testTokenizerIssue1284PHPCSlt280B */ +if ($foo) {} +[$a, $b]; + +/* testTokenizerIssue1284PHPCSlt290C */ +$foo = ${$bar}['key']; + +/* testTokenizerIssue1284PHPCSlt280D */ +$c->{$var}[ ] = 2; diff --git a/Tests/Utils/Lists/IsShortListTokenizerBC1Test.php b/Tests/Utils/Lists/IsShortListTokenizerBC1Test.php new file mode 100644 index 00000000..4dbbbc8c --- /dev/null +++ b/Tests/Utils/Lists/IsShortListTokenizerBC1Test.php @@ -0,0 +1,107 @@ +getTargetToken($testMarker, [\T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET]); + $result = Lists::isShortList(self::$phpcsFile, $stackPtr); + + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testIsShortList() For the array format. + * + * @return array + */ + public function dataIsShortList() + { + return [ + 'issue-1971-list-first-in-file' => [ + '/* testTokenizerIssue1971PHPCSlt330gt271A */', + true, + ], + 'issue-1971-list-first-in-file-nested' => [ + '/* testTokenizerIssue1971PHPCSlt330gt271B */', + true, + ], + 'issue-1381-array-dereferencing-1' => [ + '/* testTokenizerIssue1381PHPCSlt290A1 */', + false, + ], + 'issue-1381-array-dereferencing-1-deref' => [ + '/* testTokenizerIssue1381PHPCSlt290A2 */', + false, + ], + 'issue-1381-array-dereferencing-2' => [ + '/* testTokenizerIssue1381PHPCSlt290B */', + false, + ], + 'issue-1381-array-dereferencing-3' => [ + '/* testTokenizerIssue1381PHPCSlt290C */', + false, + ], + 'issue-1381-array-dereferencing-4' => [ + '/* testTokenizerIssue1381PHPCSlt290D1 */', + false, + ], + 'issue-1381-array-dereferencing-4-deref-deref' => [ + '/* testTokenizerIssue1381PHPCSlt290D2 */', + false, + ], + 'issue-1284-short-list-directly-after-close-curly-control-structure' => [ + '/* testTokenizerIssue1284PHPCSlt280A */', + true, + ], + 'issue-1284-short-array-directly-after-close-curly-control-structure' => [ + '/* testTokenizerIssue1284PHPCSlt280B */', + false, + ], + 'issue-1284-array-access-variable-variable' => [ + '/* testTokenizerIssue1284PHPCSlt290C */', + false, + ], + 'issue-1284-array-access-variable-property' => [ + '/* testTokenizerIssue1284PHPCSlt280D */', + false, + ], + ]; + } +} diff --git a/Tests/Utils/Lists/IsShortListTokenizerBC2Test.inc b/Tests/Utils/Lists/IsShortListTokenizerBC2Test.inc new file mode 100644 index 00000000..56db4b44 --- /dev/null +++ b/Tests/Utils/Lists/IsShortListTokenizerBC2Test.inc @@ -0,0 +1,5 @@ +getTargetToken($testMarker, [\T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET]); + $result = Lists::isShortList(self::$phpcsFile, $stackPtr); + + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testIsShortList() For the array format. + * + * @return array + */ + public function dataIsShortList() + { + return [ + // Make sure the utility method does not throw false positives for a short array at the start of a file. + 'issue-1971-short-array-first-in-file' => [ + '/* testTokenizerIssue1971PHPCSlt330gt271C */', + false, + ], + 'issue-1971-short-array-first-in-file-nested' => [ + '/* testTokenizerIssue1971PHPCSlt330gt271D */', + false, + ], + ]; + } +} diff --git a/Tests/Utils/Namespaces/DetermineNamespaceTest.inc b/Tests/Utils/Namespaces/DetermineNamespaceTest.inc new file mode 100644 index 00000000..911813af --- /dev/null +++ b/Tests/Utils/Namespaces/DetermineNamespaceTest.inc @@ -0,0 +1,126 @@ +assertFalse(Namespaces::findNamespacePtr(self::$phpcsFile, 100000)); + } + + /** + * Test finding the correct namespace token for an arbitrary token in a file. + * + * @dataProvider dataDetermineNamespace + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected The expected output for the functions. + * + * @return void + */ + public function testFindNamespacePtr($testMarker, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, \T_ECHO); + + if ($expected['ptr'] !== false) { + $expected['ptr'] = $this->getTargetToken($expected['ptr'], \T_NAMESPACE); + } + + $result = Namespaces::findNamespacePtr(self::$phpcsFile, $stackPtr); + + $this->assertSame($expected['ptr'], $result); + } + + /** + * Test retrieving the applicable namespace name for an arbitrary token in a file. + * + * @dataProvider dataDetermineNamespace + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected The expected output for the functions. + * + * @return void + */ + public function testDetermineNamespace($testMarker, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, \T_ECHO); + $result = Namespaces::determineNamespace(self::$phpcsFile, $stackPtr); + + $this->assertSame($expected['name'], $result); + } + + /** + * Data provider. + * + * @see testDetermineNamespace() For the array format. + * + * @return array + */ + public function dataDetermineNamespace() + { + return [ + 'no-namespace' => [ + '/* testNoNamespace */', + [ + 'ptr' => false, + 'name' => '', + ], + ], + 'no-namespace-nested' => [ + '/* testNoNamespaceNested */', + [ + 'ptr' => false, + 'name' => '', + ], + ], + 'non-scoped-namespace-1' => [ + '/* testNonScopedNamedNamespace1 */', + [ + 'ptr' => '/* Non-scoped named namespace 1 */', + 'name' => 'Vendor\Package\Baz', + ], + ], + 'non-scoped-namespace-1-nested' => [ + '/* testNonScopedNamedNamespace1Nested */', + [ + 'ptr' => '/* Non-scoped named namespace 1 */', + 'name' => 'Vendor\Package\Baz', + ], + ], + 'global-namespace-scoped' => [ + '/* testGlobalNamespaceScoped */', + [ + 'ptr' => '/* Scoped global namespace */', + 'name' => '', + ], + ], + 'global-namespace-scoped-nested' => [ + '/* testGlobalNamespaceScopedNested */', + [ + 'ptr' => '/* Scoped global namespace */', + 'name' => '', + ], + ], + 'no-namespace-after-unnamed-scoped' => [ + '/* testNoNamespaceAfterUnnamedScoped */', + [ + 'ptr' => false, + 'name' => '', + ], + ], + 'no-namespace-nested-after-unnamed-scoped' => [ + '/* testNoNamespaceNestedAfterUnnamedScoped */', + [ + 'ptr' => false, + 'name' => '', + ], + ], + 'named-namespace-scoped' => [ + '/* testNamedNamespaceScoped */', + [ + 'ptr' => '/* Scoped named namespace */', + 'name' => 'Vendor\Package\Foo', + ], + ], + 'named-namespace-scoped-nested' => [ + '/* testNamedNamespaceScopedNested */', + [ + 'ptr' => '/* Scoped named namespace */', + 'name' => 'Vendor\Package\Foo', + ], + ], + 'no-namespace-after-named-scoped' => [ + '/* testNoNamespaceAfterNamedScoped */', + [ + 'ptr' => false, + 'name' => '', + ], + ], + 'no-namespace-nested-after-named-scoped' => [ + '/* testNoNamespaceNestedAfterNamedScoped */', + [ + 'ptr' => false, + 'name' => '', + ], + ], + 'non-scoped-namespace-2' => [ + '/* testNonScopedNamedNamespace2 */', + [ + 'ptr' => '/* Non-scoped named namespace 2 */', + 'name' => 'Vendor\Package\Foz', + ], + ], + 'non-scoped-namespace-2-nested' => [ + '/* testNonScopedNamedNamespace2Nested */', + [ + 'ptr' => '/* Non-scoped named namespace 2 */', + 'name' => 'Vendor\Package\Foz', + ], + ], + ]; + } + + /** + * Test that the namespace declaration itself is not regarded as being namespaced. + * + * @return void + */ + public function testNamespaceDeclarationIsNotNamespaced() + { + $stackPtr = $this->getTargetToken('/* Non-scoped named namespace 2 */', \T_NAMESPACE); + $result = Namespaces::findNamespacePtr(self::$phpcsFile, $stackPtr); + $this->assertFalse($result, 'Failed checking that namespace declaration token is not regarded as namespaced'); + + $stackPtr = $this->getTargetToken('/* Non-scoped named namespace 2 */', \T_STRING, 'Package'); + $result = Namespaces::findNamespacePtr(self::$phpcsFile, $stackPtr); + $this->assertFalse($result, 'Failed checking that a token in the namespace name is not regarded as namespaced'); + } + + /** + * Test returning an empty string if the namespace could not be determined (parse error). + * + * @return void + */ + public function testFallbackToEmptyString() + { + $stackPtr = $this->getTargetToken('/* testParseError */', \T_COMMENT, '/* comment */'); + + $expected = $this->getTargetToken('/* testParseError */', \T_NAMESPACE); + $result = Namespaces::findNamespacePtr(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result, 'Failed test with findNamespacePtr'); + + $result = Namespaces::determineNamespace(self::$phpcsFile, $stackPtr, false); + $this->assertSame('', $result, 'Failed test with determineNamespace'); + } +} diff --git a/Tests/Utils/Namespaces/GetDeclaredNameTest.inc b/Tests/Utils/Namespaces/GetDeclaredNameTest.inc new file mode 100644 index 00000000..d2180e2e --- /dev/null +++ b/Tests/Utils/Namespaces/GetDeclaredNameTest.inc @@ -0,0 +1,49 @@ + + +assertFalse(Namespaces::getDeclaredName(self::$phpcsFile, 100000), 'Failed with non-existent token'); + + // Non namespace token. + $this->assertFalse(Namespaces::getDeclaredName(self::$phpcsFile, 0), 'Failed with non-namespace token'); + } + + /** + * Test retrieving the cleaned up namespace name based on a T_NAMESPACE token. + * + * @dataProvider dataGetDeclaredName + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected The expected output for the function. + * + * @return void + */ + public function testGetDeclaredNameClean($testMarker, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, \T_NAMESPACE); + $result = Namespaces::getDeclaredName(self::$phpcsFile, $stackPtr, true); + + $this->assertSame($expected['clean'], $result); + } + + /** + * Test retrieving the "dirty" namespace name based on a T_NAMESPACE token. + * + * @dataProvider dataGetDeclaredName + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected The expected output for the function. + * + * @return void + */ + public function testGetDeclaredNameDirty($testMarker, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, \T_NAMESPACE); + $result = Namespaces::getDeclaredName(self::$phpcsFile, $stackPtr, false); + + $this->assertSame($expected['dirty'], $result); + } + + /** + * Data provider. + * + * @see testGetDeclaredName() For the array format. + * + * @return array + */ + public function dataGetDeclaredName() + { + return [ + 'global-namespace-curlies' => [ + '/* testGlobalNamespaceCurlies */', + [ + 'clean' => '', + 'dirty' => '', + ], + ], + 'namespace-semicolon' => [ + '/* testNamespaceSemiColon */', + [ + 'clean' => 'Vendor', + 'dirty' => 'Vendor', + ], + ], + 'namespace-curlies' => [ + '/* testNamespaceCurlies */', + [ + 'clean' => 'Vendor\Package\Sublevel\End', + 'dirty' => 'Vendor\Package\Sublevel\End', + ], + ], + 'namespace-curlies-no-space-at-end' => [ + '/* testNamespaceCurliesNoSpaceAtEnd */', + [ + 'clean' => 'Vendor\Package\Sublevel\Deeperlevel\End', + 'dirty' => 'Vendor\Package\Sublevel\Deeperlevel\End', + ], + ], + 'namespace-close-tag' => [ + '/* testNamespaceCloseTag */', + [ + 'clean' => 'My\Name', + 'dirty' => 'My\Name', + ], + ], + 'namespace-close-tag-no-space-at-end' => [ + '/* testNamespaceCloseTagNoSpaceAtEnd */', + [ + 'clean' => 'My\Other\Name', + 'dirty' => 'My\Other\Name', + ], + ], + 'namespace-whitespace-tolerance' => [ + '/* testNamespaceLotsOfWhitespace */', + [ + 'clean' => 'Vendor\Package\Sub\Deeperlevel\End', + 'dirty' => 'Vendor \ + Package\ + Sub \ + Deeperlevel\ + End', + ], + ], + 'namespace-with-comments-and-annotations' => [ + '/* testNamespaceWithCommentsWhitespaceAndAnnotations */', + [ + 'clean' => 'Vendor\Package\Sublevel\Deeper\End', + 'dirty' => 'Vendor\/*comment*/ + Package\Sublevel \ //phpcs:ignore Standard.Category.Sniff -- for reasons. + Deeper\ // Another comment + End', + ], + ], + 'namespace-operator' => [ + '/* testNamespaceOperator */', + [ + 'clean' => false, + 'dirty' => false, + ], + ], + 'parse-error-reserved-keywords' => [ + '/* testParseErrorReservedKeywords */', + [ + 'clean' => 'Vendor\while\Package\protected\name\try\this', + 'dirty' => 'Vendor\while\Package\protected\name\try\this', + ], + ], + 'parse-error-semicolon' => [ + '/* testParseErrorSemiColon */', + [ + 'clean' => false, + 'dirty' => false, + ], + ], + 'live-coding' => [ + '/* testLiveCoding */', + [ + 'clean' => false, + 'dirty' => false, + ], + ], + ]; + } +} diff --git a/Tests/Utils/Namespaces/NamespaceTypeTest.inc b/Tests/Utils/Namespaces/NamespaceTypeTest.inc new file mode 100644 index 00000000..f2927f8e --- /dev/null +++ b/Tests/Utils/Namespaces/NamespaceTypeTest.inc @@ -0,0 +1,57 @@ +expectPhpcsException('$stackPtr must be of type T_NAMESPACE'); + + Namespaces::getType(self::$phpcsFile, 100000); + } + + /** + * Test receiving an expected exception when passing a non T_NAMESPACE token. + * + * @return void + */ + public function testNonNamespaceToken() + { + $this->expectPhpcsException('$stackPtr must be of type T_NAMESPACE'); + + Namespaces::getType(self::$phpcsFile, 0); + } + + /** + * Test whether a T_NAMESPACE token is used as the keyword for a namespace declaration. + * + * @dataProvider dataNamespaceType + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected The expected output for the functions. + * + * @return void + */ + public function testIsDeclaration($testMarker, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, \T_NAMESPACE); + $result = Namespaces::isDeclaration(self::$phpcsFile, $stackPtr); + + $this->assertSame($expected['declaration'], $result); + } + + /** + * Test whether a T_NAMESPACE token is used as an operator. + * + * @dataProvider dataNamespaceType + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected The expected output for the functions. + * + * @return void + */ + public function testIsOperator($testMarker, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, \T_NAMESPACE); + $result = Namespaces::isOperator(self::$phpcsFile, $stackPtr); + + $this->assertSame($expected['operator'], $result); + } + + /** + * Data provider. + * + * @see testIsDeclaration() For the array format. + * @see testIsOperator() For the array format. + * + * @return array + */ + public function dataNamespaceType() + { + return [ + 'namespace-declaration' => [ + '/* testNamespaceDeclaration */', + [ + 'declaration' => true, + 'operator' => false, + ], + ], + 'namespace-declaration-with-comment' => [ + '/* testNamespaceDeclarationWithComment */', + [ + 'declaration' => true, + 'operator' => false, + ], + ], + 'namespace-declaration-scoped' => [ + '/* testNamespaceDeclarationScoped */', + [ + 'declaration' => true, + 'operator' => false, + ], + ], + 'namespace-operator' => [ + '/* testNamespaceOperator */', + [ + 'declaration' => false, + 'operator' => true, + ], + ], + 'namespace-operator-with-annotation' => [ + '/* testNamespaceOperatorWithAnnotation */', + [ + 'declaration' => false, + 'operator' => true, + ], + ], + 'namespace-operator-in-conditional' => [ + '/* testNamespaceOperatorInConditional */', + [ + 'declaration' => false, + 'operator' => true, + ], + ], + 'namespace-operator-in-closed-scope' => [ + '/* testNamespaceOperatorInClosedScope */', + [ + 'declaration' => false, + 'operator' => true, + ], + ], + 'namespace-operator-in-parentheses' => [ + '/* testNamespaceOperatorInParentheses */', + [ + 'declaration' => false, + 'operator' => true, + ], + ], + 'parse-error-scoped-namespace-declaration' => [ + '/* testParseErrorScopedNamespaceDeclaration */', + [ + 'declaration' => false, + 'operator' => false, + ], + ], + 'parse-error-conditional-namespace' => [ + '/* testParseErrorConditionalNamespace */', + [ + 'declaration' => false, + 'operator' => false, + ], + ], + + 'fatal-error-declaration-leading-slash' => [ + '/* testFatalErrorDeclarationLeadingSlash */', + [ + 'declaration' => false, + 'operator' => false, + ], + ], + 'parse-error-double-colon' => [ + '/* testParseErrorDoubleColon */', + [ + 'declaration' => false, + 'operator' => false, + ], + ], + 'parse-error-semicolon' => [ + '/* testParseErrorSemiColon */', + [ + 'declaration' => false, + 'operator' => false, + ], + ], + 'live-coding' => [ + '/* testLiveCoding */', + [ + 'declaration' => false, + 'operator' => false, + ], + ], + ]; + } +} diff --git a/Tests/Utils/Numbers/GetCompleteNumberTest.inc b/Tests/Utils/Numbers/GetCompleteNumberTest.inc new file mode 100644 index 00000000..475b68ac --- /dev/null +++ b/Tests/Utils/Numbers/GetCompleteNumberTest.inc @@ -0,0 +1,124 @@ +'); + $maxUnsupported = \max(\array_keys(Numbers::$unsupportedPHPCSVersions)); + self::$usableBackfill = \version_compare(self::$phpcsVersion, $maxUnsupported, '>'); + } + + /** + * Test receiving an exception when a non-numeric token is passed to the method. + * + * @return void + */ + public function testNotANumberException() + { + $this->expectPhpcsException('Token type "T_STRING" is not T_LNUMBER or T_DNUMBER'); + + $stackPtr = $this->getTargetToken('/* testNotAnLNumber */', \T_STRING); + Numbers::getCompleteNumber(self::$phpcsFile, $stackPtr); + } + + /** + * Test receiving an exception when PHPCS is run on PHP < 7.4 in combination with PHPCS 3.5.3. + * + * @return void + */ + public function testUnsupportedPhpcsException() + { + self::setUpStaticProperties(); + if (isset(Numbers::$unsupportedPHPCSVersions[self::$phpcsVersion]) === false) { + $this->markTestSkipped('Test specific to a limited set of PHPCS versions'); + } + + $this->expectPhpcsException( + 'The PHPCSUtils\Utils\Numbers::getCompleteNumber() method does not support PHPCS ' + ); + + $stackPtr = $this->getTargetToken('/* testPHP74IntDecimalMultiUnderscore */', \T_LNUMBER); + Numbers::getCompleteNumber(self::$phpcsFile, $stackPtr); + } + + /** + * Test correctly identifying all tokens belonging to a numeric literal. + * + * @dataProvider dataGetCompleteNumber + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected Expected function return value. + * + * @return void + */ + public function testGetCompleteNumber($testMarker, $expected) + { + // Skip the test(s) on unsupported PHPCS versions. + self::setUpStaticProperties(); + if (isset(Numbers::$unsupportedPHPCSVersions[self::$phpcsVersion]) === true) { + $this->markTestSkipped( + 'PHPCS ' . self::$phpcsVersion . ' is not supported due to buggy numeric string literal backfill.' + ); + } + + $stackPtr = $this->getTargetToken($testMarker, [\T_LNUMBER, \T_DNUMBER]); + $expected['last_token'] += $stackPtr; + + // Allow for 32 vs 64-bit systems with different maximum integer size. + if ($expected['code'] === \T_LNUMBER && ($expected['decimal'] + 0) > \PHP_INT_MAX) { + $expected['code'] = \T_DNUMBER; + $expected['type'] = 'T_DNUMBER'; + } + + $result = Numbers::getCompleteNumber(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result); + } + + /** + * Data Provider. + * + * @see testGetCompleteNumber() For the array format. + * + * @return array + */ + public function dataGetCompleteNumber() + { + self::setUpStaticProperties(); + $multiToken = true; + if (self::$php74OrHigher === true || self::$usableBackfill === true) { + $multiToken = false; + } + + /* + * Disabling the hexnumeric string detection for the rest of the file. + * These are only strings within the context of PHPCS and need to be tested as such. + * + * @phpcs:disable PHPCompatibility.Miscellaneous.ValidIntegers.HexNumericStringFound + */ + + return [ + // Ordinary numbers. + 'normal-integer-decimal' => [ + '/* testIntDecimal */', + [ + 'orig_content' => '1000000000', + 'content' => '1000000000', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '1000000000', + 'last_token' => 0, // Offset from $stackPtr. + ], + ], + 'normal-float' => [ + '/* testFloat */', + [ + 'orig_content' => '107925284.88', + 'content' => '107925284.88', + 'code' => \T_DNUMBER, + 'type' => 'T_DNUMBER', + 'decimal' => '107925284.88', + 'last_token' => 0, // Offset from $stackPtr. + ], + ], + 'normal-float-negative' => [ + '/* testFloatNegative */', + [ + 'orig_content' => '58987.789', + 'content' => '58987.789', + 'code' => \T_DNUMBER, + 'type' => 'T_DNUMBER', + 'decimal' => '58987.789', + 'last_token' => 0, // Offset from $stackPtr. + ], + ], + 'normal-integer-binary' => [ + '/* testIntBinary */', + [ + 'orig_content' => '0b1', + 'content' => '0b1', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '1', + 'last_token' => 0, // Offset from $stackPtr. + ], + ], + 'normal-integer-hex' => [ + '/* testIntHex */', + [ + 'orig_content' => '0xA', + 'content' => '0xA', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '10', + 'last_token' => 0, // Offset from $stackPtr. + ], + ], + 'normal-integer-octal' => [ + '/* testIntOctal */', + [ + 'orig_content' => '052', + 'content' => '052', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '42', + 'last_token' => 0, // Offset from $stackPtr. + ], + ], + + // Parse error. + 'parse-error' => [ + '/* testParseError */', + [ + 'orig_content' => '100', + 'content' => '100', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '100', + 'last_token' => 0, // Offset from $stackPtr. + ], + ], + + // Numeric literal with underscore. + 'php-7.4-integer-decimal-multi-underscore' => [ + '/* testPHP74IntDecimalMultiUnderscore */', + [ + 'orig_content' => '1_000_000_000', + 'content' => '1000000000', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '1000000000', + 'last_token' => $multiToken ? 1 : 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-float' => [ + '/* testPHP74Float */', + [ + 'orig_content' => '107_925_284.88', + 'content' => '107925284.88', + 'code' => \T_DNUMBER, + 'type' => 'T_DNUMBER', + 'decimal' => '107925284.88', + 'last_token' => $multiToken ? 2 : 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-integer-decimal-single-underscore' => [ + '/* testPHP74IntDecimalSingleUnderscore */', + [ + 'orig_content' => '135_00', + 'content' => '13500', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '13500', + 'last_token' => $multiToken ? 1 : 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-float-exponent-negative' => [ + '/* testPHP74FloatExponentNegative */', + [ + 'orig_content' => '6.674_083e-11', + 'content' => '6.674083e-11', + 'code' => \T_DNUMBER, + 'type' => 'T_DNUMBER', + 'decimal' => '6.674083e-11', + 'last_token' => $multiToken ? 3 : 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-float-exponent-positive' => [ + '/* testPHP74FloatExponentPositive */', + [ + 'orig_content' => '6.674_083e+11', + 'content' => '6.674083e+11', + 'code' => \T_DNUMBER, + 'type' => 'T_DNUMBER', + 'decimal' => '6.674083e+11', + 'last_token' => $multiToken ? 3 : 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-integer-decimal-multi-underscore-2' => [ + '/* testPHP74IntDecimalMultiUnderscore2 */', + [ + 'orig_content' => '299_792_458', + 'content' => '299792458', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '299792458', + 'last_token' => $multiToken ? 1 : 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-integer-hex' => [ + '/* testPHP74IntHex */', + [ + 'orig_content' => '0xCAFE_F00D', + 'content' => '0xCAFEF00D', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '3405705229', + 'last_token' => $multiToken ? 1 : 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-integer-binary' => [ + '/* testPHP74IntBinary */', + [ + 'orig_content' => '0b0101_1111', + 'content' => '0b01011111', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '95', + 'last_token' => $multiToken ? 1 : 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-integer-octal' => [ + '/* testPHP74IntOctal */', + [ + 'orig_content' => '0137_041', + 'content' => '0137041', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '48673', + 'last_token' => $multiToken ? 1 : 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-float-exponent-multi-underscore' => [ + '/* testPHP74FloatExponentMultiUnderscore */', + [ + 'orig_content' => '1_2.3_4e1_23', + 'content' => '12.34e123', + 'code' => \T_DNUMBER, + 'type' => 'T_DNUMBER', + 'decimal' => '12.34e123', + 'last_token' => $multiToken ? 3 : 0, // Offset from $stackPtr. + ], + ], + + // Make sure the backfill doesn't do more than it should. + 'php-7.4-integer-calculation-1' => [ + '/* testPHP74IntCalc1 */', + [ + 'orig_content' => '667_083', + 'content' => '667083', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '667083', + 'last_token' => $multiToken ? 1 : 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-integer-calculation-2' => [ + '/* testPHP74IntCalc2 */', + [ + 'orig_content' => '74_083', + 'content' => '74083', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '74083', + 'last_token' => $multiToken ? 1 : 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-float-calculation-1' => [ + '/* testPHP74FloatCalc1 */', + [ + 'orig_content' => '6.674_08e3', + 'content' => '6.67408e3', + 'code' => \T_DNUMBER, + 'type' => 'T_DNUMBER', + 'decimal' => '6.67408e3', + 'last_token' => $multiToken ? 1 : 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-float-calculation-2' => [ + '/* testPHP74FloatCalc2 */', + [ + 'orig_content' => '6.674_08e3', + 'content' => '6.67408e3', + 'code' => \T_DNUMBER, + 'type' => 'T_DNUMBER', + 'decimal' => '6.67408e3', + 'last_token' => $multiToken ? 1 : 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-integer-whitespace' => [ + '/* testPHP74IntWhitespace */', + [ + 'orig_content' => '107_925_284', + 'content' => '107925284', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '107925284', + 'last_token' => $multiToken ? 1 : 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-float-comments' => [ + '/* testPHP74FloatComments */', + [ + 'orig_content' => '107_925_284', + 'content' => '107925284', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '107925284', + 'last_token' => $multiToken ? 1 : 0, // Offset from $stackPtr. + ], + ], + + // Invalid numeric literal with underscore. + 'php-7.4-invalid-1' => [ + '/* testPHP74Invalid1 */', + [ + 'orig_content' => '100', + 'content' => '100', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '100', + 'last_token' => 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-invalid-2' => [ + '/* testPHP74Invalid2 */', + [ + 'orig_content' => '1', + 'content' => '1', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '1', + 'last_token' => 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-invalid-3' => [ + '/* testPHP74Invalid3 */', + [ + 'orig_content' => '1', + 'content' => '1', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '1', + 'last_token' => 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-invalid-4' => [ + '/* testPHP74Invalid4 */', + [ + 'orig_content' => '1.', + 'content' => '1.', + 'code' => \T_DNUMBER, + 'type' => 'T_DNUMBER', + 'decimal' => '1.', + 'last_token' => 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-invalid-5' => [ + '/* testPHP74Invalid5 */', + [ + 'orig_content' => '0', + 'content' => '0', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '0', + 'last_token' => 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-invalid-6' => [ + '/* testPHP74Invalid6 */', + [ + 'orig_content' => '0', + 'content' => '0', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '0', + 'last_token' => 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-invalid-7' => [ + '/* testPHP74Invalid7 */', + [ + 'orig_content' => '1', + 'content' => '1', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '1', + 'last_token' => 0, // Offset from $stackPtr. + ], + ], + 'php-7.4-invalid-8' => [ + '/* testPHP74Invalid8 */', + [ + 'orig_content' => '1', + 'content' => '1', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '1', + 'last_token' => 0, // Offset from $stackPtr. + ], + ], + 'live-coding' => [ + '/* testLiveCoding */', + [ + 'orig_content' => '100', + 'content' => '100', + 'code' => \T_LNUMBER, + 'type' => 'T_LNUMBER', + 'decimal' => '100', + 'last_token' => 0, // Offset from $stackPtr. + ], + ], + ]; + } +} diff --git a/Tests/Utils/Numbers/GetDecimalValueTest.php b/Tests/Utils/Numbers/GetDecimalValueTest.php new file mode 100644 index 00000000..95109c19 --- /dev/null +++ b/Tests/Utils/Numbers/GetDecimalValueTest.php @@ -0,0 +1,124 @@ +assertSame($expected, Numbers::getDecimalValue($input)); + } + + /** + * Data Provider. + * + * @see testGetDecimalValue() For the array format. + * + * @return array + */ + public function dataGetDecimalValue() + { + return [ + // Decimal integers. + 'single-digit-zero' => ['0', '0'], + 'single-digit-nine' => ['9', '9'], + 'multi-digit-decimal-int' => ['123', '123'], + 'multi-digit-decimal-php-7.4' => ['12_34_56', '123456'], + + // Floats. + 'multi-digit-decimal-float' => ['0.123', '0.123'], + 'multi-digit-float-scientific-uppercase-e' => ['01E3', '01E3'], + 'multi-digit-float-scientific-lowercase-e' => ['01e3', '01e3'], + 'multi-digit-decimal-float-php-7.4' => ['0.123_456', '0.123456'], + 'multi-digit-scientific-float-with-underscores' => ['6.674_083e+11', '6.674083e+11'], + + // Hex. + // phpcs:disable PHPCompatibility.Miscellaneous.ValidIntegers.HexNumericStringFound + 'hex-int-no-numbers' => ['0xA', '10'], + 'hex-int-all-numbers' => ['0x400', '1024'], + 'hex-int-mixed-uppercase-x' => ['0XAB953C', '11244860'], + 'hex-int-mixed-php-7.4' => ['0xAB_95_3C', '11244860'], + // phpcs:enable + + // Binary. + 'binary-int-10' => ['0b1010', '10'], + 'binary-int-1024-uppercase-b' => ['0B10000000000', '1024'], + 'binary-int-1024-php-7.4' => ['0b100_000_000_00', '1024'], + + // Octal. + 'octal-int-10' => ['012', '10'], + 'octal-int-1024' => ['02000', '1024'], + 'octal-int-1024-php-7.4' => ['020_00', '1024'], + ]; + } + + /** + * Test that the method returns false when a non-string, a non-numeric string or an + * invalid numeric format is received. + * + * @dataProvider dataGetDecimalValueInvalid + * + * @param string $input The input string. + * + * @return void + */ + public function testGetDecimalValueInvalid($input) + { + $this->assertFalse(Numbers::getDecimalValue($input)); + } + + /** + * Data Provider. + * + * @see testGetDecimalValueInvalid() For the array format. + * + * @return array + */ + public function dataGetDecimalValueInvalid() + { + return [ + 'not-a-string-bool' => [true], + 'not-a-string-int' => [10], + 'not-a-string-float' => [1.23], + 'empty-string' => [''], + 'string-not-a-number' => ['foobar'], + 'string-not-a-number-with-full-stop' => ['foo? bar.'], + + // Invalid formats. + 'invalid-hex' => ['0xZBHI28'], + 'invalid-binary' => ['0b121457182'], + 'invalid-octal' => ['0289'], + ]; + } +} diff --git a/Tests/Utils/Numbers/NumberTypesTest.php b/Tests/Utils/Numbers/NumberTypesTest.php new file mode 100644 index 00000000..cd6a368c --- /dev/null +++ b/Tests/Utils/Numbers/NumberTypesTest.php @@ -0,0 +1,857 @@ +assertSame($expected['decimal'], Numbers::isDecimalInt($input)); + } + + /** + * Test correctly recognizing an arbitrary string representing a hexidecimal integer. + * + * @dataProvider dataNumbers + * @covers \PHPCSUtils\Utils\Numbers::isHexidecimalInt + * + * @param string $input The input string. + * @param string $expected The expected output for the various functions. + * + * @return void + */ + public function testIsHexidecimalInt($input, $expected) + { + $this->assertSame($expected['hex'], Numbers::isHexidecimalInt($input)); + } + + /** + * Test correctly recognizing an arbitrary string representing a binary integer. + * + * @dataProvider dataNumbers + * @covers \PHPCSUtils\Utils\Numbers::isBinaryInt + * + * @param string $input The input string. + * @param string $expected The expected output for the various functions. + * + * @return void + */ + public function testIsBinaryInt($input, $expected) + { + $this->assertSame($expected['binary'], Numbers::isBinaryInt($input)); + } + + /** + * Test correctly recognizing an arbitrary string representing an octal integer. + * + * @dataProvider dataNumbers + * @covers \PHPCSUtils\Utils\Numbers::isOctalInt + * + * @param string $input The input string. + * @param string $expected The expected output for the various functions. + * + * @return void + */ + public function testIsOctalInt($input, $expected) + { + $this->assertSame($expected['octal'], Numbers::isOctalInt($input)); + } + + /** + * Test correctly recognizing an arbitrary string representing a decimal float. + * + * @dataProvider dataNumbers + * @covers \PHPCSUtils\Utils\Numbers::isFloat + * + * @param string $input The input string. + * @param string $expected The expected output for the various functions. + * + * @return void + */ + public function testIsFloat($input, $expected) + { + $this->assertSame($expected['float'], Numbers::isFloat($input)); + } + + /** + * Data Provider. + * + * @see testIsDecimalInt() For the array format. + * @see testIsHexidecimalInt() For the array format. + * @see testIsBinaryInt() For the array format. + * @see testIsOctalInt() For the array format. + * @see testIsDecimalFloat() For the array format. + * + * @return array + */ + public static function dataNumbers() + { + return [ + // Not strings. + 'not-a-string-bool' => [ + true, + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'not-a-string-int' => [ + 10, + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + + // Not numeric strings. + 'empty-string' => [ + '', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'string-not-a-number' => [ + 'foobar', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'string-not-a-number-with-full-stop' => [ + 'foo. bar', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'invalid-hex' => [ + '0xZBHI28', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'invalid-binary' => [ + '0b121457182', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'invalid-octal' => [ + // Note: in PHP 5.x this would still be accepted, though not interpreted correctly. + '0289', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'invalid-float-two-decimal-points' => [ + '1.287.2763', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'invalid-float-plus-no-exponent' => [ + '1.287+2763', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'invalid-float-minus-no-exponent' => [ + '1287-2763', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'invalid-float-exponent-no-multiplier' => [ + '2872e', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'invalid-float-exponent-plus-no-multiplier' => [ + '1.2872e+', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'invalid-float-exponent-minus-no-multiplier' => [ + '1.2872e-', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'invalid-float-exponent-multiplier-float' => [ + '376e2.3', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'invalid-float-exponent-plus-multiplier-float' => [ + '3.76e+2.3', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'invalid-float-exponent-minus-multiplier-float' => [ + '37.6e-2.3', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'invalid-float-exponent-plus-minus-multiplier' => [ + '37.6e+-2', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'invalid-float-double-exponent' => [ + '37.6e2e6', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + + // Decimal numeric strings. + 'decimal-single-digit-zero' => [ + '0', + [ + 'decimal' => true, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'decimal-single-digit' => [ + '9', + [ + 'decimal' => true, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'decimal-multi-digit' => [ + '123456', + [ + 'decimal' => true, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'decimal-multi-digit-php-7.4' => [ + '12_34_56', + [ + 'decimal' => true, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + + // Hexidecimal numeric strings. + // phpcs:disable PHPCompatibility.Miscellaneous.ValidIntegers.HexNumericStringFound + 'hexidecimal-single-digit-zero' => [ + '0x0', + [ + 'decimal' => false, + 'hex' => true, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'hexidecimal-single-digit' => [ + '0xA', + [ + 'decimal' => false, + 'hex' => true, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'hexidecimal-multi-digit-all-numbers' => [ + '0x123456', + [ + 'decimal' => false, + 'hex' => true, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'hexidecimal-multi-digit-no-numbers' => [ + '0xABCDEF', + [ + 'decimal' => false, + 'hex' => true, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'hexidecimal-multi-digit-mixed' => [ + '0xAB02F6', + [ + 'decimal' => false, + 'hex' => true, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'hexidecimal-multi-digit-mixed-uppercase-x' => [ + '0XAB953C', + [ + 'decimal' => false, + 'hex' => true, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + 'hexidecimal-multi-digit-php-7.4' => [ + '0x23_6A_3C', + [ + 'decimal' => false, + 'hex' => true, + 'binary' => false, + 'octal' => false, + 'float' => false, + ], + ], + // phpcs:enable + + // Binary numeric strings. + 'binary-single-digit-zero' => [ + '0b0', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => true, + 'octal' => false, + 'float' => false, + ], + ], + 'binary-single-digit-one' => [ + '0b1', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => true, + 'octal' => false, + 'float' => false, + ], + ], + 'binary-multi-digit' => [ + '0b1010', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => true, + 'octal' => false, + 'float' => false, + ], + ], + 'binary-multi-digit-uppercase-b' => [ + '0B1000100100000', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => true, + 'octal' => false, + 'float' => false, + ], + ], + 'binary-multi-digit-php-7.4' => [ + '0b100_000_000_00', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => true, + 'octal' => false, + 'float' => false, + ], + ], + + // Octal numeric strings. + 'octal-single-digit-zero' => [ + '00', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => true, + 'float' => false, + ], + ], + 'octal-single-digit' => [ + '07', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => true, + 'float' => false, + ], + ], + 'octal-multi-digit' => [ + '076543210', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => true, + 'float' => false, + ], + ], + 'octal-multi-digit-php-7.4' => [ + '020_631_542', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => true, + 'float' => false, + ], + ], + + // Floating point numeric strings. Also see: decimal numeric strings. + 'float-single-digit-dot-zero' => [ + '0.', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-single-digit-dot' => [ + '1.', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-multi-digit-dot' => [ + '56458.', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-multi-digit-dot-leading-zero' => [ + '0023.', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-multi-digit-dot-php-7.4' => [ + '521_879.', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + + 'float-dot-single-digit-zero' => [ + '.0', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-dot-single-digit' => [ + '.2', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-dot-multi-digit' => [ + '.232746', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-dot-multi-digit-trailing-zero' => [ + '.345300', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-dot-multi-digit-php-7.4' => [ + '.421_789_8', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + + 'float-digit-dot-digit-single-zero' => [ + '0.0', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-digit-dot-digit-single' => [ + '9.1', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-digit-dot-digit-multi' => [ + '7483.2182', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-digit-dot-digit-multi-leading-zero' => [ + '002781.21928173', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-digit-dot-digit-multi-trailing-zero' => [ + '213.2987000', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-digit-dot-digit-multi-leading-zero-trailing-zero' => [ + '07262.2760', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-digit-dot-digit-multi--php-7.4' => [ + '07_262.276_720', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + + 'float-exponent-digit-dot-digit-zero-exp-single-digit' => [ + '0.0e1', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-exponent-single-digit-dot-exp-double-digit' => [ + '1.e28', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-exponent-multi-digit-dot-exp-plus-digit' => [ + '56458.e+2', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-exponent-multi-digit-dot-leading-zero-exp-minus-digit' => [ + '0023.e-44', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + + 'float-exponent-dot-single-digit-zero-exp-minus-digit' => [ + '.0e-1', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-exponent-dot-single-digit-exp-plus-digit-zero' => [ + '.2e+0', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-exponent-dot-multi-digit-exp-multi-digit' => [ + '.232746e41', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-exponent-dot-multi-digit-trailing-zero-exp-multi-digit' => [ + '.345300e87', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + + 'float-exponent-digit-dot-digit-single-zero-exp-uppercase' => [ + '0.0E2', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-exponent-digit-dot-digit-single-exp-uppercase' => [ + '9.1E47', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-exponent-digit-dot-digit-multi-exp-minus-digit' => [ + '7483.2182e-3', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-exponent-digit-dot-digit-multi-leading-zero-exp-uppercase' => [ + '002781.21928173E+56', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-exponent-digit-dot-digit-multi-trailing-zero-exp-plus-digit' => [ + '213.2987000e+2', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-exponent-digit-dot-digit-multi-leading-zero-trailing-zero-exp-digit' => [ + '07262.2760e4', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + 'float-exponent-digit-dot-digit-exp-digit-php-7.4' => [ + '6.674_083e+1_1', + [ + 'decimal' => false, + 'hex' => false, + 'binary' => false, + 'octal' => false, + 'float' => true, + ], + ], + ]; + } +} diff --git a/Tests/Utils/ObjectDeclarations/FindExtendedClassNameDiffTest.inc b/Tests/Utils/ObjectDeclarations/FindExtendedClassNameDiffTest.inc new file mode 100644 index 00000000..c03aee8d --- /dev/null +++ b/Tests/Utils/ObjectDeclarations/FindExtendedClassNameDiffTest.inc @@ -0,0 +1,10 @@ +getTargetToken($testMarker, [\T_CLASS, \T_ANON_CLASS, \T_INTERFACE]); + $result = ObjectDeclarations::findExtendedClassName(self::$phpcsFile, $OOToken); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testFindExtendedClassName() For the array format. + * + * @return array + */ + public function dataFindExtendedClassName() + { + return [ + 'phpcs-annotation-and-comments' => [ + '/* testDeclarationWithComments */', + '\Package\SubDir\SomeClass', + ], + 'parse-error-stray-comma' => [ + '/* testExtendedClassStrayComma */', + 'testClass', + ], + ]; + } +} diff --git a/Tests/Utils/ObjectDeclarations/FindExtendedClassNameTest.php b/Tests/Utils/ObjectDeclarations/FindExtendedClassNameTest.php new file mode 100644 index 00000000..a1bcd212 --- /dev/null +++ b/Tests/Utils/ObjectDeclarations/FindExtendedClassNameTest.php @@ -0,0 +1,59 @@ +assertFalse($result); + } + + /** + * Test getting a `false` result when a token other than one of the supported tokens is passed. + * + * @return void + */ + public function testNotAnInterface() + { + $token = $this->getTargetToken('/* testNotAnInterface */', [\T_FUNCTION]); + $result = ObjectDeclarations::findExtendedInterfaceNames(self::$phpcsFile, $token); + $this->assertFalse($result); + } + + /** + * Test retrieving the names of the interfaces being extended by another interface. + * + * @dataProvider dataFindExtendedInterfaceNames + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array|false $expected Expected function output. + * + * @return void + */ + public function testFindExtendedInterfaceNames($testMarker, $expected) + { + $interface = $this->getTargetToken($testMarker, [\T_INTERFACE]); + $result = ObjectDeclarations::findExtendedInterfaceNames(self::$phpcsFile, $interface); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testFindExtendedInterfaceNames() For the array format. + * + * @return array + */ + public function dataFindExtendedInterfaceNames() + { + return [ + 'not-extended' => [ + '/* testInterface */', + false, + ], + 'extends-one' => [ + '/* testExtendedInterface */', + ['testInterface'], + ], + 'extends-two' => [ + '/* testMultiExtendedInterface */', + [ + 'testInterfaceA', + 'testInterfaceB', + ], + ], + 'extends-one-namespaced' => [ + '/* testExtendedNamespacedInterface */', + ['\PHPCSUtils\Tests\ObjectDeclarations\testInterface'], + ], + 'extends-two-namespaced' => [ + '/* testMultiExtendedNamespacedInterface */', + [ + '\PHPCSUtils\Tests\ObjectDeclarations\testInterfaceA', + '\PHPCSUtils\Tests\ObjectDeclarations\testFEINInterfaceB', + ], + ], + 'extends-with-comments' => [ + '/* testMultiExtendedInterfaceWithComments */', + [ + 'testInterfaceA', + '\PHPCSUtils\Tests\Some\Declarations\testInterfaceB', + '\testInterfaceC', + ], + ], + 'parse-error' => [ + '/* testParseError */', + false, + ], + ]; + } +} diff --git a/Tests/Utils/ObjectDeclarations/FindImplementedInterfaceNamesDiffTest.inc b/Tests/Utils/ObjectDeclarations/FindImplementedInterfaceNamesDiffTest.inc new file mode 100644 index 00000000..0cd35a9d --- /dev/null +++ b/Tests/Utils/ObjectDeclarations/FindImplementedInterfaceNamesDiffTest.inc @@ -0,0 +1,17 @@ +getTargetToken($testMarker, [\T_CLASS, \T_ANON_CLASS, \T_INTERFACE]); + $result = ObjectDeclarations::findImplementedInterfaceNames(self::$phpcsFile, $OOToken); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testFindImplementedInterfaceNames() For the array format. + * + * @return array + */ + public function dataFindImplementedInterfaceNames() + { + return [ + 'phpcs-annotation-and-comments' => [ + '/* testDeclarationWithComments */', + [ + '\Vendor\Package\Core\SubDir\SomeInterface', + 'InterfaceB', + ], + ], + 'parse-error-stray-comma' => [ + '/* testMultiImplementedStrayComma */', + [ + 0 => 'testInterfaceA', + 1 => 'testInterfaceB', + ], + ], + ]; + } +} diff --git a/Tests/Utils/ObjectDeclarations/FindImplementedInterfaceNamesTest.php b/Tests/Utils/ObjectDeclarations/FindImplementedInterfaceNamesTest.php new file mode 100644 index 00000000..a6117555 --- /dev/null +++ b/Tests/Utils/ObjectDeclarations/FindImplementedInterfaceNamesTest.php @@ -0,0 +1,59 @@ +expectPhpcsException('$stackPtr must be of type T_CLASS'); + + ObjectDeclarations::getClassProperties(self::$phpcsFile, 10000); + } + + /** + * Test retrieving the properties for a class declaration. + * + * @dataProvider dataGetClassProperties + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected Expected function output. + * + * @return void + */ + public function testGetClassProperties($testMarker, $expected) + { + $class = $this->getTargetToken($testMarker, \T_CLASS); + $result = ObjectDeclarations::getClassProperties(self::$phpcsFile, $class); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testGetClassProperties() For the array format. + * + * @return array + */ + public function dataGetClassProperties() + { + return [ + 'phpcs-annotation' => [ + '/* testPHPCSAnnotations */', + [ + 'is_abstract' => false, + 'is_final' => true, + ], + ], + 'unorthodox-docblock-placement' => [ + '/* testWithDocblockWithWeirdlyPlacedProperty */', + [ + 'is_abstract' => false, + 'is_final' => true, + ], + ], + 'abstract-final-parse-error' => [ + '/* testParseErrorAbstractFinal */', + [ + 'is_abstract' => true, + 'is_final' => false, + ], + ], + ]; + } +} diff --git a/Tests/Utils/ObjectDeclarations/GetClassPropertiesTest.php b/Tests/Utils/ObjectDeclarations/GetClassPropertiesTest.php new file mode 100644 index 00000000..602d4fb9 --- /dev/null +++ b/Tests/Utils/ObjectDeclarations/GetClassPropertiesTest.php @@ -0,0 +1,58 @@ +assertNull($result); + } + + /** + * Test receiving "null" when passed an anonymous construct or in case of a parse error. + * + * @dataProvider dataGetNameNull + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string $targetType Token type of the token to get as stackPtr. + * + * @return void + */ + public function testGetNameNull($testMarker, $targetType) + { + $target = $this->getTargetToken($testMarker, $targetType); + $result = ObjectDeclarations::getName(self::$phpcsFile, $target); + $this->assertNull($result); + } + + /** + * Data provider. + * + * @see testGetNameNull() For the array format. + * + * @return array + */ + public function dataGetNameNull() + { + return [ + 'live-coding' => [ + '/* testLiveCoding */', + \T_CLASS, + ], + ]; + } + + /** + * Test retrieving the name of a function or OO structure. + * + * @dataProvider dataGetName + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $expected Expected function output. + * @param int|string $targetType Token type of the token to get as stackPtr. + * + * @return void + */ + public function testGetName($testMarker, $expected, $targetType = null) + { + if (isset($targetType) === false) { + $targetType = [\T_CLASS, \T_INTERFACE, \T_TRAIT, \T_FUNCTION]; + } + + $target = $this->getTargetToken($testMarker, $targetType); + $result = ObjectDeclarations::getName(self::$phpcsFile, $target); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testGetName() For the array format. + * + * @return array + */ + public function dataGetName() + { + return [ + 'trait-name-starts-with-number' => [ + '/* testTraitStartingWithNumber */', + '5InvalidNameStartingWithNumber', + ], + 'interface-fully-numeric-name' => [ + '/* testInterfaceFullyNumeric */', + '12345', + ], + 'using-reserved-keyword-as-name' => [ + '/* testInvalidInterfaceName */', + 'switch', + ], + ]; + } +} diff --git a/Tests/Utils/ObjectDeclarations/GetNameJSTest.php b/Tests/Utils/ObjectDeclarations/GetNameJSTest.php new file mode 100644 index 00000000..1bbf64a4 --- /dev/null +++ b/Tests/Utils/ObjectDeclarations/GetNameJSTest.php @@ -0,0 +1,124 @@ +expectPhpcsException('Token type "T_STRING" is not T_FUNCTION, T_CLASS, T_INTERFACE or T_TRAIT'); + + $target = $this->getTargetToken('/* testInvalidTokenPassed */', \T_STRING); + ObjectDeclarations::getName(self::$phpcsFile, $target); + } + + /** + * Test receiving "null" when passed an anonymous construct or in case of a parse error. + * + * {@internal Method name not adjusted as otherwise it wouldn't overload the parent method.} + * + * @dataProvider dataGetDeclarationNameNull + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string $targetType Token type of the token to get as stackPtr. + * + * @return void + */ + public function testGetDeclarationNameNull($testMarker, $targetType) + { + $target = $this->getTargetToken($testMarker, $targetType); + $result = ObjectDeclarations::getName(self::$phpcsFile, $target); + $this->assertNull($result); + } + + /** + * Test retrieving the name of a function or OO structure. + * + * {@internal Method name not adjusted as otherwise it wouldn't overload the parent method.} + * + * @dataProvider dataGetDeclarationName + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $expected Expected function output. + * @param int|string $targetType Token type of the token to get as stackPtr. + * + * @return void + */ + public function testGetDeclarationName($testMarker, $expected, $targetType = null) + { + if (isset($targetType) === false) { + $targetType = [\T_CLASS, \T_INTERFACE, \T_TRAIT, \T_FUNCTION]; + } + + $target = $this->getTargetToken($testMarker, $targetType); + $result = ObjectDeclarations::getName(self::$phpcsFile, $target); + $this->assertSame($expected, $result); + } + + /** + * Test retrieving the name of JS ES6 class method. + * + * {@internal Method name not adjusted as otherwise it wouldn't overload the parent method.} + * + * @return void + */ + public function testGetDeclarationNameES6Method() + { + if (\version_compare(Helper::getVersion(), '3.0.0', '<') === true) { + $this->markTestSkipped('Support for JS ES6 method has not been backfilled for PHPCS 2.x (yet)'); + } + + $target = $this->getTargetToken('/* testMethod */', [\T_CLASS, \T_INTERFACE, \T_TRAIT, \T_FUNCTION]); + $result = ObjectDeclarations::getName(self::$phpcsFile, $target); + $this->assertSame('methodName', $result); + } +} diff --git a/Tests/Utils/ObjectDeclarations/GetNameTest.php b/Tests/Utils/ObjectDeclarations/GetNameTest.php new file mode 100644 index 00000000..cddf96c2 --- /dev/null +++ b/Tests/Utils/ObjectDeclarations/GetNameTest.php @@ -0,0 +1,105 @@ +expectPhpcsException('Token type "T_STRING" is not T_FUNCTION, T_CLASS, T_INTERFACE or T_TRAIT'); + + $target = $this->getTargetToken('/* testInvalidTokenPassed */', \T_STRING); + ObjectDeclarations::getName(self::$phpcsFile, $target); + } + + /** + * Test receiving "null" when passed an anonymous construct or in case of a parse error. + * + * {@internal Method name not adjusted as otherwise it wouldn't overload the parent method.} + * + * @dataProvider dataGetDeclarationNameNull + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string $targetType Token type of the token to get as stackPtr. + * + * @return void + */ + public function testGetDeclarationNameNull($testMarker, $targetType) + { + $target = $this->getTargetToken($testMarker, $targetType); + $result = ObjectDeclarations::getName(self::$phpcsFile, $target); + $this->assertNull($result); + } + + /** + * Test retrieving the name of a function or OO structure. + * + * {@internal Method name not adjusted as otherwise it wouldn't overload the parent method.} + * + * @dataProvider dataGetDeclarationName + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $expected Expected function output. + * @param int|string $targetType Token type of the token to get as stackPtr. + * + * @return void + */ + public function testGetDeclarationName($testMarker, $expected, $targetType = null) + { + if (isset($targetType) === false) { + $targetType = [\T_CLASS, \T_INTERFACE, \T_TRAIT, \T_FUNCTION]; + } + + $target = $this->getTargetToken($testMarker, $targetType); + $result = ObjectDeclarations::getName(self::$phpcsFile, $target); + $this->assertSame($expected, $result); + } +} diff --git a/Tests/Utils/Operators/IsReferenceDiffTest.inc b/Tests/Utils/Operators/IsReferenceDiffTest.inc new file mode 100644 index 00000000..2f9f0e04 --- /dev/null +++ b/Tests/Utils/Operators/IsReferenceDiffTest.inc @@ -0,0 +1,13 @@ +assertFalse(Operators::isReference(self::$phpcsFile, 10000)); + } + + /** + * Test correctly identifying that whether a "bitwise and" token is a reference or not. + * + * @dataProvider dataIsReference + * + * @param string $identifier Comment which precedes the test case. + * @param bool $expected Expected function output. + * + * @return void + */ + public function testIsReference($identifier, $expected) + { + $bitwiseAnd = $this->getTargetToken($identifier, \T_BITWISE_AND); + $result = Operators::isReference(self::$phpcsFile, $bitwiseAnd); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testIsReference() + * + * @return array + */ + public function dataIsReference() + { + return [ + 'issue-1971-list-first-in-file' => [ + '/* testTokenizerIssue1971PHPCSlt330gt271A */', + true, + ], + 'issue-1971-list-first-in-file-nested' => [ + '/* testTokenizerIssue1971PHPCSlt330gt271B */', + true, + ], + 'issue-1284-short-list-directly-after-close-curly-control-structure' => [ + '/* testTokenizerIssue1284PHPCSlt280A */', + true, + ], + 'issue-1284-short-list-directly-after-close-curly-control-structure-second-item' => [ + '/* testTokenizerIssue1284PHPCSlt280B */', + true, + ], + 'issue-1284-short-array-directly-after-close-curly-control-structure' => [ + '/* testTokenizerIssue1284PHPCSlt280C */', + true, + ], + ]; + } +} diff --git a/Tests/Utils/Operators/IsReferenceTest.php b/Tests/Utils/Operators/IsReferenceTest.php new file mode 100644 index 00000000..75de2671 --- /dev/null +++ b/Tests/Utils/Operators/IsReferenceTest.php @@ -0,0 +1,58 @@ +assertFalse(Operators::isShortTernary(self::$phpcsFile, 10000)); + } + + /** + * Test that false is returned when a non-ternary then/else token is passed. + * + * @return void + */ + public function testNotTernaryToken() + { + $target = $this->getTargetToken('/* testNotATernaryToken */', \T_ECHO); + $this->assertFalse(Operators::isShortTernary(self::$phpcsFile, $target)); + } + + /** + * Test whether a T_INLINE_THEN or T_INLINE_ELSE token is correctly identified for being a short ternary. + * + * @dataProvider dataIsShortTernary + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param bool $expected The expected boolean return value. + * + * @return void + */ + public function testIsShortTernary($testMarker, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, \T_INLINE_THEN); + $result = Operators::isShortTernary(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result, 'Test failed with inline then'); + + $stackPtr = $this->getTargetToken($testMarker, \T_INLINE_ELSE); + $result = Operators::isShortTernary(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result, 'Test failed with inline else'); + } + + /** + * Data provider. + * + * @see testIsShortTernary() For the array format. + * + * @return array + */ + public function dataIsShortTernary() + { + return [ + 'long-ternary' => [ + '/* testLongTernary */', + false, + ], + 'short-ternary-no-space' => [ + '/* testShortTernaryNoSpace */', + true, + ], + 'short-ternary-long-space' => [ + '/* testShortTernaryLongSpace */', + true, + ], + 'short-ternary-comments-annotations' => [ + '/* testShortTernaryWithCommentAndAnnotations */', + true, + ], + ]; + } + + /** + * Safeguard that incorrectly tokenized T_INLINE_THEN or T_INLINE_ELSE tokens are correctly + * rejected as not short ternary. + * + * {@internal None of these are really problematic, but better to be safe than sorry.} + * + * @dataProvider dataIsShortTernaryTokenizerIssues + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string $tokenType The token code to look for. + * + * @return void + */ + public function testIsShortTernaryTokenizerIssues($testMarker, $tokenType = \T_INLINE_THEN) + { + $stackPtr = $this->getTargetToken($testMarker, $tokenType); + $result = Operators::isShortTernary(self::$phpcsFile, $stackPtr); + $this->assertFalse($result); + } + + /** + * Data provider. + * + * @see testIsShortTernaryTokenizerIssues() For the array format. + * + * @return array + */ + public function dataIsShortTernaryTokenizerIssues() + { + $targetCoalesce = [\T_INLINE_THEN]; + if (\defined('T_COALESCE')) { + $targetCoalesce[] = \T_COALESCE; + } + + $targetCoalesceAndEquals = $targetCoalesce; + if (\defined('T_COALESCE_EQUAL')) { + $targetCoalesceAndEquals[] = \T_COALESCE_EQUAL; + } + + $targetNullable = [\T_INLINE_THEN]; + if (\defined('T_NULLABLE')) { + $targetNullable[] = \T_NULLABLE; + } + + return [ + 'null-coalesce' => [ + '/* testDontConfuseWithNullCoalesce */', + $targetCoalesce, + ], + 'null-coalesce-equals' => [ + '/* testDontConfuseWithNullCoalesceEquals */', + $targetCoalesceAndEquals, + ], + 'nullable-property' => [ + '/* testDontConfuseWithNullable1 */', + $targetNullable, + ], + 'nullable-param-type' => [ + '/* testDontConfuseWithNullable2 */', + $targetNullable, + ], + 'nullable-return-type' => [ + '/* testDontConfuseWithNullable3 */', + $targetNullable, + ], + 'parse-error' => [ + '/* testParseError */', + ], + ]; + } +} diff --git a/Tests/Utils/Operators/IsUnaryPlusMinusJSTest.js b/Tests/Utils/Operators/IsUnaryPlusMinusJSTest.js new file mode 100644 index 00000000..a264b19d --- /dev/null +++ b/Tests/Utils/Operators/IsUnaryPlusMinusJSTest.js @@ -0,0 +1,23 @@ +/* testNonUnaryPlus */ +result = 1 + 2; + +/* testNonUnaryMinus */ +result = 1-2; + +/* testUnaryMinusColon */ +$.localScroll({offset: {top: -32}}); + +switch (result) { + /* testUnaryMinusCase */ + case -1: + break; +} + +/* testUnaryMinusTernaryThen */ +result = x?-y:z; + +/* testUnaryPlusTernaryElse */ +result = x ? y : +z; + +/* testUnaryMinusIfCondition */ +if (true || -1 == b) {} diff --git a/Tests/Utils/Operators/IsUnaryPlusMinusJSTest.php b/Tests/Utils/Operators/IsUnaryPlusMinusJSTest.php new file mode 100644 index 00000000..c09388c8 --- /dev/null +++ b/Tests/Utils/Operators/IsUnaryPlusMinusJSTest.php @@ -0,0 +1,74 @@ + [ + '/* testNonUnaryPlus */', + false, + ], + 'non-unary-minus' => [ + '/* testNonUnaryMinus */', + false, + ], + 'unary-minus-colon' => [ + '/* testUnaryMinusColon */', + true, + ], + 'unary-minus-switch-case' => [ + '/* testUnaryMinusCase */', + true, + ], + 'unary-minus-ternary-then' => [ + '/* testUnaryMinusTernaryThen */', + true, + ], + 'unary-minus-ternary-else' => [ + '/* testUnaryPlusTernaryElse */', + true, + ], + 'unary-minus-if-condition' => [ + '/* testUnaryMinusIfCondition */', + true, + ], + ]; + } +} diff --git a/Tests/Utils/Operators/IsUnaryPlusMinusTest.inc b/Tests/Utils/Operators/IsUnaryPlusMinusTest.inc new file mode 100644 index 00000000..fb9f9eee --- /dev/null +++ b/Tests/Utils/Operators/IsUnaryPlusMinusTest.inc @@ -0,0 +1,169 @@ + $b); + +/* testUnaryMinusFloatComparison */ +$a = ($b !== - 1.1); + +/* testUnaryMinusStringComparisonYoda */ +$a = (-'1' != $b); + +/* testUnaryPlusVariableBoolean */ +$a = ($a && - $b); + +/* testUnaryMinusVariableBoolean */ +$a = ($a || -$b); + +/* testUnaryPlusLogicalXor */ +$a = ($a xor -$b); + +/* testUnaryMinusTernaryThen */ +$a = $a ? -1 : + /* testUnaryPlusTernaryElse */ + + 10; + +/* testUnaryMinusCoalesce */ +$a = $a ?? -1 : + +/* testUnaryPlusIntReturn */ +return +1; + +/* testUnaryMinusFloatReturn */ +return -1.1; + +/* testUnaryPlusPrint */ +print +$b; + +/* testUnaryMinusEcho */ +echo -$a; + +/* testUnaryPlusYield */ +yield +$a; + +/* testUnaryPlusArrayAccess */ +$a = $array[ + 2 ]; + +/* testUnaryMinusStringArrayAccess */ +if ($line{-1} === ':') {} + +$array = array( + /* testUnaryPlusLongArrayAssignment */ + +1, + /* testUnaryMinusLongArrayAssignmentKey */ + -1 => + /* testUnaryPlusLongArrayAssignmentValue */ + +20, +); + +$array = [ + /* testUnaryPlusShortArrayAssignment */ + +1 => + /* testNonUnaryMinusShortArrayAssignment */ + 5-20, +]; + +/* testUnaryMinusCast */ +$a = (bool) -2; + +functionCall( + /* testUnaryPlusFunctionCallParam */ + +2, + /* testUnaryMinusFunctionCallParam */ + - 123.456, +); + +/* testUnaryPlusDeclare */ +declare( ticks = +10 ); + +switch ($a) { + /* testUnaryPlusCase */ + case +20: + // Something. + break; + + /* testUnaryMinusCase */ + case -1.23: + // Something. + break; +} + +// Testing `$a = -+-+10`; +$a = + /* testSequenceNonUnary1 */ + - + /* testSequenceNonUnary2 */ + + + /* testSequenceNonUnary3 */ + - + /* testSequenceUnaryEnd */ + + /*comment*/ 10; + +/* testPHP74NumericLiteralFloatContainingPlus */ +$a = 6.674_083e+11; + +/* testPHP74NumericLiteralFloatContainingMinus */ +$a = 6.674_083e-1_1; + +/* testPHP74NumericLiteralIntCalc1 */ +$a = 667_083 - 11; + +/* testPHP74NumericLiteralIntCalc2 */ +$a = 74_083 + 1_1; + +/* testPHP74NumericLiteralFloatCalc1 */ +$a = 6.674_08e3 - 1_1; + +/* testPHP74NumericLiteralFloatCalc2 */ +$a = 6.674_08e3 + 11; + +// Intentional parse error. This has to be the last test in the file. +/* testParseError */ +$a = - diff --git a/Tests/Utils/Operators/IsUnaryPlusMinusTest.php b/Tests/Utils/Operators/IsUnaryPlusMinusTest.php new file mode 100644 index 00000000..76190e0e --- /dev/null +++ b/Tests/Utils/Operators/IsUnaryPlusMinusTest.php @@ -0,0 +1,315 @@ +assertFalse(Operators::isUnaryPlusMinus(self::$phpcsFile, 10000)); + } + + /** + * Test that false is returned when a non-plus/minus token is passed. + * + * @return void + */ + public function testNotPlusMinusToken() + { + $target = $this->getTargetToken('/* testNonUnaryPlus */', \T_LNUMBER); + $this->assertFalse(Operators::isUnaryPlusMinus(self::$phpcsFile, $target)); + } + + /** + * Test whether a T_PLUS or T_MINUS token is a unary operator. + * + * @dataProvider dataIsUnaryPlusMinus + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param bool $expected The expected boolean return value. + * @param bool $maybeSkip Whether the "should this test be skipped" check should be executed. + * Defaults to false. + * + * @return void + */ + public function testIsUnaryPlusMinus($testMarker, $expected, $maybeSkip = false) + { + if ($maybeSkip === true) { + /* + * Skip the test if this is PHP 7.4 or a PHPCS version which backfills the token sequence + * to one token as in that case, the plus/minus token won't exist + */ + $skipMessage = 'Test irrelevant as the target token won\'t exist'; + if (\version_compare(\PHP_VERSION_ID, '70399', '>') === true) { + $this->markTestSkipped($skipMessage); + } + + $phpcsVersion = Helper::getVersion(); + $minVersionWithBackfill = \min(\array_keys(Numbers::$unsupportedPHPCSVersions)); + if (\version_compare($phpcsVersion, $minVersionWithBackfill, '>=') === true) { + $this->markTestSkipped($skipMessage); + } + } + + $stackPtr = $this->getTargetToken($testMarker, [\T_PLUS, \T_MINUS]); + $result = Operators::isUnaryPlusMinus(self::$phpcsFile, $stackPtr); + + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testIsUnaryPlusMinus() For the array format. + * + * @return array + */ + public function dataIsUnaryPlusMinus() + { + return [ + 'non-unary-plus' => [ + '/* testNonUnaryPlus */', + false, + ], + 'non-unary-minus' => [ + '/* testNonUnaryMinus */', + false, + ], + 'non-unary-plus-arrays' => [ + '/* testNonUnaryPlusArrays */', + false, + ], + 'unary-minus-arithmetic' => [ + '/* testUnaryMinusArithmetic */', + true, + ], + 'unary-plus-arithmetic' => [ + '/* testUnaryPlusArithmetic */', + true, + ], + 'unary-minus-concatenation' => [ + '/* testUnaryMinusConcatenation */', + true, + ], + 'unary-plus-int-assignment' => [ + '/* testUnaryPlusIntAssignment */', + true, + ], + 'unary-minus-variable-assignment' => [ + '/* testUnaryMinusVariableAssignment */', + true, + ], + 'unary-plus-float-assignment' => [ + '/* testUnaryPlusFloatAssignment */', + true, + ], + 'unary-minus-bool-assignment' => [ + '/* testUnaryMinusBoolAssignment */', + true, + ], + 'unary-plus-string-assignment-with-comment' => [ + '/* testUnaryPlusStringAssignmentWithComment */', + true, + ], + 'unary-minus-string-assignment' => [ + '/* testUnaryMinusStringAssignment */', + true, + ], + 'unary-plus-plus-null-assignment' => [ + '/* testUnaryPlusNullAssignment */', + true, + ], + 'unary-minus-variable-variable-assignment' => [ + '/* testUnaryMinusVariableVariableAssignment */', + true, + ], + 'unary-plus-int-comparison' => [ + '/* testUnaryPlusIntComparison */', + true, + ], + 'unary-plus-int-comparison-yoda' => [ + '/* testUnaryPlusIntComparisonYoda */', + true, + ], + 'unary-minus-float-comparison' => [ + '/* testUnaryMinusFloatComparison */', + true, + ], + 'unary-minus-string-comparison-yoda' => [ + '/* testUnaryMinusStringComparisonYoda */', + true, + ], + 'unary-plus-variable-boolean' => [ + '/* testUnaryPlusVariableBoolean */', + true, + ], + 'unary-minus-variable-boolean' => [ + '/* testUnaryMinusVariableBoolean */', + true, + ], + 'unary-plus-logical-xor' => [ + '/* testUnaryPlusLogicalXor */', + true, + ], + 'unary-minus-ternary-then' => [ + '/* testUnaryMinusTernaryThen */', + true, + ], + 'unary-plus-ternary-else' => [ + '/* testUnaryPlusTernaryElse */', + true, + ], + 'unary-minus-coalesce' => [ + '/* testUnaryMinusCoalesce */', + true, + ], + 'unary-plus-int-return' => [ + '/* testUnaryPlusIntReturn */', + true, + ], + 'unary-minus-float-return' => [ + '/* testUnaryMinusFloatReturn */', + true, + ], + 'unary-plus-print' => [ + '/* testUnaryPlusPrint */', + true, + ], + 'unary-minus-echo' => [ + '/* testUnaryMinusEcho */', + true, + ], + 'unary-plus-yield' => [ + '/* testUnaryPlusYield */', + true, + ], + 'unary-plus-array-access' => [ + '/* testUnaryPlusArrayAccess */', + true, + ], + 'unary-minus-string-array-access' => [ + '/* testUnaryMinusStringArrayAccess */', + true, + ], + 'unary-plus-long-array-assignment' => [ + '/* testUnaryPlusLongArrayAssignment */', + true, + ], + 'unary-minus-long-array-assignment-key' => [ + '/* testUnaryMinusLongArrayAssignmentKey */', + true, + ], + 'unary-plus-long-array-assignment-value' => [ + '/* testUnaryPlusLongArrayAssignmentValue */', + true, + ], + 'unary-plus-short-array-assignment' => [ + '/* testUnaryPlusShortArrayAssignment */', + true, + ], + 'non-unary-minus-short-array-assignment' => [ + '/* testNonUnaryMinusShortArrayAssignment */', + false, + ], + 'unary-minus-casts' => [ + '/* testUnaryMinusCast */', + true, + ], + 'unary-plus-function-call-param' => [ + '/* testUnaryPlusFunctionCallParam */', + true, + ], + 'unary-minus-function-call-param' => [ + '/* testUnaryMinusFunctionCallParam */', + true, + ], + 'unary-plus-declare' => [ + '/* testUnaryPlusDeclare */', + true, + ], + 'unary-plus-switch-case' => [ + '/* testUnaryPlusCase */', + true, + ], + 'unary-minus-switch-case' => [ + '/* testUnaryMinusCase */', + true, + ], + 'operator-sequence-non-unary-1' => [ + '/* testSequenceNonUnary1 */', + false, + ], + 'operator-sequence-non-unary-2' => [ + '/* testSequenceNonUnary2 */', + false, + ], + 'operator-sequence-non-unary-3' => [ + '/* testSequenceNonUnary3 */', + false, + ], + 'operator-sequence-unary-end' => [ + '/* testSequenceUnaryEnd */', + true, + ], + 'php-7.4-underscore-float-containing-plus' => [ + '/* testPHP74NumericLiteralFloatContainingPlus */', + false, + true, // Skip for PHP 7.4 & PHPCS 3.5.3+. + ], + 'php-7.4-underscore-float-containing-minus' => [ + '/* testPHP74NumericLiteralFloatContainingMinus */', + false, + true, // Skip for PHP 7.4 & PHPCS 3.5.3+. + ], + 'php-7.4-underscore-int-calculation-1' => [ + '/* testPHP74NumericLiteralIntCalc1 */', + false, + ], + 'php-7.4-underscore-int-calculation-2' => [ + '/* testPHP74NumericLiteralIntCalc2 */', + false, + ], + 'php-7.4-underscore-float-calculation-1' => [ + '/* testPHP74NumericLiteralFloatCalc1 */', + false, + ], + 'php-7.4-underscore-float-calculation-2' => [ + '/* testPHP74NumericLiteralFloatCalc2 */', + false, + ], + + 'parse-error' => [ + '/* testParseError */', + false, + ], + ]; + } +} diff --git a/Tests/Utils/Orthography/FirstCharTest.php b/Tests/Utils/Orthography/FirstCharTest.php new file mode 100644 index 00000000..6c0b437c --- /dev/null +++ b/Tests/Utils/Orthography/FirstCharTest.php @@ -0,0 +1,278 @@ +assertSame($expected['capitalized'], Orthography::isFirstCharCapitalized($input)); + } + + /** + * Test correctly detecting whether the first character of a phrase is lowercase. + * + * @dataProvider dataFirstChar + * + * @param string $input The input string. + * @param array $expected The expected function output for the respective functions. + * + * @return void + */ + public function testIsFirstCharLowercase($input, $expected) + { + $this->assertSame($expected['lowercase'], Orthography::isFirstCharLowercase($input)); + } + + /** + * Data provider. + * + * @see testIsFirstCharCapitalized() For the array format. + * @see testIsFirstCharLowercase() For the array format. + * + * @return array + */ + public function dataFirstChar() + { + $data = [ + // Quotes should be stripped before passing the string. + 'double-quoted' => [ + '"This is a test"', + [ + 'capitalized' => false, + 'lowercase' => false, + ], + ], + 'single-quoted' => [ + "'This is a test'", + [ + 'capitalized' => false, + 'lowercase' => false, + ], + ], + + // Not starting with a letter. + 'start-numeric' => [ + '12 Foostreet', + [ + 'capitalized' => false, + 'lowercase' => false, + ], + ], + 'start-bracket' => [ + '[Optional]', + [ + 'capitalized' => false, + 'lowercase' => false, + ], + ], + + // Leading whitespace. + 'english-lowercase-leading-whitespace' => [ + ' + this is a test', + [ + 'capitalized' => false, + 'lowercase' => true, + ], + ], + 'english-propercase-leading-whitespace' => [ + ' + This is a test', + [ + 'capitalized' => true, + 'lowercase' => false, + ], + ], + + // First character lowercase. + 'english-lowercase' => [ + 'this is a test', + [ + 'capitalized' => false, + 'lowercase' => true, + ], + ], + 'russian-lowercase' => [ + 'предназначена для‎', + [ + 'capitalized' => false, + 'lowercase' => true, + ], + ], + 'latvian-lowercase' => [ + 'ir domāta', + [ + 'capitalized' => false, + 'lowercase' => true, + ], + ], + 'armenian-lowercase' => [ + 'սա թեստ է', + [ + 'capitalized' => false, + 'lowercase' => true, + ], + ], + 'mandinka-lowercase' => [ + 'ŋanniya', + [ + 'capitalized' => false, + 'lowercase' => true, + ], + ], + 'greek-lowercase' => [ + 'δημιουργήθηκε από', + [ + 'capitalized' => false, + 'lowercase' => true, + ], + ], + + // First character capitalized. + 'english-propercase' => [ + 'This is a test', + [ + 'capitalized' => true, + 'lowercase' => false, + ], + ], + 'russian-propercase' => [ + 'Дата написания этой книги', + [ + 'capitalized' => true, + 'lowercase' => false, + ], + ], + 'latvian-propercase' => [ + 'Šodienas datums', + [ + 'capitalized' => true, + 'lowercase' => false, + ], + ], + 'armenian-propercase' => [ + 'Սա թեստ է', + [ + 'capitalized' => true, + 'lowercase' => false, + ], + ], + 'igbo-propercase' => [ + 'Ụbọchị tata bụ', + [ + 'capitalized' => true, + 'lowercase' => false, + ], + ], + 'greek-propercase' => [ + 'Η σημερινή ημερομηνία', + [ + 'capitalized' => true, + 'lowercase' => false, + ], + ], + + // No concept of "case", but starting with a letter. + 'arabic' => [ + 'هذا اختبار', + [ + 'capitalized' => true, + 'lowercase' => false, + ], + ], + 'pashto' => [ + 'دا یوه آزموینه ده', + [ + 'capitalized' => true, + 'lowercase' => false, + ], + ], + 'hebrew' => [ + 'זה מבחן', + [ + 'capitalized' => true, + 'lowercase' => false, + ], + ], + 'chinese-traditional' => [ + '這是一個測試', + [ + 'capitalized' => true, + 'lowercase' => false, + ], + ], + 'urdu' => [ + 'کا منشاء برائے', + [ + 'capitalized' => true, + 'lowercase' => false, + ], + ], + ]; + + /* + * PCRE2 - included in PHP 7.3+ - recognizes Georgian as a language with + * upper and lowercase letters as defined in Unicode v 11.0 / June 2018. + * While, as far as I can tell, this is linguistically incorrect - the upper + * and lowercase letters are from different alphabets used to write Georgian -, + * the unit test should allow for the reality as implemented in ICU/PCRE2/PHP. + * + * @link https://en.wikipedia.org/wiki/Georgian_scripts#Unicode + * @link https://unicode.org/charts/PDF/U10A0.pdf + */ + + if (\PCRE_VERSION >= 10) { + $data['georgian'] = [ + 'ეს ტესტია', + [ + 'capitalized' => false, + 'lowercase' => true, + ], + ]; + } else { + $data['georgian'] = [ + 'ეს ტესტია', + [ + 'capitalized' => true, + 'lowercase' => false, + ], + ]; + } + + return $data; + } +} diff --git a/Tests/Utils/Orthography/IsLastCharPunctuationTest.php b/Tests/Utils/Orthography/IsLastCharPunctuationTest.php new file mode 100644 index 00000000..a6921402 --- /dev/null +++ b/Tests/Utils/Orthography/IsLastCharPunctuationTest.php @@ -0,0 +1,130 @@ +assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testIsLastCharPunctuation() For the array format. + * + * @return array + */ + public function dataIsLastCharPunctuation() + { + return [ + // Quotes should be stripped before passing the string. + 'double-quoted' => [ + '"This is a test."', + false, + ], + 'single-quoted' => [ + "'This is a test?'", + false, + ], + + // Invalid end char. + 'no-punctuation' => [ + 'This is a test', + false, + ], + 'invalid-punctuation' => [ + 'This is a test;', + false, + ], + 'invalid-punctuationtrailing-whitespace' => [ + 'This is a test; ', + false, + ], + + // Valid end char, default charset. + 'valid' => [ + 'This is a test.', + true, + ], + 'valid-trailing-whitespace' => [ + 'This is a test. +', + true, + ], + + // Invalid end char, custom charset. + 'invalid-custom' => [ + 'This is a test.', + false, + '!?,;#', + ], + + // Valid end char, custom charset. + 'valid-custom-1' => [ + 'This is a test;', + true, + '!?,;#', + ], + 'valid-custom-2' => [ + 'This is a test!', + true, + '!?,;#', + ], + 'valid-custom-3' => [ + 'Is this is a test?', + true, + '!?,;#', + ], + 'valid-custom-4' => [ + 'This is a test,', + true, + '!?,;#', + ], + 'valid-custom-5' => [ + 'This is a test#', + true, + '!?,;#', + ], + ]; + } +} diff --git a/Tests/Utils/Parentheses/ParenthesesTest.inc b/Tests/Utils/Parentheses/ParenthesesTest.inc new file mode 100644 index 00000000..e868076e --- /dev/null +++ b/Tests/Utils/Parentheses/ParenthesesTest.inc @@ -0,0 +1,46 @@ + 0 && ((function($p) {/* do something */})($result) === true)) {} + +/* testAnonClass */ +$anonClass = new class( + new class implements Countable { + function test($param) { + do { + try { + } catch( Exception $e ) { + } + } while($a === true); + } + } +) extends DateTime { +}; + +/* testListOnCloseParens */ +list($a, $b) = $array; + +/* testNoOwnerOnCloseParens */ +$a = ($b + $c); + +// Intentional parse error. This has to be the last test in the file. +/* testParseError */ +declare(ticks=1 diff --git a/Tests/Utils/Parentheses/ParenthesesTest.php b/Tests/Utils/Parentheses/ParenthesesTest.php new file mode 100644 index 00000000..0f99cf84 --- /dev/null +++ b/Tests/Utils/Parentheses/ParenthesesTest.php @@ -0,0 +1,1059 @@ + [ + 'marker' => '/* testIfWithArray */', + 'code' => \T_VARIABLE, + 'content' => '$a', + ], + 'testIfWithArray-array' => [ + 'marker' => '/* testIfWithArray */', + 'code' => \T_ARRAY, + ], + 'testIfWithArray-$c' => [ + 'marker' => '/* testIfWithArray */', + 'code' => \T_VARIABLE, + 'content' => '$c', + ], + 'testElseIfWithClosure-$a' => [ + 'marker' => '/* testElseIfWithClosure */', + 'code' => \T_VARIABLE, + 'content' => '$a', + ], + 'testElseIfWithClosure-closure' => [ + 'marker' => '/* testElseIfWithClosure */', + 'code' => \T_CLOSURE, + ], + 'testElseIfWithClosure-$array' => [ + 'marker' => '/* testElseIfWithClosure */', + 'code' => \T_VARIABLE, + 'content' => '$array', + ], + 'testForeach-45' => [ + 'marker' => '/* testForeach */', + 'code' => \T_LNUMBER, + 'content' => '45', + ], + 'testForeach-$a' => [ + 'marker' => '/* testForeach */', + 'code' => \T_VARIABLE, + 'content' => '$a', + ], + 'testForeach-$c' => [ + 'marker' => '/* testForeach */', + 'code' => \T_VARIABLE, + 'content' => '$c', + ], + 'testFunctionwithArray-$param' => [ + 'marker' => '/* testFunctionwithArray */', + 'code' => \T_VARIABLE, + 'content' => '$param', + ], + 'testFunctionwithArray-2' => [ + 'marker' => '/* testFunctionwithArray */', + 'code' => \T_LNUMBER, + 'content' => '2', + ], + 'testForWithTernary-$a' => [ + 'marker' => '/* testForWithTernary */', + 'code' => \T_VARIABLE, + 'content' => '$a', + ], + 'testForWithTernary-$c' => [ + 'marker' => '/* testForWithTernary */', + 'code' => \T_VARIABLE, + 'content' => '$c', + ], + 'testForWithTernary-$array' => [ + 'marker' => '/* testForWithTernary */', + 'code' => \T_VARIABLE, + 'content' => '$array', + ], + 'testWhileWithClosure-$a' => [ + 'marker' => '/* testWhileWithClosure */', + 'code' => \T_VARIABLE, + 'content' => '$a', + ], + 'testWhileWithClosure-$p' => [ + 'marker' => '/* testWhileWithClosure */', + 'code' => \T_VARIABLE, + 'content' => '$p', + ], + 'testWhileWithClosure-$result' => [ + 'marker' => '/* testWhileWithClosure */', + 'code' => \T_VARIABLE, + 'content' => '$result', + ], + 'testAnonClass-implements' => [ + 'marker' => '/* testAnonClass */', + 'code' => \T_IMPLEMENTS, + ], + 'testAnonClass-$param' => [ + 'marker' => '/* testAnonClass */', + 'code' => \T_VARIABLE, + 'content' => '$param', + ], + 'testAnonClass-$e' => [ + 'marker' => '/* testAnonClass */', + 'code' => \T_VARIABLE, + 'content' => '$e', + ], + 'testAnonClass-$a' => [ + 'marker' => '/* testAnonClass */', + 'code' => \T_VARIABLE, + 'content' => '$a', + ], + 'testParseError-1' => [ + 'marker' => '/* testParseError */', + 'code' => \T_LNUMBER, + 'content' => '1', + ], + ]; + + /** + * Cache for the test token stack pointers. + * + * @var array => + */ + private static $testTokens = []; + + /** + * Base array with all the tokens which are assigned parenthesis owners. + * + * This array is merged with expected result arrays for various unit tests + * to make sure all possible parentheses owners are tested. + * + * This array should be kept in sync with the Tokens::$parenthesisOpeners array. + * This array isn't auto-generated based on the array in Tokens as for these + * tests we want to have access to the token constant names, not just their values. + * + * @var array => + */ + private $ownerDefaults = [ + 'T_ARRAY' => false, + 'T_LIST' => false, + 'T_FUNCTION' => false, + 'T_CLOSURE' => false, + 'T_ANON_CLASS' => false, + 'T_WHILE' => false, + 'T_FOR' => false, + 'T_FOREACH' => false, + 'T_SWITCH' => false, + 'T_IF' => false, + 'T_ELSEIF' => false, + 'T_CATCH' => false, + 'T_DECLARE' => false, + ]; + + /** + * Set up the token position caches for the tests. + * + * Retrieves the test tokens and marker token stack pointer positions + * only once and caches them as they won't change between the tests anyway. + * + * @before + * + * @return void + */ + protected function setUpCaches() + { + if (empty(self::$testTokens) === true) { + foreach (self::$testTargets as $testName => $targetDetails) { + if (isset($targetDetails['content']) === true) { + self::$testTokens[$testName] = $this->getTargetToken( + $targetDetails['marker'], + $targetDetails['code'], + $targetDetails['content'] + ); + } else { + self::$testTokens[$testName] = $this->getTargetToken( + $targetDetails['marker'], + $targetDetails['code'] + ); + } + } + } + } + + /** + * Test passing a non-existent token pointer. + * + * @return void + */ + public function testNonExistentToken() + { + $result = Parentheses::getOwner(self::$phpcsFile, 100000); + $this->assertFalse($result); + + $result = Parentheses::isOwnerIn(self::$phpcsFile, 100000, \T_FUNCTION); + $this->assertFalse($result); + + $result = Parentheses::hasOwner(self::$phpcsFile, 100000, \T_FOR); + $this->assertFalse($result); + } + + /** + * Test passing a token which isn't in parentheses. + * + * @return void + */ + public function testNoParentheses() + { + $stackPtr = $this->getTargetToken('/* testNoParentheses */', \T_VARIABLE); + + $result = Parentheses::getOwner(self::$phpcsFile, $stackPtr); + $this->assertFalse($result); + + $result = Parentheses::isOwnerIn(self::$phpcsFile, $stackPtr, \T_IF); + $this->assertFalse($result); + + $result = Parentheses::hasOwner(self::$phpcsFile, $stackPtr, \T_FOREACH); + $this->assertFalse($result); + + $result = Parentheses::getFirstOpener(self::$phpcsFile, $stackPtr); + $this->assertFalse($result); + + $result = Parentheses::getFirstCloser(self::$phpcsFile, $stackPtr); + $this->assertFalse($result); + + $result = Parentheses::getFirstOwner(self::$phpcsFile, $stackPtr); + $this->assertFalse($result); + + $result = Parentheses::firstOwnerIn(self::$phpcsFile, $stackPtr, [\T_FUNCTION, \T_CLOSURE]); + $this->assertFalse($result); + + $result = Parentheses::getLastOpener(self::$phpcsFile, $stackPtr); + $this->assertFalse($result); + + $result = Parentheses::getLastCloser(self::$phpcsFile, $stackPtr); + $this->assertFalse($result); + + $result = Parentheses::getLastOwner(self::$phpcsFile, $stackPtr); + $this->assertFalse($result); + + $result = Parentheses::lastOwnerIn(self::$phpcsFile, $stackPtr, [\T_FUNCTION, \T_CLOSURE]); + $this->assertFalse($result); + } + + /** + * Test passing a non-parenthesis token to methods which expect to receive an open/close parenthesis. + * + * @return void + */ + public function testPassingNonParenthesisTokenToMethodsWhichExpectParenthesis() + { + $stackPtr = self::$testTokens['testIfWithArray-$a']; + + $result = Parentheses::getOwner(self::$phpcsFile, $stackPtr); + $this->assertFalse($result); + + $result = Parentheses::isOwnerIn(self::$phpcsFile, $stackPtr, \T_IF); + $this->assertFalse($result); + } + + /** + * Test passing an open parenthesis token to methods which expect to receive an open/close parenthesis. + * + * @return void + */ + public function testPassingParenthesisTokenToMethodsWhichExpectParenthesisOpen() + { + $stackPtr = (self::$testTokens['testIfWithArray-$c'] - 1); + + $result = Parentheses::getOwner(self::$phpcsFile, $stackPtr); + $this->assertSame(($stackPtr - 1), $result); + + $result = Parentheses::isOwnerIn(self::$phpcsFile, $stackPtr, \T_IF); + $this->assertFalse($result); + + $result = Parentheses::isOwnerIn(self::$phpcsFile, $stackPtr, \T_ARRAY); + $this->assertTrue($result); + } + + /** + * Test passing a close parenthesis token to methods which expect to receive an open/close parenthesis. + * + * @return void + */ + public function testPassingParenthesisTokenToMethodsWhichExpectParenthesisClose() + { + $stackPtr = (self::$testTokens['testForeach-$c'] + 1); + + $result = Parentheses::getOwner(self::$phpcsFile, $stackPtr); + $this->assertSame(($stackPtr - 6), $result); + + $result = Parentheses::isOwnerIn(self::$phpcsFile, $stackPtr, \T_IF); + $this->assertFalse($result); + + $result = Parentheses::isOwnerIn(self::$phpcsFile, $stackPtr, \T_LIST); + $this->assertTrue($result); + } + + /** + * Test passing a close parenthesis token to methods which expect to receive an open/close parenthesis. + * + * This specifically tests the BC-layer for lists and anon classes. + * + * @return void + */ + public function testPassingParenthesisCloseHandlinginBCLayer() + { + $stackPtr = $this->getTargetToken('/* testListOnCloseParens */', \T_CLOSE_PARENTHESIS); + + $result = Parentheses::getOwner(self::$phpcsFile, $stackPtr); + $this->assertSame(($stackPtr - 6), $result); + + $result = Parentheses::isOwnerIn(self::$phpcsFile, $stackPtr, \T_LIST); + $this->assertTrue($result); + + $result = Parentheses::isOwnerIn(self::$phpcsFile, $stackPtr, \T_IF); + $this->assertFalse($result); + + $stackPtr = $this->getTargetToken('/* testNoOwnerOnCloseParens */', \T_CLOSE_PARENTHESIS); + + $result = Parentheses::getOwner(self::$phpcsFile, $stackPtr); + $this->assertFalse($result); + + $result = Parentheses::isOwnerIn(self::$phpcsFile, $stackPtr, BCTokens::scopeOpeners()); + $this->assertFalse($result); + } + + /** + * Test correctly retrieving the first parenthesis opener for an arbitrary token. + * + * @dataProvider dataWalkParentheses + * + * @param string $testName The name of this test as set in the cached $testTokens array. + * @param array $expectedResults Expected function output for the various functions. + * + * @return void + */ + public function testGetFirstOpener($testName, $expectedResults) + { + $stackPtr = self::$testTokens[$testName]; + + $result = Parentheses::getFirstOpener(self::$phpcsFile, $stackPtr); + $expected = $expectedResults['firstOpener']; + if ($expected !== false) { + $expected += $stackPtr; + } + + $this->assertSame($expected, $result, 'Assertion without owners failed'); + + $result = Parentheses::getFirstOpener(self::$phpcsFile, $stackPtr, BCTokens::scopeOpeners()); + $expected = $expectedResults['firstScopeOwnerOpener']; + if ($expected !== false) { + $expected += $stackPtr; + } + + $this->assertSame($expected, $result, 'Assertion with $validOwners failed'); + } + + /** + * Test correctly retrieving the first parenthesis closer for an arbitrary token. + * + * @dataProvider dataWalkParentheses + * + * @param string $testName The name of this test as set in the cached $testTokens array. + * @param array $expectedResults Expected function output for the various functions. + * + * @return void + */ + public function testGetFirstCloser($testName, $expectedResults) + { + $stackPtr = self::$testTokens[$testName]; + + $result = Parentheses::getFirstCloser(self::$phpcsFile, $stackPtr); + $expected = $expectedResults['firstCloser']; + if ($expected !== false) { + $expected += $stackPtr; + } + + $this->assertSame($expected, $result, 'Assertion without owners failed'); + + $result = Parentheses::getFirstCloser(self::$phpcsFile, $stackPtr, BCTokens::scopeOpeners()); + $expected = $expectedResults['firstScopeOwnerCloser']; + if ($expected !== false) { + $expected += $stackPtr; + } + + $this->assertSame($expected, $result, 'Assertion with $validOwners failed'); + } + + /** + * Test correctly retrieving the first parenthesis owner for an arbitrary token. + * + * @dataProvider dataWalkParentheses + * + * @param string $testName The name of this test as set in the cached $testTokens array. + * @param array $expectedResults Expected function output for the various functions. + * + * @return void + */ + public function testGetFirstOwner($testName, $expectedResults) + { + $stackPtr = self::$testTokens[$testName]; + + $result = Parentheses::getFirstOwner(self::$phpcsFile, $stackPtr); + $expected = $expectedResults['firstOwner']; + if ($expected !== false) { + $expected += $stackPtr; + } + + $this->assertSame($expected, $result, 'Assertion without owners failed'); + + $result = Parentheses::getFirstOwner(self::$phpcsFile, $stackPtr, BCTokens::scopeOpeners()); + $expected = $expectedResults['firstScopeOwnerOwner']; + if ($expected !== false) { + $expected += $stackPtr; + } + + $this->assertSame($expected, $result, 'Assertion with $validOwners failed'); + } + + /** + * Test correctly retrieving the last parenthesis opener for an arbitrary token. + * + * @dataProvider dataWalkParentheses + * + * @param string $testName The name of this test as set in the cached $testTokens array. + * @param array $expectedResults Expected function output for the various functions. + * + * @return void + */ + public function testGetLastOpener($testName, $expectedResults) + { + $stackPtr = self::$testTokens[$testName]; + + $result = Parentheses::getLastOpener(self::$phpcsFile, $stackPtr); + $expected = $expectedResults['lastOpener']; + if ($expected !== false) { + $expected += $stackPtr; + } + + $this->assertSame($expected, $result, 'Assertion without owners failed'); + + $result = Parentheses::getLastOpener(self::$phpcsFile, $stackPtr, [\T_ARRAY]); + $expected = $expectedResults['lastArrayOpener']; + if ($expected !== false) { + $expected += $stackPtr; + } + + $this->assertSame($expected, $result, 'Assertion with $validOwners failed'); + } + + /** + * Test correctly retrieving the last parenthesis closer for an arbitrary token. + * + * @dataProvider dataWalkParentheses + * + * @param string $testName The name of this test as set in the cached $testTokens array. + * @param array $expectedResults Expected function output for the various functions. + * + * @return void + */ + public function testGetLastCloser($testName, $expectedResults) + { + $stackPtr = self::$testTokens[$testName]; + + $result = Parentheses::getLastCloser(self::$phpcsFile, $stackPtr); + $expected = $expectedResults['lastCloser']; + if ($expected !== false) { + $expected += $stackPtr; + } + + $this->assertSame($expected, $result, 'Assertion without owners failed'); + + $result = Parentheses::getLastCloser(self::$phpcsFile, $stackPtr, [\T_FUNCTION, \T_CLOSURE]); + $expected = $expectedResults['lastFunctionCloser']; + if ($expected !== false) { + $expected += $stackPtr; + } + + $this->assertSame($expected, $result, 'Assertion with $validOwners failed'); + } + + /** + * Test correctly retrieving the last parenthesis owner for an arbitrary token. + * + * @dataProvider dataWalkParentheses + * + * @param string $testName The name of this test as set in the cached $testTokens array. + * @param array $expectedResults Expected function output for the various functions. + * + * @return void + */ + public function testGetLastOwner($testName, $expectedResults) + { + $stackPtr = self::$testTokens[$testName]; + + $result = Parentheses::getLastOwner(self::$phpcsFile, $stackPtr); + $expected = $expectedResults['lastOwner']; + if ($expected !== false) { + $expected += $stackPtr; + } + + $this->assertSame($expected, $result, 'Assertion without owners failed'); + + $result = Parentheses::getLastOwner(self::$phpcsFile, $stackPtr, [\T_IF, \T_ELSEIF, \T_ELSE]); + $expected = $expectedResults['lastIfElseOwner']; + if ($expected !== false) { + $expected += $stackPtr; + } + + $this->assertSame($expected, $result, 'Assertion with $validOwners failed'); + } + + /** + * Data provider. + * + * @see testGetFirstOpener() For the array format. + * @see testGetFirstCloser(() For the array format. + * @see testGetFirstOwner() For the array format. + * @see testGetLastOpener() For the array format. + * @see testGetLastCloser() For the array format. + * @see testGetLastOwner() For the array format. + * + * @return array + */ + public function dataWalkParentheses() + { + $data = [ + 'testIfWithArray-$a' => [ + 'testIfWithArray-$a', + [ + 'firstOpener' => -2, + 'firstCloser' => 19, + 'firstOwner' => -4, + 'firstScopeOwnerOpener' => -2, + 'firstScopeOwnerCloser' => 19, + 'firstScopeOwnerOwner' => -4, + 'lastOpener' => -1, + 'lastCloser' => 5, + 'lastOwner' => false, + 'lastArrayOpener' => false, + 'lastFunctionCloser' => false, + 'lastIfElseOwner' => -4, + ], + ], + 'testIfWithArray-array' => [ + 'testIfWithArray-array', + [ + 'firstOpener' => -13, + 'firstCloser' => 8, + 'firstOwner' => -15, + 'firstScopeOwnerOpener' => -13, + 'firstScopeOwnerCloser' => 8, + 'firstScopeOwnerOwner' => -15, + 'lastOpener' => -1, + 'lastCloser' => 7, + 'lastOwner' => false, + 'lastArrayOpener' => false, + 'lastFunctionCloser' => false, + 'lastIfElseOwner' => -15, + ], + ], + 'testIfWithArray-$c' => [ + 'testIfWithArray-$c', + [ + 'firstOpener' => -15, + 'firstCloser' => 6, + 'firstOwner' => -17, + 'firstScopeOwnerOpener' => -15, + 'firstScopeOwnerCloser' => 6, + 'firstScopeOwnerOwner' => -17, + 'lastOpener' => -1, + 'lastCloser' => 4, + 'lastOwner' => -2, + 'lastArrayOpener' => -1, + 'lastFunctionCloser' => false, + 'lastIfElseOwner' => -17, + ], + ], + 'testWhileWithClosure-$a' => [ + 'testWhileWithClosure-$a', + [ + 'firstOpener' => -9, + 'firstCloser' => 30, + 'firstOwner' => -11, + 'firstScopeOwnerOpener' => -9, + 'firstScopeOwnerCloser' => 30, + 'firstScopeOwnerOwner' => -11, + 'lastOpener' => -2, + 'lastCloser' => 2, + 'lastOwner' => false, + 'lastArrayOpener' => false, + 'lastFunctionCloser' => false, + 'lastIfElseOwner' => false, + ], + ], + 'testWhileWithClosure-$p' => [ + 'testWhileWithClosure-$p', + [ + 'firstOpener' => -24, + 'firstCloser' => 15, + 'firstOwner' => -26, + 'firstScopeOwnerOpener' => -24, + 'firstScopeOwnerCloser' => 15, + 'firstScopeOwnerOwner' => -26, + 'lastOpener' => -1, + 'lastCloser' => 1, + 'lastOwner' => -2, + 'lastArrayOpener' => false, + 'lastFunctionCloser' => 1, + 'lastIfElseOwner' => false, + ], + ], + 'testWhileWithClosure-$result' => [ + 'testWhileWithClosure-$result', + [ + 'firstOpener' => -2, + 'firstCloser' => 37, + 'firstOwner' => -4, + 'firstScopeOwnerOpener' => -2, + 'firstScopeOwnerCloser' => 37, + 'firstScopeOwnerOwner' => -4, + 'lastOpener' => -1, + 'lastCloser' => 10, + 'lastOwner' => false, + 'lastArrayOpener' => false, + 'lastFunctionCloser' => false, + 'lastIfElseOwner' => false, + ], + ], + 'testParseError-1' => [ + 'testParseError-1', + [ + 'firstOpener' => false, + 'firstCloser' => false, + 'firstOwner' => false, + 'firstScopeOwnerOpener' => false, + 'firstScopeOwnerCloser' => false, + 'firstScopeOwnerOwner' => false, + 'lastOpener' => false, + 'lastCloser' => false, + 'lastOwner' => false, + 'lastArrayOpener' => false, + 'lastFunctionCloser' => false, + 'lastIfElseOwner' => false, + ], + ], + ]; + + return $data; + } + + /** + * Test correctly determining whether a token has an owner of a certain type. + * + * @dataProvider dataHasOwner + * + * @param string $testName The name of this test as set in the cached $testTokens array. + * @param array $expectedResults Array with the owner token type to search for as key + * and the expected result as a value. + * + * @return void + */ + public function testHasOwner($testName, $expectedResults) + { + $stackPtr = self::$testTokens[$testName]; + + // Add expected results for all owner types not listed in the data provider. + $expectedResults += $this->ownerDefaults; + + foreach ($expectedResults as $ownerType => $expected) { + $result = Parentheses::hasOwner(self::$phpcsFile, $stackPtr, \constant($ownerType)); + $this->assertSame( + $expected, + $result, + "Assertion failed for test marker '{$testName}' with owner {$ownerType}" + ); + } + } + + /** + * Data Provider. + * + * Only list the "true" owners in the $results array. + * All other potential owners will automatically also be tested + * and will expect "false" as a result. + * + * @see testHasOwner() For the array format. + * + * @return array + */ + public function dataHasOwner() + { + return [ + 'testIfWithArray-$a' => [ + 'testIfWithArray-$a', + ['T_IF' => true], + ], + + 'testIfWithArray-array' => [ + 'testIfWithArray-array', + ['T_IF' => true], + ], + 'testIfWithArray-$c' => [ + 'testIfWithArray-$c', + [ + 'T_ARRAY' => true, + 'T_IF' => true, + ], + ], + 'testElseIfWithClosure-$a' => [ + 'testElseIfWithClosure-$a', + [ + 'T_CLOSURE' => true, + 'T_ELSEIF' => true, + ], + ], + 'testElseIfWithClosure-closure' => [ + 'testElseIfWithClosure-closure', + ['T_ELSEIF' => true], + ], + 'testElseIfWithClosure-$array' => [ + 'testElseIfWithClosure-$array', + ['T_ELSEIF' => true], + ], + 'testForeach-45' => [ + 'testForeach-45', + [ + 'T_ARRAY' => true, + 'T_FOREACH' => true, + ], + ], + 'testForeach-$a' => [ + 'testForeach-$a', + [ + 'T_LIST' => true, + 'T_FOREACH' => true, + ], + ], + 'testForeach-$c' => [ + 'testForeach-$c', + [ + 'T_LIST' => true, + 'T_FOREACH' => true, + ], + ], + 'testFunctionwithArray-$param' => [ + 'testFunctionwithArray-$param', + ['T_FUNCTION' => true], + ], + 'testFunctionwithArray-2' => [ + 'testFunctionwithArray-2', + [ + 'T_ARRAY' => true, + 'T_FUNCTION' => true, + ], + ], + 'testForWithTernary-$a' => [ + 'testForWithTernary-$a', + ['T_FOR' => true], + ], + 'testForWithTernary-$c' => [ + 'testForWithTernary-$c', + ['T_FOR' => true], + ], + 'testForWithTernary-$array' => [ + 'testForWithTernary-$array', + ['T_FOR' => true], + ], + 'testWhileWithClosure-$a' => [ + 'testWhileWithClosure-$a', + ['T_WHILE' => true], + ], + 'testWhileWithClosure-$p' => [ + 'testWhileWithClosure-$p', + [ + 'T_CLOSURE' => true, + 'T_WHILE' => true, + ], + ], + 'testWhileWithClosure-$result' => [ + 'testWhileWithClosure-$result', + ['T_WHILE' => true], + ], + 'testAnonClass-implements' => [ + 'testAnonClass-implements', + ['T_ANON_CLASS' => true], + ], + 'testAnonClass-$param' => [ + 'testAnonClass-$param', + [ + 'T_ANON_CLASS' => true, + 'T_FUNCTION' => true, + ], + ], + 'testAnonClass-$e' => [ + 'testAnonClass-$e', + [ + 'T_ANON_CLASS' => true, + 'T_CATCH' => true, + ], + ], + 'testAnonClass-$a' => [ + 'testAnonClass-$a', + [ + 'T_ANON_CLASS' => true, + 'T_WHILE' => true, + ], + ], + 'testParseError-1' => [ + 'testParseError-1', + [], + ], + ]; + } + + /** + * Test correctly determining whether a token is nested in parentheses with an owner + * of a certain type, with multiple allowed possibilities. + * + * @return void + */ + public function testHasOwnerMultipleTypes() + { + $stackPtr = self::$testTokens['testElseIfWithClosure-$array']; + + $result = Parentheses::hasOwner(self::$phpcsFile, $stackPtr, [\T_FUNCTION, \T_CLOSURE]); + $this->assertFalse( + $result, + 'Failed asserting that $array in "testElseIfWithClosure" does not have a "function" nor a "closure" owner' + ); + + $result = Parentheses::hasOwner(self::$phpcsFile, $stackPtr, [\T_IF, \T_ELSEIF, \T_ELSE]); + $this->assertTrue( + $result, + 'Failed asserting that $array in "testElseIfWithClosure" has an "if", "elseif" or "else" owner' + ); + + $stackPtr = self::$testTokens['testForWithTernary-$array']; + + $result = Parentheses::hasOwner(self::$phpcsFile, $stackPtr, [\T_ARRAY, \T_LIST]); + $this->assertFalse( + $result, + 'Failed asserting that $array in "testForWithTernary" does not have an array or list condition' + ); + + $result = Parentheses::hasOwner(self::$phpcsFile, $stackPtr, BCTokens::scopeOpeners()); + $this->assertTrue( + $result, + 'Failed asserting that $array in "testForWithTernary" has an owner which is also a scope opener' + ); + } + + /** + * Test correctly determining whether the first set of parenthesis around an arbitrary token + * has an owner of a certain type. + * + * @dataProvider dataFirstOwnerIn + * + * @param string $testName The name of this test as set in the cached $testTokens array. + * @param array $validOwners Valid owners to test against. + * @param int|false $expected Expected function output + * + * @return void + */ + public function testFirstOwnerIn($testName, $validOwners, $expected) + { + $stackPtr = self::$testTokens[$testName]; + if ($expected !== false) { + $expected += $stackPtr; + } + + $result = Parentheses::firstOwnerIn(self::$phpcsFile, $stackPtr, $validOwners); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testFirstOwnerIn() For the array format. + * + * @return array + */ + public function dataFirstOwnerIn() + { + return [ + 'testElseIfWithClosure-$a-elseif' => [ + 'testElseIfWithClosure-$a', + [\T_ELSEIF], + -10, + ], + 'testElseIfWithClosure-$a-array' => [ + 'testElseIfWithClosure-$a', + [\T_ARRAY], + false, + ], + 'testForeach-45-foreach-for' => [ + 'testForeach-45', + [\T_FOREACH, \T_FOR], + -27, + ], + 'testForeach-45-array' => [ + 'testForeach-45', + [\T_ARRAY], + false, + ], + 'testForeach-$a-foreach-for' => [ + 'testForeach-$a', + [\T_FOREACH, \T_FOR], + -43, + ], + 'testForeach-$a-list' => [ + 'testForeach-$a', + [\T_LIST], + false, + ], + 'testFunctionwithArray-$param-function-closure' => [ + 'testFunctionwithArray-$param', + [\T_FUNCTION, \T_CLOSURE], + -4, + ], + 'testFunctionwithArray-$param-if-elseif-else' => [ + 'testFunctionwithArray-$param', + [\T_IF, \T_ELSEIF, \T_ELSE], + false, + ], + 'testAnonClass-implements-anon-class' => [ + 'testAnonClass-implements', + [\T_ANON_CLASS], + -8, + ], + 'testAnonClass-$e-function' => [ + 'testAnonClass-$e', + [\T_FUNCTION], + false, + ], + 'testAnonClass-$e-catch' => [ + 'testAnonClass-$e', + [\T_CATCH], + false, + ], + ]; + } + + /** + * Test correctly determining whether the last set of parenthesis around an arbitrary token + * has an owner of a certain type. + * + * @dataProvider dataLastOwnerIn + * + * @param string $testName The name of this test as set in the cached $testTokens array. + * @param array $validOwners Valid owners to test against. + * @param int|false $expected Expected function output + * + * @return void + */ + public function testLastOwnerIn($testName, $validOwners, $expected) + { + $stackPtr = self::$testTokens[$testName]; + if ($expected !== false) { + $expected += $stackPtr; + } + + $result = Parentheses::lastOwnerIn(self::$phpcsFile, $stackPtr, $validOwners); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testLastOwnerIn() For the array format. + * + * @return array + */ + public function dataLastOwnerIn() + { + return [ + 'testElseIfWithClosure-$a-closure' => [ + 'testElseIfWithClosure-$a', + [\T_CLOSURE], + -3, + ], + 'testElseIfWithClosure-$a-array' => [ + 'testElseIfWithClosure-$a', + [\T_ARRAY], + false, + ], + 'testForeach-45-array' => [ + 'testForeach-45', + [\T_ARRAY], + -2, + ], + 'testForeach-45-foreach-for' => [ + 'testForeach-45', + [\T_FOREACH, \T_FOR], + false, + ], + 'testForeach-$a-foreach-for' => [ + 'testForeach-$a', + [\T_FOREACH, \T_FOR], + false, + ], + 'testForeach-$a-list' => [ + 'testForeach-$a', + [\T_LIST], + -6, + ], + 'testFunctionwithArray-$param-function-closure' => [ + 'testFunctionwithArray-$param', + [\T_FUNCTION, \T_CLOSURE], + -4, + ], + 'testFunctionwithArray-$param-if-elseif-else' => [ + 'testFunctionwithArray-$param', + [\T_IF, \T_ELSEIF, \T_ELSE], + false, + ], + 'testAnonClass-implements-anon-class' => [ + 'testAnonClass-implements', + [\T_ANON_CLASS], + -8, + ], + 'testAnonClass-$e-function' => [ + 'testAnonClass-$e', + [\T_FUNCTION], + false, + ], + 'testAnonClass-$e-catch' => [ + 'testAnonClass-$e', + [\T_CATCH], + -5, + ], + ]; + } +} diff --git a/Tests/Utils/PassedParameters/GetParameterCountTest.inc b/Tests/Utils/PassedParameters/GetParameterCountTest.inc new file mode 100644 index 00000000..781e2c30 --- /dev/null +++ b/Tests/Utils/PassedParameters/GetParameterCountTest.inc @@ -0,0 +1,199 @@ + 'b',]); + +/* testFunctionCall27 */ +json_encode(['a' => $a,]); + +/* testFunctionCall28 */ +json_encode(['a' => $a,] + (isset($b) ? ['b' => $b,] : [])); + +/* testFunctionCall29 */ +json_encode(['a' => $a,] + (isset($b) ? ['b' => $b, 'c' => $c,] : [])); + +/* testFunctionCall30 */ +json_encode(['a' => $a, 'b' => $b] + (isset($c) ? ['c' => $c, 'd' => $d] : [])); + +/* testFunctionCall31 */ +json_encode(['a' => $a, 'b' => $b] + (isset($c) ? ['c' => $c, 'd' => $d,] : [])); + +/* testFunctionCall32 */ +json_encode(['a' => $a, 'b' => $b] + (isset($c) ? ['c' => $c, 'd' => $d, $c => 'c'] : [])); + +/* testFunctionCall33 */ +json_encode(['a' => $a,] + (isset($b) ? ['b' => $b,] : []) + ['c' => $c, 'd' => $d,]); + +/* testFunctionCall34 */ +json_encode(['a' => 'b', 'c' => 'd',]); + +/* testFunctionCall35 */ +json_encode(['a' => ['b',],]); + +/* testFunctionCall36 */ +json_encode(['a' => ['b' => 'c',],]); + +/* testFunctionCall37 */ +json_encode(['a' => ['b' => 'c',], 'd' => ['e' => 'f',],]); + +/* testFunctionCall38 */ +json_encode(['a' => $a, 'b' => $b,]); + +/* testFunctionCall39 */ +json_encode(['a' => $a,] + ['b' => $b,]); + +/* testFunctionCall40 */ +json_encode(['a' => $a] + ['b' => $b, 'c' => $c,]); + +/* testFunctionCall41 */ +json_encode(['a' => $a, 'b' => $b] + ['c' => $c, 'd' => $d]); + +/* testFunctionCall42 */ +json_encode(['a' => $a, 'b' => $b] + ['c' => $c, 'd' => $d,]); + +/* testFunctionCall43 */ +json_encode(['a' => $a, 'b' => $b] + ['c' => $c, 'd' => $d, $c => 'c']); + +/* testFunctionCall44 */ +json_encode(['a' => $a, 'b' => $b,] + ['c' => $c]); + +/* testFunctionCall45 */ +json_encode(['a' => $a, 'b' => $b,] + ['c' => $c,]); + +/* testFunctionCall46 */ +json_encode(['a' => $a, 'b' => $b, 'c' => $c]); + +/* testFunctionCall47 */ +json_encode(['a' => $a, 'b' => $b, 'c' => $c,] + ['c' => $c, 'd' => $d,]); + +/* testLongArray1 */ +$foo = array( 1, 2, 3, 4, 5, 6, true ); + +/* testLongArray2 */ +$foo = array(str_replace("../", "/", trim($value))); // 1 + +/* testLongArray3 */ +$foo = array($stHour, 0, 0, $arrStDt[0], $arrStDt[1], $arrStDt[2]); // 6 + +/* testLongArray4 */ +$foo = array(0, 0, date('s'), date('m'), date('d'), date('Y')); // 6 + +/* testLongArray5 */ +$foo = array(some_call(5, 1), another(1), why(5, 1, 2), 4, 5, 6); // 6 + +/* testLongArray6 */ +$foo = array('a' => $a, 'b' => $b, 'c' => $c); + +/* testLongArray7 */ +$foo = array('a' => $a, 'b' => $b, 'c' => (isset($c) ? $c : null)); + +/* testLongArray8 */ +$foo = array(0 => $a, 2 => $b, 6 => (isset($c) ? $c : null)); + +/* testShortArray1 */ +$bar = [ 1, 2, 3, 4, 5, 6, true ]; + +/* testShortArray2 */ +$bar = [str_replace("../", "/", trim($value))]; // 1 + +/* testShortArray3 */ +$bar = [$stHour, 0, 0, $arrStDt[0], $arrStDt[1], $arrStDt[2]]; // 6 + +/* testShortArray4 */ +$bar = [0, 0, date('s'), date('m'), date('d'), date('Y')]; // 6 + +/* testShortArray5 */ +$bar = [some_call(5, 1), another(1), why(5, 1, 2), 4, 5, 6]; // 6 + +/* testShortArray6 */ +$bar = ['a' => $a, 'b' => $b, 'c' => $c]; + +/* testShortArray7 */ +$bar = ['a' => $a, 'b' => $b, 'c' => (isset($c) ? $c : null)]; + +/* testShortArray8 */ +$bar = [0 => $a, 2 => $b, 6 => (isset($c) ? $c : null)]; diff --git a/Tests/Utils/PassedParameters/GetParameterCountTest.php b/Tests/Utils/PassedParameters/GetParameterCountTest.php new file mode 100644 index 00000000..844c556d --- /dev/null +++ b/Tests/Utils/PassedParameters/GetParameterCountTest.php @@ -0,0 +1,322 @@ +getTargetToken( + $testMarker, + [\T_STRING, \T_ARRAY, \T_OPEN_SHORT_ARRAY, \T_ISSET, \T_UNSET] + ); + $result = PassedParameters::getParameterCount(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testGetParameterCount() For the array format. + * + * @return array + */ + public function dataGetParameterCount() + { + return [ + 'function-call-0' => [ + '/* testFunctionCall0 */', + 0, + ], + 'function-call-1' => [ + '/* testFunctionCall1 */', + 1, + ], + 'function-call-2' => [ + '/* testFunctionCall2 */', + 2, + ], + 'function-call-3' => [ + '/* testFunctionCall3 */', + 3, + ], + 'function-call-4' => [ + '/* testFunctionCall4 */', + 4, + ], + 'function-call-5' => [ + '/* testFunctionCall5 */', + 5, + ], + 'function-call-6' => [ + '/* testFunctionCall6 */', + 6, + ], + 'function-call-7' => [ + '/* testFunctionCall7 */', + 7, + ], + 'function-call-8' => [ + '/* testFunctionCall8 */', + 1, + ], + 'function-call-9' => [ + '/* testFunctionCall9 */', + 1, + ], + 'function-call-10' => [ + '/* testFunctionCall10 */', + 1, + ], + 'function-call-11' => [ + '/* testFunctionCall11 */', + 2, + ], + 'function-call-12' => [ + '/* testFunctionCall12 */', + 1, + ], + 'function-call-13' => [ + '/* testFunctionCall13 */', + 1, + ], + 'function-call-14' => [ + '/* testFunctionCall14 */', + 1, + ], + 'function-call-15' => [ + '/* testFunctionCall15 */', + 2, + ], + 'function-call-16' => [ + '/* testFunctionCall16 */', + 6, + ], + 'function-call-17' => [ + '/* testFunctionCall17 */', + 6, + ], + 'function-call-18' => [ + '/* testFunctionCall18 */', + 6, + ], + 'function-call-19' => [ + '/* testFunctionCall19 */', + 6, + ], + 'function-call-20' => [ + '/* testFunctionCall20 */', + 6, + ], + 'function-call-21' => [ + '/* testFunctionCall21 */', + 6, + ], + 'function-call-22' => [ + '/* testFunctionCall22 */', + 6, + ], + 'function-call-23' => [ + '/* testFunctionCall23 */', + 3, + ], + 'function-call-24' => [ + '/* testFunctionCall24 */', + 1, + ], + 'function-call-25' => [ + '/* testFunctionCall25 */', + 1, + ], + 'function-call-26' => [ + '/* testFunctionCall26 */', + 1, + ], + 'function-call-27' => [ + '/* testFunctionCall27 */', + 1, + ], + 'function-call-28' => [ + '/* testFunctionCall28 */', + 1, + ], + 'function-call-29' => [ + '/* testFunctionCall29 */', + 1, + ], + 'function-call-30' => [ + '/* testFunctionCall30 */', + 1, + ], + 'function-call-31' => [ + '/* testFunctionCall31 */', + 1, + ], + 'function-call-32' => [ + '/* testFunctionCall32 */', + 1, + ], + 'function-call-33' => [ + '/* testFunctionCall33 */', + 1, + ], + 'function-call-34' => [ + '/* testFunctionCall34 */', + 1, + ], + 'function-call-35' => [ + '/* testFunctionCall35 */', + 1, + ], + 'function-call-36' => [ + '/* testFunctionCall36 */', + 1, + ], + 'function-call-37' => [ + '/* testFunctionCall37 */', + 1, + ], + 'function-call-38' => [ + '/* testFunctionCall38 */', + 1, + ], + 'function-call-39' => [ + '/* testFunctionCall39 */', + 1, + ], + 'function-call-40' => [ + '/* testFunctionCall40 */', + 1, + ], + 'function-call-41' => [ + '/* testFunctionCall41 */', + 1, + ], + 'function-call-42' => [ + '/* testFunctionCall42 */', + 1, + ], + 'function-call-43' => [ + '/* testFunctionCall43 */', + 1, + ], + 'function-call-44' => [ + '/* testFunctionCall44 */', + 1, + ], + 'function-call-45' => [ + '/* testFunctionCall45 */', + 1, + ], + 'function-call-46' => [ + '/* testFunctionCall46 */', + 1, + ], + 'function-call-47' => [ + '/* testFunctionCall47 */', + 1, + ], + + // Long arrays. + 'long-array-1' => [ + '/* testLongArray1 */', + 7, + ], + 'long-array-2' => [ + '/* testLongArray2 */', + 1, + ], + 'long-array-3' => [ + '/* testLongArray3 */', + 6, + ], + 'long-array-4' => [ + '/* testLongArray4 */', + 6, + ], + 'long-array-5' => [ + '/* testLongArray5 */', + 6, + ], + 'long-array-6' => [ + '/* testLongArray6 */', + 3, + ], + 'long-array-7' => [ + '/* testLongArray7 */', + 3, + ], + 'long-array-8' => [ + '/* testLongArray8 */', + 3, + ], + + // Short arrays. + 'short-array-1' => [ + '/* testShortArray1 */', + 7, + ], + 'short-array-2' => [ + '/* testShortArray2 */', + 1, + ], + 'short-array-3' => [ + '/* testShortArray3 */', + 6, + ], + 'short-array-4' => [ + '/* testShortArray4 */', + 6, + ], + 'short-array-5' => [ + '/* testShortArray5 */', + 6, + ], + 'short-array-6' => [ + '/* testShortArray6 */', + 3, + ], + 'short-array-7' => [ + '/* testShortArray7 */', + 3, + ], + 'short-array-8' => [ + '/* testShortArray8 */', + 3, + ], + ]; + } +} diff --git a/Tests/Utils/PassedParameters/GetParametersTest.inc b/Tests/Utils/PassedParameters/GetParametersTest.inc new file mode 100644 index 00000000..c5f3a326 --- /dev/null +++ b/Tests/Utils/PassedParameters/GetParametersTest.inc @@ -0,0 +1,114 @@ + $a,] + (isset($b) ? ['b' => $b,] : [])); + +/* testLongArrayNestedFunctionCalls */ +$foo = array(some_call(5, 1), another(1), why(5, 1, 2), 4, 5, 6); // 6 + +/* testSimpleLongArray */ +$foo = array( 1, 2, 3, 4, 5, 6, true ); + +/* testLongArrayWithKeys */ +$foo = array('a' => $a, 'b' => $b, 'c' => $c); + +/* testShortArrayNestedFunctionCalls */ +$bar = [0, 0, date('s', $timestamp), date('m'), date('d'), date('Y')]; // 6 + +/* testShortArrayMoreNestedFunctionCalls */ +$bar = [str_replace("../", "/", trim($value))]; // 1 + +/* testShortArrayWithKeysAndTernary */ +$bar = [0 => $a, 2 => $b, 6 => (isset($c) ? $c : null)]; + +/* testShortArrayWithKeysTernaryAndNullCoalesce */ +$bar = [ + 'foo' => 'foo', + 'bar' => $baz ? + ['abc'] : + ['def'], + 'hey' => $baz ?? + ['one'] ?? + ['two'], +]; + +/* testNestedArraysToplevel */ +$array = array( + '1' => array( + 0 => 'more nesting', + /* testNestedArraysLevel2 */ + 1 => array(1,2,3), + ), + /* testNestedArraysLevel1 */ + '2' => [ + 0 => 'more nesting', + 1 => [1,2,3], + ], +); + +/* testFunctionCallNestedArrayNestedClosureWithCommas */ +preg_replace_callback_array( + /* testShortArrayNestedClosureWithCommas */ + [ + '~'.$dyn.'~J' => function ($match) { + echo strlen($match[0]), ' matches for "a" found', PHP_EOL; + }, + '~'.function_call().'~i' => function ($match) { + echo strlen($match[0]), ' matches for "b" found', PHP_EOL; + }, + ], + $subject +); + +/* testShortArrayNestedAnonClass */ +$array = [ + /** + * Docblock to skip over. + */ + 'class' => new class() { + public $prop = [1,2,3]; + public function test( $foo, $bar ) { + echo $foo, $bar; + } + }, + /** + * Docblock to skip over. + */ + 'anotherclass' => new class() { + public function test( $foo, $bar ) { + echo $foo, $bar; + } + }, +]; + +/* testVariableFunctionCall */ +$closure($a, (1 + 20), $a & $b ); + +/* testStaticVariableFunctionCall */ +self::$closureInStaticProperty($a->property, $b->call() ); + +/* testIsset */ +if ( isset( + $variable, + $object->property, + static::$property, + $array[$name][$sub], +)) {} + +/* testUnset */ +unset( $variable, $object->property, static::$property, $array[$name], ); diff --git a/Tests/Utils/PassedParameters/GetParametersTest.php b/Tests/Utils/PassedParameters/GetParametersTest.php new file mode 100644 index 00000000..88353977 --- /dev/null +++ b/Tests/Utils/PassedParameters/GetParametersTest.php @@ -0,0 +1,671 @@ +getTargetToken('/* testNoParams */', \T_STRING); + + $result = PassedParameters::getParameters(self::$phpcsFile, $stackPtr); + $this->assertSame([], $result); + + $result = PassedParameters::getParameter(self::$phpcsFile, $stackPtr, 2); + $this->assertFalse($result); + } + + /** + * Test retrieving the parameter details from a function call or construct. + * + * @dataProvider dataGetParameters + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string $targetType The type of token to look for. + * @param array $expected The expected parameter array. + * + * @return void + */ + public function testGetParameters($testMarker, $targetType, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, [$targetType]); + + // Start/end token position values in the expected array are set as offsets + // in relation to the target token. + // Change these to exact positions based on the retrieved stackPtr. + foreach ($expected as $key => $value) { + $expected[$key]['start'] = ($stackPtr + $value['start']); + $expected[$key]['end'] = ($stackPtr + $value['end']); + } + + $result = PassedParameters::getParameters(self::$phpcsFile, $stackPtr); + + foreach ($result as $key => $value) { + $this->assertArrayHasKey('clean', $value); + + // The GetTokensAsString functions have their own tests, no need to duplicate it here. + unset($result[$key]['clean']); + } + + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testGetParameters() For the array format. + * + * @return array + */ + public function dataGetParameters() + { + return [ + 'function-call' => [ + '/* testFunctionCall */', + \T_STRING, + [ + 1 => [ + 'start' => 2, + 'end' => 3, + 'raw' => '1', + ], + 2 => [ + 'start' => 5, + 'end' => 6, + 'raw' => '2', + ], + 3 => [ + 'start' => 8, + 'end' => 9, + 'raw' => '3', + ], + 4 => [ + 'start' => 11, + 'end' => 12, + 'raw' => '4', + ], + 5 => [ + 'start' => 14, + 'end' => 15, + 'raw' => '5', + ], + 6 => [ + 'start' => 17, + 'end' => 18, + 'raw' => '6', + ], + 7 => [ + 'start' => 20, + 'end' => 22, + 'raw' => 'true', + ], + ], + ], + 'function-call-nested' => [ + '/* testFunctionCallNestedFunctionCall */', + \T_STRING, + [ + 1 => [ + 'start' => 2, + 'end' => 9, + 'raw' => 'dirname( __FILE__ )', + ], + ], + ], + 'another-function-call' => [ + '/* testAnotherFunctionCall */', + \T_STRING, + [ + 1 => [ + 'start' => 2, + 'end' => 2, + 'raw' => '$stHour', + ], + 2 => [ + 'start' => 4, + 'end' => 5, + 'raw' => '0', + ], + 3 => [ + 'start' => 7, + 'end' => 8, + 'raw' => '0', + ], + 4 => [ + 'start' => 10, + 'end' => 14, + 'raw' => '$arrStDt[0]', + ], + 5 => [ + 'start' => 16, + 'end' => 20, + 'raw' => '$arrStDt[1]', + ], + 6 => [ + 'start' => 22, + 'end' => 26, + 'raw' => '$arrStDt[2]', + ], + ], + + ], + 'function-call-trailing-comma' => [ + '/* testFunctionCallTrailingComma */', + \T_STRING, + [ + 1 => [ + 'start' => 2, + 'end' => 5, + 'raw' => 'array()', + ], + ], + ], + 'function-call-nested-short-array' => [ + '/* testFunctionCallNestedShortArray */', + \T_STRING, + [ + 1 => [ + 'start' => 2, + 'end' => 34, + 'raw' => '[\'a\' => $a,] + (isset($b) ? [\'b\' => $b,] : [])', + ], + ], + ], + 'function-call-nested-array-nested-closure-with-commas' => [ + '/* testFunctionCallNestedArrayNestedClosureWithCommas */', + \T_STRING, + [ + 1 => [ + 'start' => 2, + 'end' => 90, + 'raw' => '/* testShortArrayNestedClosureWithCommas */ + [ + \'~\'.$dyn.\'~J\' => function ($match) { + echo strlen($match[0]), \' matches for "a" found\', PHP_EOL; + }, + \'~\'.function_call().\'~i\' => function ($match) { + echo strlen($match[0]), \' matches for "b" found\', PHP_EOL; + }, + ]', + ], + 2 => [ + 'start' => 92, + 'end' => 95, + 'raw' => '$subject', + ], + ], + ], + + // Long array. + 'long-array' => [ + '/* testLongArrayNestedFunctionCalls */', + \T_ARRAY, + [ + 1 => [ + 'start' => 2, + 'end' => 8, + 'raw' => 'some_call(5, 1)', + ], + 2 => [ + 'start' => 10, + 'end' => 14, + 'raw' => 'another(1)', + ], + 3 => [ + 'start' => 16, + 'end' => 26, + 'raw' => 'why(5, 1, 2)', + ], + 4 => [ + 'start' => 28, + 'end' => 29, + 'raw' => '4', + ], + 5 => [ + 'start' => 31, + 'end' => 32, + 'raw' => '5', + ], + 6 => [ + 'start' => 34, + 'end' => 35, + 'raw' => '6', + ], + ], + ], + + // Short array. + 'short-array' => [ + '/* testShortArrayNestedFunctionCalls */', + \T_OPEN_SHORT_ARRAY, + [ + 1 => [ + 'start' => 1, + 'end' => 1, + 'raw' => '0', + ], + 2 => [ + 'start' => 3, + 'end' => 4, + 'raw' => '0', + ], + 3 => [ + 'start' => 6, + 'end' => 13, + 'raw' => 'date(\'s\', $timestamp)', + ], + 4 => [ + 'start' => 15, + 'end' => 19, + 'raw' => 'date(\'m\')', + ], + 5 => [ + 'start' => 21, + 'end' => 25, + 'raw' => 'date(\'d\')', + ], + 6 => [ + 'start' => 27, + 'end' => 31, + 'raw' => 'date(\'Y\')', + ], + ], + ], + 'short-array-with-keys-ternary-and-null-coalesce' => [ + '/* testShortArrayWithKeysTernaryAndNullCoalesce */', + \T_OPEN_SHORT_ARRAY, + [ + 1 => [ + 'start' => 1, + 'end' => 7, + 'raw' => "'foo' => 'foo'", + ], + 2 => [ + 'start' => 9, + 'end' => 29, + 'raw' => '\'bar\' => $baz ? + [\'abc\'] : + [\'def\']', + ], + 3 => [ + 'start' => 31, + // Account for null coalesce tokenization difference. + 'end' => (Helper::getVersion() === '2.6.0' && \PHP_VERSION_ID < 59999) + ? 53 + : 51, + 'raw' => '\'hey\' => $baz ?? + [\'one\'] ?? + [\'two\']', + ], + ], + ], + + // Nested arrays. + 'nested-arrays-top-level' => [ + '/* testNestedArraysToplevel */', + \T_ARRAY, + [ + 1 => [ + 'start' => 2, + 'end' => 38, + 'raw' => '\'1\' => array( + 0 => \'more nesting\', + /* testNestedArraysLevel2 */ + 1 => array(1,2,3), + )', + ], + 2 => [ + 'start' => 40, + 'end' => 74, + 'raw' => '/* testNestedArraysLevel1 */ + \'2\' => [ + 0 => \'more nesting\', + 1 => [1,2,3], + ]', + ], + ], + ], + + // Array containing closure. + 'short-array-nested-closure-with-commas' => [ + '/* testShortArrayNestedClosureWithCommas */', + \T_OPEN_SHORT_ARRAY, + [ + 1 => [ + 'start' => 1, + 'end' => 38, + 'raw' => '\'~\'.$dyn.\'~J\' => function ($match) { + echo strlen($match[0]), \' matches for "a" found\', PHP_EOL; + }', + ], + 2 => [ + 'start' => 40, + 'end' => 79, + 'raw' => '\'~\'.function_call().\'~i\' => function ($match) { + echo strlen($match[0]), \' matches for "b" found\', PHP_EOL; + }', + ], + ], + ], + + // Array containing anonymous class. + 'short-array-nested-anon-class' => [ + '/* testShortArrayNestedAnonClass */', + \T_OPEN_SHORT_ARRAY, + [ + 1 => [ + 'start' => 1, + 'end' => 72, + 'raw' => '/** + * Docblock to skip over. + */ + \'class\' => new class() { + public $prop = [1,2,3]; + public function test( $foo, $bar ) { + echo $foo, $bar; + } + }', + ], + 2 => [ + 'start' => 74, + 'end' => 129, + 'raw' => '/** + * Docblock to skip over. + */ + \'anotherclass\' => new class() { + public function test( $foo, $bar ) { + echo $foo, $bar; + } + }', + ], + ], + ], + + // Function calling closure in variable. + 'variable-function-call' => [ + '/* testVariableFunctionCall */', + \T_VARIABLE, + [ + 1 => [ + 'start' => 2, + 'end' => 2, + 'raw' => '$a', + ], + 2 => [ + 'start' => 4, + 'end' => 11, + 'raw' => '(1 + 20)', + ], + 3 => [ + 'start' => 13, + 'end' => 19, + 'raw' => '$a & $b', + ], + ], + ], + 'static-variable-function-call' => [ + '/* testStaticVariableFunctionCall */', + \T_VARIABLE, + [ + 1 => [ + 'start' => 2, + 'end' => 4, + 'raw' => '$a->property', + ], + 2 => [ + 'start' => 6, + 'end' => 12, + 'raw' => '$b->call()', + ], + ], + ], + 'isset' => [ + '/* testIsset */', + \T_ISSET, + [ + 1 => [ + 'start' => 2, + 'end' => 4, + 'raw' => '$variable', + ], + 2 => [ + 'start' => 6, + 'end' => 10, + 'raw' => '$object->property', + ], + 3 => [ + 'start' => 12, + 'end' => 16, + 'raw' => 'static::$property', + ], + 4 => [ + 'start' => 18, + 'end' => 26, + 'raw' => '$array[$name][$sub]', + ], + ], + ], + 'unset' => [ + '/* testUnset */', + \T_UNSET, + [ + 1 => [ + 'start' => 2, + 'end' => 3, + 'raw' => '$variable', + ], + 2 => [ + 'start' => 5, + 'end' => 8, + 'raw' => '$object->property', + ], + 3 => [ + 'start' => 10, + 'end' => 13, + 'raw' => 'static::$property', + ], + 4 => [ + 'start' => 15, + 'end' => 19, + 'raw' => '$array[$name]', + ], + ], + ], + ]; + } + + /** + * Test retrieving the details for a specific parameter from a function call or construct. + * + * @dataProvider dataGetParameter + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string $targetType The type of token to look for. + * @param int $paramPosition The position of the parameter we want to retrieve the details for. + * @param array $expected The expected array for the specific parameter. + * + * @return void + */ + public function testGetParameter($testMarker, $targetType, $paramPosition, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, [$targetType]); + + // Start/end token position values in the expected array are set as offsets + // in relation to the target token. + // Change these to exact positions based on the retrieved stackPtr. + $expected['start'] += $stackPtr; + $expected['end'] += $stackPtr; + + $result = PassedParameters::getParameter(self::$phpcsFile, $stackPtr, $paramPosition); + + $this->assertArrayHasKey('clean', $result); + + // The GetTokensAsString functions have their own tests, no need to duplicate it here. + unset($result['clean']); + + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testGetParameter() For the array format. + * + * @return array + */ + public function dataGetParameter() + { + return [ + 'function-call-param-4' => [ + '/* testFunctionCall */', + \T_STRING, + 4, + [ + 'start' => 11, + 'end' => 12, + 'raw' => '4', + ], + ], + 'function-call-nested-param-1' => [ + '/* testFunctionCallNestedFunctionCall */', + \T_STRING, + 1, + [ + 'start' => 2, + 'end' => 9, + 'raw' => 'dirname( __FILE__ )', + ], + ], + 'another-function-call-param-1' => [ + '/* testAnotherFunctionCall */', + \T_STRING, + 1, + [ + 'start' => 2, + 'end' => 2, + 'raw' => '$stHour', + ], + ], + 'another-function-call-param-6' => [ + '/* testAnotherFunctionCall */', + \T_STRING, + 6, + [ + 'start' => 22, + 'end' => 26, + 'raw' => '$arrStDt[2]', + ], + ], + 'long-array-nested-function-calls-param-3' => [ + '/* testLongArrayNestedFunctionCalls */', + \T_ARRAY, + 3, + [ + 'start' => 16, + 'end' => 26, + 'raw' => 'why(5, 1, 2)', + ], + ], + 'simple-long-array-param-1' => [ + '/* testSimpleLongArray */', + \T_ARRAY, + 1, + [ + 'start' => 2, + 'end' => 3, + 'raw' => '1', + ], + ], + 'simple-long-array-param-7' => [ + '/* testSimpleLongArray */', + \T_ARRAY, + 7, + [ + 'start' => 20, + 'end' => 22, + 'raw' => 'true', + ], + ], + 'long-array-with-keys-param-' => [ + '/* testLongArrayWithKeys */', + \T_ARRAY, + 2, + [ + 'start' => 8, + 'end' => 13, + 'raw' => '\'b\' => $b', + ], + ], + 'short-array-more-nested-function-calls-param-1' => [ + '/* testShortArrayMoreNestedFunctionCalls */', + \T_OPEN_SHORT_ARRAY, + 1, + [ + 'start' => 1, + 'end' => 13, + 'raw' => 'str_replace("../", "/", trim($value))', + ], + ], + 'short-array-with-keys-and-ternary-param-3' => [ + '/* testShortArrayWithKeysAndTernary */', + \T_OPEN_SHORT_ARRAY, + 3, + [ + 'start' => 14, + 'end' => 32, + 'raw' => '6 => (isset($c) ? $c : null)', + ], + ], + 'nested-arrays-level-2-param-1' => [ + '/* testNestedArraysLevel2 */', + \T_ARRAY, + 1, + [ + 'start' => 2, + 'end' => 2, + 'raw' => '1', + ], + ], + 'nested-arrays-level-1-param-2' => [ + '/* testNestedArraysLevel1 */', + \T_OPEN_SHORT_ARRAY, + 2, + [ + 'start' => 9, + 'end' => 21, + 'raw' => '1 => [1,2,3]', + ], + ], + ]; + } +} diff --git a/Tests/Utils/PassedParameters/HasParametersTest.inc b/Tests/Utils/PassedParameters/HasParametersTest.inc new file mode 100644 index 00000000..3c05a5c7 --- /dev/null +++ b/Tests/Utils/PassedParameters/HasParametersTest.inc @@ -0,0 +1,120 @@ +expectPhpcsException( + 'The hasParameters() method expects a function call, array, isset or unset token to be passed' + ); + + PassedParameters::hasParameters(self::$phpcsFile, 100000); + } + + /** + * Test receiving an expected exception when a token which is not supported by + * these methods is passed. + * + * @return void + */ + public function testNotAnAcceptedTokenException() + { + $this->expectPhpcsException( + 'The hasParameters() method expects a function call, array, isset or unset token to be passed.' + ); + + $interface = $this->getTargetToken('/* testNotAnAcceptedToken */', \T_INTERFACE); + PassedParameters::hasParameters(self::$phpcsFile, $interface); + } + + /** + * Test receiving an expected exception when T_SELF is passed not preceeded by `new`. + * + * @return void + */ + public function testNotACallToConstructor() + { + $this->expectPhpcsException( + 'The hasParameters() method expects a function call, array, isset or unset token to be passed.' + ); + + $self = $this->getTargetToken('/* testNotACallToConstructor */', \T_SELF); + PassedParameters::hasParameters(self::$phpcsFile, $self); + } + + /** + * Test receiving an expected exception when T_OPEN_SHORT_ARRAY is passed but represents a short list. + * + * @return void + */ + public function testNotAShortArray() + { + $this->expectPhpcsException( + 'The hasParameters() method expects a function call, array, isset or unset token to be passed.' + ); + + $self = $this->getTargetToken( + '/* testShortListNotShortArray */', + [\T_OPEN_SHORT_ARRAY, \T_OPEN_SQUARE_BRACKET] + ); + PassedParameters::hasParameters(self::$phpcsFile, $self); + } + + /** + * Test correctly identifying whether parameters were passed to a function call or construct. + * + * @dataProvider dataHasParameters + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int|string $targetType The type of token to look for. + * @param bool $expected Whether or not the function/array has parameters/values. + * + * @return void + */ + public function testHasParameters($testMarker, $targetType, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, $targetType); + $result = PassedParameters::hasParameters(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testHasParameters() For the array format. + * + * @return array + */ + public function dataHasParameters() + { + return [ + // Function calls. + 'no-params-function-call-1' => [ + '/* testNoParamsFunctionCall1 */', + \T_STRING, + false, + ], + 'no-params-function-call-2' => [ + '/* testNoParamsFunctionCall2 */', + \T_STRING, + false, + ], + 'no-params-function-call-3' => [ + '/* testNoParamsFunctionCall3 */', + \T_STRING, + false, + ], + 'no-params-function-call-4' => [ + '/* testNoParamsFunctionCall4 */', + \T_VARIABLE, + false, + ], + 'has-params-function-call-1' => [ + '/* testHasParamsFunctionCall1 */', + \T_STRING, + true, + ], + 'has-params-function-call-2' => [ + '/* testHasParamsFunctionCall2 */', + \T_VARIABLE, + true, + ], + 'has-params-function-call-3' => [ + '/* testHasParamsFunctionCall3 */', + // In PHPCS < 2.8.0, self in "new self" is tokenized as T_STRING. + [\T_SELF, \T_STRING], + true, + ], + + // Arrays. + 'no-params-long-array-1' => [ + '/* testNoParamsLongArray1 */', + \T_ARRAY, + false, + ], + 'no-params-long-array-2' => [ + '/* testNoParamsLongArray2 */', + \T_ARRAY, + false, + ], + 'no-params-long-array-3' => [ + '/* testNoParamsLongArray3 */', + \T_ARRAY, + false, + ], + 'no-params-long-array-4' => [ + '/* testNoParamsLongArray4 */', + \T_ARRAY, + false, + ], + 'no-params-short-array-1' => [ + '/* testNoParamsShortArray1 */', + \T_OPEN_SHORT_ARRAY, + false, + ], + 'no-params-short-array-2' => [ + '/* testNoParamsShortArray2 */', + \T_OPEN_SHORT_ARRAY, + false, + ], + 'no-params-short-array-3' => [ + '/* testNoParamsShortArray3 */', + \T_OPEN_SHORT_ARRAY, + false, + ], + 'no-params-short-array-4' => [ + '/* testNoParamsShortArray4 */', + \T_OPEN_SHORT_ARRAY, + false, + ], + 'has-params-long-array-1' => [ + '/* testHasParamsLongArray1 */', + \T_ARRAY, + true, + ], + 'has-params-long-array-2' => [ + '/* testHasParamsLongArray2 */', + \T_ARRAY, + true, + ], + 'has-params-long-array-3' => [ + '/* testHasParamsLongArray3 */', + \T_ARRAY, + true, + ], + 'has-params-short-array-1' => [ + '/* testHasParamsShortArray1 */', + \T_OPEN_SHORT_ARRAY, + true, + ], + 'has-params-short-array-2' => [ + '/* testHasParamsShortArray2 */', + \T_OPEN_SHORT_ARRAY, + true, + ], + 'has-params-short-array-3' => [ + '/* testHasParamsShortArray3 */', + \T_OPEN_SHORT_ARRAY, + true, + ], + + // Isset. + 'no-params-isset' => [ + '/* testNoParamsIsset */', + \T_ISSET, + false, + ], + 'has-params-isset' => [ + '/* testHasParamsIsset */', + \T_ISSET, + true, + ], + + // Unset. + 'no-params-unset' => [ + '/* testNoParamsUnset */', + \T_UNSET, + false, + ], + 'has-params-unset' => [ + '/* testHasParamsUnset */', + \T_UNSET, + true, + ], + + // Defensive coding against parse errors and live coding. + 'defense-in-depth-no-close-parens' => [ + '/* testNoCloseParenthesis */', + \T_ARRAY, + false, + ], + 'defense-in-depth-no-open-parens' => [ + '/* testNoOpenParenthesis */', + \T_STRING, + false, + ], + 'defense-in-depth-live-coding' => [ + '/* testLiveCoding */', + \T_ARRAY, + false, + ], + ]; + } +} diff --git a/Tests/Utils/Scopes/IsOOConstantTest.inc b/Tests/Utils/Scopes/IsOOConstantTest.inc new file mode 100644 index 00000000..3654d886 --- /dev/null +++ b/Tests/Utils/Scopes/IsOOConstantTest.inc @@ -0,0 +1,39 @@ +assertFalse($result); + } + + /** + * Test passing a non const token. + * + * @covers \PHPCSUtils\Utils\Scopes::isOOConstant + * + * @return void + */ + public function testNonConstToken() + { + $result = Scopes::isOOConstant(self::$phpcsFile, 0); + $this->assertFalse($result); + } + + /** + * Test correctly identifying whether a T_CONST token is a class constant. + * + * @dataProvider dataIsOOConstant + * + * @covers \PHPCSUtils\Utils\Scopes::isOOConstant + * @covers \PHPCSUtils\Utils\Scopes::validDirectScope + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param bool $expected The expected function return value. + * + * @return void + */ + public function testIsOOConstant($testMarker, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, \T_CONST); + $result = Scopes::isOOConstant(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testIsOOConstant() For the array format. + * + * @return array + */ + public function dataIsOOConstant() + { + return [ + 'global-const' => [ + '/* testGlobalConst */', + false, + ], + 'function-const' => [ + '/* testFunctionConst */', + false, + ], + 'class-const' => [ + '/* testClassConst */', + true, + ], + 'method-const' => [ + '/* testClassMethodConst */', + false, + ], + 'anon-class-const' => [ + '/* testAnonClassConst */', + true, + ], + 'interface-const' => [ + '/* testInterfaceConst */', + true, + ], + 'trait-const' => [ + '/* testTraitConst */', + false, + ], + ]; + } +} diff --git a/Tests/Utils/Scopes/IsOOMethodTest.inc b/Tests/Utils/Scopes/IsOOMethodTest.inc new file mode 100644 index 00000000..d4e0c2ec --- /dev/null +++ b/Tests/Utils/Scopes/IsOOMethodTest.inc @@ -0,0 +1,40 @@ +assertFalse($result); + } + + /** + * Test passing a non function token. + * + * @covers \PHPCSUtils\Utils\Scopes::isOOMethod + * + * @return void + */ + public function testNonFunctionToken() + { + $result = Scopes::isOOMethod(self::$phpcsFile, 0); + $this->assertFalse($result); + } + + /** + * Test correctly identifying whether a T_FUNCTION token is a class method declaration. + * + * @dataProvider dataIsOOMethod + * + * @covers \PHPCSUtils\Utils\Scopes::isOOMethod + * @covers \PHPCSUtils\Utils\Scopes::validDirectScope + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param bool $expected The expected function return value. + * + * @return void + */ + public function testIsOOMethod($testMarker, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, [\T_FUNCTION, \T_CLOSURE]); + $result = Scopes::isOOMethod(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testIsOOMethod() For the array format. + * + * @return array + */ + public function dataIsOOMethod() + { + return [ + 'global-function' => [ + '/* testGlobalFunction */', + false, + ], + 'nested-function' => [ + '/* testNestedFunction */', + false, + ], + 'nested-closure' => [ + '/* testNestedClosure */', + false, + ], + 'class-method' => [ + '/* testClassMethod */', + true, + ], + 'class-nested-function' => [ + '/* testClassNestedFunction */', + false, + ], + 'class-nested-closure' => [ + '/* testClassNestedClosure */', + false, + ], + 'class-abstract-method' => [ + '/* testClassAbstractMethod */', + true, + ], + 'anon-class-method' => [ + '/* testAnonClassMethod */', + true, + ], + 'interface-method' => [ + '/* testInterfaceMethod */', + true, + ], + 'trait-method' => [ + '/* testTraitMethod */', + true, + ], + ]; + } +} diff --git a/Tests/Utils/Scopes/IsOOPropertyTest.inc b/Tests/Utils/Scopes/IsOOPropertyTest.inc new file mode 100644 index 00000000..9385111a --- /dev/null +++ b/Tests/Utils/Scopes/IsOOPropertyTest.inc @@ -0,0 +1,89 @@ +setLogger( + new class { + /* testNestedAnonClassProp */ + private $varName = 'hello'; +}); + +if ( has_filter( 'comments_open' ) === false ) { + add_filter( 'comments_open', new class { + /* testDoubleNestedAnonClassProp */ + public $year = 2017; // Ok. + + /* testDoubleNestedAnonClassMethodParameter */ + public function __construct( $open, $post_id ) { + /* testDoubleNestedAnonClassMethodLocalVar */ + global $page; + } + /* testFunctionCallParameter */ + }, $priority, 2 ); +} diff --git a/Tests/Utils/Scopes/IsOOPropertyTest.php b/Tests/Utils/Scopes/IsOOPropertyTest.php new file mode 100644 index 00000000..f4081806 --- /dev/null +++ b/Tests/Utils/Scopes/IsOOPropertyTest.php @@ -0,0 +1,180 @@ +assertFalse($result); + } + + /** + * Test passing a non variable token. + * + * @covers \PHPCSUtils\Utils\Scopes::isOOProperty + * + * @return void + */ + public function testNonVariableToken() + { + $result = Scopes::isOOProperty(self::$phpcsFile, 0); + $this->assertFalse($result); + } + + /** + * Test correctly identifying whether a T_VARIABLE token is a class property declaration. + * + * @dataProvider dataIsOOProperty + * + * @covers \PHPCSUtils\Utils\Scopes::isOOProperty + * @covers \PHPCSUtils\Utils\Scopes::validDirectScope + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param bool $expected The expected function return value. + * + * @return void + */ + public function testIsOOProperty($testMarker, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, \T_VARIABLE); + $result = Scopes::isOOProperty(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testIsOOProperty() For the array format. + * + * @return array + */ + public function dataIsOOProperty() + { + return [ + 'global-var' => [ + '/* testGlobalVar */', + false, + ], + 'function-param' => [ + '/* testFunctionParameter */', + false, + ], + 'function-local-var' => [ + '/* testFunctionLocalVar */', + false, + ], + 'class-property-public' => [ + '/* testClassPropPublic */', + true, + ], + 'class-property-var' => [ + '/* testClassPropVar */', + true, + ], + 'class-property-static-protected' => [ + '/* testClassPropStaticProtected */', + true, + ], + 'method-param' => [ + '/* testMethodParameter */', + false, + ], + 'method-local-var' => [ + '/* testMethodLocalVar */', + false, + ], + 'anon-class-property-private' => [ + '/* testAnonClassPropPrivate */', + true, + ], + 'anon-class-method-param' => [ + '/* testAnonMethodParameter */', + false, + ], + 'anon-class-method-local-var' => [ + '/* testAnonMethodLocalVar */', + false, + ], + 'interface-property' => [ + '/* testInterfaceProp */', + false, + ], + 'interface-method-param' => [ + '/* testInterfaceMethodParameter */', + false, + ], + 'trait-property' => [ + '/* testTraitProp */', + true, + ], + 'trait-method-param' => [ + '/* testTraitMethodParameter */', + false, + ], + 'class-multi-property-1' => [ + '/* testClassMultiProp1 */', + true, + ], + 'class-multi-property-2' => [ + '/* testClassMultiProp2 */', + true, + ], + 'class-multi-property-3' => [ + '/* testClassMultiProp3 */', + true, + ], + 'global-var-obj-access' => [ + '/* testGlobalVarObj */', + false, + ], + 'nested-anon-class-property' => [ + '/* testNestedAnonClassProp */', + true, + ], + 'double-nested-anon-class-property' => [ + '/* testDoubleNestedAnonClassProp */', + true, + ], + 'double-nested-anon-class-method-param' => [ + '/* testDoubleNestedAnonClassMethodParameter */', + false, + ], + 'double-nested-anon-class-method-local-var' => [ + '/* testDoubleNestedAnonClassMethodLocalVar */', + false, + ], + 'function-call-param' => [ + '/* testFunctionCallParameter */', + false, + ], + ]; + } +} diff --git a/Tests/Utils/TextStrings/GetCompleteTextStringTest.inc b/Tests/Utils/TextStrings/GetCompleteTextStringTest.inc new file mode 100644 index 00000000..a425ac4e --- /dev/null +++ b/Tests/Utils/TextStrings/GetCompleteTextStringTest.inc @@ -0,0 +1,49 @@ +expectPhpcsException( + '$stackPtr must be of type T_START_HEREDOC, T_START_NOWDOC, T_CONSTANT_ENCAPSED_STRING' + . ' or T_DOUBLE_QUOTED_STRING' + ); + + TextStrings::getCompleteTextString(self::$phpcsFile, 100000); + } + + /** + * Test receiving an expected exception when a non text string is passed. + * + * @return void + */ + public function testNotATextStringException() + { + $this->expectPhpcsException( + '$stackPtr must be of type T_START_HEREDOC, T_START_NOWDOC, T_CONSTANT_ENCAPSED_STRING' + . ' or T_DOUBLE_QUOTED_STRING' + ); + + $next = $this->getTargetToken('/* testNotATextString */', \T_RETURN); + TextStrings::getCompleteTextString(self::$phpcsFile, $next); + } + + /** + * Test receiving an expected exception when a text string token is not the first token + * of a multi-line text string. + * + * @return void + */ + public function testNotFirstTextStringException() + { + $this->expectPhpcsException('$stackPtr must be the start of the text string'); + + $next = $this->getTargetToken( + '/* testNotFirstTextStringToken */', + \T_CONSTANT_ENCAPSED_STRING, + 'second line +' + ); + TextStrings::getCompleteTextString(self::$phpcsFile, $next); + } + + /** + * Test correctly retrieving the contents of a (potentially) multi-line text string. + * + * @dataProvider dataGetCompleteTextString + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $expected The expected function return value. + * @param string $expectedWithQuotes The expected function return value when $stripQuotes is set to "false". + * + * @return void + */ + public function testGetCompleteTextString($testMarker, $expected, $expectedWithQuotes) + { + $stackPtr = $this->getTargetToken($testMarker, $this->targets); + + $result = TextStrings::getCompleteTextString(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result, 'Test failed getting the correct string with quotes stripped'); + + $result = TextStrings::getCompleteTextString(self::$phpcsFile, $stackPtr, false); + $this->assertSame($expectedWithQuotes, $result, 'Test failed getting the correct string (unchanged)'); + } + + /** + * Data provider. + * + * @see testGetCompleteTextString() For the array format. + * + * @return array + */ + public function dataGetCompleteTextString() + { + return [ + 'single-line-constant-encapsed-string' => [ + '/* testSingleLineConstantEncapsedString */', + 'single line text string', + "'single line text string'", + ], + 'multi-line-constant-encapsed-string' => [ + '/* testMultiLineConstantEncapsedString */', + 'first line +second line +third line +fourth line', + '"first line +second line +third line +fourth line"', + ], + 'single-line-double-quoted-string' => [ + '/* testSingleLineDoubleQuotedString */', + 'single $line text string', + '"single $line text string"', + ], + 'multi-line-double-quoted-string' => [ + '/* testMultiLineDoubleQuotedString */', + 'first line +second $line +third line +fourth line', + '"first line +second $line +third line +fourth line"', + ], + 'heredoc' => [ + '/* testHeredocString */', + 'first line +second $line +third line +fourth line +', + 'first line +second $line +third line +fourth line +', + ], + 'nowdoc' => [ + '/* testNowdocString */', + 'first line +second line +third line +fourth line +', + 'first line +second line +third line +fourth line +', + ], + 'text-string-at-end-of-file' => [ + '/* testTextStringAtEndOfFile */', + 'first line +last line', + "'first line +last line'", + ], + ]; + } +} diff --git a/Tests/Utils/TextStrings/StripQuotesTest.php b/Tests/Utils/TextStrings/StripQuotesTest.php new file mode 100644 index 00000000..9623b609 --- /dev/null +++ b/Tests/Utils/TextStrings/StripQuotesTest.php @@ -0,0 +1,97 @@ +assertSame($expected, TextStrings::stripQuotes($input)); + } + + /** + * Data provider. + * + * @see testStripQuotes() For the array format. + * + * @return array + */ + public function dataStripQuotes() + { + return [ + 'simple-string-double-quotes' => [ + '"dir_name"', + 'dir_name', + ], + 'simple-string-single-quotes' => [ + "'soap.wsdl_cache'", + 'soap.wsdl_cache', + ], + 'string-with-escaped-quotes-within-1' => [ + '"arbitrary-\'string\" with\' quotes within"', + 'arbitrary-\'string\" with\' quotes within', + ], + 'string-with-escaped-quotes-within-2' => [ + '"\'quoted_name\'"', + '\'quoted_name\'', + ], + 'string-with-different-quotes-at-start-of-string' => [ + "'\"quoted\" start of string'", + '"quoted" start of string', + ], + 'incomplete-quote-set-only-start-quote' => [ + "'no stripping when there is only a start quote", + "'no stripping when there is only a start quote", + ], + 'incomplete-quote-set-only-end-quote' => [ + 'no stripping when there is only an end quote"', + 'no stripping when there is only an end quote"', + ], + 'start-end-quote-mismatch' => [ + "'no stripping when quotes at start/end are mismatched\"", + "'no stripping when quotes at start/end are mismatched\"", + ], + 'multi-line-string-single-quotes' => [ + "'some + text + and +more'", + 'some + text + and +more', + ], + ]; + } +} diff --git a/Tests/Utils/UseStatements/SplitImportUseStatementTest.inc b/Tests/Utils/UseStatements/SplitImportUseStatementTest.inc new file mode 100644 index 00000000..29e01e6a --- /dev/null +++ b/Tests/Utils/UseStatements/SplitImportUseStatementTest.inc @@ -0,0 +1,97 @@ + + +expectPhpcsException('$stackPtr must be of type T_USE'); + + UseStatements::splitImportUseStatement(self::$phpcsFile, 10000); + } + + /** + * Test receiving an expected exception when a non-supported token is passed. + * + * @return void + */ + public function testInvalidTokenPassed() + { + $this->expectPhpcsException('$stackPtr must be of type T_USE'); + + // 0 = PHP open tag. + UseStatements::splitImportUseStatement(self::$phpcsFile, 0); + } + + /** + * Test receiving an expected exception when a non-import use statement token is passed. + * + * @dataProvider dataNonImportUseTokenPassed + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @return void + */ + public function testNonImportUseTokenPassed($testMarker) + { + $this->expectPhpcsException('$stackPtr must be an import use statement'); + + $stackPtr = $this->getTargetToken($testMarker, \T_USE); + UseStatements::splitImportUseStatement(self::$phpcsFile, $stackPtr); + } + + /** + * Data provider. + * + * @see testSplitImportUseStatement() For the array format. + * + * @return array + */ + public function dataNonImportUseTokenPassed() + { + return [ + 'closure-use' => ['/* testClosureUse */'], + 'trait-use' => ['/* testTraitUse */'], + ]; + } + + /** + * Test correctly splitting a T_USE statement into individual statements. + * + * @dataProvider dataSplitImportUseStatement + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected The expected return value of the function. + * + * @return void + */ + public function testSplitImportUseStatement($testMarker, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, \T_USE); + $result = UseStatements::splitImportUseStatement(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testSplitImportUseStatement() For the array format. + * + * @return array + */ + public function dataSplitImportUseStatement() + { + return [ + 'plain' => [ + '/* testUsePlain */', + [ + 'name' => ['MyClass' => 'MyNamespace\MyClass'], + 'function' => [], + 'const' => [], + ], + ], + 'plain-aliased' => [ + '/* testUsePlainAliased */', + [ + 'name' => ['ClassAlias' => 'MyNamespace\YourClass'], + 'function' => [], + 'const' => [], + ], + ], + 'multiple-with-comments' => [ + '/* testUseMultipleWithComments */', + [ + 'name' => [ + 'ClassABC' => 'Vendor\Foo\ClassA', + 'InterfaceB' => 'Vendor\Bar\InterfaceB', + 'ClassC' => 'Vendor\Baz\ClassC', + ], + 'function' => [], + 'const' => [], + ], + ], + 'function-plain-ends-on-close-tag' => [ + '/* testUseFunctionPlainEndsOnCloseTag */', + [ + 'name' => [], + 'function' => ['myFunction' => 'MyNamespace\myFunction'], + 'const' => [], + ], + ], + 'function-plain-aliased' => [ + '/* testUseFunctionPlainAliased */', + [ + 'name' => [], + 'function' => ['FunctionAlias' => 'Vendor\YourNamespace\yourFunction'], + 'const' => [], + ], + ], + 'function-multiple' => [ + '/* testUseFunctionMultiple */', + [ + 'name' => [], + 'function' => [ + 'sin' => 'foo\math\sin', + 'FooCos' => 'foo\math\cos', + 'cosh' => 'foo\math\cosh', + ], + 'const' => [], + ], + ], + 'const-plain-uppercase-const-keyword' => [ + '/* testUseConstPlainUppercaseConstKeyword */', + [ + 'name' => [], + 'function' => [], + 'const' => ['MY_CONST' => 'MyNamespace\MY_CONST'], + ], + ], + 'const-plain-aliased' => [ + '/* testUseConstPlainAliased */', + [ + 'name' => [], + 'function' => [], + 'const' => ['CONST_ALIAS' => 'MyNamespace\YOUR_CONST'], + ], + ], + 'const-multiple' => [ + '/* testUseConstMultiple */', + [ + 'name' => [], + 'function' => [], + 'const' => [ + 'PI' => 'foo\math\PI', + 'MATH_GOLDEN' => 'foo\math\GOLDEN_RATIO', + ], + ], + ], + 'group' => [ + '/* testGroupUse */', + [ + 'name' => [ + 'SomeClassA' => 'some\namespacing\SomeClassA', + 'SomeClassB' => 'some\namespacing\deeper\level\SomeClassB', + 'C' => 'some\namespacing\another\level\SomeClassC', + ], + 'function' => [], + 'const' => [], + ], + ], + 'group-function-trailing-comma' => [ + '/* testGroupUseFunctionTrailingComma */', + [ + 'name' => [], + 'function' => [ + 'Msin' => 'bar\math\Msin', + 'BarCos' => 'bar\math\level\Mcos', + 'Mcosh' => 'bar\math\Mcosh', + ], + 'const' => [], + ], + ], + 'group-const' => [ + '/* testGroupUseConst */', + [ + 'name' => [], + 'function' => [], + 'const' => [ + 'BAR_GAMMA' => 'bar\math\BGAMMA', + 'BGOLDEN_RATIO' => 'bar\math\BGOLDEN_RATIO', + ], + ], + ], + 'group-mixed' => [ + '/* testGroupUseMixed */', + [ + 'name' => [ + 'ClassName' => 'Some\NS\ClassName', + 'AnotherLevel' => 'Some\NS\AnotherLevel', + ], + 'function' => [ + 'functionName' => 'Some\NS\SubLevel\functionName', + 'AnotherName' => 'Some\NS\SubLevel\AnotherName', + ], + 'const' => ['SOME_CONSTANT' => 'Some\NS\Constants\CONSTANT_NAME'], + ], + ], + 'parse-error-function-plain-reserved-keyword' => [ + '/* testUseFunctionPlainReservedKeyword */', + [ + 'name' => [], + 'function' => ['yourFunction' => 'Vendor\YourNamespace\switch\yourFunction'], + 'const' => [], + ], + ], + 'parse-error-const-plain-reserved-keyword' => [ + '/* testUseConstPlainReservedKeyword */', + [ + 'name' => [], + 'function' => [], + 'const' => ['yourConst' => 'Vendor\YourNamespace\function\yourConst'], + ], + ], + 'parse-error-plain-alias-reserved-keyword' => [ + '/* testUsePlainAliasReservedKeyword */', + [ + 'name' => ['class' => 'Vendor\YourNamespace\ClassName'], + 'function' => [], + 'const' => [], + ], + ], + 'parse-error-plain-alias-reserved-keyword-function' => [ + '/* testUsePlainAliasReservedKeywordFunction */', + [ + 'name' => ['function' => 'Vendor\YourNamespace\ClassName'], + 'function' => [], + 'const' => [], + ], + ], + 'parse-error-plain-alias-reserved-keyword-const' => [ + '/* testUsePlainAliasReservedKeywordConst */', + [ + 'name' => [], + 'function' => [], + 'const' => [], + ], + ], + 'parse-error' => [ + '/* testParseError */', + [ + 'name' => [], + 'function' => [], + 'const' => [], + ], + ], + ]; + } +} diff --git a/Tests/Utils/UseStatements/UseTypeTest.inc b/Tests/Utils/UseStatements/UseTypeTest.inc new file mode 100644 index 00000000..cd0582ed --- /dev/null +++ b/Tests/Utils/UseStatements/UseTypeTest.inc @@ -0,0 +1,53 @@ +expectPhpcsException('$stackPtr must be of type T_USE'); + + UseStatements::getType(self::$phpcsFile, 100000); + } + + /** + * Test receiving an expected exception when passing a non T_USE token. + * + * @return void + */ + public function testNonUseToken() + { + $this->expectPhpcsException('$stackPtr must be of type T_USE'); + + $result = UseStatements::getType(self::$phpcsFile, 0); + } + + /** + * Test correctly identifying whether a T_USE token is used as a closure use statement. + * + * @dataProvider dataUseType + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected The expected return values for the various functions. + * + * @return void + */ + public function testIsClosureUse($testMarker, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, \T_USE); + + $result = UseStatements::isClosureUse(self::$phpcsFile, $stackPtr); + $this->assertSame($expected['closure'], $result); + } + + /** + * Test correctly identifying whether a T_USE token is used as an import use statement. + * + * @dataProvider dataUseType + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected The expected return values for the various functions. + * + * @return void + */ + public function testIsImportUse($testMarker, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, \T_USE); + + $result = UseStatements::isImportUse(self::$phpcsFile, $stackPtr); + $this->assertSame($expected['import'], $result); + } + + /** + * Test correctly identifying whether a T_USE token is used as a trait import use statement. + * + * @dataProvider dataUseType + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected The expected return values for the various functions. + * + * @return void + */ + public function testIsTraitUse($testMarker, $expected) + { + $stackPtr = $this->getTargetToken($testMarker, \T_USE); + + $result = UseStatements::isTraitUse(self::$phpcsFile, $stackPtr); + $this->assertSame($expected['trait'], $result, 'isTraitUseStatement() test failed'); + } + + /** + * Data provider. + * + * @see testIsClosureUse() For the array format. + * @see testIsImportUse() For the array format. + * @see testIsTraitUse() For the array format. + * + * @return array + */ + public function dataUseType() + { + return [ + 'import-1' => [ + '/* testUseImport1 */', + [ + 'closure' => false, + 'import' => true, + 'trait' => false, + ], + ], + 'import-2' => [ + '/* testUseImport2 */', + [ + 'closure' => false, + 'import' => true, + 'trait' => false, + ], + ], + 'import-3' => [ + '/* testUseImport3 */', + [ + 'closure' => false, + 'import' => true, + 'trait' => false, + ], + ], + 'import-4' => [ + '/* testUseImport4 */', + [ + 'closure' => false, + 'import' => true, + 'trait' => false, + ], + ], + 'closure' => [ + '/* testClosureUse */', + [ + 'closure' => true, + 'import' => false, + 'trait' => false, + ], + ], + 'trait' => [ + '/* testUseTrait */', + [ + 'closure' => false, + 'import' => false, + 'trait' => true, + ], + ], + 'closure-in-nested-class' => [ + '/* testClosureUseNestedInClass */', + [ + 'closure' => true, + 'import' => false, + 'trait' => false, + ], + ], + 'trait-in-nested-anon-class' => [ + '/* testUseTraitInNestedAnonClass */', + [ + 'closure' => false, + 'import' => false, + 'trait' => true, + ], + ], + 'trait-in-trait' => [ + '/* testUseTraitInTrait */', + [ + 'closure' => false, + 'import' => false, + 'trait' => true, + ], + ], + 'closure-nested-in-trait' => [ + '/* testClosureUseNestedInTrait */', + [ + 'closure' => true, + 'import' => false, + 'trait' => false, + ], + ], + 'trait-in-interface' => [ + '/* testUseTraitInInterface */', + [ + 'closure' => false, + 'import' => false, + 'trait' => false, + ], + ], + 'live-coding' => [ + '/* testLiveCoding */', + [ + 'closure' => false, + 'import' => false, + 'trait' => false, + ], + ], + ]; + } +} diff --git a/Tests/Utils/Variables/GetMemberPropertiesDiffTest.inc b/Tests/Utils/Variables/GetMemberPropertiesDiffTest.inc new file mode 100644 index 00000000..4cfc073d --- /dev/null +++ b/Tests/Utils/Variables/GetMemberPropertiesDiffTest.inc @@ -0,0 +1,7 @@ +expectPhpcsException('$stackPtr must be of type T_VARIABLE'); + + Variables::getMemberProperties(self::$phpcsFile, 10000); + } + + /** + * Test receiving an expected exception when an (invalid) interface property is passed. + * + * @return void + */ + public function testNotClassPropertyException() + { + $this->expectPhpcsException('$stackPtr is not a class member var'); + + $variable = $this->getTargetToken('/* testInterfaceProperty */', \T_VARIABLE); + Variables::getMemberProperties(self::$phpcsFile, $variable); + } +} diff --git a/Tests/Utils/Variables/GetMemberPropertiesTest.php b/Tests/Utils/Variables/GetMemberPropertiesTest.php new file mode 100644 index 00000000..21e5fb4c --- /dev/null +++ b/Tests/Utils/Variables/GetMemberPropertiesTest.php @@ -0,0 +1,83 @@ + $value) { + if ($value[0] === '/* testInterfaceProperty */') { + unset($data[$key]); + break; + } + } + + return $data; + } +} diff --git a/Tests/Utils/Variables/IsPHPReservedVarNameTest.php b/Tests/Utils/Variables/IsPHPReservedVarNameTest.php new file mode 100644 index 00000000..3257f84a --- /dev/null +++ b/Tests/Utils/Variables/IsPHPReservedVarNameTest.php @@ -0,0 +1,155 @@ +assertTrue(Variables::isPHPReservedVarName($name)); + } + + /** + * Data provider. + * + * @see testIsPHPReservedVarName() For the array format. + * + * @return array + */ + public function dataIsPHPReservedVarName() + { + return [ + // With dollar sign. + '$_SERVER' => ['$_SERVER'], + '$_GET' => ['$_GET'], + '$_POST' => ['$_POST'], + '$_REQUEST' => ['$_REQUEST'], + '$_SESSION' => ['$_SESSION'], + '$_ENV' => ['$_ENV'], + '$_COOKIE' => ['$_COOKIE'], + '$_FILES' => ['$_FILES'], + '$GLOBALS' => ['$GLOBALS'], + '$http_response_header' => ['$http_response_header'], + '$argc' => ['$argc'], + '$argv' => ['$argv'], + '$php_errormsg' => ['$php_errormsg'], + '$HTTP_SERVER_VARS' => ['$HTTP_SERVER_VARS'], + '$HTTP_GET_VARS' => ['$HTTP_GET_VARS'], + '$HTTP_POST_VARS' => ['$HTTP_POST_VARS'], + '$HTTP_SESSION_VARS' => ['$HTTP_SESSION_VARS'], + '$HTTP_ENV_VARS' => ['$HTTP_ENV_VARS'], + '$HTTP_COOKIE_VARS' => ['$HTTP_COOKIE_VARS'], + '$HTTP_POST_FILES' => ['$HTTP_POST_FILES'], + '$HTTP_RAW_POST_DATA' => ['$HTTP_RAW_POST_DATA'], + + // Without dollar sign. + '_SERVER' => ['_SERVER'], + '_GET' => ['_GET'], + '_POST' => ['_POST'], + '_REQUEST' => ['_REQUEST'], + '_SESSION' => ['_SESSION'], + '_ENV' => ['_ENV'], + '_COOKIE' => ['_COOKIE'], + '_FILES' => ['_FILES'], + 'GLOBALS' => ['GLOBALS'], + 'http_response_header' => ['http_response_header'], + 'argc' => ['argc'], + 'argv' => ['argv'], + 'php_errormsg' => ['php_errormsg'], + 'HTTP_SERVER_VARS' => ['HTTP_SERVER_VARS'], + 'HTTP_GET_VARS' => ['HTTP_GET_VARS'], + 'HTTP_POST_VARS' => ['HTTP_POST_VARS'], + 'HTTP_SESSION_VARS' => ['HTTP_SESSION_VARS'], + 'HTTP_ENV_VARS' => ['HTTP_ENV_VARS'], + 'HTTP_COOKIE_VARS' => ['HTTP_COOKIE_VARS'], + 'HTTP_POST_FILES' => ['HTTP_POST_FILES'], + 'HTTP_RAW_POST_DATA' => ['HTTP_RAW_POST_DATA'], + ]; + } + + /** + * Test non-reserved variable names. + * + * @dataProvider dataIsPHPReservedVarNameFalse + * + * @param string $name The variable name to test. + * + * @return void + */ + public function testIsPHPReservedVarNameFalse($name) + { + $this->assertFalse(Variables::isPHPReservedVarName($name)); + } + + /** + * Data provider. + * + * @see testIsPHPReservedVarNameFalse() For the array format. + * + * @return array + */ + public function dataIsPHPReservedVarNameFalse() + { + return [ + // Different case. + 'different-case-1' => ['$_Server'], + 'different-case-2' => ['$_get'], + 'different-case-3' => ['$_pOST'], + 'different-case-4' => ['$HTTP_RESPONSE_HEADER'], + 'different-case-5' => ['_EnV'], + 'different-case-6' => ['PHP_errormsg'], + + // Shouldn't be possible, but all the same: double dollar. + 'double-dollar' => ['$$_REQUEST'], + + // No underscore. + 'missing-underscore-var' => ['$SERVER'], + 'missing-underscore-string' => ['SERVER'], + + // Double underscore. + 'double-underscore-var' => ['$__SERVER'], + 'double-underscore-string' => ['__SERVER'], + + // Globals with underscore. + 'globals-with-underscore-var' => ['$_GLOBALS'], + 'globals-with-underscore-string' => ['_GLOBALS'], + + // Array key with quotes. + 'array-key-with-quotes-1' => ['"argc"'], + 'array-key-with-quotes-2' => ["'argv'"], + + // Some completely different variable name. + 'name-not-in-list' => ['my_php_errormsg'], + ]; + } +} diff --git a/Tests/Utils/Variables/IsSuperglobalTest.inc b/Tests/Utils/Variables/IsSuperglobalTest.inc new file mode 100644 index 00000000..9186bd65 --- /dev/null +++ b/Tests/Utils/Variables/IsSuperglobalTest.inc @@ -0,0 +1,41 @@ +assertFalse($result); + } + + /** + * Test correctly detecting superglobal variables. + * + * @dataProvider dataIsSuperglobal + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param bool $expected The expected function return value. + * @param int|string $testTargetType Optional. The token type for the target token in the test file. + * @param string $testTargetValue Optional. The token content for the target token in the test file. + * + * @return void + */ + public function testIsSuperglobal($testMarker, $expected, $testTargetType = \T_VARIABLE, $testTargetValue = null) + { + $stackPtr = $this->getTargetToken($testMarker, $testTargetType, $testTargetValue); + $result = Variables::isSuperglobal(self::$phpcsFile, $stackPtr); + $this->assertSame($expected, $result); + } + + /** + * Data provider. + * + * @see testIsSuperglobal() For the array format. + * + * @return array + */ + public function dataIsSuperglobal() + { + return [ + 'not-a-variable' => [ + '/* testNotAVariable */', + false, + \T_RETURN, + ], + 'not-a-reserved-var' => [ + '/* testNotAReservedVar */', + false, + ], + 'reserved-var-not-superglobal' => [ + '/* testReservedVarNotSuperglobal */', + false, + ], + 'reserved-var-superglobal' => [ + '/* testReservedVarIsSuperglobal */', + true, + ], + 'array-key-not-a-reserved-var' => [ + '/* testGLOBALSArrayKeyNotAReservedVar */', + false, + \T_CONSTANT_ENCAPSED_STRING, + ], + 'array-key-variable' => [ + '/* testGLOBALSArrayKeyVar */', + false, + \T_VARIABLE, + '$something', + ], + 'array-key-reserved-var-not-superglobal' => [ + '/* testGLOBALSArrayKeyReservedVar */', + false, + \T_VARIABLE, + '$php_errormsg', + ], + 'array-key-var-superglobal' => [ + '/* testGLOBALSArrayKeySuperglobal */', + true, + \T_VARIABLE, + '$_COOKIE', + ], + 'array-key-not-single-string' => [ + '/* testGLOBALSArrayKeyNotSingleString */', + false, + \T_CONSTANT_ENCAPSED_STRING, + ], + 'array-key-interpolated-var' => [ + '/* testGLOBALSArrayKeyInterpolatedVar */', + false, + \T_DOUBLE_QUOTED_STRING, + ], + 'array-key-string-superglobal' => [ + '/* testGLOBALSArrayKeySingleStringSuperglobal */', + true, + \T_CONSTANT_ENCAPSED_STRING, + ], + 'array-key-var-superglobal-with-array-access' => [ + '/* testGLOBALSArrayKeySuperglobalWithKey */', + true, + \T_VARIABLE, + '$_GET', + ], + 'array-key-not-globals-array' => [ + '/* testSuperglobalKeyNotGLOBALSArray */', + false, + \T_CONSTANT_ENCAPSED_STRING, + ], + ]; + } + + /** + * Test valid PHP superglobal names. + * + * @dataProvider dataIsSuperglobalName + * + * @param string $name The variable name to test. + * + * @return void + */ + public function testIsSuperglobalName($name) + { + $this->assertTrue(Variables::isSuperglobalName($name)); + } + + /** + * Data provider. + * + * @see testIsSuperglobalName() For the array format. + * + * @return array + */ + public function dataIsSuperglobalName() + { + return [ + '$_SERVER' => ['$_SERVER'], + '$_GET' => ['$_GET'], + '$_POST' => ['$_POST'], + '$_REQUEST' => ['$_REQUEST'], + '_SESSION' => ['_SESSION'], + '_ENV' => ['_ENV'], + '_COOKIE' => ['_COOKIE'], + '_FILES' => ['_FILES'], + 'GLOBALS' => ['GLOBALS'], + ]; + } + + /** + * Test non-superglobal variable names. + * + * @dataProvider dataIsSuperglobalNameFalse + * + * @param string $name The variable name to test. + * + * @return void + */ + public function testIsSuperglobalNameFalse($name) + { + $this->assertFalse(Variables::isSuperglobalName($name)); + } + + /** + * Data provider. + * + * @see testIsSuperglobalNameFalse() For the array format. + * + * @return array + */ + public function dataIsSuperglobalNameFalse() + { + return [ + 'non-reserved-var' => ['$not_a_superglobal'], + 'php-reserved-var-not-superglobal-1' => ['$http_response_header'], + 'php-reserved-var-not-superglobal-2' => ['$argc'], + 'php-reserved-var-not-superglobal-3' => ['$argv'], + 'php-reserved-var-not-superglobal-4' => ['$HTTP_RAW_POST_DATA'], + 'php-reserved-var-not-superglobal-5' => ['$php_errormsg'], + 'php-reserved-var-not-superglobal-6' => ['HTTP_SERVER_VARS'], + 'php-reserved-var-not-superglobal-7' => ['HTTP_GET_VARS'], + 'php-reserved-var-not-superglobal-8' => ['HTTP_POST_VARS'], + 'php-reserved-var-not-superglobal-9' => ['HTTP_SESSION_VARS'], + 'php-reserved-var-not-superglobal-10' => ['HTTP_ENV_VARS'], + 'php-reserved-var-not-superglobal-11' => ['HTTP_COOKIE_VARS'], + 'php-reserved-var-not-superglobal-12' => ['HTTP_POST_FILES'], + ]; + } +} diff --git a/Tests/bootstrap.php b/Tests/bootstrap.php new file mode 100644 index 00000000..80b00d3c --- /dev/null +++ b/Tests/bootstrap.php @@ -0,0 +1,97 @@ +=5.4", - "squizlabs/php_codesniffer" : "^2.6.0 || ^3.0.2", - "dealerdirect/phpcodesniffer-composer-installer" : "^0.5" + "squizlabs/php_codesniffer" : "^2.6.0 || ^3.1.0", + "dealerdirect/phpcodesniffer-composer-installer" : "^0.3 || ^0.4.1 || ^0.5 || ^0.6" }, "require-dev" : { - "roave/security-advisories" : "dev-master" + "jakub-onderka/php-parallel-lint": "^1.0", + "jakub-onderka/php-console-highlighter": "^0.4", + "phpunit/phpunit" : "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0" + }, + "conflict": { + "squizlabs/php_codesniffer": "3.5.3" + }, + "minimum-stability": "dev", + "prefer-stable": true, + "autoload": { + "classmap": ["PHPCSUtils/"] + }, + "autoload-dev" : { + "psr-4": { + "PHPCSUtils\\Tests\\": "Tests/" + } + }, + "scripts" : { + "lint": [ + "@php ./vendor/jakub-onderka/php-parallel-lint/parallel-lint . -e php --exclude vendor --exclude .git" + ], + "install-devtools": [ + "composer require phpcsstandards/phpcsdevtools:\"^1.0 || dev-develop\" --no-suggest --update-no-dev" + ], + "remove-devtools": [ + "composer remove phpcsstandards/phpcsdevtools --update-no-dev" + ], + "checkcs": [ + "@install-devtools", + "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs", + "@remove-devtools" + ], + "fixcs": [ + "@install-devtools", + "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf", + "@remove-devtools" + ], + "travis-checkcs": [ + "@install-devtools", + "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs" + ], + "test": [ + "@php ./vendor/phpunit/phpunit/phpunit --no-coverage" + ], + "coverage": [ + "@php ./vendor/phpunit/phpunit/phpunit" + ], + "coverage-local": [ + "@php ./vendor/phpunit/phpunit/phpunit --coverage-html ./build/coverage-html" + ] } } diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 00000000..b2cd4fab --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,113 @@ + + + Check the code of the PHPCSUtils standard. + + + + . + + + */vendor/* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /phpcsutils-autoload\.php$ + /PHPCS23Utils/Sniffs/Load/LoadUtilsSniff\.php$ + /Tests/bootstrap\.php$ + + + + + /Tests/*Test\.php$ + + + + + /PHPCSUtils/BackCompat/BCFile\.php$ + + + /PHPCSUtils/BackCompat/BCFile\.php$ + /Tests/BackCompat/BCFile/*Test\.php$ + + + /PHPCSUtils/BackCompat/BCFile\.php$ + /Tests/BackCompat/BCFile/*Test\.php$ + + + /PHPCSUtils/BackCompat/BCFile\.php$ + /Tests/BackCompat/BCFile/*Test\.php$ + + + /PHPCSUtils/BackCompat/BCFile\.php$ + /Tests/BackCompat/BCFile/*Test\.php$ + + + /PHPCSUtils/BackCompat/BCFile\.php$ + /Tests/BackCompat/BCFile/*Test\.php$ + + + /PHPCSUtils/BackCompat/BCFile\.php$ + /Tests/BackCompat/BCFile/*Test\.php$ + + + + + + + /PHPCSUtils/Utils/Namespaces\.php$ + + + diff --git a/phpcsutils-autoload.php b/phpcsutils-autoload.php new file mode 100644 index 00000000..db48196c --- /dev/null +++ b/phpcsutils-autoload.php @@ -0,0 +1,120 @@ += 3.1.0 and uses the PHPCS + * native unit test framework, this file does not need to be included. + * + * - When PHPCS 2.x support is desired, include the "PHPCS23Utils" standard + * in the ruleset of the external standard and this file will be included + * automatically. + * Including this file will allow PHPCSUtils to work in both PHPCS 2.x + * as well as PHPCS 3.x. + * + * - If an external standard uses its own unit test setup, this file should + * be included from the unit test bootstrap file. + * + * - If an external standard uses the PHPCSUtils `UtilityMethodTestCase` + * class to test their own utility methods, this file should be included from + * the unit test bootstrap file. + * + * @package PHPCSUtils + * @copyright 2019 PHPCSUtils Contributors + * @license https://opensource.org/licenses/LGPL-3.0 LGPL3 + * @link https://github.com/PHPCSStandards/PHPCSUtils + * + * @since 1.0.0 + */ + +if (defined('PHPCSUTILS_AUTOLOAD') === false) { + /* + * Register an autoloader. + * + * External PHPCS standards which have their own unit test suite + * should include this file in their test runner bootstrap. + */ + spl_autoload_register(function ($class) { + // Only try & load our own classes. + if (stripos($class, 'PHPCSUtils') !== 0) { + return; + } + + $file = realpath(__DIR__) . DIRECTORY_SEPARATOR . strtr($class, '\\', DIRECTORY_SEPARATOR) . '.php'; + + if (file_exists($file)) { + include_once $file; + } + }); + + define('PHPCSUTILS_AUTOLOAD', true); +} + +if (defined('PHPCSUTILS_PHPCS_ALIASES_SET') === false) { + /* + * Alias a number of PHPCS 2.x classes to their PHPCS 3.x equivalents. + * + * {@internal The PHPCS file have been reorganized in PHPCS 3.x, quite + * a few "old" classes have been split and spread out over several "new" + * classes. In other words, this will only work for a limited number + * of classes.}} + * + * {@internal The `class_exists` wrappers are needed to play nice with other + * external PHPCS standards creating cross-version compatibility in a + * similar manner.}} + */ + if (interface_exists('\PHP_CodeSniffer_Sniff') === true + && interface_exists('\PHP_CodeSniffer\Sniffs\Sniff') === false + ) { + class_alias('\PHP_CodeSniffer_Sniff', '\PHP_CodeSniffer\Sniffs\Sniff'); + } + + if (class_exists('\PHP_CodeSniffer_File') === true + && class_exists('\PHP_CodeSniffer\Files\File') === false + ) { + class_alias('\PHP_CodeSniffer_File', '\PHP_CodeSniffer\Files\File'); + } + + if (class_exists('\PHP_CodeSniffer_Tokens') === true + && class_exists('\PHP_CodeSniffer\Util\Tokens') === false + ) { + class_alias('\PHP_CodeSniffer_Tokens', '\PHP_CodeSniffer\Util\Tokens'); + } + + if (class_exists('\PHP_CodeSniffer_Exception') === true + && class_exists('\PHP_CodeSniffer\Exceptions\RuntimeException') === false + ) { + class_alias('\PHP_CodeSniffer_Exception', '\PHP_CodeSniffer\Exceptions\RuntimeException'); + } + + if (class_exists('\PHP_CodeSniffer_Exception') === true + && class_exists('\PHP_CodeSniffer\Exceptions\TokenizerException') === false + ) { + class_alias('\PHP_CodeSniffer_Exception', '\PHP_CodeSniffer\Exceptions\TokenizerException'); + } + + define('PHPCSUTILS_PHPCS_ALIASES_SET', true); +} + +if (defined('PHPCSUTILS_PHPUNIT_ALIASES_SET') === false) { + /* + * Alias the PHPUnit 4/5 TestCase class to its PHPUnit 6+ name. + * + * This allows the both the PHPCSUtils native unit tests as well as the + * `UtilityMethodTestCase` class to work cross-version with PHPUnit + * below 6.x and above. + * + * {@internal The `class_exists` wrappers are needed to play nice with + * PHPUnit bootstrap files of external standards which may be creating + * cross-version compatibility in a similar manner.}} + */ + if (class_exists('PHPUnit_Framework_TestCase') === true + && class_exists('PHPUnit\Framework\TestCase') === false + ) { + class_alias('PHPUnit_Framework_TestCase', 'PHPUnit\Framework\TestCase'); + } + + define('PHPCSUTILS_PHPUNIT_ALIASES_SET', true); +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..a5223261 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,28 @@ + + + + + + ./Tests/ + + + + + + + ./PHPCSUtils/ + + + + + + + +