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/
+
+
+
+
+
+
+
+