diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 0d33a0d8ca..65e9a6427b 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -33,7 +33,7 @@ jobs: # Dependencies need to be installed to make sure the PHPUnit classes are recognized. # @link https://github.com/marketplace/actions/install-composer-dependencies - name: Install Composer dependencies - uses: "ramsey/composer-install@v1" + uses: "ramsey/composer-install@v2" - name: Run PHPStan run: phpstan analyse --configuration=phpstan.neon diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4b6736d980..f9366e074f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,8 +13,60 @@ on: workflow_dispatch: jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + php: ['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] + + name: "Build Phar on PHP: ${{ matrix.php }}" + + continue-on-error: ${{ matrix.php == '8.2' }} + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + ini-values: phar.readonly=Off, error_reporting=-1, display_errors=On + + - name: Build the phar + run: php scripts/build-phar.php + + - name: Upload the PHPCS phar + uses: actions/upload-artifact@v3 + if: ${{ success() && matrix.php == '8.0' }} + with: + name: phpcs-phar + path: ./phpcs.phar + if-no-files-found: error + retention-days: 28 + + - name: Upload the PHPCBF phar + uses: actions/upload-artifact@v3 + if: ${{ success() && matrix.php == '8.0' }} + with: + name: phpcbf-phar + path: ./phpcbf.phar + if-no-files-found: error + retention-days: 28 + + # Both the below only check a few files which are rarely changed and therefore unlikely to have issues. + # This test is about testing that the phars are functional, *not* about whether the code style complies. + - name: 'PHPCS: check code style using the Phar file to test the Phar is functional' + run: php phpcs.phar ./scripts + + - name: 'PHPCBF: fix code style using the Phar file to test the Phar is functional' + run: php phpcbf.phar ./scripts + test: runs-on: ubuntu-latest + needs: build strategy: # Keys: @@ -37,7 +89,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup ini config id: set_ini @@ -45,11 +97,11 @@ jobs: # Set the "short_open_tag" ini to make sure specific conditions are tested. # Also turn on error_reporting to ensure all notices are shown. if [[ ${{ matrix.custom_ini }} == true && "${{ matrix.php }}" == '5.5' ]]; then - echo '::set-output name=PHP_INI::phar.readonly=Off, error_reporting=-1, display_errors=On, date.timezone=Australia/Sydney, short_open_tag=On, asp_tags=On' + echo 'PHP_INI=error_reporting=-1, display_errors=On, date.timezone=Australia/Sydney, short_open_tag=On, asp_tags=On' >> $GITHUB_OUTPUT elif [[ ${{ matrix.custom_ini }} == true && "${{ matrix.php }}" == '7.0' ]]; then - echo '::set-output name=PHP_INI::phar.readonly=Off, error_reporting=-1, display_errors=On, date.timezone=Australia/Sydney, short_open_tag=On' + echo 'PHP_INI=error_reporting=-1, display_errors=On, date.timezone=Australia/Sydney, short_open_tag=On' >> $GITHUB_OUTPUT else - echo '::set-output name=PHP_INI::phar.readonly=Off, error_reporting=-1, display_errors=On' + echo 'PHP_INI=error_reporting=-1, display_errors=On' >> $GITHUB_OUTPUT fi - name: Install PHP @@ -64,12 +116,12 @@ jobs: # @link https://github.com/marketplace/actions/install-composer-dependencies - name: Install Composer dependencies - normal if: ${{ matrix.php < '8.0' }} - uses: "ramsey/composer-install@v1" + uses: "ramsey/composer-install@v2" # For PHP 8.0+, we need to install with ignore platform reqs as PHPUnit 7 is still used. - name: Install Composer dependencies - with ignore platform if: ${{ matrix.php >= '8.0' }} - uses: "ramsey/composer-install@v1" + uses: "ramsey/composer-install@v2" with: composer-options: --ignore-platform-reqs @@ -106,10 +158,12 @@ jobs: if: ${{ matrix.custom_ini == false }} run: composer validate --no-check-all --strict - - name: Build the phar - if: ${{ matrix.custom_ini == false }} - run: php scripts/build-phar.php + - name: Download the PHPCS phar + uses: actions/download-artifact@v3 + with: + name: phpcs-phar + # This test specifically tests that the Phar which will be released works correctly on all PHP versions. - name: 'PHPCS: check code style using the Phar file' if: ${{ matrix.custom_ini == false }} run: php phpcs.phar diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 9fb5685d32..887b9c2b7b 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -22,10 +22,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install xmllint - run: sudo apt-get install --no-install-recommends -y libxml2-utils + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y libxml2-utils - name: Retrieve XML Schema run: curl -O https://www.w3.org/2012/04/XMLSchema.xsd @@ -63,7 +65,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install PHP uses: shivammathur/setup-php@v2 diff --git a/CodeSniffer.conf.dist b/CodeSniffer.conf.dist index 62dc395cf8..f95058faa7 100644 --- a/CodeSniffer.conf.dist +++ b/CodeSniffer.conf.dist @@ -5,5 +5,5 @@ 'show_warnings' => '0', 'show_progress' => '1', 'report_width' => '120', -) +); ?> diff --git a/README.md b/README.md index c0ea8e391a..859de432c6 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,9 @@ php phpcbf.phar -h ### Composer If you use Composer, you can install PHP_CodeSniffer system-wide with the following command: - - composer global require "squizlabs/php_codesniffer=*" - +```bash +composer global require "squizlabs/php_codesniffer=*" +``` Make sure you have the composer bin dir in your PATH. The default value is `~/.composer/vendor/bin/`, but you can check the value that you need to use by running `composer global config bin-dir --absolute`. Or alternatively, include a dependency for `squizlabs/php_codesniffer` in your `composer.json` file. For example: @@ -49,47 +49,48 @@ Or alternatively, include a dependency for `squizlabs/php_codesniffer` in your ` ``` You will then be able to run PHP_CodeSniffer from the vendor bin directory: - - ./vendor/bin/phpcs -h - ./vendor/bin/phpcbf -h - +```bash +./vendor/bin/phpcs -h +./vendor/bin/phpcbf -h +``` ### Phive If you use Phive, you can install PHP_CodeSniffer as a project tool using the following commands: - - phive install phpcs - phive install phpcbf - +```bash +phive install phpcs +phive install phpcbf +``` You will then be able to run PHP_CodeSniffer from the tools directory: - - ./tools/phpcs -h - ./tools/phpcbf -h - +```bash +./tools/phpcs -h +./tools/phpcbf -h +``` ### PEAR If you use PEAR, you can install PHP_CodeSniffer using the PEAR installer. This will make the `phpcs` and `phpcbf` commands immediately available for use. To install PHP_CodeSniffer using the PEAR installer, first ensure you have [installed PEAR](http://pear.php.net/manual/en/installation.getting.php) and then run the following command: - - pear install PHP_CodeSniffer - +```bash +pear install PHP_CodeSniffer +``` ### Git Clone You can also download the PHP_CodeSniffer source and run the `phpcs` and `phpcbf` commands directly from the Git clone: - - git clone https://github.com/squizlabs/PHP_CodeSniffer.git - cd PHP_CodeSniffer - php bin/phpcs -h - php bin/phpcbf -h - +```bash +git clone https://github.com/squizlabs/PHP_CodeSniffer.git +cd PHP_CodeSniffer +php bin/phpcs -h +php bin/phpcbf -h +``` ## Getting Started The default coding standard used by PHP_CodeSniffer is the PEAR coding standard. To check a file against the PEAR coding standard, simply specify the file's location: - - $ phpcs /path/to/code/myfile.php - +```bash +phpcs /path/to/code/myfile.php +``` Or if you wish to check an entire directory you can specify the directory location instead of a file. - - $ phpcs /path/to/code-directory - +```bash +phpcs /path/to/code-directory +``` If you wish to check your code against the PSR-12 coding standard, use the `--standard` command line argument: - - $ phpcs --standard=PSR12 /path/to/code-directory +```bash +phpcs --standard=PSR12 /path/to/code-directory +``` If PHP_CodeSniffer finds any coding standard errors, a report will be shown after running the command. diff --git a/composer.json b/composer.json index 7605a5df91..37f41a0b80 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,8 @@ "type": "library", "keywords": [ "phpcs", - "standards" + "standards", + "static analysis" ], "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", "license": "BSD-3-Clause", diff --git a/package.xml b/package.xml index 4cfe75f2da..9338b46e79 100644 --- a/package.xml +++ b/package.xml @@ -14,11 +14,11 @@ http://pear.php.net/dtd/package-2.0.xsd"> gsherwood@squiz.net yes - 2021-12-13 - + 2022-06-18 + - 3.6.2 - 3.6.2 + 3.7.2 + 3.7.2 stable @@ -26,28 +26,35 @@ http://pear.php.net/dtd/package-2.0.xsd"> BSD 3-Clause License - - Processing large code bases that use tab indenting inside comments and strings will now be faster - -- Thanks to Thiemo Kreuz for the patch - - Fixed bug #3388 : phpcs does not work when run from WSL drives - -- Thanks to Juliette Reinders Folmer and Graham Wharton for the patch - - Fixed bug #3422 : Squiz.WhiteSpace.ScopeClosingBrace fixer removes HTML content when fixing closing brace alignment - -- Thanks to Juliette Reinders Folmer for the patch - - Fixed bug #3437 : PSR12 does not forbid blank lines at the start of the class body - -- Added new PSR12.Classes.OpeningBraceSpace sniff to enforce this - - Fixed bug #3440 : Squiz.WhiteSpace.MemberVarSpacing false positives when attributes used without docblock - -- Thanks to Vadim Borodavko for the patch - - Fixed bug #3448 : PHP 8.1 deprecation notice while generating running time value - -- Thanks to Juliette Reinders Folmer and Andy Postnikov for the patch - - Fixed bug #3456 : PSR12.Classes.ClassInstantiation.MissingParentheses false positive using attributes on anonymous class - -- Thanks to Juliette Reinders Folmer for the patch - - Fixed bug #3460 : Generic.Formatting.MultipleStatementAlignment false positive on closure with parameters - -- Thanks to Juliette Reinders Folmer for the patch - - Fixed bug #3468 : do/while loops are double-counted in Generic.Metrics.CyclomaticComplexity - -- Thanks to Mark Baker for the patch - - Fixed bug #3469 : Ternary Operator and Null Coalescing Operator are not counted in Generic.Metrics.CyclomaticComplexity - -- Thanks to Mark Baker for the patch - - Fixed bug #3472 : PHP 8 match() expression is not counted in Generic.Metrics.CyclomaticComplexity - -- Thanks to Mark Baker for the patch + - Newer versions of Composer will now suggest installing PHPCS using require-dev instead of require + -- Thanks to Gary Jones (@GaryJones) for the patch + - A custom Out Of Memory error will now be shown if PHPCS or PHPCBF run out of memory during a run + -- Error message provides actionable information about how to fix the problem and ensures the error is not silent + -- Thanks to Juliette Reinders Folmer (@jrfnl) and Alain Schlesser (@schlessera) for the patch + - Generic.PHP.LowerCaseType sniff now correctly examines types inside arrow functions + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Squiz.Formatting.OperatorBracket no longer reports false positives in match() structures + - Fixed bug #3616 : Squiz.PHP.DisallowComparisonAssignment false positive for PHP 8 match expression + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3618 : Generic.WhiteSpace.ArbitraryParenthesesSpacing false positive for return new parent() + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3632 : Short list not tokenized correctly in control structures without braces + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3639 : Tokenizer not applying tab replacement to heredoc/nowdoc closers + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3640 : Generic.WhiteSpace.DisallowTabIndent not reporting errors for PHP 7.3 flexible heredoc/nowdoc syntax + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3645 : PHPCS can show 0 exit code when running in parallel even if child process has fatal error + -- Thanks to Alex Panshin (@enl) for the patch + - Fixed bug #3653 : False positives for match() in OperatorSpacingSniff + -- Thanks to Jaroslav Hanslík (@kukulich) for the patch + - Fixed bug #3666 : PEAR.Functions.FunctionCallSignature incorrect indent fix when checking mixed HTML/PHP files + - Fixed bug #3668 : PSR12.Classes.ClassInstantiation.MissingParentheses false positive when instantiating parent classes + -- Similar issues also fixed in Generic.Functions.FunctionCallArgumentSpacing and Squiz.Formatting.OperatorBracket + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3672 : Incorrect ScopeIndent.IncorrectExact report for match inside array literal + - Fixed bug #3694 : Generic.WhiteSpace.SpreadOperatorSpacingAfter does not ignore spread operator in PHP 8.1 first class callables + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch @@ -135,6 +142,10 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + @@ -143,18 +154,30 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + + + + + + + + + @@ -163,6 +186,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + @@ -789,6 +814,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + @@ -984,6 +1011,7 @@ http://pear.php.net/dtd/package-2.0.xsd"> + @@ -1072,6 +1100,7 @@ http://pear.php.net/dtd/package-2.0.xsd"> + @@ -1623,6 +1652,7 @@ http://pear.php.net/dtd/package-2.0.xsd"> + @@ -1740,6 +1770,7 @@ http://pear.php.net/dtd/package-2.0.xsd"> + @@ -2090,6 +2121,10 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + @@ -2098,18 +2133,30 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + + + + + + + + + @@ -2118,6 +2165,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + @@ -2180,6 +2229,10 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + @@ -2188,18 +2241,30 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + + + + + + + + + @@ -2208,6 +2273,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + @@ -2217,6 +2284,134 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + 3.7.2 + 3.7.2 + + + stable + stable + + 2023-02-23 + BSD License + + - Newer versions of Composer will now suggest installing PHPCS using require-dev instead of require + -- Thanks to Gary Jones (@GaryJones) for the patch + - A custom Out Of Memory error will now be shown if PHPCS or PHPCBF run out of memory during a run + -- Error message provides actionable information about how to fix the problem and ensures the error is not silent + -- Thanks to Juliette Reinders Folmer (@jrfnl) and Alain Schlesser (@schlessera) for the patch + - Generic.PHP.LowerCaseType sniff now correctly examines types inside arrow functions + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Squiz.Formatting.OperatorBracket no longer reports false positives in match() structures + - Fixed bug #3616 : Squiz.PHP.DisallowComparisonAssignment false positive for PHP 8 match expression + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3618 : Generic.WhiteSpace.ArbitraryParenthesesSpacing false positive for return new parent() + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3632 : Short list not tokenized correctly in control structures without braces + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3639 : Tokenizer not applying tab replacement to heredoc/nowdoc closers + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3640 : Generic.WhiteSpace.DisallowTabIndent not reporting errors for PHP 7.3 flexible heredoc/nowdoc syntax + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3645 : PHPCS can show 0 exit code when running in parallel even if child process has fatal error + -- Thanks to Alex Panshin (@enl) for the patch + - Fixed bug #3653 : False positives for match() in OperatorSpacingSniff + -- Thanks to Jaroslav Hanslík (@kukulich) for the patch + - Fixed bug #3666 : PEAR.Functions.FunctionCallSignature incorrect indent fix when checking mixed HTML/PHP files + - Fixed bug #3668 : PSR12.Classes.ClassInstantiation.MissingParentheses false positive when instantiating parent classes + -- Similar issues also fixed in Generic.Functions.FunctionCallArgumentSpacing and Squiz.Formatting.OperatorBracket + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3672 : Incorrect ScopeIndent.IncorrectExact report for match inside array literal + - Fixed bug #3694 : Generic.WhiteSpace.SpreadOperatorSpacingAfter does not ignore spread operator in PHP 8.1 first class callables + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + + + + + 3.7.1 + 3.7.1 + + + stable + stable + + 2022-06-18 + BSD License + + - Fixed bug #3609 : Methods/constants with name empty/isset/unset are always reported as error + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + + + + + 3.7.0 + 3.7.0 + + + stable + stable + + 2022-06-13 + BSD License + + - Added support for PHP 8.1 explicit octal notation + -- This new syntax has been backfilled for PHP versions less than 8.1 + -- Thanks to Mark Baker for the patch + -- Thanks to Juliette Reinders Folmer for additional fixes + - Added support for PHP 8.1 enums + -- This new syntax has been backfilled for PHP versions less than 8.1 + -- Includes a new T_ENUM_CASE token to represent the case statements inside an enum + -- Thanks to Jaroslav Hanslík for the patch + -- Thanks to Juliette Reinders Folmer for additional core and sniff support + - Added support for the PHP 8.1 readonly token + -- Tokenzing of the readonly keyword has been backfilled for PHP versions less than 8.1 + -- Thanks to Jaroslav Hanslík for the patch + - Added support for PHP 8.1 intersection types + -- Includes a new T_TYPE_INTERSECTION token to represent the ampersand character inside intersection types + -- Thanks to Jaroslav Hanslík for the patch + - File::getMethodParameters now supports the new PHP 8.1 readonly token + -- When constructor property promotion is used, a new property_readonly array index is included in the return value + --- This is a boolean value indicating if the property is readonly + -- If the readonly token is detected, a new readonly_token array index is included in the return value + --- This contains the token index of the readonly keyword + -- Thanks to Juliette Reinders Folmer for the patch + - Support for new PHP 8.1 readonly keyword has been added to the following sniffs: + -- Generic.PHP.LowerCaseKeyword + -- PSR2.Classes.PropertyDeclaration + -- Squiz.Commenting.BlockComment + -- Squiz.Commenting.DocCommentAlignment + -- Squiz.Commenting.VariableComment + -- Squiz.WhiteSpace.ScopeKeywordSpacing + -- Thanks to Juliette Reinders Folmer for the patches + - The parallel feature is now more efficient and runs faster in some situations due to improved process management + -- Thanks to Sergei Morozov for the patch + - The list of installed coding standards now has consistent ordering across all platforms + -- Thanks to Juliette Reinders Folmer for the patch + - Generic.PHP.UpperCaseConstant and Generic.PHP.LowerCaseConstant now ignore type declarations + -- These sniffs now only report errors for true/false/null when used as values + -- Thanks to Juliette Reinders Folmer for the patch + - Generic.PHP.LowerCaseType now supports the PHP 8.1 never type + -- Thanks to Jaroslav Hanslík for the patch + - Fixed bug #3502 : A match statement within an array produces Squiz.Arrays.ArrayDeclaration.NoKeySpecified + - Fixed bug #3503 : Squiz.Commenting.FunctionComment.ThrowsNoFullStop false positive when one line @throw + - Fixed bug #3505 : The nullsafe operator is not counted in Generic.Metrics.CyclomaticComplexity + -- Thanks to Mark Baker for the patch + - Fixed bug #3526 : PSR12.Properties.ConstantVisibility false positive when using public final const syntax + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed bug #3530 : Line indented incorrectly false positive when using match-expression inside switch case + - Fixed bug #3534 : Name of typed enum tokenized as T_GOTO_LABEL + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed bug #3546 : Tokenizer/PHP: bug fix - parent/static keywords in class instantiations + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed bug #3550 : False positive from PSR2.ControlStructures.SwitchDeclaration.TerminatingComment when using trailing comment + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed bug #3575: Squiz.Scope.MethodScope misses visibility keyword on previous line + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed bug #3604: Tokenizer/PHP: bug fix for double quoted strings using ${ + -- Thanks to Juliette Reinders Folmer for the patch + + 3.6.2 diff --git a/scripts/build-phar.php b/scripts/build-phar.php index 6b2800c883..45e44bab2e 100644 --- a/scripts/build-phar.php +++ b/scripts/build-phar.php @@ -14,6 +14,12 @@ * @link http://pear.php.net/package/PHP_CodeSniffer */ +use PHP_CodeSniffer\Config; +use PHP_CodeSniffer\Exceptions\RuntimeException; +use PHP_CodeSniffer\Exceptions\TokenizerException; +use PHP_CodeSniffer\Tokenizers\PHP; +use PHP_CodeSniffer\Util\Tokens; + error_reporting(E_ALL | E_STRICT); if (ini_get('phar.readonly') === '1') { @@ -21,6 +27,60 @@ exit(1); } +require_once dirname(__DIR__).'/autoload.php'; +require_once dirname(__DIR__).'/src/Util/Tokens.php'; + +if (defined('PHP_CODESNIFFER_VERBOSITY') === false) { + define('PHP_CODESNIFFER_VERBOSITY', 0); +} + + +/** + * Replacement for the PHP native php_strip_whitespace() function, + * which doesn't handle attributes correctly for cross-version PHP. + * + * @param string $fullpath Path to file. + * @param \PHP_CodeSniffer\Config $config Perfunctory Config. + * + * @return string + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException When tokenizer errors are encountered. + */ +function stripWhitespaceAndComments($fullpath, $config) +{ + $contents = file_get_contents($fullpath); + + try { + $tokenizer = new PHP($contents, $config, "\n"); + $tokens = $tokenizer->getTokens(); + } catch (TokenizerException $e) { + throw new RuntimeException('Failed to tokenize file '.$fullpath); + } + + $stripped = ''; + foreach ($tokens as $token) { + if ($token['code'] === T_ATTRIBUTE_END || $token['code'] === T_OPEN_TAG) { + $stripped .= $token['content']."\n"; + continue; + } + + if (isset(Tokens::$emptyTokens[$token['code']]) === false) { + $stripped .= $token['content']; + continue; + } + + if ($token['code'] === T_WHITESPACE) { + $stripped .= ' '; + } + } + + return $stripped; + +}//end stripWhitespaceAndComments() + + +$startTime = microtime(true); + $scripts = [ 'phpcs', 'phpcbf', @@ -51,6 +111,9 @@ $rdi = new \RecursiveDirectoryIterator($srcDir, \RecursiveDirectoryIterator::FOLLOW_SYMLINKS); $di = new \RecursiveIteratorIterator($rdi, 0, \RecursiveIteratorIterator::CATCH_GET_CHILD); + $config = new Config(); + $fileCount = 0; + foreach ($di as $file) { $filename = $file->getFilename(); @@ -60,22 +123,30 @@ } $fullpath = $file->getPathname(); - if (strpos($fullpath, '/Tests/') !== false) { + if (strpos($fullpath, DIRECTORY_SEPARATOR.'Tests'.DIRECTORY_SEPARATOR) !== false) { continue; } $path = 'src'.substr($fullpath, $srcDirLen); - $phar->addFromString($path, php_strip_whitespace($fullpath)); - } + if (substr($filename, -4) === '.xml') { + $phar->addFile($fullpath, $path); + } else { + // PHP file. + $phar->addFromString($path, stripWhitespaceAndComments($fullpath, $config)); + } + + ++$fileCount; + }//end foreach // Add autoloader. - $phar->addFromString('autoload.php', php_strip_whitespace(realpath(__DIR__.'/../autoload.php'))); + $phar->addFromString('autoload.php', stripWhitespaceAndComments(realpath(__DIR__.'/../autoload.php'), $config)); // Add licence file. - $phar->addFromString('licence.txt', php_strip_whitespace(realpath(__DIR__.'/../licence.txt'))); + $phar->addFile(realpath(__DIR__.'/../licence.txt'), 'licence.txt'); echo 'done'.PHP_EOL; + echo "\t Added ".$fileCount.' files'.PHP_EOL; /* Add the stub. @@ -94,3 +165,16 @@ echo 'done'.PHP_EOL; }//end foreach + +$timeTaken = ((microtime(true) - $startTime) * 1000); +if ($timeTaken < 1000) { + $timeTaken = round($timeTaken); + echo "DONE in {$timeTaken}ms".PHP_EOL; +} else { + $timeTaken = round(($timeTaken / 1000), 2); + echo "DONE in $timeTaken secs".PHP_EOL; +} + +echo PHP_EOL; +echo 'Filesize generated phpcs.phar file: '.filesize(dirname(__DIR__).'/phpcs.phar').' bytes'.PHP_EOL; +echo 'Filesize generated phpcs.phar file: '.filesize(dirname(__DIR__).'/phpcbf.phar').' bytes'.PHP_EOL; diff --git a/src/Config.php b/src/Config.php index 6bb5b3298f..bfca9e1d18 100644 --- a/src/Config.php +++ b/src/Config.php @@ -80,7 +80,7 @@ class Config * * @var string */ - const VERSION = '3.6.2'; + const VERSION = '3.7.2'; /** * Package stability; either stable, beta or alpha. @@ -368,7 +368,7 @@ public function __construct(array $cliArgs=[], $dieOnUnknownArg=true) }//end if if (defined('STDIN') === false - || strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' + || stripos(PHP_OS, 'WIN') === 0 ) { return; } @@ -1517,7 +1517,7 @@ public static function getExecutablePath($name) return self::$executablePaths[$name]; } - if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + if (stripos(PHP_OS, 'WIN') === 0) { $cmd = 'where '.escapeshellarg($name).' 2> nul'; } else { $cmd = 'which '.escapeshellarg($name).' 2> /dev/null'; @@ -1597,7 +1597,7 @@ public static function setConfigData($key, $value, $temp=false) if ($temp === false) { $output = '<'.'?php'."\n".' $phpCodeSnifferConfig = '; $output .= var_export($phpCodeSnifferConfig, true); - $output .= "\n?".'>'; + $output .= ";\n?".'>'; if (file_put_contents($configFile, $output) === false) { $error = 'ERROR: Config file '.$configFile.' could not be written'.PHP_EOL.PHP_EOL; diff --git a/src/Files/File.php b/src/Files/File.php index e551dcbbf1..ad1ca0b236 100644 --- a/src/Files/File.php +++ b/src/Files/File.php @@ -1223,7 +1223,7 @@ public function getFilename() /** - * Returns the declaration names for classes, interfaces, traits, and functions. + * Returns the declaration name for classes, interfaces, traits, enums, and functions. * * @param int $stackPtr The position of the declaration token which * declared the class, interface, trait, or function. @@ -1232,7 +1232,7 @@ public function getFilename() * or NULL if the function or class is anonymous. * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token is not of type * T_FUNCTION, T_CLASS, T_ANON_CLASS, - * T_CLOSURE, T_TRAIT, or T_INTERFACE. + * T_CLOSURE, T_TRAIT, T_ENUM, or T_INTERFACE. */ public function getDeclarationName($stackPtr) { @@ -1246,8 +1246,9 @@ public function getDeclarationName($stackPtr) && $tokenCode !== T_CLASS && $tokenCode !== T_INTERFACE && $tokenCode !== T_TRAIT + && $tokenCode !== T_ENUM ) { - throw new RuntimeException('Token type "'.$this->tokens[$stackPtr]['type'].'" is not T_FUNCTION, T_CLASS, T_INTERFACE or T_TRAIT'); + throw new RuntimeException('Token type "'.$this->tokens[$stackPtr]['type'].'" is not T_FUNCTION, T_CLASS, T_INTERFACE, T_TRAIT or T_ENUM'); } if ($tokenCode === T_FUNCTION @@ -1310,6 +1311,8 @@ public function getDeclarationName($stackPtr) * Parameters declared using PHP 8 constructor property promotion, have these additional array indexes: * 'property_visibility' => string, // The property visibility as declared. * 'visibility_token' => integer, // The stack pointer to the visibility modifier token. + * 'property_readonly' => bool, // TRUE if the readonly keyword was found. + * 'readonly_token' => integer, // The stack pointer to the readonly modifier token. * * @param int $stackPtr The position in the stack of the function token * to acquire the parameters for. @@ -1366,6 +1369,7 @@ public function getMethodParameters($stackPtr) $typeHintEndToken = false; $nullableType = false; $visibilityToken = null; + $readonlyToken = null; for ($i = $paramStart; $i <= $closer; $i++) { // Check to see if this token has a parenthesis or bracket opener. If it does @@ -1465,6 +1469,7 @@ public function getMethodParameters($stackPtr) case T_NAMESPACE: case T_NS_SEPARATOR: case T_TYPE_UNION: + case T_TYPE_INTERSECTION: case T_FALSE: case T_NULL: // Part of a type hint or default value. @@ -1491,6 +1496,11 @@ public function getMethodParameters($stackPtr) $visibilityToken = $i; } break; + case T_READONLY: + if ($defaultStart === null) { + $readonlyToken = $i; + } + break; case T_CLOSE_PARENTHESIS: case T_COMMA: // If it's null, then there must be no parameters for this @@ -1523,6 +1533,12 @@ public function getMethodParameters($stackPtr) if ($visibilityToken !== null) { $vars[$paramCount]['property_visibility'] = $this->tokens[$visibilityToken]['content']; $vars[$paramCount]['visibility_token'] = $visibilityToken; + $vars[$paramCount]['property_readonly'] = false; + } + + if ($readonlyToken !== null) { + $vars[$paramCount]['property_readonly'] = true; + $vars[$paramCount]['readonly_token'] = $readonlyToken; } if ($this->tokens[$i]['code'] === T_COMMA) { @@ -1546,6 +1562,7 @@ public function getMethodParameters($stackPtr) $typeHintEndToken = false; $nullableType = false; $visibilityToken = null; + $readonlyToken = null; $paramCount++; break; @@ -1669,16 +1686,17 @@ public function getMethodProperties($stackPtr) } $valid = [ - T_STRING => T_STRING, - T_CALLABLE => T_CALLABLE, - T_SELF => T_SELF, - T_PARENT => T_PARENT, - T_STATIC => T_STATIC, - T_FALSE => T_FALSE, - T_NULL => T_NULL, - T_NAMESPACE => T_NAMESPACE, - T_NS_SEPARATOR => T_NS_SEPARATOR, - T_TYPE_UNION => T_TYPE_UNION, + T_STRING => T_STRING, + T_CALLABLE => T_CALLABLE, + T_SELF => T_SELF, + T_PARENT => T_PARENT, + T_STATIC => T_STATIC, + T_FALSE => T_FALSE, + T_NULL => T_NULL, + T_NAMESPACE => T_NAMESPACE, + T_NS_SEPARATOR => T_NS_SEPARATOR, + T_TYPE_UNION => T_TYPE_UNION, + T_TYPE_INTERSECTION => T_TYPE_INTERSECTION, ]; for ($i = $this->tokens[$stackPtr]['parenthesis_closer']; $i < $this->numTokens; $i++) { @@ -1743,6 +1761,7 @@ public function getMethodProperties($stackPtr) * 'scope' => string, // Public, private, or protected. * 'scope_specified' => boolean, // TRUE if the scope was explicitly specified. * 'is_static' => boolean, // TRUE if the static keyword was found. + * 'is_readonly' => boolean, // TRUE if the readonly keyword was found. * 'type' => string, // The type of the var (empty if no type specified). * 'type_token' => integer, // The stack pointer to the start of the type * // or FALSE if there is no type. @@ -1775,23 +1794,26 @@ public function getMemberProperties($stackPtr) && $this->tokens[$ptr]['code'] !== T_TRAIT) ) { if (isset($this->tokens[$ptr]) === true - && $this->tokens[$ptr]['code'] === T_INTERFACE + && ($this->tokens[$ptr]['code'] === T_INTERFACE + || $this->tokens[$ptr]['code'] === T_ENUM) ) { - // T_VARIABLEs in interfaces can actually be method arguments - // but they wont be seen as being inside the method because there + // T_VARIABLEs in interfaces/enums can actually be method arguments + // but they won't 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($this->tokens[$stackPtr]['nested_parenthesis']) === false || empty($this->tokens[$stackPtr]['nested_parenthesis']) === true ) { - $error = 'Possible parse error: interfaces may not include member vars'; - $this->addWarning($error, $stackPtr, 'Internal.ParseError.InterfaceHasMemberVar'); + $error = 'Possible parse error: %ss may not include member vars'; + $code = sprintf('Internal.ParseError.%sHasMemberVar', ucfirst($this->tokens[$ptr]['content'])); + $data = [strtolower($this->tokens[$ptr]['content'])]; + $this->addWarning($error, $stackPtr, $code, $data); return []; } } else { throw new RuntimeException('$stackPtr is not a class member var'); } - } + }//end if // Make sure it's not a method parameter. if (empty($this->tokens[$stackPtr]['nested_parenthesis']) === false) { @@ -1811,6 +1833,7 @@ public function getMemberProperties($stackPtr) T_PROTECTED => T_PROTECTED, T_STATIC => T_STATIC, T_VAR => T_VAR, + T_READONLY => T_READONLY, ]; $valid += Util\Tokens::$emptyTokens; @@ -1818,6 +1841,7 @@ public function getMemberProperties($stackPtr) $scope = 'public'; $scopeSpecified = false; $isStatic = false; + $isReadonly = false; $startOfStatement = $this->findPrevious( [ @@ -1850,6 +1874,9 @@ public function getMemberProperties($stackPtr) case T_STATIC: $isStatic = true; break; + case T_READONLY: + $isReadonly = true; + break; } }//end for @@ -1861,15 +1888,16 @@ public function getMemberProperties($stackPtr) if ($i < $stackPtr) { // We've found a type. $valid = [ - T_STRING => T_STRING, - T_CALLABLE => T_CALLABLE, - T_SELF => T_SELF, - T_PARENT => T_PARENT, - T_FALSE => T_FALSE, - T_NULL => T_NULL, - T_NAMESPACE => T_NAMESPACE, - T_NS_SEPARATOR => T_NS_SEPARATOR, - T_TYPE_UNION => T_TYPE_UNION, + T_STRING => T_STRING, + T_CALLABLE => T_CALLABLE, + T_SELF => T_SELF, + T_PARENT => T_PARENT, + T_FALSE => T_FALSE, + T_NULL => T_NULL, + T_NAMESPACE => T_NAMESPACE, + T_NS_SEPARATOR => T_NS_SEPARATOR, + T_TYPE_UNION => T_TYPE_UNION, + T_TYPE_INTERSECTION => T_TYPE_INTERSECTION, ]; for ($i; $i < $stackPtr; $i++) { @@ -1901,6 +1929,7 @@ public function getMemberProperties($stackPtr) 'scope' => $scope, 'scope_specified' => $scopeSpecified, 'is_static' => $isStatic, + 'is_readonly' => $isReadonly, 'type' => $type, 'type_token' => $typeToken, 'type_end_token' => $typeEndToken, @@ -2743,11 +2772,11 @@ public function findExtendedClassName($stackPtr) /** - * Returns the names of the interfaces that the specified class implements. + * Returns the names of the interfaces that the specified class or enum implements. * * Returns FALSE on error or if there are no implemented interface names. * - * @param int $stackPtr The stack position of the class. + * @param int $stackPtr The stack position of the class or enum token. * * @return array|false */ @@ -2760,6 +2789,7 @@ public function findImplementedInterfaceNames($stackPtr) if ($this->tokens[$stackPtr]['code'] !== T_CLASS && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS + && $this->tokens[$stackPtr]['code'] !== T_ENUM ) { return false; } diff --git a/src/Reports/Code.php b/src/Reports/Code.php index c54c1e1a4e..47c5581e25 100644 --- a/src/Reports/Code.php +++ b/src/Reports/Code.php @@ -225,7 +225,7 @@ public function generateFileReport($report, File $phpcsFile, $showSources=false, if (strpos($tokenContent, "\t") !== false) { $token = $tokens[$i]; $token['content'] = $tokenContent; - if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + if (stripos(PHP_OS, 'WIN') === 0) { $tab = "\000"; } else { $tab = "\033[30;1m»\033[0m"; diff --git a/src/Reports/Gitblame.php b/src/Reports/Gitblame.php index 947f3d80b1..6427567f8d 100644 --- a/src/Reports/Gitblame.php +++ b/src/Reports/Gitblame.php @@ -78,7 +78,7 @@ protected function getBlameContent($filename) } $rawContent = stream_get_contents($handle); - fclose($handle); + pclose($handle); $blames = explode("\n", $rawContent); chdir($cwd); diff --git a/src/Reports/Hgblame.php b/src/Reports/Hgblame.php index b0af6643fd..f88a06836b 100644 --- a/src/Reports/Hgblame.php +++ b/src/Reports/Hgblame.php @@ -97,7 +97,7 @@ protected function getBlameContent($filename) } $rawContent = stream_get_contents($handle); - fclose($handle); + pclose($handle); $blames = explode("\n", $rawContent); chdir($cwd); diff --git a/src/Reports/Svnblame.php b/src/Reports/Svnblame.php index f4719fe5dc..a7f65e154b 100644 --- a/src/Reports/Svnblame.php +++ b/src/Reports/Svnblame.php @@ -61,7 +61,7 @@ protected function getBlameContent($filename) } $rawContent = stream_get_contents($handle); - fclose($handle); + pclose($handle); $blames = explode("\n", $rawContent); diff --git a/src/Ruleset.php b/src/Ruleset.php index 7e659706c8..f90e7b6d2b 100644 --- a/src/Ruleset.php +++ b/src/Ruleset.php @@ -249,7 +249,12 @@ public function explain() // one last time and clear the output buffer. $sniffs[] = ''; - echo PHP_EOL."The $this->name standard contains $sniffCount sniffs".PHP_EOL; + $summaryLine = PHP_EOL."The $this->name standard contains 1 sniff".PHP_EOL; + if ($sniffCount !== 1) { + $summaryLine = str_replace('1 sniff', "$sniffCount sniffs", $summaryLine); + } + + echo $summaryLine; ob_start(); diff --git a/src/Runner.php b/src/Runner.php index 253ec91f9f..e0cbca7160 100644 --- a/src/Runner.php +++ b/src/Runner.php @@ -53,6 +53,8 @@ class Runner */ public function runPHPCS() { + $this->registerOutOfMemoryShutdownMessage('phpcs'); + try { Util\Timing::startTiming(); Runner::checkRequirements(); @@ -153,6 +155,8 @@ public function runPHPCS() */ public function runPHPCBF() { + $this->registerOutOfMemoryShutdownMessage('phpcbf'); + if (defined('PHP_CODESNIFFER_CBF') === false) { define('PHP_CODESNIFFER_CBF', true); } @@ -460,10 +464,7 @@ private function run() if ($pid === -1) { throw new RuntimeException('Failed to create child process'); } else if ($pid !== 0) { - $childProcs[] = [ - 'pid' => $pid, - 'out' => $childOutFilename, - ]; + $childProcs[$pid] = $childOutFilename; } else { // Move forward to the start of the batch. $todo->rewind(); @@ -536,7 +537,7 @@ private function run() $output .= ";\n?".'>'; file_put_contents($childOutFilename, $output); - exit($pid); + exit(); }//end if }//end for @@ -719,54 +720,61 @@ private function processChildProcs($childProcs) $success = true; while (count($childProcs) > 0) { - foreach ($childProcs as $key => $procData) { - $res = pcntl_waitpid($procData['pid'], $status, WNOHANG); - if ($res === $procData['pid']) { - if (file_exists($procData['out']) === true) { - include $procData['out']; - - unlink($procData['out']); - unset($childProcs[$key]); - - $numProcessed++; - - if (isset($childOutput) === false) { - // The child process died, so the run has failed. - $file = new DummyFile('', $this->ruleset, $this->config); - $file->setErrorCounts(1, 0, 0, 0); - $this->printProgress($file, $totalBatches, $numProcessed); - $success = false; - continue; - } + $pid = pcntl_waitpid(0, $status); + if ($pid <= 0) { + continue; + } - $this->reporter->totalFiles += $childOutput['totalFiles']; - $this->reporter->totalErrors += $childOutput['totalErrors']; - $this->reporter->totalWarnings += $childOutput['totalWarnings']; - $this->reporter->totalFixable += $childOutput['totalFixable']; - $this->reporter->totalFixed += $childOutput['totalFixed']; + $childProcessStatus = pcntl_wexitstatus($status); + if ($childProcessStatus !== 0) { + $success = false; + } - if (isset($debugOutput) === true) { - echo $debugOutput; - } + $out = $childProcs[$pid]; + unset($childProcs[$pid]); + if (file_exists($out) === false) { + continue; + } - if (isset($childCache) === true) { - foreach ($childCache as $path => $cache) { - Cache::set($path, $cache); - } - } + include $out; + unlink($out); - // Fake a processed file so we can print progress output for the batch. - $file = new DummyFile('', $this->ruleset, $this->config); - $file->setErrorCounts( - $childOutput['totalErrors'], - $childOutput['totalWarnings'], - $childOutput['totalFixable'], - $childOutput['totalFixed'] - ); - $this->printProgress($file, $totalBatches, $numProcessed); - }//end if - }//end if - }//end foreach + $numProcessed++; + + if (isset($childOutput) === false) { + // The child process died, so the run has failed. + $file = new DummyFile('', $this->ruleset, $this->config); + $file->setErrorCounts(1, 0, 0, 0); + $this->printProgress($file, $totalBatches, $numProcessed); + $success = false; + continue; + } + + $this->reporter->totalFiles += $childOutput['totalFiles']; + $this->reporter->totalErrors += $childOutput['totalErrors']; + $this->reporter->totalWarnings += $childOutput['totalWarnings']; + $this->reporter->totalFixable += $childOutput['totalFixable']; + $this->reporter->totalFixed += $childOutput['totalFixed']; + + if (isset($debugOutput) === true) { + echo $debugOutput; + } + + if (isset($childCache) === true) { + foreach ($childCache as $path => $cache) { + Cache::set($path, $cache); + } + } + + // Fake a processed file so we can print progress output for the batch. + $file = new DummyFile('', $this->ruleset, $this->config); + $file->setErrorCounts( + $childOutput['totalErrors'], + $childOutput['totalWarnings'], + $childOutput['totalFixable'], + $childOutput['totalFixed'] + ); + $this->printProgress($file, $totalBatches, $numProcessed); }//end while return $success; @@ -887,4 +895,42 @@ public function printProgress(File $file, $numFiles, $numProcessed) }//end printProgress() + /** + * Registers a PHP shutdown function to provide a more informative out of memory error. + * + * @param string $command The command which was used to initiate the PHPCS run. + * + * @return void + */ + private function registerOutOfMemoryShutdownMessage($command) + { + // Allocate all needed memory beforehand as much as possible. + $errorMsg = PHP_EOL.'The PHP_CodeSniffer "%1$s" command ran out of memory.'.PHP_EOL; + $errorMsg .= 'Either raise the "memory_limit" of PHP in the php.ini file or raise the memory limit at runtime'.PHP_EOL; + $errorMsg .= 'using `%1$s -d memory_limit=512M` (replace 512M with the desired memory limit).'.PHP_EOL; + $errorMsg = sprintf($errorMsg, $command); + $memoryError = 'Allowed memory size of'; + $errorArray = [ + 'type' => 42, + 'message' => 'Some random dummy string to take up memory and take up some more memory and some more', + 'file' => 'Another random string, which would be a filename this time. Should be relatively long to allow for deeply nested files', + 'line' => 31427, + ]; + + register_shutdown_function( + static function () use ( + $errorMsg, + $memoryError, + $errorArray + ) { + $errorArray = error_get_last(); + if (is_array($errorArray) === true && strpos($errorArray['message'], $memoryError) !== false) { + echo $errorMsg; + } + } + ); + + }//end registerOutOfMemoryShutdownMessage() + + }//end class diff --git a/src/Sniffs/AbstractArraySniff.php b/src/Sniffs/AbstractArraySniff.php index 141b9a133c..efe9969d8d 100644 --- a/src/Sniffs/AbstractArraySniff.php +++ b/src/Sniffs/AbstractArraySniff.php @@ -104,9 +104,9 @@ public function process(File $phpcsFile, $stackPtr) /** * Find next separator in array - either: comma or double arrow. * - * @param File $phpcsFile The current file being checked. - * @param int $ptr The position of current token. - * @param int $arrayEnd The token that ends the array definition. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The current file being checked. + * @param int $ptr The position of current token. + * @param int $arrayEnd The token that ends the array definition. * * @return int */ diff --git a/src/Sniffs/Sniff.php b/src/Sniffs/Sniff.php index c9d7daea82..3f0fb6a1cf 100644 --- a/src/Sniffs/Sniff.php +++ b/src/Sniffs/Sniff.php @@ -45,7 +45,7 @@ public function register(); * is found. * * The stackPtr variable indicates where in the stack the token was found. - * A sniff can acquire information this token, along with all the other + * A sniff can acquire information about this token, along with all the other * tokens within the stack by first acquiring the token stack: * * diff --git a/src/Standards/Generic/Docs/Classes/OpeningBraceSameLineStandard.xml b/src/Standards/Generic/Docs/Classes/OpeningBraceSameLineStandard.xml index 6226a3fff6..6fa08be7a4 100644 --- a/src/Standards/Generic/Docs/Classes/OpeningBraceSameLineStandard.xml +++ b/src/Standards/Generic/Docs/Classes/OpeningBraceSameLineStandard.xml @@ -15,6 +15,14 @@ class Foo { { +} + ]]> + + + + + { } ]]> diff --git a/src/Standards/Generic/Docs/Formatting/SpaceAfterNotStandard.xml b/src/Standards/Generic/Docs/Formatting/SpaceAfterNotStandard.xml index dd3e773118..aea863695e 100644 --- a/src/Standards/Generic/Docs/Formatting/SpaceAfterNotStandard.xml +++ b/src/Standards/Generic/Docs/Formatting/SpaceAfterNotStandard.xml @@ -10,13 +10,10 @@ if (! $someVar || ! $x instanceOf stdClass) {}; ]]> - + $someVar || !$x instanceOf stdClass) {}; - ]]> - - - $someVar || ! $x instanceOf stdClass) {}; ]]> diff --git a/src/Standards/Generic/Docs/WhiteSpace/ArbitraryParenthesesSpacingStandard.xml b/src/Standards/Generic/Docs/WhiteSpace/ArbitraryParenthesesSpacingStandard.xml index 30e0def93c..338c838933 100644 --- a/src/Standards/Generic/Docs/WhiteSpace/ArbitraryParenthesesSpacingStandard.xml +++ b/src/Standards/Generic/Docs/WhiteSpace/ArbitraryParenthesesSpacingStandard.xml @@ -10,13 +10,10 @@ $a = (null !== $extra); ]]> - + - - - findNext($this->register(), ($stackPtr + 1)); + $tokens = $phpcsFile->getTokens(); + $start = ($stackPtr + 1); + if (isset($tokens[$stackPtr]['scope_closer']) === true) { + $start = ($tokens[$stackPtr]['scope_closer'] + 1); + } + + $nextClass = $phpcsFile->findNext($this->register(), $start); if ($nextClass !== false) { $error = 'Only one class is allowed in a file'; $phpcsFile->addError($error, $nextClass, 'MultipleFound'); diff --git a/src/Standards/Generic/Sniffs/Files/OneInterfacePerFileSniff.php b/src/Standards/Generic/Sniffs/Files/OneInterfacePerFileSniff.php index f7cfa8d30b..9a6f5bccd5 100644 --- a/src/Standards/Generic/Sniffs/Files/OneInterfacePerFileSniff.php +++ b/src/Standards/Generic/Sniffs/Files/OneInterfacePerFileSniff.php @@ -39,7 +39,13 @@ public function register() */ public function process(File $phpcsFile, $stackPtr) { - $nextInterface = $phpcsFile->findNext($this->register(), ($stackPtr + 1)); + $tokens = $phpcsFile->getTokens(); + $start = ($stackPtr + 1); + if (isset($tokens[$stackPtr]['scope_closer']) === true) { + $start = ($tokens[$stackPtr]['scope_closer'] + 1); + } + + $nextInterface = $phpcsFile->findNext($this->register(), $start); if ($nextInterface !== false) { $error = 'Only one interface is allowed in a file'; $phpcsFile->addError($error, $nextInterface, 'MultipleFound'); diff --git a/src/Standards/Generic/Sniffs/Files/OneObjectStructurePerFileSniff.php b/src/Standards/Generic/Sniffs/Files/OneObjectStructurePerFileSniff.php index d9d71b6996..4d417e069e 100644 --- a/src/Standards/Generic/Sniffs/Files/OneObjectStructurePerFileSniff.php +++ b/src/Standards/Generic/Sniffs/Files/OneObjectStructurePerFileSniff.php @@ -27,6 +27,7 @@ public function register() T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, ]; }//end register() @@ -43,7 +44,13 @@ public function register() */ public function process(File $phpcsFile, $stackPtr) { - $nextClass = $phpcsFile->findNext($this->register(), ($stackPtr + 1)); + $tokens = $phpcsFile->getTokens(); + $start = ($stackPtr + 1); + if (isset($tokens[$stackPtr]['scope_closer']) === true) { + $start = ($tokens[$stackPtr]['scope_closer'] + 1); + } + + $nextClass = $phpcsFile->findNext($this->register(), $start); if ($nextClass !== false) { $error = 'Only one object structure is allowed in a file'; $phpcsFile->addError($error, $nextClass, 'MultipleFound'); diff --git a/src/Standards/Generic/Sniffs/Files/OneTraitPerFileSniff.php b/src/Standards/Generic/Sniffs/Files/OneTraitPerFileSniff.php index dd97da85b4..7ae523f70e 100644 --- a/src/Standards/Generic/Sniffs/Files/OneTraitPerFileSniff.php +++ b/src/Standards/Generic/Sniffs/Files/OneTraitPerFileSniff.php @@ -39,7 +39,13 @@ public function register() */ public function process(File $phpcsFile, $stackPtr) { - $nextClass = $phpcsFile->findNext($this->register(), ($stackPtr + 1)); + $tokens = $phpcsFile->getTokens(); + $start = ($stackPtr + 1); + if (isset($tokens[$stackPtr]['scope_closer']) === true) { + $start = ($tokens[$stackPtr]['scope_closer'] + 1); + } + + $nextClass = $phpcsFile->findNext($this->register(), $start); if ($nextClass !== false) { $error = 'Only one trait is allowed in a file'; $phpcsFile->addError($error, $nextClass, 'MultipleFound'); diff --git a/src/Standards/Generic/Sniffs/Functions/FunctionCallArgumentSpacingSniff.php b/src/Standards/Generic/Sniffs/Functions/FunctionCallArgumentSpacingSniff.php index ca9223814b..136a1d4ae9 100644 --- a/src/Standards/Generic/Sniffs/Functions/FunctionCallArgumentSpacingSniff.php +++ b/src/Standards/Generic/Sniffs/Functions/FunctionCallArgumentSpacingSniff.php @@ -30,6 +30,7 @@ public function register() T_UNSET, T_SELF, T_STATIC, + T_PARENT, T_VARIABLE, T_CLOSE_CURLY_BRACKET, T_CLOSE_PARENTHESIS, diff --git a/src/Standards/Generic/Sniffs/Metrics/CyclomaticComplexitySniff.php b/src/Standards/Generic/Sniffs/Metrics/CyclomaticComplexitySniff.php index 18100c2d67..9bd0dff3de 100644 --- a/src/Standards/Generic/Sniffs/Metrics/CyclomaticComplexitySniff.php +++ b/src/Standards/Generic/Sniffs/Metrics/CyclomaticComplexitySniff.php @@ -71,18 +71,19 @@ public function process(File $phpcsFile, $stackPtr) // Predicate nodes for PHP. $find = [ - T_CASE => true, - T_DEFAULT => true, - T_CATCH => true, - T_IF => true, - T_FOR => true, - T_FOREACH => true, - T_WHILE => true, - T_ELSEIF => true, - T_INLINE_THEN => true, - T_COALESCE => true, - T_COALESCE_EQUAL => true, - T_MATCH_ARROW => true, + T_CASE => true, + T_DEFAULT => true, + T_CATCH => true, + T_IF => true, + T_FOR => true, + T_FOREACH => true, + T_WHILE => true, + T_ELSEIF => true, + T_INLINE_THEN => true, + T_COALESCE => true, + T_COALESCE_EQUAL => true, + T_MATCH_ARROW => true, + T_NULLSAFE_OBJECT_OPERATOR => true, ]; $complexity = 1; diff --git a/src/Standards/Generic/Sniffs/PHP/DisallowRequestSuperglobalSniff.php b/src/Standards/Generic/Sniffs/PHP/DisallowRequestSuperglobalSniff.php index d5c7ddb724..1c2b1aee93 100644 --- a/src/Standards/Generic/Sniffs/PHP/DisallowRequestSuperglobalSniff.php +++ b/src/Standards/Generic/Sniffs/PHP/DisallowRequestSuperglobalSniff.php @@ -31,8 +31,9 @@ public function register() /** * Processes this sniff, when one of its tokens is encountered. * - * @param File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the stack + * passed in $tokens. * * @return void */ diff --git a/src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php b/src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php index 8b8c76e922..4376daa9dd 100644 --- a/src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php +++ b/src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php @@ -11,6 +11,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Tokens; class LowerCaseConstantSniff implements Sniff { @@ -25,6 +26,17 @@ class LowerCaseConstantSniff implements Sniff 'JS', ]; + /** + * The tokens this sniff is targetting. + * + * @var array + */ + private $targets = [ + T_TRUE => T_TRUE, + T_FALSE => T_FALSE, + T_NULL => T_NULL, + ]; + /** * Returns an array of tokens this test wants to listen for. @@ -33,11 +45,14 @@ class LowerCaseConstantSniff implements Sniff */ public function register() { - return [ - T_TRUE, - T_FALSE, - T_NULL, - ]; + $targets = $this->targets; + + // Register function keywords to filter out type declarations. + $targets[] = T_FUNCTION; + $targets[] = T_CLOSURE; + $targets[] = T_FN; + + return $targets; }//end register() @@ -52,10 +67,89 @@ public function register() * @return void */ public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + // Handle function declarations separately as they may contain the keywords in type declarations. + if ($tokens[$stackPtr]['code'] === T_FUNCTION + || $tokens[$stackPtr]['code'] === T_CLOSURE + || $tokens[$stackPtr]['code'] === T_FN + ) { + if (isset($tokens[$stackPtr]['parenthesis_closer']) === false) { + return; + } + + $end = $tokens[$stackPtr]['parenthesis_closer']; + if (isset($tokens[$stackPtr]['scope_opener']) === true) { + $end = $tokens[$stackPtr]['scope_opener']; + } + + // Do a quick check if any of the targets exist in the declaration. + $found = $phpcsFile->findNext($this->targets, $tokens[$stackPtr]['parenthesis_opener'], $end); + if ($found === false) { + // Skip forward, no need to examine these tokens again. + return $end; + } + + // Handle the whole function declaration in one go. + $params = $phpcsFile->getMethodParameters($stackPtr); + foreach ($params as $param) { + if (isset($param['default_token']) === false) { + continue; + } + + $paramEnd = $param['comma_token']; + if ($param['comma_token'] === false) { + $paramEnd = $tokens[$stackPtr]['parenthesis_closer']; + } + + for ($i = $param['default_token']; $i < $paramEnd; $i++) { + if (isset($this->targets[$tokens[$i]['code']]) === true) { + $this->processConstant($phpcsFile, $i); + } + } + } + + // Skip over return type declarations. + return $end; + }//end if + + // Handle property declarations separately as they may contain the keywords in type declarations. + if (isset($tokens[$stackPtr]['conditions']) === true) { + $conditions = $tokens[$stackPtr]['conditions']; + $lastCondition = end($conditions); + if (isset(Tokens::$ooScopeTokens[$lastCondition]) === true) { + // This can only be an OO constant or property declaration as methods are handled above. + $equals = $phpcsFile->findPrevious(T_EQUAL, ($stackPtr - 1), null, false, null, true); + if ($equals !== false) { + $this->processConstant($phpcsFile, $stackPtr); + } + + return; + } + } + + // Handle everything else. + $this->processConstant($phpcsFile, $stackPtr); + + }//end process() + + + /** + * Processes a non-type declaration constant. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the + * stack passed in $tokens. + * + * @return void + */ + protected function processConstant(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); $keyword = $tokens[$stackPtr]['content']; $expected = strtolower($keyword); + if ($keyword !== $expected) { if ($keyword === strtoupper($keyword)) { $phpcsFile->recordMetric($stackPtr, 'PHP constant case', 'upper'); @@ -77,7 +171,7 @@ public function process(File $phpcsFile, $stackPtr) $phpcsFile->recordMetric($stackPtr, 'PHP constant case', 'lower'); } - }//end process() + }//end processConstant() }//end class diff --git a/src/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php b/src/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php index e10047b64e..e6b4917834 100644 --- a/src/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php +++ b/src/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php @@ -11,7 +11,8 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; -use PHP_CodeSniffer\Util; +use PHP_CodeSniffer\Util\Common; +use PHP_CodeSniffer\Util\Tokens; class LowerCaseKeywordSniff implements Sniff { @@ -24,81 +25,21 @@ class LowerCaseKeywordSniff implements Sniff */ public function register() { - return [ - T_ABSTRACT, - T_ARRAY, - T_AS, - T_BREAK, - T_CALLABLE, - T_CASE, - T_CATCH, - T_CLASS, - T_CLONE, - T_CLOSURE, - T_CONST, - T_CONTINUE, - T_DECLARE, - T_DEFAULT, - T_DO, - T_ECHO, - T_ELSE, - T_ELSEIF, - T_EMPTY, - T_ENDDECLARE, - T_ENDFOR, - T_ENDFOREACH, - T_ENDIF, - T_ENDSWITCH, - T_ENDWHILE, - T_EVAL, - T_EXIT, - T_EXTENDS, - T_FINAL, - T_FINALLY, - T_FN, - T_FOR, - T_FOREACH, - T_FUNCTION, - T_GLOBAL, - T_GOTO, - T_IF, - T_IMPLEMENTS, - T_INCLUDE, - T_INCLUDE_ONCE, - T_INSTANCEOF, - T_INSTEADOF, - T_INTERFACE, - T_ISSET, - T_LIST, - T_LOGICAL_AND, - T_LOGICAL_OR, - T_LOGICAL_XOR, - T_MATCH, - T_MATCH_DEFAULT, - T_NAMESPACE, - T_NEW, - T_PARENT, - T_PRINT, - T_PRIVATE, - T_PROTECTED, - T_PUBLIC, - T_REQUIRE, - T_REQUIRE_ONCE, - T_RETURN, - T_SELF, - T_STATIC, - T_SWITCH, - T_THROW, - T_TRAIT, - T_TRY, - T_UNSET, - T_USE, - T_VAR, - T_WHILE, - T_YIELD, - T_YIELD_FROM, + $targets = Tokens::$contextSensitiveKeywords; + $targets += [ + T_CLOSURE => T_CLOSURE, + T_EMPTY => T_EMPTY, + T_ENUM_CASE => T_ENUM_CASE, + T_EVAL => T_EVAL, + T_ISSET => T_ISSET, + T_MATCH_DEFAULT => T_MATCH_DEFAULT, + T_PARENT => T_PARENT, + T_SELF => T_SELF, + T_UNSET => T_UNSET, ]; + return $targets; + }//end register() @@ -122,7 +63,7 @@ public function process(File $phpcsFile, $stackPtr) $phpcsFile->recordMetric($stackPtr, 'PHP keyword case', 'mixed'); } - $messageKeyword = Util\Common::prepareForOutput($keyword); + $messageKeyword = Common::prepareForOutput($keyword); $error = 'PHP keywords must be lowercase; expected "%s" but found "%s"'; $data = [ diff --git a/src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php b/src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php index a738c30d66..4d463f28fb 100644 --- a/src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php +++ b/src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php @@ -38,6 +38,7 @@ class LowerCaseTypeSniff implements Sniff 'static' => true, 'false' => true, 'null' => true, + 'never' => true, ]; @@ -51,6 +52,7 @@ public function register() $tokens = Tokens::$castTokens; $tokens[] = T_FUNCTION; $tokens[] = T_CLOSURE; + $tokens[] = T_FN; $tokens[] = T_VARIABLE; return $tokens; @@ -102,7 +104,9 @@ public function process(File $phpcsFile, $stackPtr) $error = 'PHP property type declarations must be lowercase; expected "%s" but found "%s"'; $errorCode = 'PropertyTypeFound'; - if (strpos($type, '|') !== false) { + if ($props['type_token'] === T_TYPE_INTERSECTION) { + // Intersection types don't support simple types. + } else if (strpos($type, '|') !== false) { $this->processUnionType( $phpcsFile, $props['type_token'], @@ -131,7 +135,9 @@ public function process(File $phpcsFile, $stackPtr) $error = 'PHP return type declarations must be lowercase; expected "%s" but found "%s"'; $errorCode = 'ReturnTypeFound'; - if (strpos($returnType, '|') !== false) { + if ($props['return_type_token'] === T_TYPE_INTERSECTION) { + // Intersection types don't support simple types. + } else if (strpos($returnType, '|') !== false) { $this->processUnionType( $phpcsFile, $props['return_type_token'], @@ -161,7 +167,9 @@ public function process(File $phpcsFile, $stackPtr) $error = 'PHP parameter type declarations must be lowercase; expected "%s" but found "%s"'; $errorCode = 'ParamTypeFound'; - if (strpos($typeHint, '|') !== false) { + if ($param['type_hint_token'] === T_TYPE_INTERSECTION) { + // Intersection types don't support simple types. + } else if (strpos($typeHint, '|') !== false) { $this->processUnionType( $phpcsFile, $param['type_hint_token'], diff --git a/src/Standards/Generic/Sniffs/PHP/UpperCaseConstantSniff.php b/src/Standards/Generic/Sniffs/PHP/UpperCaseConstantSniff.php index 54aa07f22a..2740884bb4 100644 --- a/src/Standards/Generic/Sniffs/PHP/UpperCaseConstantSniff.php +++ b/src/Standards/Generic/Sniffs/PHP/UpperCaseConstantSniff.php @@ -10,30 +10,13 @@ namespace PHP_CodeSniffer\Standards\Generic\Sniffs\PHP; use PHP_CodeSniffer\Files\File; -use PHP_CodeSniffer\Sniffs\Sniff; -class UpperCaseConstantSniff implements Sniff +class UpperCaseConstantSniff extends LowerCaseConstantSniff { /** - * Returns an array of tokens this test wants to listen for. - * - * @return array - */ - public function register() - { - return [ - T_TRUE, - T_FALSE, - T_NULL, - ]; - - }//end register() - - - /** - * Processes this sniff, when one of its tokens is encountered. + * Processes a non-type declaration constant. * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token in the @@ -41,11 +24,12 @@ public function register() * * @return void */ - public function process(File $phpcsFile, $stackPtr) + protected function processConstant(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); $keyword = $tokens[$stackPtr]['content']; $expected = strtoupper($keyword); + if ($keyword !== $expected) { if ($keyword === strtolower($keyword)) { $phpcsFile->recordMetric($stackPtr, 'PHP constant case', 'lower'); @@ -67,7 +51,7 @@ public function process(File $phpcsFile, $stackPtr) $phpcsFile->recordMetric($stackPtr, 'PHP constant case', 'upper'); } - }//end process() + }//end processConstant() }//end class diff --git a/src/Standards/Generic/Sniffs/WhiteSpace/DisallowTabIndentSniff.php b/src/Standards/Generic/Sniffs/WhiteSpace/DisallowTabIndentSniff.php index 3e20cf4964..2140e55ef5 100644 --- a/src/Standards/Generic/Sniffs/WhiteSpace/DisallowTabIndentSniff.php +++ b/src/Standards/Generic/Sniffs/WhiteSpace/DisallowTabIndentSniff.php @@ -76,6 +76,8 @@ public function process(File $phpcsFile, $stackPtr) T_DOC_COMMENT_WHITESPACE => true, T_DOC_COMMENT_STRING => true, T_COMMENT => true, + T_END_HEREDOC => true, + T_END_NOWDOC => true, ]; for ($i = 0; $i < $phpcsFile->numTokens; $i++) { diff --git a/src/Standards/Generic/Sniffs/WhiteSpace/ScopeIndentSniff.php b/src/Standards/Generic/Sniffs/WhiteSpace/ScopeIndentSniff.php index a01449197a..7fd604849d 100644 --- a/src/Standards/Generic/Sniffs/WhiteSpace/ScopeIndentSniff.php +++ b/src/Standards/Generic/Sniffs/WhiteSpace/ScopeIndentSniff.php @@ -142,12 +142,13 @@ public function process(File $phpcsFile, $stackPtr) } } - $lastOpenTag = $stackPtr; - $lastCloseTag = null; - $openScopes = []; - $adjustments = []; - $setIndents = []; - $disableExactEnd = 0; + $lastOpenTag = $stackPtr; + $lastCloseTag = null; + $openScopes = []; + $adjustments = []; + $setIndents = []; + $disableExactStack = []; + $disableExactEnd = 0; $tokens = $phpcsFile->getTokens(); $first = $phpcsFile->findFirstOnLine(T_INLINE_HTML, $stackPtr); @@ -232,6 +233,7 @@ public function process(File $phpcsFile, $stackPtr) if ($tokens[$i]['code'] === T_OPEN_PARENTHESIS && isset($tokens[$i]['parenthesis_closer']) === true ) { + $disableExactStack[$tokens[$i]['parenthesis_closer']] = $tokens[$i]['parenthesis_closer']; $disableExactEnd = max($disableExactEnd, $tokens[$i]['parenthesis_closer']); if ($this->debug === true) { $line = $tokens[$i]['line']; @@ -616,11 +618,11 @@ public function process(File $phpcsFile, $stackPtr) // Scope closers reset the required indent to the same level as the opening condition. if (($checkToken !== null - && isset($openScopes[$checkToken]) === true + && (isset($openScopes[$checkToken]) === true || (isset($tokens[$checkToken]['scope_condition']) === true && isset($tokens[$checkToken]['scope_closer']) === true && $tokens[$checkToken]['scope_closer'] === $checkToken - && $tokens[$checkToken]['line'] !== $tokens[$tokens[$checkToken]['scope_opener']]['line'])) + && $tokens[$checkToken]['line'] !== $tokens[$tokens[$checkToken]['scope_opener']]['line']))) || ($checkToken === null && isset($openScopes[$i]) === true) ) { @@ -802,9 +804,17 @@ public function process(File $phpcsFile, $stackPtr) && isset($tokens[$checkToken]['scope_opener']) === true ) { $exact = true; + if ($disableExactEnd > $checkToken) { - if ($tokens[$checkToken]['conditions'] === $tokens[$disableExactEnd]['conditions']) { - $exact = false; + foreach ($disableExactStack as $disableExactStackEnd) { + if ($disableExactStackEnd < $checkToken) { + continue; + } + + if ($tokens[$checkToken]['conditions'] === $tokens[$disableExactStackEnd]['conditions']) { + $exact = false; + break; + } } } @@ -1035,6 +1045,7 @@ public function process(File $phpcsFile, $stackPtr) // Don't check indents exactly between arrays as they tend to have custom rules. if ($tokens[$i]['code'] === T_OPEN_SHORT_ARRAY) { + $disableExactStack[$tokens[$i]['bracket_closer']] = $tokens[$i]['bracket_closer']; $disableExactEnd = max($disableExactEnd, $tokens[$i]['bracket_closer']); if ($this->debug === true) { $line = $tokens[$i]['line']; @@ -1056,7 +1067,6 @@ public function process(File $phpcsFile, $stackPtr) ) { if ($this->debug === true) { $line = $tokens[$i]['line']; - $type = $tokens[$disableExactEnd]['type']; echo "Here/nowdoc found on line $line".PHP_EOL; } @@ -1325,11 +1335,14 @@ public function process(File $phpcsFile, $stackPtr) continue; }//end if - // Closing an anon class or function. + // Closing an anon class, closure, or match. + // Each may be returned, which can confuse control structures that + // use return as a closer, like CASE statements. if (isset($tokens[$i]['scope_condition']) === true && $tokens[$i]['scope_closer'] === $i && ($tokens[$tokens[$i]['scope_condition']]['code'] === T_CLOSURE - || $tokens[$tokens[$i]['scope_condition']]['code'] === T_ANON_CLASS) + || $tokens[$tokens[$i]['scope_condition']]['code'] === T_ANON_CLASS + || $tokens[$tokens[$i]['scope_condition']]['code'] === T_MATCH) ) { if ($this->debug === true) { $type = str_replace('_', ' ', strtolower(substr($tokens[$tokens[$i]['scope_condition']]['type'], 2))); diff --git a/src/Standards/Generic/Sniffs/WhiteSpace/SpreadOperatorSpacingAfterSniff.php b/src/Standards/Generic/Sniffs/WhiteSpace/SpreadOperatorSpacingAfterSniff.php index 3146717e84..070bbcd1fb 100644 --- a/src/Standards/Generic/Sniffs/WhiteSpace/SpreadOperatorSpacingAfterSniff.php +++ b/src/Standards/Generic/Sniffs/WhiteSpace/SpreadOperatorSpacingAfterSniff.php @@ -62,6 +62,11 @@ public function process(File $phpcsFile, $stackPtr) return; } + if ($tokens[$nextNonEmpty]['code'] === T_CLOSE_PARENTHESIS) { + // Ignore PHP 8.1 first class callable syntax. + return; + } + if ($this->ignoreNewlines === true && $tokens[$stackPtr]['line'] !== $tokens[$nextNonEmpty]['line'] ) { diff --git a/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc b/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc index af56fd497d..075fc34c5b 100644 --- a/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc +++ b/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc @@ -98,3 +98,16 @@ $array = [ match ($test) { 1 => 'a', 2 => 'b' } => 'dynamic keys, woho!', ]; + +// Ensure that PHP 8.0 named parameters don't affect the sniff. +$array = [ + functionCall( + name: $value + ), +]; + +$array = [ + functionCall( + name: $value + ), +]; diff --git a/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc.fixed b/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc.fixed index 495145956b..505de5f780 100644 --- a/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc.fixed @@ -99,3 +99,16 @@ $array = [ match ($test) { 1 => 'a', 2 => 'b' } => 'dynamic keys, woho!', ]; + +// Ensure that PHP 8.0 named parameters don't affect the sniff. +$array = [ + functionCall( + name: $value + ), +]; + +$array = [ + functionCall( + name: $value + ), +]; diff --git a/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.php b/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.php index 0b99f2d68f..4861041f62 100644 --- a/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.php +++ b/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.php @@ -26,24 +26,25 @@ class ArrayIndentUnitTest extends AbstractSniffUnitTest public function getErrorList() { return [ - 14 => 1, - 15 => 1, - 17 => 1, - 30 => 1, - 31 => 1, - 33 => 1, - 41 => 1, - 62 => 1, - 63 => 1, - 69 => 1, - 77 => 1, - 78 => 1, - 79 => 1, - 85 => 1, - 86 => 1, - 87 => 1, - 88 => 1, - 98 => 1, + 14 => 1, + 15 => 1, + 17 => 1, + 30 => 1, + 31 => 1, + 33 => 1, + 41 => 1, + 62 => 1, + 63 => 1, + 69 => 1, + 77 => 1, + 78 => 1, + 79 => 1, + 85 => 1, + 86 => 1, + 87 => 1, + 88 => 1, + 98 => 1, + 110 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.1.inc b/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.1.inc index d0c136cbfa..91ab9d3975 100644 --- a/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.1.inc +++ b/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.1.inc @@ -5,7 +5,10 @@ interface MyInterface {} interface YourInterface {} trait MyTrait {} trait YourTrait {} +enum MyEnum {} +enum YourEnum {} class MyClass {} interface MyInterface {} trait MyTrait {} -?> \ No newline at end of file +enum MyEnum {} +?> diff --git a/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.2.inc b/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.2.inc index e6f92eb130..6829748a5f 100644 --- a/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.2.inc +++ b/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.2.inc @@ -2,4 +2,5 @@ class MyClass {} interface MyInterface {} trait MyTrait {} +enum MyEnum {} ?> \ No newline at end of file diff --git a/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.php b/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.php index b3b3edb502..68982f8686 100644 --- a/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.php +++ b/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.php @@ -45,9 +45,10 @@ public function getWarningList($testFile='') switch ($testFile) { case 'DuplicateClassNameUnitTest.1.inc': return [ - 8 => 1, - 9 => 1, 10 => 1, + 11 => 1, + 12 => 1, + 13 => 1, ]; break; case 'DuplicateClassNameUnitTest.2.inc': @@ -55,6 +56,7 @@ public function getWarningList($testFile='') 2 => 1, 3 => 1, 4 => 1, + 5 => 1, ]; break; case 'DuplicateClassNameUnitTest.5.inc': diff --git a/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc b/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc index 8147b4c73e..fd3abc030e 100644 --- a/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc +++ b/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc @@ -89,3 +89,7 @@ class Test_Class_Bad_G /*some comment*/ { } + +enum Test_Enum +{ +} diff --git a/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc.fixed b/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc.fixed index 406eb230fd..a755ae4725 100644 --- a/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc.fixed @@ -89,3 +89,7 @@ class Test_Class_Bad_G /*some comment*/ { } + +enum Test_Enum { + +} diff --git a/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.php b/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.php index 61cd4c9e8f..c357afc927 100644 --- a/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.php +++ b/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.php @@ -38,6 +38,7 @@ public function getErrorList() 70 => 1, 79 => 1, 90 => 1, + 94 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.php b/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.php index b613748414..269102dd3e 100644 --- a/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.php +++ b/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.php @@ -25,7 +25,7 @@ protected function shouldSkipTest() // PEAR doesn't preserve the executable flag, so skip // tests when running in a PEAR install. // Also skip on Windows which doesn't have the concept of executable files. - return ($GLOBALS['PHP_CODESNIFFER_PEAR'] || (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN')); + return ($GLOBALS['PHP_CODESNIFFER_PEAR'] || stripos(PHP_OS, 'WIN') === 0); }//end shouldSkipTest() diff --git a/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.inc b/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.inc index 94cebb4ee2..9f76d5d8d0 100644 --- a/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.inc +++ b/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.inc @@ -12,10 +12,15 @@ class baz { } trait barTrait { - + } interface barInterface { } -?> \ No newline at end of file + +enum barEnum { + +} + +?> diff --git a/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.php b/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.php index f429f98095..36a4bb9385 100644 --- a/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.php +++ b/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.php @@ -30,6 +30,7 @@ public function getErrorList() 10 => 1, 14 => 1, 18 => 1, + 22 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc b/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc index c46517df04..7c86a89f01 100644 --- a/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc +++ b/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc @@ -153,3 +153,12 @@ $foobar = functionCallAnonClassParam( $result = myFunction(param1: $arg1, param2: $arg2); $result = myFunction(param1: $arg1 , param2:$arg2); $result = myFunction(param1: $arg1, param2:$arg2, param3: $arg3,param4:$arg4, param5:$arg5); + +class Testing extends Bar +{ + public static function baz($foo, $bar) + { + $a = new parent($foo, $bar); + $a = new parent($foo ,$bar); + } +} diff --git a/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc.fixed b/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc.fixed index 1b51933eb9..8c9800725a 100644 --- a/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc.fixed @@ -153,3 +153,12 @@ $foobar = functionCallAnonClassParam( $result = myFunction(param1: $arg1, param2: $arg2); $result = myFunction(param1: $arg1, param2:$arg2); $result = myFunction(param1: $arg1, param2:$arg2, param3: $arg3, param4:$arg4, param5:$arg5); + +class Testing extends Bar +{ + public static function baz($foo, $bar) + { + $a = new parent($foo, $bar); + $a = new parent($foo, $bar); + } +} diff --git a/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.php b/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.php index 9f83fc9b77..789b60b37c 100644 --- a/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.php +++ b/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.php @@ -54,6 +54,7 @@ public function getErrorList() 134 => 1, 154 => 2, 155 => 1, + 162 => 2, ]; }//end getErrorList() diff --git a/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.inc b/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.inc index f6c6bb757b..494dcc7694 100644 --- a/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.inc +++ b/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.inc @@ -433,4 +433,22 @@ function complexityFourteenWithMatch() }; } + +function complexitySevenWithNullSafeOperator() +{ + $foo = $object1->getX()?->getY()?->getZ(); + $bar = $object2->getX()?->getY()?->getZ(); + $baz = $object3->getX()?->getY()?->getZ(); +} + + +function complexityElevenWithNullSafeOperator() +{ + $foo = $object1->getX()?->getY()?->getZ(); + $bar = $object2->getX()?->getY()?->getZ(); + $baz = $object3->getX()?->getY()?->getZ(); + $bacon = $object4->getX()?->getY()?->getZ(); + $bits = $object5->getX()?->getY()?->getZ(); +} + ?> diff --git a/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.php b/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.php index 92635fc44d..d3860dff9e 100644 --- a/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.php +++ b/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.php @@ -49,6 +49,7 @@ public function getWarningList() 333 => 1, 381 => 1, 417 => 1, + 445 => 1, ]; }//end getWarningList() diff --git a/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.inc b/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.inc index 9bda11472e..8441060d27 100644 --- a/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.inc +++ b/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.inc @@ -165,3 +165,21 @@ abstract class My_Class { public function my_class() {} public function _MY_CLASS() {} } + +enum Suit: string implements Colorful, CardGame { + // Magic methods. + function __call($name, $args) {} + static function __callStatic($name, $args) {} + function __invoke() {} + + // Valid Method Name. + public function getSomeValue() {} + + // Double underscore non-magic methods not allowed. + function __myFunction() {} + function __my_function() {} + + // Non-camelcase. + public function parseMyDSN() {} + public function get_some_value() {} +} diff --git a/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.php b/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.php index 502498d8bc..3d0320c3ae 100644 --- a/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.php +++ b/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.php @@ -63,6 +63,10 @@ public function getErrorList() 147 => 2, 158 => 1, 159 => 1, + 179 => 1, + 180 => 2, + 183 => 1, + 184 => 1, ]; return $errors; diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc index 63aea51872..0307a0559d 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc +++ b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc @@ -81,3 +81,20 @@ var_dump(MyClass::TRUE); function tRUE() {} $input->getFilterChain()->attachByName('Null', ['type' => Null::TYPE_STRING]); + +// Issue #3332 - ignore type declarations, but not default values. +class TypedThings { + const MYCONST = FALSE; + + public int|FALSE $int = FALSE; + public Type|NULL $int = new MyObj(NULL); + + private function typed(int|FALSE $param = NULL, Type|NULL $obj = new MyObj(FALSE)) : string|FALSE|NULL + { + if (TRUE === FALSE) { + return NULL; + } + } +} + +$cl = function (int|FALSE $param = NULL, Type|NULL $obj = new MyObj(FALSE)) : string|FALSE|NULL {}; diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc.fixed b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc.fixed index b3c3d8a136..3a6b094c86 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc.fixed @@ -81,3 +81,20 @@ var_dump(MyClass::TRUE); function tRUE() {} $input->getFilterChain()->attachByName('Null', ['type' => Null::TYPE_STRING]); + +// Issue #3332 - ignore type declarations, but not default values. +class TypedThings { + const MYCONST = false; + + public int|FALSE $int = false; + public Type|NULL $int = new MyObj(null); + + private function typed(int|FALSE $param = null, Type|NULL $obj = new MyObj(false)) : string|FALSE|NULL + { + if (true === false) { + return null; + } + } +} + +$cl = function (int|FALSE $param = null, Type|NULL $obj = new MyObj(false)) : string|FALSE|NULL {}; diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php index 85e8f7011d..2fb2d6f6dc 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php +++ b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php @@ -30,20 +30,27 @@ public function getErrorList($testFile='LowerCaseConstantUnitTest.inc') switch ($testFile) { case 'LowerCaseConstantUnitTest.inc': return [ - 7 => 1, - 10 => 1, - 15 => 1, - 16 => 1, - 23 => 1, - 26 => 1, - 31 => 1, - 32 => 1, - 39 => 1, - 42 => 1, - 47 => 1, - 48 => 1, - 70 => 1, - 71 => 1, + 7 => 1, + 10 => 1, + 15 => 1, + 16 => 1, + 23 => 1, + 26 => 1, + 31 => 1, + 32 => 1, + 39 => 1, + 42 => 1, + 47 => 1, + 48 => 1, + 70 => 1, + 71 => 1, + 87 => 1, + 89 => 1, + 90 => 1, + 92 => 2, + 94 => 2, + 95 => 1, + 100 => 2, ]; break; case 'LowerCaseConstantUnitTest.js': diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc index 8d003c3bcc..37579d3217 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc +++ b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc @@ -35,5 +35,14 @@ $r = Match ($x) { DEFAULT, => 3, }; +class Reading { + Public READOnly int $var; +} + +EnuM ENUM: string +{ + Case HEARTS; +} + __HALT_COMPILER(); // An exception due to phar support. function diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc.fixed b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc.fixed index bbe76b9e18..7063327ae8 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc.fixed @@ -35,5 +35,14 @@ $r = match ($x) { default, => 3, }; +class Reading { + public readonly int $var; +} + +enum ENUM: string +{ + case HEARTS; +} + __HALT_COMPILER(); // An exception due to phar support. function diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.php b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.php index b272196b8d..6d08e12751 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.php +++ b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.php @@ -40,6 +40,9 @@ public function getErrorList() 31 => 1, 32 => 1, 35 => 1, + 39 => 2, + 42 => 1, + 44 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc index 8e2dca17e0..011adcd530 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc +++ b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc @@ -81,3 +81,14 @@ class ConstructorPropertyPromotionWithTypes { class ConstructorPropertyPromotionAndNormalParams { public function __construct(public Int $promotedProp, ?Int $normalArg) {} } + +function (): NeVeR { + exit; +}; + +function intersectionParamTypes (\Package\ClassName&\Package\Other_Class $var) {} + +function intersectionReturnTypes ($var): \Package\ClassName&\Package\Other_Class {} + +$arrow = fn (int $a, string $b, bool $c, array $d, Foo\Bar $e) : int => $a * $b; +$arrow = fn (Int $a, String $b, BOOL $c, Array $d, Foo\Bar $e) : Float => $a * $b; diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc.fixed b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc.fixed index 6e03f7c2d5..d866101b6f 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc.fixed @@ -81,3 +81,14 @@ class ConstructorPropertyPromotionWithTypes { class ConstructorPropertyPromotionAndNormalParams { public function __construct(public int $promotedProp, ?int $normalArg) {} } + +function (): never { + exit; +}; + +function intersectionParamTypes (\Package\ClassName&\Package\Other_Class $var) {} + +function intersectionReturnTypes ($var): \Package\ClassName&\Package\Other_Class {} + +$arrow = fn (int $a, string $b, bool $c, array $d, Foo\Bar $e) : int => $a * $b; +$arrow = fn (int $a, string $b, bool $c, array $d, Foo\Bar $e) : float => $a * $b; diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.php b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.php index d447e8f47e..fa05aba6a3 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.php +++ b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.php @@ -65,6 +65,8 @@ public function getErrorList() 74 => 3, 78 => 3, 82 => 2, + 85 => 1, + 94 => 5, ]; }//end getErrorList() diff --git a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc index 965bf3b511..30c6d2980d 100644 --- a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc +++ b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc @@ -78,4 +78,21 @@ class MyClass var_dump(MyClass::true); -function true() {} \ No newline at end of file +function true() {} + +// Issue #3332 - ignore type declarations, but not default values. +class TypedThings { + const MYCONST = false; + + public int|false $int = false; + public Type|null $int = new MyObj(null); + + private function typed(int|false $param = null, Type|null $obj = new MyObj(false)) : string|false|null + { + if (true === false) { + return null; + } + } +} + +$cl = function (int|false $param = null, Type|null $obj = new MyObj(false)) : string|false|null {}; diff --git a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc.fixed b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc.fixed index ae83dc91de..7705198c81 100644 --- a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc.fixed @@ -78,4 +78,21 @@ class MyClass var_dump(MyClass::true); -function true() {} \ No newline at end of file +function true() {} + +// Issue #3332 - ignore type declarations, but not default values. +class TypedThings { + const MYCONST = FALSE; + + public int|false $int = FALSE; + public Type|null $int = new MyObj(NULL); + + private function typed(int|false $param = NULL, Type|null $obj = new MyObj(FALSE)) : string|false|null + { + if (TRUE === FALSE) { + return NULL; + } + } +} + +$cl = function (int|false $param = NULL, Type|null $obj = new MyObj(FALSE)) : string|false|null {}; diff --git a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.php b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.php index 0bdafe6a43..30e5776337 100644 --- a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.php +++ b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.php @@ -40,6 +40,13 @@ public function getErrorList() 48 => 1, 70 => 1, 71 => 1, + 85 => 1, + 87 => 1, + 88 => 1, + 90 => 2, + 92 => 2, + 93 => 1, + 98 => 2, ]; }//end getErrorList() diff --git a/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc b/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc index bad3998f97..2399a387e1 100644 --- a/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc +++ b/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc @@ -163,3 +163,15 @@ $a = ( if (true) {} ( 1+2) === 3 ? $a = 1 : $a = 2; class A {} ( 1+2) === 3 ? $a = 1 : $a = 2; function foo() {} ( 1+2) === 3 ? $a = 1 : $a = 2; + +// Issue #3618. +class NonArbitraryParenthesesWithKeywords { + public static function baz( $foo, $bar ) { + $a = new self(); + $b = new parent(); + $c = new static(); + + // self/static are already tested above, round line 45. + $d = new parent( $foo,$bar ); + } +} diff --git a/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc.fixed b/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc.fixed index 08fcd6244a..9162728e17 100644 --- a/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc.fixed @@ -151,3 +151,15 @@ $a = ( if (true) {} (1+2) === 3 ? $a = 1 : $a = 2; class A {} (1+2) === 3 ? $a = 1 : $a = 2; function foo() {} (1+2) === 3 ? $a = 1 : $a = 2; + +// Issue #3618. +class NonArbitraryParenthesesWithKeywords { + public static function baz( $foo, $bar ) { + $a = new self(); + $b = new parent(); + $c = new static(); + + // self/static are already tested above, round line 45. + $d = new parent( $foo,$bar ); + } +} diff --git a/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.3.inc b/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.3.inc new file mode 100644 index 0000000000..68b18938d0 --- /dev/null +++ b/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.3.inc @@ -0,0 +1,13 @@ + 1, 93 => 1, ]; - break; + case 'DisallowTabIndentUnitTest.2.inc': return [ 6 => 1, @@ -96,23 +96,33 @@ public function getErrorList($testFile='') 13 => 1, 19 => 1, ]; - break; + + case 'DisallowTabIndentUnitTest.3.inc': + if (\PHP_VERSION_ID >= 70300) { + return [ + 7 => 1, + 13 => 1, + ]; + } + + // PHP 7.2 or lower: PHP version which doesn't support flexible heredocs/nowdocs yet. + return []; + case 'DisallowTabIndentUnitTest.js': return [ 3 => 1, 5 => 1, 6 => 1, ]; - break; + case 'DisallowTabIndentUnitTest.css': return [ 1 => 1, 2 => 1, ]; - break; + default: return []; - break; }//end switch }//end getErrorList() diff --git a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.1.inc b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.1.inc index 6aadcc2095..4061aff567 100644 --- a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.1.inc +++ b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.1.inc @@ -1556,6 +1556,29 @@ $a = [ ] ]; +switch ($foo) { + case 'a': + $foo = match ($foo) { + 'bar' => 'custom_1', + default => 'a' + }; + return $foo; + case 'b': + return match ($foo) { + 'bar' => 'custom_1', + default => 'b' + }; + default: + return 'default'; +} + +foo(function ($foo) { + return [ + match ($foo) { + } + ]; +}); + /* ADD NEW TESTS ABOVE THIS LINE AND MAKE SURE THAT THE 1 (space-based) AND 2 (tab-based) FILES ARE IN SYNC! */ ?> @@ -1564,7 +1587,7 @@ $a = [ 'custom_1', + default => 'a' + }; + return $foo; + case 'b': + return match ($foo) { + 'bar' => 'custom_1', + default => 'b' + }; + default: + return 'default'; +} + +foo(function ($foo) { + return [ + match ($foo) { + } + ]; +}); + /* ADD NEW TESTS ABOVE THIS LINE AND MAKE SURE THAT THE 1 (space-based) AND 2 (tab-based) FILES ARE IN SYNC! */ ?> @@ -1564,7 +1587,7 @@ $a = [ 'custom_1', + default => 'a' + }; + return $foo; + case 'b': + return match ($foo) { + 'bar' => 'custom_1', + default => 'b' + }; + default: + return 'default'; +} + +foo(function ($foo) { + return [ + match ($foo) { + } + ]; +}); + /* ADD NEW TESTS ABOVE THIS LINE AND MAKE SURE THAT THE 1 (space-based) AND 2 (tab-based) FILES ARE IN SYNC! */ ?> @@ -1564,7 +1587,7 @@ $a = [ 'custom_1', + default => 'a' + }; + return $foo; + case 'b': + return match ($foo) { + 'bar' => 'custom_1', + default => 'b' + }; + default: + return 'default'; +} + +foo(function ($foo) { + return [ + match ($foo) { + } + ]; +}); + /* ADD NEW TESTS ABOVE THIS LINE AND MAKE SURE THAT THE 1 (space-based) AND 2 (tab-based) FILES ARE IN SYNC! */ ?> @@ -1564,7 +1587,7 @@ $a = [ 1, 1529 => 1, 1530 => 1, - 1567 => 1, - 1568 => 1, - 1569 => 1, - 1570 => 1, + 1590 => 1, + 1591 => 1, + 1592 => 1, + 1593 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc b/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc index edce4dcc69..fb5c181429 100644 --- a/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc +++ b/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc @@ -67,7 +67,13 @@ function bar( & ... $spread ) { ); } +// Ignore PHP 8.1 first class callable declarations. +$map = array_map(strtolower(...), $map); + // phpcs:set Generic.WhiteSpace.SpreadOperatorSpacingAfter spacing 0 +// Ignore PHP 8.1 first class callable declarations. +$map = array_map(strtolower( ... ), $map); + // Intentional parse error. This has to be the last test in the file. function bar( ... diff --git a/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc.fixed b/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc.fixed index efec7ac15c..9388acfc63 100644 --- a/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc.fixed @@ -62,7 +62,13 @@ function bar( & ... $spread ) { ); } +// Ignore PHP 8.1 first class callable declarations. +$map = array_map(strtolower(...), $map); + // phpcs:set Generic.WhiteSpace.SpreadOperatorSpacingAfter spacing 0 +// Ignore PHP 8.1 first class callable declarations. +$map = array_map(strtolower( ... ), $map); + // Intentional parse error. This has to be the last test in the file. function bar( ... diff --git a/src/Standards/PEAR/Sniffs/Classes/ClassDeclarationSniff.php b/src/Standards/PEAR/Sniffs/Classes/ClassDeclarationSniff.php index a5b07a9e47..dbd61f3d23 100644 --- a/src/Standards/PEAR/Sniffs/Classes/ClassDeclarationSniff.php +++ b/src/Standards/PEAR/Sniffs/Classes/ClassDeclarationSniff.php @@ -27,6 +27,7 @@ public function register() T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, ]; }//end register() diff --git a/src/Standards/PEAR/Sniffs/Commenting/ClassCommentSniff.php b/src/Standards/PEAR/Sniffs/Commenting/ClassCommentSniff.php index ac3351e02b..a01ea2cc7e 100644 --- a/src/Standards/PEAR/Sniffs/Commenting/ClassCommentSniff.php +++ b/src/Standards/PEAR/Sniffs/Commenting/ClassCommentSniff.php @@ -27,6 +27,7 @@ public function register() T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, ]; }//end register() diff --git a/src/Standards/PEAR/Sniffs/Commenting/FileCommentSniff.php b/src/Standards/PEAR/Sniffs/Commenting/FileCommentSniff.php index e0672ffe3a..6a44584686 100644 --- a/src/Standards/PEAR/Sniffs/Commenting/FileCommentSniff.php +++ b/src/Standards/PEAR/Sniffs/Commenting/FileCommentSniff.php @@ -161,6 +161,7 @@ public function process(File $phpcsFile, $stackPtr) T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, T_FUNCTION, T_CLOSURE, T_PUBLIC, diff --git a/src/Standards/PEAR/Sniffs/Functions/FunctionCallSignatureSniff.php b/src/Standards/PEAR/Sniffs/Functions/FunctionCallSignatureSniff.php index 3a339abe4c..b5e8695c15 100644 --- a/src/Standards/PEAR/Sniffs/Functions/FunctionCallSignatureSniff.php +++ b/src/Standards/PEAR/Sniffs/Functions/FunctionCallSignatureSniff.php @@ -390,11 +390,14 @@ public function processMultiLineCall(File $phpcsFile, $stackPtr, $openBracket, $ $padding = str_repeat(' ', $functionIndent); if ($foundFunctionIndent === 0) { $phpcsFile->fixer->addContentBefore($first, $padding); + } else if ($tokens[$first]['code'] === T_INLINE_HTML) { + $newContent = $padding.ltrim($tokens[$first]['content']); + $phpcsFile->fixer->replaceToken($first, $newContent); } else { $phpcsFile->fixer->replaceToken(($first - 1), $padding); } } - } + }//end if $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($openBracket + 1), null, true); if ($tokens[$next]['line'] === $tokens[$openBracket]['line']) { diff --git a/src/Standards/PEAR/Sniffs/NamingConventions/ValidClassNameSniff.php b/src/Standards/PEAR/Sniffs/NamingConventions/ValidClassNameSniff.php index 34ca2830b5..00e68bfec5 100644 --- a/src/Standards/PEAR/Sniffs/NamingConventions/ValidClassNameSniff.php +++ b/src/Standards/PEAR/Sniffs/NamingConventions/ValidClassNameSniff.php @@ -27,6 +27,7 @@ public function register() T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, ]; }//end register() diff --git a/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc b/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc index d97ef4d29d..6942944b5c 100644 --- a/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc +++ b/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc @@ -110,3 +110,5 @@ if (!class_exists('ClassOpeningBraceTooMuchIndentation')) { { } } + +enum IncorrectBracePlacement {} diff --git a/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc.fixed b/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc.fixed index 5b0a2f93fb..26688b1527 100644 --- a/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc.fixed +++ b/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc.fixed @@ -119,3 +119,7 @@ if (!class_exists('ClassOpeningBraceTooMuchIndentation')) { { } } + +enum IncorrectBracePlacement +{ +} diff --git a/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.php b/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.php index 1514a70378..4c1d28e733 100644 --- a/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.php +++ b/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.php @@ -61,6 +61,7 @@ public function getErrorList($testFile='') 99 => 1, 104 => 1, 110 => 1, + 114 => 1, ]; default: diff --git a/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.inc b/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.inc index 8414efbedb..da53b99e87 100644 --- a/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.inc +++ b/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.inc @@ -120,6 +120,16 @@ trait Empty_Trait_Doc }//end trait +/** + * + * + */ +enum Empty_Enum_Doc +{ + +}//end enum + + /** * Sample class comment * diff --git a/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.php b/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.php index 9a4bcf7dd6..004a064b35 100644 --- a/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.php +++ b/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.php @@ -44,6 +44,7 @@ public function getErrorList() 96 => 5, 106 => 5, 116 => 5, + 126 => 5, ]; }//end getErrorList() diff --git a/src/Standards/PEAR/Tests/Commenting/FileCommentUnitTest.3.inc b/src/Standards/PEAR/Tests/Commenting/FileCommentUnitTest.3.inc new file mode 100644 index 0000000000..c076fd4572 --- /dev/null +++ b/src/Standards/PEAR/Tests/Commenting/FileCommentUnitTest.3.inc @@ -0,0 +1,8 @@ + 1]; + case 'FileCommentUnitTest.3.inc': + return [1 => 1]; + default: return []; }//end switch diff --git a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc index ed3d2c43e1..612748fedf 100644 --- a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc +++ b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc @@ -548,3 +548,22 @@ array_fill_keys( ), value: true, ); // phpcs:set PEAR.Functions.FunctionCallSignature allowMultipleArguments true + +?> +
+

+
+
+

+
+ +content +

diff --git a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc.fixed b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc.fixed index 8d02e7467d..00226de562 100644 --- a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc.fixed +++ b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc.fixed @@ -563,3 +563,22 @@ array_fill_keys( value: true, ); // phpcs:set PEAR.Functions.FunctionCallSignature allowMultipleArguments true + +?> +
+

+
+
+

+
+ +content +

diff --git a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.php b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.php index 1dd59188f3..4984de2bf6 100644 --- a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.php +++ b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.php @@ -131,6 +131,9 @@ public function getErrorList($testFile='FunctionCallSignatureUnitTest.inc') 546 => 1, 547 => 1, 548 => 1, + 559 => 1, + 567 => 1, + 568 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.inc b/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.inc index c6d15df7f0..053a4fee2f 100644 --- a/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.inc +++ b/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.inc @@ -66,3 +66,25 @@ trait _Invalid_Name {} trait ___ {} trait Invalid__Name {} + +enum Valid_Name: string {} + +enum invalid_Name : String {} + +enum invalid_name {} + +enum Invalid_name: Int {} + +enum VALID_Name {} + +enum VALID_NAME {} + +enum VALID_Name : int {} + +enum ValidName {} + +enum _Invalid_Name {} + +enum ___ {} + +enum Invalid__Name {} diff --git a/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.php b/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.php index 8ff9c674b2..54ee74aac1 100644 --- a/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.php +++ b/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.php @@ -44,6 +44,12 @@ public function getErrorList() 64 => 1, 66 => 2, 68 => 1, + 72 => 1, + 74 => 2, + 76 => 1, + 86 => 1, + 88 => 2, + 90 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.inc b/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.inc index 78280cdd90..18b1a48176 100644 --- a/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.inc +++ b/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.inc @@ -220,3 +220,24 @@ abstract class My_Class { public function my_class() {} public function _MY_CLASS() {} } + +enum Suit: string implements Colorful, CardGame { + // Magic methods. + function __call($name, $args) {} + static function __callStatic($name, $args) {} + function __invoke() {} + + // Valid Method Name. + public function parseMyDSN() {} + private function _getAnotherValue() {} + + // Double underscore non-magic methods not allowed. + function __myFunction() {} + function __my_function() {} + + // Non-camelcase. + public function get_some_value() {} + + // Private without underscore prefix. + private function getMe() {} +} diff --git a/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.php b/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.php index 9bb6de0d84..4639a1e2ab 100644 --- a/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.php +++ b/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.php @@ -122,6 +122,10 @@ public function getErrorList() 212 => 1, 213 => 1, 214 => 1, + 235 => 1, + 236 => 2, + 239 => 1, + 242 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc b/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc index c52023781f..3f90067920 100644 --- a/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc +++ b/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc @@ -152,3 +152,13 @@ $match = match ($test) { 1 => 'a', 2 => 'b' }; + +enum Enum +{ +} + +enum Suits {} + +enum Cards +{ + } diff --git a/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc.fixed b/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc.fixed index 23156e4100..f369c2b2f6 100644 --- a/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc.fixed +++ b/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc.fixed @@ -157,3 +157,14 @@ $match = match ($test) { 1 => 'a', 2 => 'b' }; + +enum Enum +{ +} + +enum Suits { +} + +enum Cards +{ +} diff --git a/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.php b/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.php index 1b01ee9d4e..265932549a 100644 --- a/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.php +++ b/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.php @@ -42,6 +42,8 @@ public function getErrorList() 146 => 1, 149 => 1, 154 => 1, + 160 => 1, + 164 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PSR1/Sniffs/Classes/ClassDeclarationSniff.php b/src/Standards/PSR1/Sniffs/Classes/ClassDeclarationSniff.php index ac6407d6f1..3db26f6fd1 100644 --- a/src/Standards/PSR1/Sniffs/Classes/ClassDeclarationSniff.php +++ b/src/Standards/PSR1/Sniffs/Classes/ClassDeclarationSniff.php @@ -27,6 +27,7 @@ public function register() T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, ]; }//end register() @@ -50,7 +51,7 @@ public function process(File $phpcsFile, $stackPtr) $errorData = [strtolower($tokens[$stackPtr]['content'])]; - $nextClass = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT], ($tokens[$stackPtr]['scope_closer'] + 1)); + $nextClass = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], ($tokens[$stackPtr]['scope_closer'] + 1)); if ($nextClass !== false) { $error = 'Each %s must be in a file by itself'; $phpcsFile->addError($error, $nextClass, 'MultipleClasses', $errorData); @@ -59,7 +60,7 @@ public function process(File $phpcsFile, $stackPtr) $phpcsFile->recordMetric($stackPtr, 'One class per file', 'yes'); } - $namespace = $phpcsFile->findNext([T_NAMESPACE, T_CLASS, T_INTERFACE, T_TRAIT], 0); + $namespace = $phpcsFile->findNext([T_NAMESPACE, T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], 0); if ($tokens[$namespace]['code'] !== T_NAMESPACE) { $error = 'Each %s must be in a namespace of at least one level (a top-level vendor name)'; $phpcsFile->addError($error, $stackPtr, 'MissingNamespace', $errorData); diff --git a/src/Standards/PSR1/Sniffs/Files/SideEffectsSniff.php b/src/Standards/PSR1/Sniffs/Files/SideEffectsSniff.php index 27454cc1d7..3f8c5e0d56 100644 --- a/src/Standards/PSR1/Sniffs/Files/SideEffectsSniff.php +++ b/src/Standards/PSR1/Sniffs/Files/SideEffectsSniff.php @@ -82,6 +82,7 @@ private function searchForConflict($phpcsFile, $start, $end, $tokens) T_CLASS => T_CLASS, T_INTERFACE => T_INTERFACE, T_TRAIT => T_TRAIT, + T_ENUM => T_ENUM, T_FUNCTION => T_FUNCTION, ]; diff --git a/src/Standards/PSR1/Tests/Classes/ClassDeclarationUnitTest.3.inc b/src/Standards/PSR1/Tests/Classes/ClassDeclarationUnitTest.3.inc new file mode 100644 index 0000000000..302908eb12 --- /dev/null +++ b/src/Standards/PSR1/Tests/Classes/ClassDeclarationUnitTest.3.inc @@ -0,0 +1,3 @@ + T_NS_SEPARATOR, T_SELF => T_SELF, T_STATIC => T_STATIC, + T_PARENT => T_PARENT, T_VARIABLE => T_VARIABLE, T_DOLLAR => T_DOLLAR, T_OBJECT_OPERATOR => T_OBJECT_OPERATOR, diff --git a/src/Standards/PSR12/Sniffs/Classes/ClosingBraceSniff.php b/src/Standards/PSR12/Sniffs/Classes/ClosingBraceSniff.php index 0f9752b1f4..fa1f8d60ed 100644 --- a/src/Standards/PSR12/Sniffs/Classes/ClosingBraceSniff.php +++ b/src/Standards/PSR12/Sniffs/Classes/ClosingBraceSniff.php @@ -27,6 +27,7 @@ public function register() T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, T_FUNCTION, ]; diff --git a/src/Standards/PSR12/Sniffs/Properties/ConstantVisibilitySniff.php b/src/Standards/PSR12/Sniffs/Properties/ConstantVisibilitySniff.php index 8a1000f72f..7f63d1e87a 100644 --- a/src/Standards/PSR12/Sniffs/Properties/ConstantVisibilitySniff.php +++ b/src/Standards/PSR12/Sniffs/Properties/ConstantVisibilitySniff.php @@ -47,7 +47,10 @@ public function process(File $phpcsFile, $stackPtr) return; } - $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + $ignore = Tokens::$emptyTokens; + $ignore[] = T_FINAL; + + $prev = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true); if (isset(Tokens::$scopeModifiers[$tokens[$prev]['code']]) === true) { return; } diff --git a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc index d933ee273e..9fd1548072 100644 --- a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc +++ b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc @@ -42,3 +42,6 @@ $class = new ${$obj?->classname}; $anonWithAttribute = new #[SomeAttribute('summary')] class { public const SOME_STUFF = 'foo'; }; + +$foo = new parent(); +$foo = new parent; diff --git a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc.fixed b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc.fixed index 02e3544fa6..aa9d0c7209 100644 --- a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc.fixed +++ b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc.fixed @@ -42,3 +42,6 @@ $class = new ${$obj?->classname}(); $anonWithAttribute = new #[SomeAttribute('summary')] class { public const SOME_STUFF = 'foo'; }; + +$foo = new parent(); +$foo = new parent(); diff --git a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.php b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.php index 3fb1ab992b..0a16af8fc0 100644 --- a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.php +++ b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.php @@ -43,6 +43,7 @@ public function getErrorList() 34 => 1, 37 => 1, 38 => 1, + 47 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.inc b/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.inc index 1d2e92c97e..2562d26c06 100644 --- a/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.inc +++ b/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.inc @@ -45,3 +45,8 @@ $instance = new class extends \Foo implements \HandleableInterface { $app->get('/hello/{name}', function ($name) use ($app) { return 'Hello ' . $app->escape($name); }); + +enum Foo4 +{ + +}//end diff --git a/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.php b/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.php index 1deac1cdf5..d402f1bb94 100644 --- a/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.php +++ b/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.php @@ -31,6 +31,7 @@ public function getErrorList() 19 => 1, 24 => 1, 31 => 1, + 52 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.inc b/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.inc index 509c48c316..2c41bde994 100644 --- a/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.inc +++ b/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.inc @@ -47,3 +47,11 @@ $instance = new class extends \Foo implements \HandleableInterface { // Class content }; +enum Valid +{ +} + +enum Invalid +{ + +} diff --git a/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.inc.fixed b/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.inc.fixed index de3d4b49b0..ea58573392 100644 --- a/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.inc.fixed +++ b/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.inc.fixed @@ -39,3 +39,10 @@ $instance = new class extends \Foo implements \HandleableInterface { // Class content }; +enum Valid +{ +} + +enum Invalid +{ +} diff --git a/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.php b/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.php index 2ea4fd273e..71d9d6f48f 100644 --- a/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.php +++ b/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.php @@ -31,6 +31,7 @@ public function getErrorList() 24 => 1, 34 => 1, 41 => 1, + 55 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PSR12/Tests/Files/ImportStatementUnitTest.inc b/src/Standards/PSR12/Tests/Files/ImportStatementUnitTest.inc index 904e3f4080..6d024eaa95 100644 --- a/src/Standards/PSR12/Tests/Files/ImportStatementUnitTest.inc +++ b/src/Standards/PSR12/Tests/Files/ImportStatementUnitTest.inc @@ -17,3 +17,10 @@ class ClassName3 } $foo = function() use($bar) {}; + +enum SomeEnum +{ + use \FirstTrait; + use SecondTrait; + use ThirdTrait; +} diff --git a/src/Standards/PSR12/Tests/Files/ImportStatementUnitTest.inc.fixed b/src/Standards/PSR12/Tests/Files/ImportStatementUnitTest.inc.fixed index d5b3f67eed..dfed46fc17 100644 --- a/src/Standards/PSR12/Tests/Files/ImportStatementUnitTest.inc.fixed +++ b/src/Standards/PSR12/Tests/Files/ImportStatementUnitTest.inc.fixed @@ -17,3 +17,10 @@ class ClassName3 } $foo = function() use($bar) {}; + +enum SomeEnum +{ + use \FirstTrait; + use SecondTrait; + use ThirdTrait; +} diff --git a/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.inc b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.inc index c07b1b91eb..84ea24b2e8 100644 --- a/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.inc +++ b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.inc @@ -5,3 +5,18 @@ class Foo { } const APPLICATION_ENV = 'development'; + +// Issue 3526, PHP 8.1 final constants. +class SampleEnum +{ + final const FOO = 'SAMPLE'; + + public final const BAR = 'SAMPLE'; + + final private const BAZ = 'SAMPLE'; +} + +enum SomeEnum { + public const BAR = 'bar'; + const BAZ = 'baz'; +} diff --git a/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.php b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.php index 3917eb6821..b738706da2 100644 --- a/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.php +++ b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.php @@ -40,7 +40,11 @@ public function getErrorList() */ public function getWarningList() { - return [4 => 1]; + return [ + 4 => 1, + 12 => 1, + 21 => 1, + ]; }//end getWarningList() diff --git a/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc b/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc index e62489fe03..c8ad746a73 100644 --- a/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc +++ b/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc @@ -207,3 +207,15 @@ class Foo implements Bar */ use Baz; } + +enum SomeEnum1 +{ + use FirstTrait; +} + +enum SomeEnum2 +{ + + use FirstTrait; + +} diff --git a/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc.fixed b/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc.fixed index 0090b4e931..1c5d8185c0 100644 --- a/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc.fixed +++ b/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc.fixed @@ -201,3 +201,13 @@ class Foo implements Bar */ use Baz; } + +enum SomeEnum1 +{ + use FirstTrait; +} + +enum SomeEnum2 +{ + use FirstTrait; +} diff --git a/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.php b/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.php index c406805a9b..797a2912e7 100644 --- a/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.php +++ b/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.php @@ -47,6 +47,7 @@ public function getErrorList() 165 => 1, 170 => 1, 208 => 1, + 219 => 3, ]; }//end getErrorList() diff --git a/src/Standards/PSR2/Sniffs/Classes/PropertyDeclarationSniff.php b/src/Standards/PSR2/Sniffs/Classes/PropertyDeclarationSniff.php index 8a158d966d..efdbb43827 100644 --- a/src/Standards/PSR2/Sniffs/Classes/PropertyDeclarationSniff.php +++ b/src/Standards/PSR2/Sniffs/Classes/PropertyDeclarationSniff.php @@ -41,6 +41,7 @@ protected function processMemberVar(File $phpcsFile, $stackPtr) $find = Tokens::$scopeModifiers; $find[] = T_VARIABLE; $find[] = T_VAR; + $find[] = T_READONLY; $find[] = T_SEMICOLON; $find[] = T_OPEN_CURLY_BRACKET; diff --git a/src/Standards/PSR2/Sniffs/ControlStructures/SwitchDeclarationSniff.php b/src/Standards/PSR2/Sniffs/ControlStructures/SwitchDeclarationSniff.php index e36b134247..de81c530ee 100644 --- a/src/Standards/PSR2/Sniffs/ControlStructures/SwitchDeclarationSniff.php +++ b/src/Standards/PSR2/Sniffs/ControlStructures/SwitchDeclarationSniff.php @@ -247,7 +247,7 @@ private function findNestedTerminator($phpcsFile, $stackPtr, $end) { $tokens = $phpcsFile->getTokens(); - $lastToken = $phpcsFile->findPrevious(T_WHITESPACE, ($end - 1), $stackPtr, true); + $lastToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($end - 1), $stackPtr, true); if ($lastToken === false) { return false; } diff --git a/src/Standards/PSR2/Sniffs/Namespaces/UseDeclarationSniff.php b/src/Standards/PSR2/Sniffs/Namespaces/UseDeclarationSniff.php index 21a9dbe9a3..aba9caa717 100644 --- a/src/Standards/PSR2/Sniffs/Namespaces/UseDeclarationSniff.php +++ b/src/Standards/PSR2/Sniffs/Namespaces/UseDeclarationSniff.php @@ -285,7 +285,7 @@ private function shouldIgnoreUse($phpcsFile, $stackPtr) } // Ignore USE keywords for traits. - if ($phpcsFile->hasCondition($stackPtr, [T_CLASS, T_TRAIT]) === true) { + if ($phpcsFile->hasCondition($stackPtr, [T_CLASS, T_TRAIT, T_ENUM]) === true) { return true; } diff --git a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc index 031d2a8378..33bec44e70 100644 --- a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc +++ b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc @@ -71,3 +71,13 @@ class MyClass public int $var = null; public static int/*comment*/$var = null; } + +class ReadOnlyProp { + public readonly int $foo, + $bar, + $var = null; + + protected readonly ?string $foo; + + readonly array $foo; +} diff --git a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed index aca7c2fcc3..df83112af2 100644 --- a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed +++ b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed @@ -68,3 +68,13 @@ class MyClass public int $var = null; public static int /*comment*/$var = null; } + +class ReadOnlyProp { + public readonly int $foo, + $bar, + $var = null; + + protected readonly ?string $foo; + + readonly array $foo; +} diff --git a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php index 20da24d976..f1dd0194d2 100644 --- a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php +++ b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php @@ -46,6 +46,9 @@ public function getErrorList() 69 => 1, 71 => 1, 72 => 1, + 76 => 1, + 80 => 1, + 82 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc b/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc index c425a1f20d..2ca60a93e8 100644 --- a/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc +++ b/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc @@ -511,7 +511,7 @@ switch ( $a ) { case 1: if ($a) { try { - return true; + return true; // Comment. } catch (MyException $e) { throw new Exception($e->getMessage()); } @@ -584,3 +584,15 @@ switch ( $a ) { $other = $code; break; } + +// Issue 3550 - comment after terminating statement. +switch (rand()) { + case 1: + if (rand() === 1) { + break; + } else { + break; // comment + } + default: + break; +} diff --git a/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc.fixed b/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc.fixed index 47ae8d3de3..bbc8b7c48e 100644 --- a/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc.fixed +++ b/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc.fixed @@ -506,7 +506,7 @@ switch ( $a ) { case 1: if ($a) { try { - return true; + return true; // Comment. } catch (MyException $e) { throw new Exception($e->getMessage()); } @@ -579,3 +579,15 @@ switch ( $a ) { $other = $code; break; } + +// Issue 3550 - comment after terminating statement. +switch (rand()) { + case 1: + if (rand() === 1) { + break; + } else { + break; // comment + } + default: + break; +} diff --git a/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc b/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc index 21c03119dd..096b44bc8a 100644 --- a/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc +++ b/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc @@ -64,3 +64,12 @@ class Nested_Function { }; } } + +enum MyEnum +{ + function _myFunction() {} + function __myFunction() {} + public static function myFunction() {} + static public function myFunction() {} + public function _() {} +} diff --git a/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc.fixed b/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc.fixed index 5fb88613ed..eae8d28f5c 100644 --- a/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc.fixed +++ b/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc.fixed @@ -64,3 +64,12 @@ class Nested_Function { }; } } + +enum MyEnum +{ + function _myFunction() {} + function __myFunction() {} + public static function myFunction() {} + public static function myFunction() {} + public function _() {} +} diff --git a/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.php b/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.php index 2d508d4c62..a8dcdfea58 100644 --- a/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.php +++ b/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.php @@ -40,6 +40,7 @@ public function getErrorList() 54 => 1, 56 => 3, 63 => 2, + 73 => 1, ]; }//end getErrorList() @@ -61,6 +62,7 @@ public function getWarningList() 30 => 1, 46 => 1, 63 => 1, + 70 => 1, ]; }//end getWarningList() diff --git a/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.1.inc b/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.1.inc index c4e83da440..61befc9e73 100644 --- a/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.1.inc +++ b/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.1.inc @@ -30,9 +30,14 @@ trait HelloWorld use Hello, World; } +enum SomeEnum +{ + use Hello, World; +} + $x = $foo ? function ($foo) use /* comment */ ($bar): int { return 1; } : $bar; // Testcase must be on last line in the file. -use \ No newline at end of file +use diff --git a/src/Standards/Squiz/Sniffs/Arrays/ArrayDeclarationSniff.php b/src/Standards/Squiz/Sniffs/Arrays/ArrayDeclarationSniff.php index 17991bd79e..b45a4709de 100644 --- a/src/Standards/Squiz/Sniffs/Arrays/ArrayDeclarationSniff.php +++ b/src/Standards/Squiz/Sniffs/Arrays/ArrayDeclarationSniff.php @@ -374,6 +374,7 @@ public function processMultiLineArray($phpcsFile, $stackPtr, $arrayStart, $array || $tokens[$nextToken]['code'] === T_OPEN_SHORT_ARRAY || $tokens[$nextToken]['code'] === T_CLOSURE || $tokens[$nextToken]['code'] === T_FN + || $tokens[$nextToken]['code'] === T_MATCH ) { // Let subsequent calls of this test handle nested arrays. if ($tokens[$lastToken]['code'] !== T_DOUBLE_ARROW) { diff --git a/src/Standards/Squiz/Sniffs/Classes/ClassFileNameSniff.php b/src/Standards/Squiz/Sniffs/Classes/ClassFileNameSniff.php index afbec4fa7e..88a7e0daae 100644 --- a/src/Standards/Squiz/Sniffs/Classes/ClassFileNameSniff.php +++ b/src/Standards/Squiz/Sniffs/Classes/ClassFileNameSniff.php @@ -27,6 +27,7 @@ public function register() T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, ]; }//end register() diff --git a/src/Standards/Squiz/Sniffs/Classes/ValidClassNameSniff.php b/src/Standards/Squiz/Sniffs/Classes/ValidClassNameSniff.php index 10de719dca..ffddd2cde7 100644 --- a/src/Standards/Squiz/Sniffs/Classes/ValidClassNameSniff.php +++ b/src/Standards/Squiz/Sniffs/Classes/ValidClassNameSniff.php @@ -28,6 +28,7 @@ public function register() T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, ]; }//end register() @@ -58,7 +59,7 @@ public function process(File $phpcsFile, $stackPtr) // starting with the number will be multiple tokens. $opener = $tokens[$stackPtr]['scope_opener']; $nameStart = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), $opener, true); - $nameEnd = $phpcsFile->findNext(T_WHITESPACE, $nameStart, $opener); + $nameEnd = $phpcsFile->findNext([T_WHITESPACE, T_COLON], $nameStart, $opener); if ($nameEnd === false) { $name = $tokens[$nameStart]['content']; } else { diff --git a/src/Standards/Squiz/Sniffs/Commenting/BlockCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/BlockCommentSniff.php index 6a12bb7368..eb647f5fe7 100644 --- a/src/Standards/Squiz/Sniffs/Commenting/BlockCommentSniff.php +++ b/src/Standards/Squiz/Sniffs/Commenting/BlockCommentSniff.php @@ -84,6 +84,7 @@ public function process(File $phpcsFile, $stackPtr) T_CLASS => true, T_INTERFACE => true, T_TRAIT => true, + T_ENUM => true, T_FUNCTION => true, T_PUBLIC => true, T_PRIVATE => true, @@ -93,6 +94,7 @@ public function process(File $phpcsFile, $stackPtr) T_ABSTRACT => true, T_CONST => true, T_VAR => true, + T_READONLY => true, ]; if (isset($ignore[$tokens[$nextToken]['code']]) === true) { return; diff --git a/src/Standards/Squiz/Sniffs/Commenting/ClosingDeclarationCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/ClosingDeclarationCommentSniff.php index 6ab6280f60..cd509d0c89 100644 --- a/src/Standards/Squiz/Sniffs/Commenting/ClosingDeclarationCommentSniff.php +++ b/src/Standards/Squiz/Sniffs/Commenting/ClosingDeclarationCommentSniff.php @@ -27,6 +27,7 @@ public function register() T_FUNCTION, T_CLASS, T_INTERFACE, + T_ENUM, ]; }//end register() @@ -69,8 +70,10 @@ public function process(File $phpcsFile, $stackPtr) $comment = '//end '.$decName.'()'; } else if ($tokens[$stackPtr]['code'] === T_CLASS) { $comment = '//end class'; - } else { + } else if ($tokens[$stackPtr]['code'] === T_INTERFACE) { $comment = '//end interface'; + } else { + $comment = '//end enum'; }//end if if (isset($tokens[$stackPtr]['scope_closer']) === false) { diff --git a/src/Standards/Squiz/Sniffs/Commenting/DocCommentAlignmentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/DocCommentAlignmentSniff.php index 2624bc2246..1f49d2c0c9 100644 --- a/src/Standards/Squiz/Sniffs/Commenting/DocCommentAlignmentSniff.php +++ b/src/Standards/Squiz/Sniffs/Commenting/DocCommentAlignmentSniff.php @@ -64,6 +64,8 @@ public function process(File $phpcsFile, $stackPtr) $ignore = [ T_CLASS => true, T_INTERFACE => true, + T_ENUM => true, + T_ENUM_CASE => true, T_FUNCTION => true, T_PUBLIC => true, T_PRIVATE => true, @@ -74,6 +76,7 @@ public function process(File $phpcsFile, $stackPtr) T_OBJECT => true, T_PROTOTYPE => true, T_VAR => true, + T_READONLY => true, ]; if ($nextToken === false || isset($ignore[$tokens[$nextToken]['code']]) === false) { diff --git a/src/Standards/Squiz/Sniffs/Commenting/FileCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/FileCommentSniff.php index 2685854769..08aaae29a6 100644 --- a/src/Standards/Squiz/Sniffs/Commenting/FileCommentSniff.php +++ b/src/Standards/Squiz/Sniffs/Commenting/FileCommentSniff.php @@ -95,6 +95,7 @@ public function process(File $phpcsFile, $stackPtr) T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, T_FUNCTION, T_CLOSURE, T_PUBLIC, diff --git a/src/Standards/Squiz/Sniffs/Commenting/FunctionCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/FunctionCommentSniff.php index a23032e280..ba3e1710f0 100644 --- a/src/Standards/Squiz/Sniffs/Commenting/FunctionCommentSniff.php +++ b/src/Standards/Squiz/Sniffs/Commenting/FunctionCommentSniff.php @@ -245,6 +245,8 @@ protected function processThrows(File $phpcsFile, $stackPtr, $commentStart) } } + $comment = trim($comment); + // Starts with a capital letter and ends with a fullstop. $firstChar = $comment[0]; if (strtoupper($firstChar) !== $firstChar) { diff --git a/src/Standards/Squiz/Sniffs/Commenting/InlineCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/InlineCommentSniff.php index 7d7ee40e96..8ce950414a 100644 --- a/src/Standards/Squiz/Sniffs/Commenting/InlineCommentSniff.php +++ b/src/Standards/Squiz/Sniffs/Commenting/InlineCommentSniff.php @@ -74,6 +74,7 @@ public function process(File $phpcsFile, $stackPtr) T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, T_FUNCTION, T_CLOSURE, T_PUBLIC, diff --git a/src/Standards/Squiz/Sniffs/Commenting/VariableCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/VariableCommentSniff.php index a7731bb661..32e89789a7 100644 --- a/src/Standards/Squiz/Sniffs/Commenting/VariableCommentSniff.php +++ b/src/Standards/Squiz/Sniffs/Commenting/VariableCommentSniff.php @@ -35,6 +35,7 @@ public function processMemberVar(File $phpcsFile, $stackPtr) T_PROTECTED => T_PROTECTED, T_VAR => T_VAR, T_STATIC => T_STATIC, + T_READONLY => T_READONLY, T_WHITESPACE => T_WHITESPACE, T_STRING => T_STRING, T_NS_SEPARATOR => T_NS_SEPARATOR, diff --git a/src/Standards/Squiz/Sniffs/ControlStructures/ForLoopDeclarationSniff.php b/src/Standards/Squiz/Sniffs/ControlStructures/ForLoopDeclarationSniff.php index 6803f9abb4..370eab1890 100644 --- a/src/Standards/Squiz/Sniffs/ControlStructures/ForLoopDeclarationSniff.php +++ b/src/Standards/Squiz/Sniffs/ControlStructures/ForLoopDeclarationSniff.php @@ -222,7 +222,7 @@ public function process(File $phpcsFile, $stackPtr) $semicolon = $openingBracket; $targetNestinglevel = 0; if (isset($tokens[$openingBracket]['conditions']) === true) { - $targetNestinglevel += count($tokens[$openingBracket]['conditions']); + $targetNestinglevel = count($tokens[$openingBracket]['conditions']); } do { diff --git a/src/Standards/Squiz/Sniffs/Files/FileExtensionSniff.php b/src/Standards/Squiz/Sniffs/Files/FileExtensionSniff.php index bae882146d..6277b809e6 100644 --- a/src/Standards/Squiz/Sniffs/Files/FileExtensionSniff.php +++ b/src/Standards/Squiz/Sniffs/Files/FileExtensionSniff.php @@ -42,7 +42,7 @@ public function process(File $phpcsFile, $stackPtr) $tokens = $phpcsFile->getTokens(); $fileName = $phpcsFile->getFilename(); $extension = substr($fileName, strrpos($fileName, '.')); - $nextClass = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT], $stackPtr); + $nextClass = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], $stackPtr); if ($nextClass !== false) { $phpcsFile->recordMetric($stackPtr, 'File extension for class files', $extension); diff --git a/src/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php b/src/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php index 60b113d3a9..8becb74a4e 100644 --- a/src/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php +++ b/src/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php @@ -109,6 +109,7 @@ public function process(File $phpcsFile, $stackPtr) T_OPEN_SHORT_ARRAY => true, T_CASE => true, T_EXIT => true, + T_MATCH_ARROW => true, ]; if (isset($invalidTokens[$tokens[$previousToken]['code']]) === true) { @@ -141,6 +142,7 @@ public function process(File $phpcsFile, $stackPtr) T_THIS, T_SELF, T_STATIC, + T_PARENT, T_OBJECT_OPERATOR, T_NULLSAFE_OBJECT_OPERATOR, T_DOUBLE_COLON, diff --git a/src/Standards/Squiz/Sniffs/PHP/DisallowComparisonAssignmentSniff.php b/src/Standards/Squiz/Sniffs/PHP/DisallowComparisonAssignmentSniff.php index 7c7e2246f5..9eb2124233 100644 --- a/src/Standards/Squiz/Sniffs/PHP/DisallowComparisonAssignmentSniff.php +++ b/src/Standards/Squiz/Sniffs/PHP/DisallowComparisonAssignmentSniff.php @@ -52,17 +52,18 @@ public function process(File $phpcsFile, $stackPtr) } } - // Ignore values in array definitions. - $array = $phpcsFile->findNext( - T_ARRAY, + // Ignore values in array definitions or match structures. + $nextNonEmpty = $phpcsFile->findNext( + Tokens::$emptyTokens, ($stackPtr + 1), null, - false, - null, true ); - if ($array !== false) { + if ($nextNonEmpty !== false + && ($tokens[$nextNonEmpty]['code'] === T_ARRAY + || $tokens[$nextNonEmpty]['code'] === T_MATCH) + ) { return; } diff --git a/src/Standards/Squiz/Sniffs/Scope/MethodScopeSniff.php b/src/Standards/Squiz/Sniffs/Scope/MethodScopeSniff.php index 8c60208c6c..8a34a4f29f 100644 --- a/src/Standards/Squiz/Sniffs/Scope/MethodScopeSniff.php +++ b/src/Standards/Squiz/Sniffs/Scope/MethodScopeSniff.php @@ -54,17 +54,8 @@ protected function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScop return; } - $modifier = null; - for ($i = ($stackPtr - 1); $i > 0; $i--) { - if ($tokens[$i]['line'] < $tokens[$stackPtr]['line']) { - break; - } else if (isset(Tokens::$scopeModifiers[$tokens[$i]['code']]) === true) { - $modifier = $i; - break; - } - } - - if ($modifier === null) { + $properties = $phpcsFile->getMethodProperties($stackPtr); + if ($properties['scope_specified'] === false) { $error = 'Visibility must be declared on method "%s"'; $data = [$methodName]; $phpcsFile->addError($error, $stackPtr, 'Missing', $data); diff --git a/src/Standards/Squiz/Sniffs/Scope/StaticThisUsageSniff.php b/src/Standards/Squiz/Sniffs/Scope/StaticThisUsageSniff.php index 0bafbf4db4..f3b5495d90 100644 --- a/src/Standards/Squiz/Sniffs/Scope/StaticThisUsageSniff.php +++ b/src/Standards/Squiz/Sniffs/Scope/StaticThisUsageSniff.php @@ -22,7 +22,7 @@ class StaticThisUsageSniff extends AbstractScopeSniff */ public function __construct() { - parent::__construct([T_CLASS, T_TRAIT, T_ANON_CLASS], [T_FUNCTION]); + parent::__construct([T_CLASS, T_TRAIT, T_ENUM, T_ANON_CLASS], [T_FUNCTION]); }//end __construct() @@ -76,9 +76,9 @@ public function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScope) /** * Check for $this variable usage between $next and $end tokens. * - * @param File $phpcsFile The current file being scanned. - * @param int $next The position of the next token to check. - * @param int $end The position of the last token to check. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The current file being scanned. + * @param int $next The position of the next token to check. + * @param int $end The position of the last token to check. * * @return void */ diff --git a/src/Standards/Squiz/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php b/src/Standards/Squiz/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php index f38fd0e89b..808888f404 100644 --- a/src/Standards/Squiz/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php +++ b/src/Standards/Squiz/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php @@ -145,6 +145,7 @@ public function process(File $phpcsFile, $stackPtr) T_CLASS => true, T_INTERFACE => true, T_TRAIT => true, + T_ENUM => true, T_DOC_COMMENT_OPEN_TAG => true, ]; diff --git a/src/Standards/Squiz/Sniffs/WhiteSpace/OperatorSpacingSniff.php b/src/Standards/Squiz/Sniffs/WhiteSpace/OperatorSpacingSniff.php index 2627d10d98..f1e2fce692 100644 --- a/src/Standards/Squiz/Sniffs/WhiteSpace/OperatorSpacingSniff.php +++ b/src/Standards/Squiz/Sniffs/WhiteSpace/OperatorSpacingSniff.php @@ -77,12 +77,13 @@ public function register() // Returning/printing a negative value; eg. (return -1). $this->nonOperandTokens += [ - T_RETURN => T_RETURN, - T_ECHO => T_ECHO, - T_EXIT => T_EXIT, - T_PRINT => T_PRINT, - T_YIELD => T_YIELD, - T_FN_ARROW => T_FN_ARROW, + T_RETURN => T_RETURN, + T_ECHO => T_ECHO, + T_EXIT => T_EXIT, + T_PRINT => T_PRINT, + T_YIELD => T_YIELD, + T_FN_ARROW => T_FN_ARROW, + T_MATCH_ARROW => T_MATCH_ARROW, ]; // Trying to use a negative value; eg. myFunction($var, -2). diff --git a/src/Standards/Squiz/Sniffs/WhiteSpace/ScopeKeywordSpacingSniff.php b/src/Standards/Squiz/Sniffs/WhiteSpace/ScopeKeywordSpacingSniff.php index ad995dc45b..2d800f0520 100644 --- a/src/Standards/Squiz/Sniffs/WhiteSpace/ScopeKeywordSpacingSniff.php +++ b/src/Standards/Squiz/Sniffs/WhiteSpace/ScopeKeywordSpacingSniff.php @@ -26,6 +26,7 @@ public function register() { $register = Tokens::$scopeModifiers; $register[] = T_STATIC; + $register[] = T_READONLY; return $register; }//end register() diff --git a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc index 750aaebcc0..2774660c0c 100644 --- a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc +++ b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc @@ -475,6 +475,13 @@ yield array( static fn () : string => '', ); +$foo = [ + 'foo' => match ($anything) { + 'foo' => 'bar', + default => null, + }, + ]; + // Intentional syntax error. $a = array( 'a' => diff --git a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc.fixed b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc.fixed index 3ecc091da8..b452006488 100644 --- a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc.fixed +++ b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc.fixed @@ -511,6 +511,13 @@ yield array( static fn () : string => '', ); +$foo = [ + 'foo' => match ($anything) { + 'foo' => 'bar', + default => null, + }, + ]; + // Intentional syntax error. $a = array( 'a' => diff --git a/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.inc b/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.inc index a346a00f21..8b5a5aa708 100644 --- a/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.inc +++ b/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.inc @@ -5,7 +5,8 @@ class ClassFileNameUnitTest {} interface ClassFileNameUnitTest {} trait ClassFileNameUnitTest {} - +enum ClassFileNameUnitTest {} +enum ClassFileNameUnitTest: int {} // Invalid filename matching class name (case sensitive). class classFileNameUnitTest {} @@ -17,6 +18,9 @@ interface CLASSFILENAMEUNITTEST {} trait classFileNameUnitTest {} trait classfilenameunittest {} trait CLASSFILENAMEUNITTEST {} +enum classFileNameUnitTest {} +enum classfilenameunittest {} +enum CLASSFILENAMEUNITTEST {} // Invalid non-filename matching class names. @@ -32,6 +36,10 @@ trait CompletelyWrongClassName {} trait ClassFileNameUnitTestExtra {} trait ClassFileNameUnitTestInc {} trait ExtraClassFileNameUnitTest {} +enum CompletelyWrongClassName {} +enum ClassFileNameUnitTestExtra {} +enum ClassFileNameUnitTestInc {} +enum ExtraClassFileNameUnitTest {} -?> \ No newline at end of file +?> diff --git a/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.php b/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.php index b229a2fc3f..5964d2b1ec 100644 --- a/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.php +++ b/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.php @@ -26,7 +26,6 @@ class ClassFileNameUnitTest extends AbstractSniffUnitTest public function getErrorList() { return [ - 11 => 1, 12 => 1, 13 => 1, 14 => 1, @@ -35,10 +34,10 @@ public function getErrorList() 17 => 1, 18 => 1, 19 => 1, + 20 => 1, + 21 => 1, + 22 => 1, 23 => 1, - 24 => 1, - 25 => 1, - 26 => 1, 27 => 1, 28 => 1, 29 => 1, @@ -47,6 +46,14 @@ public function getErrorList() 32 => 1, 33 => 1, 34 => 1, + 35 => 1, + 36 => 1, + 37 => 1, + 38 => 1, + 39 => 1, + 40 => 1, + 41 => 1, + 42 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc b/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc index 511bbe4710..ea8cd89ec5 100644 --- a/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc +++ b/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc @@ -3,6 +3,7 @@ Abstract Class MyClass Extends MyClass {} Final Class MyClass Implements MyInterface {} Interface MyInterface {} Trait MyTrait {} +Enum MyEnum IMPLEMENTS Colorful {} class MyClass { diff --git a/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc.fixed index 859d0d2db2..f573905217 100644 --- a/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc.fixed @@ -3,6 +3,7 @@ abstract class MyClass extends MyClass {} final class MyClass implements MyInterface {} interface MyInterface {} trait MyTrait {} +enum MyEnum implements Colorful {} class MyClass { diff --git a/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.php b/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.php index f2fc20b438..8c4d10c79a 100644 --- a/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.php +++ b/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.php @@ -30,9 +30,10 @@ public function getErrorList() 3 => 3, 4 => 1, 5 => 1, - 9 => 1, + 6 => 2, 10 => 1, - 13 => 1, + 11 => 1, + 14 => 1, ]; return $errors; diff --git a/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.inc b/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.inc index aadbab5b8e..3fe39435b4 100644 --- a/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.inc +++ b/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.inc @@ -137,6 +137,50 @@ trait Base } } +// Valid enum name. +enum ValidCamelCaseClass: string {} + + +// Incorrect usage of camel case. +enum invalidCamelCaseClass {} +enum Invalid_Camel_Case_Class_With_Underscores {} + + +// All lowercase. +enum invalidlowercaseclass: INT {} +enum invalid_lowercase_class_with_underscores {} + + +// All uppercase. +enum VALIDUPPERCASECLASS: int {} +enum INVALID_UPPERCASE_CLASS_WITH_UNDERSCORES {} + + +// Mix camel case with uppercase. +enum ValidCamelCaseClassWithUPPERCASE : string {} + + +// Usage of numeric characters. +enum ValidCamelCaseClassWith1Number {} +enum ValidCamelCaseClassWith12345Numbers : string {} +enum ValidCamelCaseClassEndingWithNumber5 {} + +enum Testing{} + +enum Base +{ + public function __construct() + { + $this->anonymous = new class extends ArrayObject + { + public function __construct() + { + parent::__construct(['a' => 1, 'b' => 2]); + } + }; + } +} + if ( class_exists( Test :: class ) ) {} if ( class_exists( Test2 ::class ) ) {} diff --git a/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.php b/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.php index 70777c541b..b7de260b65 100644 --- a/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.php +++ b/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.php @@ -47,6 +47,11 @@ public function getErrorList() 108 => 1, 118 => 1, 120 => 1, + 145 => 1, + 146 => 1, + 150 => 1, + 151 => 1, + 156 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc index eed554b3f3..7cd04a2114 100644 --- a/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc +++ b/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc @@ -288,3 +288,22 @@ final class MyClass #[AttributeB] final public function test() {} } + +/** + * Comment should be ignored. + */ +abstract class MyClass +{ + /** + * Comment should be ignored. + */ + readonly public string $prop; +} + +/** + * Comment should be ignored + * + */ +enum MyEnum { + +} diff --git a/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc.fixed index 7471b582a9..2e97614e2c 100644 --- a/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc.fixed @@ -290,3 +290,22 @@ final class MyClass #[AttributeB] final public function test() {} } + +/** + * Comment should be ignored. + */ +abstract class MyClass +{ + /** + * Comment should be ignored. + */ + readonly public string $prop; +} + +/** + * Comment should be ignored + * + */ +enum MyEnum { + +} diff --git a/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.inc index 9c3255cf9b..1a57149b26 100644 --- a/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.inc +++ b/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.inc @@ -79,4 +79,8 @@ class TestClass } //end class -?> \ No newline at end of file +enum MissingClosingComment { +} + +enum HasClosingComment { +}//end enum diff --git a/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.php index cdc89ba670..6f5166ec6b 100644 --- a/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.php +++ b/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.php @@ -34,6 +34,7 @@ public function getErrorList() 63 => 1, 67 => 1, 79 => 1, + 83 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc index e7d880d682..e42cf8ab27 100644 --- a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc +++ b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc @@ -77,6 +77,26 @@ class MyClass2 var $x; } +abstract class MyClass +{ + /** +* Property comment + */ + readonly public string $prop; +} + +/** + * Some info about the enum here + * +*/ +enum Suits: string +{ + /** + * Some info about the case here. + */ + case HEARTS; +} + /** ************************************************************************ * Example with no errors. **************************************************************************/ diff --git a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc.fixed index 4d8cb39273..6182b539cd 100644 --- a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc.fixed @@ -77,6 +77,26 @@ class MyClass2 var $x; } +abstract class MyClass +{ + /** + * Property comment + */ + readonly public string $prop; +} + +/** + * Some info about the enum here + * + */ +enum Suits: string +{ + /** + * Some info about the case here. + */ + case HEARTS; +} + /** ************************************************************************ * Example with no errors. **************************************************************************/ diff --git a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.php index 974951ce42..acbf13e869 100644 --- a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.php +++ b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.php @@ -45,6 +45,12 @@ public function getErrorList($testFile='DocCommentAlignmentUnitTest.inc') if ($testFile === 'DocCommentAlignmentUnitTest.inc') { $errors[75] = 1; + $errors[83] = 1; + $errors[84] = 1; + $errors[90] = 1; + $errors[91] = 1; + $errors[95] = 1; + $errors[96] = 1; } return $errors; diff --git a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.inc.fixed b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.inc.fixed index 5cc4764205..3bcd945388 100644 --- a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.inc.fixed +++ b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.inc.fixed @@ -25,7 +25,7 @@ * @author * @copyright 1997 Squiz Pty Ltd (ABN 77 084 670 600) * @copyright 1994-1997 Squiz Pty Ltd (ABN 77 084 670 600) -* @copyright 2021 Squiz Pty Ltd (ABN 77 084 670 600) +* @copyright 2023 Squiz Pty Ltd (ABN 77 084 670 600) * @license http://www.php.net/license/3_0.txt * @summary An unknown summary tag * diff --git a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.js.fixed b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.js.fixed index b2b071f485..56a392d985 100644 --- a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.js.fixed +++ b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.js.fixed @@ -25,7 +25,7 @@ * @author * @copyright 1997 Squiz Pty Ltd (ABN 77 084 670 600) * @copyright 1994-1997 Squiz Pty Ltd (ABN 77 084 670 600) -* @copyright 2021 Squiz Pty Ltd (ABN 77 084 670 600) +* @copyright 2023 Squiz Pty Ltd (ABN 77 084 670 600) * @license http://www.php.net/license/3_0.txt * @summary An unknown summary tag * diff --git a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.9.inc b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.9.inc new file mode 100644 index 0000000000..f6c9d99682 --- /dev/null +++ b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.9.inc @@ -0,0 +1,12 @@ + + * @copyright 2010-2014 Squiz Pty Ltd (ABN 77 084 670 600) + */ + +enum Foo { +} diff --git a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.php index 080df3aafc..ee81369ab7 100644 --- a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.php +++ b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.php @@ -45,6 +45,7 @@ public function getErrorList($testFile='FileCommentUnitTest.inc') case 'FileCommentUnitTest.4.inc': case 'FileCommentUnitTest.6.inc': case 'FileCommentUnitTest.7.inc': + case 'FileCommentUnitTest.9.inc': return [1 => 1]; case 'FileCommentUnitTest.5.inc': diff --git a/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc index deaa966eae..4f59f60b71 100644 --- a/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc +++ b/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc @@ -1041,3 +1041,8 @@ public function ignored() { } // phpcs:set Squiz.Commenting.FunctionComment specialMethods[] __construct,__destruct + +/** + * @return void + * @throws Exception If any other error occurs. */ +function throwCommentOneLine() {} diff --git a/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc.fixed index b46df26b54..21a4103eb5 100644 --- a/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc.fixed @@ -1041,3 +1041,8 @@ public function ignored() { } // phpcs:set Squiz.Commenting.FunctionComment specialMethods[] __construct,__destruct + +/** + * @return void + * @throws Exception If any other error occurs. */ +function throwCommentOneLine() {} diff --git a/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc index 1b97af0b92..10a0b4b4c6 100644 --- a/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc +++ b/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc @@ -165,6 +165,14 @@ final class MyClass final public function test() {} } +/** + * Comment should be ignored. + * + */ +enum MyEnum { + +} + /* * N.B.: The below test line must be the last test in the file. * Testing that a new line after an inline comment when it's the last non-whitespace diff --git a/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc.fixed index 6b66624176..97ae01490d 100644 --- a/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc.fixed @@ -158,6 +158,14 @@ final class MyClass final public function test() {} } +/** + * Comment should be ignored. + * + */ +enum MyEnum { + +} + /* * N.B.: The below test line must be the last test in the file. * Testing that a new line after an inline comment when it's the last non-whitespace diff --git a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc index c2046b179f..36efc443bf 100644 --- a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc +++ b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc @@ -383,3 +383,22 @@ class HasAttributes #[ORM\Column(ORM\Column::T_INTEGER)] protected $height; } + +class ReadOnlyProps +{ + /** + * Short description of the member variable. + * + * @var array + */ + public readonly array $variableName = array(); + + /** + * Short description of the member variable. + * + * @var + */ + readonly protected ?int $variableName = 10; + + private readonly string $variable; +} diff --git a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc.fixed index 37ca1cebbf..5c652f5402 100644 --- a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc.fixed @@ -383,3 +383,22 @@ class HasAttributes #[ORM\Column(ORM\Column::T_INTEGER)] protected $height; } + +class ReadOnlyProps +{ + /** + * Short description of the member variable. + * + * @var array + */ + public readonly array $variableName = array(); + + /** + * Short description of the member variable. + * + * @var + */ + readonly protected ?int $variableName = 10; + + private readonly string $variable; +} diff --git a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.php index f3ee3c76dd..1af5e14845 100644 --- a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.php +++ b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.php @@ -58,6 +58,8 @@ public function getErrorList() 336 => 1, 361 => 1, 364 => 1, + 399 => 1, + 403 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/Files/FileExtensionUnitTest.5.inc b/src/Standards/Squiz/Tests/Files/FileExtensionUnitTest.5.inc new file mode 100644 index 0000000000..d777aff6b1 --- /dev/null +++ b/src/Standards/Squiz/Tests/Files/FileExtensionUnitTest.5.inc @@ -0,0 +1,3 @@ + diff --git a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc index 2a480bb926..8e62896387 100644 --- a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc +++ b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc @@ -193,3 +193,11 @@ $expr = match (true) { if ($pos === count(value: $this->tokens) - 1) { $file = '...'.substr(string: $file, offset: $padding * -1 + 3); } + +match ($a) { + 'a' => -1, + 'b', 'c', 'd' => -2, + default => -3, +}; + +$cntPages = ceil(count($items) / parent::ON_PAGE); diff --git a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc.fixed index 669b16b2bc..9fa0216cb6 100644 --- a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc.fixed @@ -193,3 +193,11 @@ $expr = match (true) { if ($pos === (count(value: $this->tokens) - 1)) { $file = '...'.substr(string: $file, offset: ($padding * -1 + 3)); } + +match ($a) { + 'a' => -1, + 'b', 'c', 'd' => -2, + default => -3, +}; + +$cntPages = ceil(count($items) / parent::ON_PAGE); diff --git a/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.inc b/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.inc index c7b8a2b6b2..87c3bdf2e7 100644 --- a/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.inc +++ b/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.inc @@ -146,3 +146,12 @@ echo $obj?->varName; echo $obj?->var_name; echo $obj?->varname; echo $obj?->_varName; + +enum SomeEnum +{ + public function foo($foo, $_foo, $foo_bar) { + $bar = 1; + $_bar = 2; + $bar_foo = 3; + } +} diff --git a/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.php b/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.php index aaa9d099a7..9acbe241a8 100644 --- a/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.php +++ b/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.php @@ -61,6 +61,8 @@ public function getErrorList() 138 => 1, 141 => 1, 146 => 1, + 152 => 1, + 155 => 1, ]; return $errors; diff --git a/src/Standards/Squiz/Tests/PHP/DisallowComparisonAssignmentUnitTest.inc b/src/Standards/Squiz/Tests/PHP/DisallowComparisonAssignmentUnitTest.inc index 022aca739e..a07047b196 100644 --- a/src/Standards/Squiz/Tests/PHP/DisallowComparisonAssignmentUnitTest.inc +++ b/src/Standards/Squiz/Tests/PHP/DisallowComparisonAssignmentUnitTest.inc @@ -71,3 +71,13 @@ $callback = function ($value) { return false; } }; + +function issue3616() { + $food = 'cake'; + + $returnValue = match (true) { + $food === 'apple' => 'This food is an apple', + $food === 'bar' => 'This food is a bar', + $food === 'cake' => 'This food is a cake', + }; +} diff --git a/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.2.inc b/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.2.inc index 407c4740b1..c9bf052f34 100644 --- a/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.2.inc +++ b/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.2.inc @@ -45,6 +45,12 @@ trait Something { } } +enum Something { + function getReturnType() { + echo 'no error'; + } +} + $a = new class { public function log($msg) { diff --git a/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.php b/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.php index f66eb3f7fe..f5f90c0b92 100644 --- a/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.php +++ b/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.php @@ -83,7 +83,7 @@ public function getWarningList($testFile='') 9 => 1, 10 => 2, 14 => 1, - 48 => 2, + 54 => 2, ]; break; default: diff --git a/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.inc b/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.inc index cec0355c43..3cc617d75b 100644 --- a/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.inc +++ b/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.inc @@ -40,3 +40,18 @@ class Nested { }; } } + +enum SomeEnum +{ + function func1() {} + public function func1() {} + private function func1() {} + protected function func1() {} +} + +class UnconventionalSpacing { + public + static + function + myFunction() {} +} diff --git a/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.php b/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.php index 7fdab23b77..4dc7177953 100644 --- a/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.php +++ b/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.php @@ -29,6 +29,7 @@ public function getErrorList() 6 => 1, 30 => 1, 39 => 1, + 46 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.inc b/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.inc index 38b443f2fd..dd6530e802 100644 --- a/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.inc +++ b/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.inc @@ -115,3 +115,13 @@ $b = new class() return $This; } } + +enum MyEnum { + private function notStatic () { + $this->doSomething(); + } + + public static function myFunc() { + $this->doSomething(); + } +} diff --git a/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.php b/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.php index 2935241b44..b1a5dd6afd 100644 --- a/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.php +++ b/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.php @@ -26,18 +26,19 @@ class StaticThisUsageUnitTest extends AbstractSniffUnitTest public function getErrorList() { return [ - 7 => 1, - 8 => 1, - 9 => 1, - 14 => 1, - 20 => 1, - 41 => 1, - 61 => 1, - 69 => 1, - 76 => 1, - 80 => 1, - 84 => 1, - 99 => 1, + 7 => 1, + 8 => 1, + 9 => 1, + 14 => 1, + 20 => 1, + 41 => 1, + 61 => 1, + 69 => 1, + 76 => 1, + 80 => 1, + 84 => 1, + 99 => 1, + 125 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc b/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc index 0371ce49b4..70abae434d 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc +++ b/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc @@ -261,3 +261,9 @@ $expr = match( $foo ){ }; echo $expr; + +if($true) { + + enum SomeEnum {} + +} diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc.fixed b/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc.fixed index ad4505c3e1..c64de25e16 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc.fixed @@ -253,3 +253,9 @@ $expr = match($foo){ }; echo $expr; + +if($true) { + + enum SomeEnum {} + +} diff --git a/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc b/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc index 038072dfe0..12b55176c4 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc +++ b/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc @@ -365,3 +365,10 @@ class HasAttributes #[ThirdAttribute] protected $propertyWithoutSpacing; } + +enum SomeEnum +{ + // Enum cannot have properties + + case ONE = 'one'; +} diff --git a/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc.fixed b/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc.fixed index 3cb2ca3a48..d683eaadfb 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc.fixed @@ -350,3 +350,10 @@ class HasAttributes #[ThirdAttribute] protected $propertyWithoutSpacing; } + +enum SomeEnum +{ + // Enum cannot have properties + + case ONE = 'one'; +} diff --git a/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc b/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc index f89cf08d5c..06462acc35 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc +++ b/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc @@ -477,5 +477,11 @@ $a = 'a '.-$b; $a = 'a '.- MY_CONSTANT; $a = 'a '.- $b; +match ($a) { + 'a' => -1, + 'b', 'c', 'd' => -2, + default => -3, +}; + /* Intentional parse error. This has to be the last test in the file. */ $a = 10 + diff --git a/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc.fixed b/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc.fixed index 138616e752..8b92a4875a 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc.fixed @@ -471,5 +471,11 @@ $a = 'a '.-$b; $a = 'a '.- MY_CONSTANT; $a = 'a '.- $b; +match ($a) { + 'a' => -1, + 'b', 'c', 'd' => -2, + default => -3, +}; + /* Intentional parse error. This has to be the last test in the file. */ $a = 10 + diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc b/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc index ecd80b5504..ecae5c6d53 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc +++ b/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc @@ -120,3 +120,15 @@ $match = match ($test) {
+ + + + 1, 116 => 1, 122 => 1, + 130 => 1, + 134 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc b/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc index 2213817116..12685dc97d 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc +++ b/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc @@ -126,3 +126,16 @@ class ConstructorPropertyPromotionTest { class ConstructorPropertyPromotionWithTypesTest { public function __construct(protected float|int $x, public?string &$y = 'test', private mixed $z) {} } + +// PHP 8.1 readonly keywords. +class ReadonlyTest { + public readonly int $publicReadonlyProperty; + + protected readonly int $protectedReadonlyProperty; + + readonly protected int $protectedReadonlyProperty; + + readonly private int $privateReadonlyProperty; + + public function __construct(readonly protected float|int $x, public readonly?string &$y = 'test') {} +} diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc.fixed b/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc.fixed index e642f0c7b3..d3b682ed75 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc.fixed @@ -120,3 +120,16 @@ class ConstructorPropertyPromotionTest { class ConstructorPropertyPromotionWithTypesTest { public function __construct(protected float|int $x, public ?string &$y = 'test', private mixed $z) {} } + +// PHP 8.1 readonly keywords. +class ReadonlyTest { + public readonly int $publicReadonlyProperty; + + protected readonly int $protectedReadonlyProperty; + + readonly protected int $protectedReadonlyProperty; + + readonly private int $privateReadonlyProperty; + + public function __construct(readonly protected float|int $x, public readonly ?string &$y = 'test') {} +} diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.php b/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.php index de4697c0cb..30b66215c9 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.php +++ b/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.php @@ -44,6 +44,9 @@ public function getErrorList() 119 => 1, 121 => 1, 127 => 2, + 134 => 2, + 138 => 2, + 140 => 3, ]; }//end getErrorList() diff --git a/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.inc b/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.inc index 8ed8509d2a..3325e1152d 100644 --- a/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.inc +++ b/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.inc @@ -120,3 +120,12 @@ $anonClass = new class() { echo $obj?->varName; echo $obj?->var_name; echo $obj?->varName; + +enum SomeEnum +{ + public function foo($foo, $_foo, $foo_bar) { + $bar = 1; + $_bar = 2; + $bar_foo = 3; + } +} diff --git a/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.php b/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.php index 916b334f1c..e57c735646 100644 --- a/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.php +++ b/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.php @@ -54,6 +54,8 @@ public function getErrorList() 113 => 1, 116 => 1, 121 => 1, + 126 => 1, + 129 => 1, ]; }//end getErrorList() diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php index 1ba26363af..3fc67b0c81 100644 --- a/src/Tokenizers/PHP.php +++ b/src/Tokenizers/PHP.php @@ -152,6 +152,13 @@ class PHP extends Tokenizer 'shared' => false, 'with' => [], ], + T_ENUM => [ + 'start' => [T_OPEN_CURLY_BRACKET => T_OPEN_CURLY_BRACKET], + 'end' => [T_CLOSE_CURLY_BRACKET => T_CLOSE_CURLY_BRACKET], + 'strict' => true, + 'shared' => false, + 'with' => [], + ], T_USE => [ 'start' => [T_OPEN_CURLY_BRACKET => T_OPEN_CURLY_BRACKET], 'end' => [T_CLOSE_CURLY_BRACKET => T_CLOSE_CURLY_BRACKET], @@ -339,6 +346,8 @@ class PHP extends Tokenizer T_ENDIF => 5, T_ENDSWITCH => 9, T_ENDWHILE => 8, + T_ENUM => 4, + T_ENUM_CASE => 4, T_EVAL => 4, T_EXTENDS => 7, T_FILE => 8, @@ -393,6 +402,7 @@ class PHP extends Tokenizer T_PRIVATE => 7, T_PUBLIC => 6, T_PROTECTED => 9, + T_READONLY => 8, T_REQUIRE => 7, T_REQUIRE_ONCE => 12, T_RETURN => 6, @@ -452,6 +462,7 @@ class PHP extends Tokenizer T_OPEN_SHORT_ARRAY => 1, T_CLOSE_SHORT_ARRAY => 1, T_TYPE_UNION => 1, + T_TYPE_INTERSECTION => 1, ]; /** @@ -466,6 +477,8 @@ class PHP extends Tokenizer T_CLASS => true, T_INTERFACE => true, T_TRAIT => true, + T_ENUM => true, + T_ENUM_CASE => true, T_EXTENDS => true, T_IMPLEMENTS => true, T_ATTRIBUTE => true, @@ -501,7 +514,7 @@ protected function tokenize($string) if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t*** START PHP TOKENIZING ***".PHP_EOL; $isWin = false; - if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + if (stripos(PHP_OS, 'WIN') === 0) { $isWin = true; } } @@ -588,6 +601,86 @@ protected function tokenize($string) echo PHP_EOL; } + /* + Tokenize context sensitive keyword as string when it should be string. + */ + + if ($tokenIsArray === true + && isset(Util\Tokens::$contextSensitiveKeywords[$token[0]]) === true + && (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true + || $finalTokens[$lastNotEmptyToken]['content'] === '&') + ) { + if (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true) { + $preserveKeyword = false; + + // `new class`, and `new static` should be preserved. + if ($finalTokens[$lastNotEmptyToken]['code'] === T_NEW + && ($token[0] === T_CLASS + || $token[0] === T_STATIC) + ) { + $preserveKeyword = true; + } + + // `new class extends` `new class implements` should be preserved + if (($token[0] === T_EXTENDS || $token[0] === T_IMPLEMENTS) + && $finalTokens[$lastNotEmptyToken]['code'] === T_CLASS + ) { + $preserveKeyword = true; + } + + // `namespace\` should be preserved + if ($token[0] === T_NAMESPACE) { + for ($i = ($stackPtr + 1); $i < $numTokens; $i++) { + if (is_array($tokens[$i]) === false) { + break; + } + + if (isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === true) { + continue; + } + + if ($tokens[$i][0] === T_NS_SEPARATOR) { + $preserveKeyword = true; + } + + break; + } + } + }//end if + + if ($finalTokens[$lastNotEmptyToken]['content'] === '&') { + $preserveKeyword = true; + + for ($i = ($lastNotEmptyToken - 1); $i >= 0; $i--) { + if (isset(Util\Tokens::$emptyTokens[$finalTokens[$i]['code']]) === true) { + continue; + } + + if ($finalTokens[$i]['code'] === T_FUNCTION) { + $preserveKeyword = false; + } + + break; + } + } + + if ($preserveKeyword === false) { + if (PHP_CODESNIFFER_VERBOSITY > 1) { + $type = Util\Tokens::tokenName($token[0]); + echo "\t\t* token $stackPtr changed from $type to T_STRING".PHP_EOL; + } + + $finalTokens[$newStackPtr] = [ + 'code' => T_STRING, + 'type' => 'T_STRING', + 'content' => $token[1], + ]; + + $newStackPtr++; + continue; + } + }//end if + /* Parse doc blocks into something that can be easily iterated over. */ @@ -646,6 +739,46 @@ protected function tokenize($string) }//end if }//end if + /* + For Explicit Octal Notation prior to PHP 8.1 we need to combine the + T_LNUMBER and T_STRING token values into a single token value, and + then ignore the T_STRING token. + */ + + if (PHP_VERSION_ID < 80100 + && $tokenIsArray === true && $token[1] === '0' + && (isset($tokens[($stackPtr + 1)]) === true + && is_array($tokens[($stackPtr + 1)]) === true + && $tokens[($stackPtr + 1)][0] === T_STRING + && strtolower($tokens[($stackPtr + 1)][1][0]) === 'o' + && $tokens[($stackPtr + 1)][1][1] !== '_') + && preg_match('`^(o[0-7]+(?:_[0-7]+)?)([0-9_]*)$`i', $tokens[($stackPtr + 1)][1], $matches) === 1 + ) { + $finalTokens[$newStackPtr] = [ + 'code' => T_LNUMBER, + 'type' => 'T_LNUMBER', + 'content' => $token[1] .= $matches[1], + ]; + $newStackPtr++; + + if (isset($matches[2]) === true && $matches[2] !== '') { + $type = 'T_LNUMBER'; + if ($matches[2][0] === '_') { + $type = 'T_STRING'; + } + + $finalTokens[$newStackPtr] = [ + 'code' => constant($type), + 'type' => $type, + 'content' => $matches[2], + ]; + $newStackPtr++; + } + + $stackPtr++; + continue; + }//end if + /* PHP 8.1 introduced two dedicated tokens for the & character. Retokenizing both of these to T_BITWISE_AND, which is the @@ -691,7 +824,8 @@ protected function tokenize($string) if ($subTokenIsArray === true) { $tokenContent .= $subToken[1]; - if ($subToken[1] === '{' + if (($subToken[1] === '{' + || $subToken[1] === '${') && $subToken[0] !== T_ENCAPSED_AND_WHITESPACE ) { $nestedVars[] = $i; @@ -870,6 +1004,104 @@ protected function tokenize($string) continue; }//end if + /* + Enum keyword for PHP < 8.1 + */ + + if ($tokenIsArray === true + && $token[0] === T_STRING + && strtolower($token[1]) === 'enum' + ) { + // Get the next non-empty token. + for ($i = ($stackPtr + 1); $i < $numTokens; $i++) { + if (is_array($tokens[$i]) === false + || isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === false + ) { + break; + } + } + + if (isset($tokens[$i]) === true + && is_array($tokens[$i]) === true + && $tokens[$i][0] === T_STRING + ) { + // Modify $tokens directly so we can use it later when converting enum "case". + $tokens[$stackPtr][0] = T_ENUM; + + $newToken = []; + $newToken['code'] = T_ENUM; + $newToken['type'] = 'T_ENUM'; + $newToken['content'] = $token[1]; + $finalTokens[$newStackPtr] = $newToken; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* token $stackPtr changed from T_STRING to T_ENUM".PHP_EOL; + } + + $newStackPtr++; + continue; + } + }//end if + + /* + Convert enum "case" to T_ENUM_CASE + */ + + if ($tokenIsArray === true + && $token[0] === T_CASE + && isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false + ) { + $isEnumCase = false; + $scope = 1; + + for ($i = ($stackPtr - 1); $i > 0; $i--) { + if ($tokens[$i] === '}') { + $scope++; + continue; + } + + if ($tokens[$i] === '{') { + $scope--; + continue; + } + + if (is_array($tokens[$i]) === false) { + continue; + } + + if ($scope !== 0) { + continue; + } + + if ($tokens[$i][0] === T_SWITCH) { + break; + } + + if ($tokens[$i][0] === T_ENUM || $tokens[$i][0] === T_ENUM_CASE) { + $isEnumCase = true; + break; + } + }//end for + + if ($isEnumCase === true) { + // Modify $tokens directly so we can use it as optimisation for other enum "case". + $tokens[$stackPtr][0] = T_ENUM_CASE; + + $newToken = []; + $newToken['code'] = T_ENUM_CASE; + $newToken['type'] = 'T_ENUM_CASE'; + $newToken['content'] = $token[1]; + $finalTokens[$newStackPtr] = $newToken; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* token $stackPtr changed from T_CASE to T_ENUM_CASE".PHP_EOL; + } + + $newStackPtr++; + continue; + } + }//end if + /* As of PHP 8.0 fully qualified, partially qualified and namespace relative identifier names are tokenized differently. @@ -991,7 +1223,7 @@ protected function tokenize($string) /* Tokenize the parameter labels for PHP 8.0 named parameters as a special T_PARAM_NAME - token and ensure that the colon after it is always T_COLON. + token and ensures that the colon after it is always T_COLON. */ if ($tokenIsArray === true @@ -1046,6 +1278,38 @@ protected function tokenize($string) }//end if }//end if + /* + "readonly" keyword for PHP < 8.1 + */ + + if (PHP_VERSION_ID < 80100 + && $tokenIsArray === true + && strtolower($token[1]) === 'readonly' + && isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false + ) { + // Get the next non-whitespace token. + for ($i = ($stackPtr + 1); $i < $numTokens; $i++) { + if (is_array($tokens[$i]) === false + || $tokens[$i][0] !== T_WHITESPACE + ) { + break; + } + } + + if (isset($tokens[$i]) === false + || $tokens[$i] !== '(' + ) { + $finalTokens[$newStackPtr] = [ + 'code' => T_READONLY, + 'type' => 'T_READONLY', + 'content' => $token[1], + ]; + $newStackPtr++; + + continue; + } + }//end if + /* Before PHP 7.0, the "yield from" was tokenized as T_YIELD, T_WHITESPACE and T_STRING. So look for @@ -1089,6 +1353,7 @@ protected function tokenize($string) && $tokenIsArray === true && $token[0] === T_STRING && strtolower($token[1]) === 'yield' + && isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false ) { if (isset($tokens[($stackPtr + 1)]) === true && isset($tokens[($stackPtr + 2)]) === true @@ -1330,6 +1595,7 @@ protected function tokenize($string) if ($newType === T_LNUMBER && ((stripos($newContent, '0x') === 0 && hexdec(str_replace('_', '', $newContent)) > PHP_INT_MAX) || (stripos($newContent, '0b') === 0 && bindec(str_replace('_', '', $newContent)) > PHP_INT_MAX) + || (stripos($newContent, '0o') === 0 && octdec(str_replace('_', '', $newContent)) > PHP_INT_MAX) || (stripos($newContent, '0x') !== 0 && stripos($newContent, 'e') !== false || strpos($newContent, '.') !== false) || (strpos($newContent, '0') === 0 && stripos($newContent, '0x') !== 0 @@ -1421,57 +1687,42 @@ protected function tokenize($string) if ($tokenIsArray === true && $token[0] === T_DEFAULT + && isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false ) { - if (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false) { - for ($x = ($stackPtr + 1); $x < $numTokens; $x++) { - if ($tokens[$x] === ',') { - // Skip over potential trailing comma (supported in PHP). - continue; - } - - if (is_array($tokens[$x]) === false - || isset(Util\Tokens::$emptyTokens[$tokens[$x][0]]) === false - ) { - // Non-empty, non-comma content. - break; - } + for ($x = ($stackPtr + 1); $x < $numTokens; $x++) { + if ($tokens[$x] === ',') { + // Skip over potential trailing comma (supported in PHP). + continue; } - if (isset($tokens[$x]) === true - && is_array($tokens[$x]) === true - && $tokens[$x][0] === T_DOUBLE_ARROW + if (is_array($tokens[$x]) === false + || isset(Util\Tokens::$emptyTokens[$tokens[$x][0]]) === false ) { - // Modify the original token stack for the double arrow so that - // future checks can disregard the double arrow token more easily. - // For match expression "case" statements, this is handled - // in PHP::processAdditional(). - $tokens[$x][0] = T_MATCH_ARROW; - if (PHP_CODESNIFFER_VERBOSITY > 1) { - echo "\t\t* token $x changed from T_DOUBLE_ARROW to T_MATCH_ARROW".PHP_EOL; - } - - $newToken = []; - $newToken['code'] = T_MATCH_DEFAULT; - $newToken['type'] = 'T_MATCH_DEFAULT'; - $newToken['content'] = $token[1]; + // Non-empty, non-comma content. + break; + } + } - if (PHP_CODESNIFFER_VERBOSITY > 1) { - echo "\t\t* token $stackPtr changed from T_DEFAULT to T_MATCH_DEFAULT".PHP_EOL; - } + if (isset($tokens[$x]) === true + && is_array($tokens[$x]) === true + && $tokens[$x][0] === T_DOUBLE_ARROW + ) { + // Modify the original token stack for the double arrow so that + // future checks can disregard the double arrow token more easily. + // For match expression "case" statements, this is handled + // in PHP::processAdditional(). + $tokens[$x][0] = T_MATCH_ARROW; + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* token $x changed from T_DOUBLE_ARROW to T_MATCH_ARROW".PHP_EOL; + } - $finalTokens[$newStackPtr] = $newToken; - $newStackPtr++; - continue; - }//end if - } else { - // Definitely not the "default" keyword. $newToken = []; - $newToken['code'] = T_STRING; - $newToken['type'] = 'T_STRING'; + $newToken['code'] = T_MATCH_DEFAULT; + $newToken['type'] = 'T_MATCH_DEFAULT'; $newToken['content'] = $token[1]; if (PHP_CODESNIFFER_VERBOSITY > 1) { - echo "\t\t* token $stackPtr changed from T_DEFAULT to T_STRING".PHP_EOL; + echo "\t\t* token $stackPtr changed from T_DEFAULT to T_MATCH_DEFAULT".PHP_EOL; } $finalTokens[$newStackPtr] = $newToken; @@ -1668,14 +1919,9 @@ protected function tokenize($string) } /* - The string-like token after a function keyword should always be - tokenized as T_STRING even if it appears to be a different token, - such as when writing code like: function default(): foo - so go forward and change the token type before it is processed. - - Note: this should not be done for `function Level\Name` within a - group use statement for the PHP 8 identifier name tokens as it - would interfere with the re-tokenization of those. + This is a special condition for T_ARRAY tokens used for + function return types. We want to keep the parenthesis map clean, + so let's tag these tokens as T_STRING. */ if ($tokenIsArray === true @@ -1683,37 +1929,6 @@ protected function tokenize($string) || $token[0] === T_FN) && $finalTokens[$lastNotEmptyToken]['code'] !== T_USE ) { - if ($token[0] === T_FUNCTION) { - for ($x = ($stackPtr + 1); $x < $numTokens; $x++) { - if (is_array($tokens[$x]) === false - || (isset(Util\Tokens::$emptyTokens[$tokens[$x][0]]) === false - && $tokens[$x][1] !== '&') - ) { - // Non-empty content. - break; - } - } - - if ($x < $numTokens - && is_array($tokens[$x]) === true - && $tokens[$x][0] !== T_STRING - && $tokens[$x][0] !== T_NAME_QUALIFIED - ) { - if (PHP_CODESNIFFER_VERBOSITY > 1) { - $oldType = Util\Tokens::tokenName($tokens[$x][0]); - echo "\t\t* token $x changed from $oldType to T_STRING".PHP_EOL; - } - - $tokens[$x][0] = T_STRING; - } - }//end if - - /* - This is a special condition for T_ARRAY tokens used for - function return types. We want to keep the parenthesis map clean, - so let's tag these tokens as T_STRING. - */ - // Go looking for the colon to start the return type hint. // Start by finding the closing parenthesis of the function. $parenthesisStack = []; @@ -1753,22 +1968,6 @@ function return types. We want to keep the parenthesis map clean, && is_array($tokens[$x]) === false && $tokens[$x] === ':' ) { - $allowed = [ - T_STRING => T_STRING, - T_NAME_FULLY_QUALIFIED => T_NAME_FULLY_QUALIFIED, - T_NAME_RELATIVE => T_NAME_RELATIVE, - T_NAME_QUALIFIED => T_NAME_QUALIFIED, - T_ARRAY => T_ARRAY, - T_CALLABLE => T_CALLABLE, - T_SELF => T_SELF, - T_PARENT => T_PARENT, - T_NAMESPACE => T_NAMESPACE, - T_STATIC => T_STATIC, - T_NS_SEPARATOR => T_NS_SEPARATOR, - ]; - - $allowed += Util\Tokens::$emptyTokens; - // Find the start of the return type. for ($x += 1; $x < $numTokens; $x++) { if (is_array($tokens[$x]) === true @@ -1843,6 +2042,7 @@ function return types. We want to keep the parenthesis map clean, T_OPEN_TAG => true, T_OPEN_CURLY_BRACKET => true, T_INLINE_THEN => true, + T_ENUM => true, ]; for ($x = ($newStackPtr - 1); $x > 0; $x--) { @@ -1853,6 +2053,7 @@ function return types. We want to keep the parenthesis map clean, if ($finalTokens[$x]['code'] !== T_CASE && $finalTokens[$x]['code'] !== T_INLINE_THEN + && $finalTokens[$x]['code'] !== T_ENUM ) { $finalTokens[$newStackPtr] = [ 'content' => $token[1].':', @@ -1901,31 +2102,39 @@ function return types. We want to keep the parenthesis map clean, $newStackPtr++; } } else { - if ($tokenIsArray === true && $token[0] === T_STRING) { - // Some T_STRING tokens should remain that way - // due to their context. - if (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true) { - // Special case for syntax like: return new self - // where self should not be a string. - if ($finalTokens[$lastNotEmptyToken]['code'] === T_NEW - && strtolower($token[1]) === 'self' - ) { - $finalTokens[$newStackPtr] = [ - 'content' => $token[1], - 'code' => T_SELF, - 'type' => 'T_SELF', - ]; - } else { - $finalTokens[$newStackPtr] = [ - 'content' => $token[1], - 'code' => T_STRING, - 'type' => 'T_STRING', - ]; + // Some T_STRING tokens should remain that way due to their context. + if ($tokenIsArray === true + && $token[0] === T_STRING + && isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true + ) { + // Special case for syntax like: return new self/new parent + // where self/parent should not be a string. + $tokenContentLower = strtolower($token[1]); + if ($finalTokens[$lastNotEmptyToken]['code'] === T_NEW + && ($tokenContentLower === 'self' || $tokenContentLower === 'parent') + ) { + $finalTokens[$newStackPtr] = [ + 'content' => $token[1], + ]; + if ($tokenContentLower === 'self') { + $finalTokens[$newStackPtr]['code'] = T_SELF; + $finalTokens[$newStackPtr]['type'] = 'T_SELF'; } - $newStackPtr++; - continue; - }//end if + if ($tokenContentLower === 'parent') { + $finalTokens[$newStackPtr]['code'] = T_PARENT; + $finalTokens[$newStackPtr]['type'] = 'T_PARENT'; + } + } else { + $finalTokens[$newStackPtr] = [ + 'content' => $token[1], + 'code' => T_STRING, + 'type' => 'T_STRING', + ]; + } + + $newStackPtr++; + continue; }//end if $newToken = null; @@ -2073,7 +2282,7 @@ function return types. We want to keep the parenthesis map clean, } } - if ($tokens[$i] !== '(' && $i !== $numTokens) { + if ($i !== $numTokens && $tokens[$i] !== '(') { $newToken['code'] = T_STRING; $newToken['type'] = 'T_STRING'; } @@ -2089,16 +2298,6 @@ function return types. We want to keep the parenthesis map clean, $newToken['type'] = 'T_FINALLY'; } - // This is a special case for the PHP 5.5 classname::class syntax - // where "class" should be T_STRING instead of T_CLASS. - if (($newToken['code'] === T_CLASS - || $newToken['code'] === T_FUNCTION) - && $finalTokens[$lastNotEmptyToken]['code'] === T_DOUBLE_COLON - ) { - $newToken['code'] = T_STRING; - $newToken['type'] = 'T_STRING'; - } - // This is a special case for PHP 5.6 use function and use const // where "function" and "const" should be T_STRING instead of T_FUNCTION // and T_CONST. @@ -2276,18 +2475,19 @@ protected function processAdditional() if (isset($this->tokens[$x]) === true && $this->tokens[$x]['code'] === T_OPEN_PARENTHESIS) { $ignore = Util\Tokens::$emptyTokens; $ignore += [ - T_ARRAY => T_ARRAY, - T_CALLABLE => T_CALLABLE, - T_COLON => T_COLON, - T_NAMESPACE => T_NAMESPACE, - T_NS_SEPARATOR => T_NS_SEPARATOR, - T_NULL => T_NULL, - T_NULLABLE => T_NULLABLE, - T_PARENT => T_PARENT, - T_SELF => T_SELF, - T_STATIC => T_STATIC, - T_STRING => T_STRING, - T_TYPE_UNION => T_TYPE_UNION, + T_ARRAY => T_ARRAY, + T_CALLABLE => T_CALLABLE, + T_COLON => T_COLON, + T_NAMESPACE => T_NAMESPACE, + T_NS_SEPARATOR => T_NS_SEPARATOR, + T_NULL => T_NULL, + T_NULLABLE => T_NULLABLE, + T_PARENT => T_PARENT, + T_SELF => T_SELF, + T_STATIC => T_STATIC, + T_STRING => T_STRING, + T_TYPE_UNION => T_TYPE_UNION, + T_TYPE_INTERSECTION => T_TYPE_INTERSECTION, ]; $closer = $this->tokens[$x]['parenthesis_closer']; @@ -2488,13 +2688,18 @@ protected function processAdditional() } if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === false) { - if (isset($allowed[$this->tokens[$x]['code']]) === false) { + // Allow for control structures without braces. + if (($this->tokens[$x]['code'] === T_CLOSE_PARENTHESIS + && isset($this->tokens[$x]['parenthesis_owner']) === true + && isset(Util\Tokens::$scopeOpeners[$this->tokens[$this->tokens[$x]['parenthesis_owner']]['code']]) === true) + || isset($allowed[$this->tokens[$x]['code']]) === false + ) { $isShortArray = true; } break; } - } + }//end for if ($isShortArray === true) { $this->tokens[$i]['code'] = T_OPEN_SHORT_ARRAY; @@ -2583,9 +2788,12 @@ protected function processAdditional() }//end if continue; - } else if ($this->tokens[$i]['code'] === T_BITWISE_OR) { + } else if ($this->tokens[$i]['code'] === T_BITWISE_OR + || $this->tokens[$i]['code'] === T_BITWISE_AND + ) { /* Convert "|" to T_TYPE_UNION or leave as T_BITWISE_OR. + Convert "&" to T_TYPE_INTERSECTION or leave as T_BITWISE_AND. */ $allowed = [ @@ -2650,12 +2858,12 @@ protected function processAdditional() }//end for if ($typeTokenCount === 0 || isset($suspectedType) === false) { - // Definitely not a union type, move on. + // Definitely not a union or intersection type, move on. continue; } $typeTokenCount = 0; - $unionOperators = [$i]; + $typeOperators = [$i]; $confirmed = false; for ($x = ($i - 1); $x >= 0; $x--) { @@ -2668,13 +2876,13 @@ protected function processAdditional() continue; } - // Union types can't use the nullable operator, but be tolerant to parse errors. + // Union and intersection types can't use the nullable operator, but be tolerant to parse errors. if ($typeTokenCount > 0 && $this->tokens[$x]['code'] === T_NULLABLE) { continue; } - if ($this->tokens[$x]['code'] === T_BITWISE_OR) { - $unionOperators[] = $x; + if ($this->tokens[$x]['code'] === T_BITWISE_OR || $this->tokens[$x]['code'] === T_BITWISE_AND) { + $typeOperators[] = $x; continue; } @@ -2685,7 +2893,8 @@ protected function processAdditional() if ($suspectedType === 'property or parameter' && (isset(Util\Tokens::$scopeModifiers[$this->tokens[$x]['code']]) === true - || $this->tokens[$x]['code'] === T_VAR) + || $this->tokens[$x]['code'] === T_VAR + || $this->tokens[$x]['code'] === T_READONLY) ) { // This will also confirm constructor property promotion parameters, but that's fine. $confirmed = true; @@ -2739,17 +2948,27 @@ protected function processAdditional() }//end if if ($confirmed === false) { - // Not a union type after all, move on. + // Not a union or intersection type after all, move on. continue; } - foreach ($unionOperators as $x) { - $this->tokens[$x]['code'] = T_TYPE_UNION; - $this->tokens[$x]['type'] = 'T_TYPE_UNION'; + foreach ($typeOperators as $x) { + if ($this->tokens[$x]['code'] === T_BITWISE_OR) { + $this->tokens[$x]['code'] = T_TYPE_UNION; + $this->tokens[$x]['type'] = 'T_TYPE_UNION'; - if (PHP_CODESNIFFER_VERBOSITY > 1) { - $line = $this->tokens[$x]['line']; - echo "\t* token $x on line $line changed from T_BITWISE_OR to T_TYPE_UNION".PHP_EOL; + if (PHP_CODESNIFFER_VERBOSITY > 1) { + $line = $this->tokens[$x]['line']; + echo "\t* token $x on line $line changed from T_BITWISE_OR to T_TYPE_UNION".PHP_EOL; + } + } else { + $this->tokens[$x]['code'] = T_TYPE_INTERSECTION; + $this->tokens[$x]['type'] = 'T_TYPE_INTERSECTION'; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + $line = $this->tokens[$x]['line']; + echo "\t* token $x on line $line changed from T_BITWISE_AND to T_TYPE_INTERSECTION".PHP_EOL; + } } } @@ -2793,25 +3012,6 @@ protected function processAdditional() $this->tokens[$i]['code'] = T_STRING; $this->tokens[$i]['type'] = 'T_STRING'; } - } else if ($this->tokens[$i]['code'] === T_CONST) { - // Context sensitive keywords support. - for ($x = ($i + 1); $i < $numTokens; $x++) { - if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === false) { - // Non-whitespace content. - break; - } - } - - if ($this->tokens[$x]['code'] !== T_STRING) { - if (PHP_CODESNIFFER_VERBOSITY > 1) { - $line = $this->tokens[$x]['line']; - $type = $this->tokens[$x]['type']; - echo "\t* token $x on line $line changed from $type to T_STRING".PHP_EOL; - } - - $this->tokens[$x]['code'] = T_STRING; - $this->tokens[$x]['type'] = 'T_STRING'; - } }//end if if (($this->tokens[$i]['code'] !== T_CASE diff --git a/src/Tokenizers/Tokenizer.php b/src/Tokenizers/Tokenizer.php index c79323ccd3..0e00bf7f22 100644 --- a/src/Tokenizers/Tokenizer.php +++ b/src/Tokenizers/Tokenizer.php @@ -194,6 +194,8 @@ private function createPositionMap() T_DOUBLE_QUOTED_STRING => true, T_HEREDOC => true, T_NOWDOC => true, + T_END_HEREDOC => true, + T_END_NOWDOC => true, T_INLINE_HTML => true, ]; diff --git a/src/Util/Common.php b/src/Util/Common.php index 204f44de14..ce7967cc33 100644 --- a/src/Util/Common.php +++ b/src/Util/Common.php @@ -250,7 +250,7 @@ public static function escapeshellcmd($cmd) { $cmd = escapeshellcmd($cmd); - if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + if (stripos(PHP_OS, 'WIN') === 0) { // Spaces are not escaped by escapeshellcmd on Windows, but need to be // for the command to be able to execute. $cmd = preg_replace('`(?= 48 && $ascii <= 57) { - // The character is a number, so it cant be a capital. + // The character is a number, so it can't be a capital. $isCaps = false; } else { if (strtoupper($string[$i]) === $string[$i]) { diff --git a/src/Util/Standards.php b/src/Util/Standards.php index 50f58f0294..65e5d6cb3c 100644 --- a/src/Util/Standards.php +++ b/src/Util/Standards.php @@ -180,7 +180,8 @@ public static function getInstalledStandards( // Check if the installed dir is actually a standard itself. $csFile = $standardsDir.'/ruleset.xml'; if (is_file($csFile) === true) { - $installedStandards[] = basename($standardsDir); + $basename = basename($standardsDir); + $installedStandards[$basename] = $basename; continue; } @@ -190,6 +191,7 @@ public static function getInstalledStandards( } $di = new \DirectoryIterator($standardsDir); + $standardsInDir = []; foreach ($di as $file) { if ($file->isDir() === true && $file->isDot() === false) { $filename = $file->getFilename(); @@ -202,10 +204,13 @@ public static function getInstalledStandards( // Valid coding standard dirs include a ruleset. $csFile = $file->getPathname().'/ruleset.xml'; if (is_file($csFile) === true) { - $installedStandards[] = $filename; + $standardsInDir[$filename] = $filename; } } } + + natsort($standardsInDir); + $installedStandards += $standardsInDir; }//end foreach return $installedStandards; diff --git a/src/Util/Tokens.php b/src/Util/Tokens.php index f501c7f0a4..bb1fb2ca99 100644 --- a/src/Util/Tokens.php +++ b/src/Util/Tokens.php @@ -80,6 +80,8 @@ define('T_MATCH_ARROW', 'PHPCS_T_MATCH_ARROW'); define('T_MATCH_DEFAULT', 'PHPCS_T_MATCH_DEFAULT'); define('T_ATTRIBUTE_END', 'PHPCS_T_ATTRIBUTE_END'); +define('T_ENUM_CASE', 'PHPCS_T_ENUM_CASE'); +define('T_TYPE_INTERSECTION', 'PHPCS_T_TYPE_INTERSECTION'); // Some PHP 5.5 tokens, replicated for lower versions. if (defined('T_FINALLY') === false) { @@ -163,6 +165,14 @@ define('T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG', 'PHPCS_T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG'); } +if (defined('T_READONLY') === false) { + define('T_READONLY', 'PHPCS_T_READONLY'); +} + +if (defined('T_ENUM') === false) { + define('T_ENUM', 'PHPCS_T_ENUM'); +} + // Tokens used for parsing doc blocks. define('T_DOC_COMMENT_STAR', 'PHPCS_T_DOC_COMMENT_STAR'); define('T_DOC_COMMENT_WHITESPACE', 'PHPCS_T_DOC_COMMENT_WHITESPACE'); @@ -190,6 +200,7 @@ final class Tokens T_CLASS => 1000, T_INTERFACE => 1000, T_TRAIT => 1000, + T_ENUM => 1000, T_NAMESPACE => 1000, T_FUNCTION => 100, T_CLOSURE => 100, @@ -415,6 +426,7 @@ final class Tokens T_ANON_CLASS => T_ANON_CLASS, T_INTERFACE => T_INTERFACE, T_TRAIT => T_TRAIT, + T_ENUM => T_ENUM, T_NAMESPACE => T_NAMESPACE, T_FUNCTION => T_FUNCTION, T_CLOSURE => T_CLOSURE, @@ -616,6 +628,7 @@ final class Tokens T_UNSET => T_UNSET, T_EMPTY => T_EMPTY, T_SELF => T_SELF, + T_PARENT => T_PARENT, T_STATIC => T_STATIC, ]; @@ -629,6 +642,7 @@ final class Tokens T_ANON_CLASS => T_ANON_CLASS, T_INTERFACE => T_INTERFACE, T_TRAIT => T_TRAIT, + T_ENUM => T_ENUM, ]; /** @@ -649,6 +663,86 @@ final class Tokens T_TRAIT_C => T_TRAIT_C, ]; + /** + * Tokens representing context sensitive keywords in PHP. + * + * @var array + * + * https://wiki.php.net/rfc/context_sensitive_lexer + */ + public static $contextSensitiveKeywords = [ + T_ABSTRACT => T_ABSTRACT, + T_ARRAY => T_ARRAY, + T_AS => T_AS, + T_BREAK => T_BREAK, + T_CALLABLE => T_CALLABLE, + T_CASE => T_CASE, + T_CATCH => T_CATCH, + T_CLASS => T_CLASS, + T_CLONE => T_CLONE, + T_CONST => T_CONST, + T_CONTINUE => T_CONTINUE, + T_DECLARE => T_DECLARE, + T_DEFAULT => T_DEFAULT, + T_DO => T_DO, + T_ECHO => T_ECHO, + T_ELSE => T_ELSE, + T_ELSEIF => T_ELSEIF, + T_EMPTY => T_EMPTY, + T_ENDDECLARE => T_ENDDECLARE, + T_ENDFOR => T_ENDFOR, + T_ENDFOREACH => T_ENDFOREACH, + T_ENDIF => T_ENDIF, + T_ENDSWITCH => T_ENDSWITCH, + T_ENDWHILE => T_ENDWHILE, + T_ENUM => T_ENUM, + T_EVAL => T_EVAL, + T_EXIT => T_EXIT, + T_EXTENDS => T_EXTENDS, + T_FINAL => T_FINAL, + T_FINALLY => T_FINALLY, + T_FN => T_FN, + T_FOR => T_FOR, + T_FOREACH => T_FOREACH, + T_FUNCTION => T_FUNCTION, + T_GLOBAL => T_GLOBAL, + T_GOTO => T_GOTO, + T_IF => T_IF, + T_IMPLEMENTS => T_IMPLEMENTS, + T_INCLUDE => T_INCLUDE, + T_INCLUDE_ONCE => T_INCLUDE_ONCE, + T_INSTANCEOF => T_INSTANCEOF, + T_INSTEADOF => T_INSTEADOF, + T_INTERFACE => T_INTERFACE, + T_ISSET => T_ISSET, + T_LIST => T_LIST, + T_LOGICAL_AND => T_LOGICAL_AND, + T_LOGICAL_OR => T_LOGICAL_OR, + T_LOGICAL_XOR => T_LOGICAL_XOR, + T_MATCH => T_MATCH, + T_NAMESPACE => T_NAMESPACE, + T_NEW => T_NEW, + T_PRINT => T_PRINT, + T_PRIVATE => T_PRIVATE, + T_PROTECTED => T_PROTECTED, + T_PUBLIC => T_PUBLIC, + T_READONLY => T_READONLY, + T_REQUIRE => T_REQUIRE, + T_REQUIRE_ONCE => T_REQUIRE_ONCE, + T_RETURN => T_RETURN, + T_STATIC => T_STATIC, + T_SWITCH => T_SWITCH, + T_THROW => T_THROW, + T_TRAIT => T_TRAIT, + T_TRY => T_TRY, + T_UNSET => T_UNSET, + T_USE => T_USE, + T_VAR => T_VAR, + T_WHILE => T_WHILE, + T_YIELD => T_YIELD, + T_YIELD_FROM => T_YIELD_FROM, + ]; + /** * Given a token, returns the name of the token. diff --git a/tests/Core/Autoloader/DetermineLoadedClassTest.php b/tests/Core/Autoloader/DetermineLoadedClassTest.php index 65542b670b..c0f38fa6f1 100644 --- a/tests/Core/Autoloader/DetermineLoadedClassTest.php +++ b/tests/Core/Autoloader/DetermineLoadedClassTest.php @@ -1,6 +1,6 @@ * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600) @@ -9,6 +9,7 @@ namespace PHP_CodeSniffer\Tests\Core\Autoloader; +use PHP_CodeSniffer\Autoload; use PHPUnit\Framework\TestCase; class DetermineLoadedClassTest extends TestCase @@ -51,7 +52,7 @@ public function testOrdered() 'traits' => [], ]; - $className = \PHP_CodeSniffer\Autoload::determineLoadedClass($classesBeforeLoad, $classesAfterLoad); + $className = Autoload::determineLoadedClass($classesBeforeLoad, $classesAfterLoad); $this->assertEquals('PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C', $className); }//end testOrdered() @@ -81,7 +82,7 @@ public function testUnordered() 'traits' => [], ]; - $className = \PHP_CodeSniffer\Autoload::determineLoadedClass($classesBeforeLoad, $classesAfterLoad); + $className = Autoload::determineLoadedClass($classesBeforeLoad, $classesAfterLoad); $this->assertEquals('PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C', $className); $classesAfterLoad = [ @@ -95,7 +96,7 @@ public function testUnordered() 'traits' => [], ]; - $className = \PHP_CodeSniffer\Autoload::determineLoadedClass($classesBeforeLoad, $classesAfterLoad); + $className = Autoload::determineLoadedClass($classesBeforeLoad, $classesAfterLoad); $this->assertEquals('PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C', $className); $classesAfterLoad = [ @@ -109,7 +110,7 @@ public function testUnordered() 'traits' => [], ]; - $className = \PHP_CodeSniffer\Autoload::determineLoadedClass($classesBeforeLoad, $classesAfterLoad); + $className = Autoload::determineLoadedClass($classesBeforeLoad, $classesAfterLoad); $this->assertEquals('PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C', $className); }//end testUnordered() diff --git a/tests/Core/File/FindEndOfStatementTest.inc b/tests/Core/File/FindEndOfStatementTest.inc index 6dfd0a2807..8351679891 100644 --- a/tests/Core/File/FindEndOfStatementTest.inc +++ b/tests/Core/File/FindEndOfStatementTest.inc @@ -39,6 +39,8 @@ $a = [ /* testStaticArrowFunction */ static fn ($a) => $a; +return 0; + /* testArrowFunctionReturnValue */ fn(): array => [a($a, $b)]; @@ -101,5 +103,3 @@ $result = match ($key) { 2 => 'one', }, }; - -return 0; diff --git a/tests/Core/File/FindImplementedInterfaceNamesTest.inc b/tests/Core/File/FindImplementedInterfaceNamesTest.inc index 3885b27e1d..44c0f64321 100644 --- a/tests/Core/File/FindImplementedInterfaceNamesTest.inc +++ b/tests/Core/File/FindImplementedInterfaceNamesTest.inc @@ -24,3 +24,12 @@ class testFECNClassThatExtendsAndImplements extends testFECNClass implements Int /* testClassThatImplementsAndExtends */ class testFECNClassThatImplementsAndExtends implements \InterfaceA, InterfaceB extends testFECNClass {} + +/* testBackedEnumWithoutImplements */ +enum Suit:string {} + +/* testEnumImplements */ +enum Suit implements Colorful {} + +/* testBackedEnumImplements */ +enum Suit: string implements Colorful, \Deck {} diff --git a/tests/Core/File/FindImplementedInterfaceNamesTest.php b/tests/Core/File/FindImplementedInterfaceNamesTest.php index 834e083202..2395032897 100644 --- a/tests/Core/File/FindImplementedInterfaceNamesTest.php +++ b/tests/Core/File/FindImplementedInterfaceNamesTest.php @@ -27,7 +27,7 @@ class FindImplementedInterfaceNamesTest extends AbstractMethodUnitTest */ public function testFindImplementedInterfaceNames($identifier, $expected) { - $OOToken = $this->getTargetToken($identifier, [T_CLASS, T_ANON_CLASS, T_INTERFACE]); + $OOToken = $this->getTargetToken($identifier, [T_CLASS, T_ANON_CLASS, T_INTERFACE, T_ENUM]); $result = self::$phpcsFile->findImplementedInterfaceNames($OOToken); $this->assertSame($expected, $result); @@ -81,6 +81,21 @@ public function dataImplementedInterface() 'InterfaceB', ], ], + [ + '/* testBackedEnumWithoutImplements */', + false, + ], + [ + '/* testEnumImplements */', + ['Colorful'], + ], + [ + '/* testBackedEnumImplements */', + [ + 'Colorful', + '\Deck', + ], + ], ]; }//end dataImplementedInterface() diff --git a/tests/Core/File/FindStartOfStatementTest.php b/tests/Core/File/FindStartOfStatementTest.php index 464021f6fc..ff859eca1c 100644 --- a/tests/Core/File/FindStartOfStatementTest.php +++ b/tests/Core/File/FindStartOfStatementTest.php @@ -469,7 +469,7 @@ public function testNestedMatch() /** - * Test nested match expressions. + * Test PHP open tag. * * @return void */ @@ -485,7 +485,7 @@ public function testOpenTag() /** - * Test nested match expressions. + * Test PHP short open echo tag. * * @return void */ diff --git a/tests/Core/File/GetMemberPropertiesTest.inc b/tests/Core/File/GetMemberPropertiesTest.inc index ea47e9fc57..f40b6021f4 100644 --- a/tests/Core/File/GetMemberPropertiesTest.inc +++ b/tests/Core/File/GetMemberPropertiesTest.inc @@ -239,6 +239,21 @@ $anon = class() { /* testPHP8DuplicateTypeInUnionWhitespaceAndComment */ // Intentional fatal error - duplicate types are not allowed in union types, but that's not the concern of the method. public int |string| /*comment*/ INT $duplicateTypeInUnion; + + /* testPHP81Readonly */ + public readonly int $readonly; + + /* testPHP81ReadonlyWithNullableType */ + public readonly ?array $array; + + /* testPHP81ReadonlyWithUnionType */ + public readonly string|int $readonlyWithUnionType; + + /* testPHP81ReadonlyWithUnionTypeWithNull */ + protected ReadOnly string|null $readonlyWithUnionTypeWithNull; + + /* testPHP81OnlyReadonlyWithUnionType */ + readonly string|int $onlyReadonly; }; $anon = class { @@ -256,3 +271,34 @@ $anon = class { ] private mixed $baz; }; + +enum Suit +{ + /* testEnumProperty */ + protected $anonymous; +} + +enum Direction implements ArrayAccess +{ + case Up; + case Down; + + /* testEnumMethodParamNotProperty */ + public function offsetGet($val) { ... } +} + +$anon = class() { + /* testPHP81IntersectionTypes */ + public Foo&Bar $intersectionType; + + /* testPHP81MoreIntersectionTypes */ + public Foo&Bar&Baz $moreIntersectionTypes; + + /* testPHP81IllegalIntersectionTypes */ + // Intentional fatal error - types which are not allowed for intersection type, but that's not the concern of the method. + public int&string $illegalIntersectionType; + + /* testPHP81NulltableIntersectionType */ + // Intentional fatal error - nullability is not allowed with intersection type, but that's not the concern of the method. + public ?Foo&Bar $nullableIntersectionType; +}; diff --git a/tests/Core/File/GetMemberPropertiesTest.php b/tests/Core/File/GetMemberPropertiesTest.php index 0af0437932..934d8e2890 100644 --- a/tests/Core/File/GetMemberPropertiesTest.php +++ b/tests/Core/File/GetMemberPropertiesTest.php @@ -51,6 +51,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -61,6 +62,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => false, + 'is_readonly' => false, 'type' => '?int', 'nullable_type' => true, ], @@ -71,6 +73,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -81,6 +84,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'string', 'nullable_type' => false, ], @@ -91,6 +95,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -101,6 +106,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'bool', 'nullable_type' => false, ], @@ -111,6 +117,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -121,6 +128,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'array', 'nullable_type' => false, ], @@ -131,6 +139,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -141,6 +150,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => true, + 'is_readonly' => false, 'type' => '?string', 'nullable_type' => true, ], @@ -151,6 +161,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -161,6 +172,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -171,6 +183,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -181,6 +194,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -191,6 +205,18 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, + 'type' => '', + 'nullable_type' => false, + ], + ], + [ + '/* testNoPrefix */', + [ + 'scope' => 'public', + 'scope_specified' => false, + 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -201,6 +227,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -211,6 +238,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -221,6 +249,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -231,6 +260,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'float', 'nullable_type' => false, ], @@ -241,6 +271,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'float', 'nullable_type' => false, ], @@ -251,6 +282,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '?string', 'nullable_type' => true, ], @@ -261,26 +293,18 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '?string', 'nullable_type' => true, ], ], - [ - '/* testNoPrefix */', - [ - 'scope' => 'public', - 'scope_specified' => false, - 'is_static' => false, - 'type' => '', - 'nullable_type' => false, - ], - ], [ '/* testGroupProtectedStatic 1 */', [ 'scope' => 'protected', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -291,6 +315,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -301,6 +326,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -311,6 +337,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -321,6 +348,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -331,6 +359,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -341,6 +370,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -351,6 +381,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -361,6 +392,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -371,6 +403,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -381,6 +414,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '?array', 'nullable_type' => true, ], @@ -391,6 +425,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '\MyNamespace\MyClass', 'nullable_type' => false, ], @@ -401,6 +436,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '?ClassName', 'nullable_type' => true, ], @@ -411,6 +447,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '?Folder\ClassName', 'nullable_type' => true, ], @@ -421,6 +458,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '\MyNamespace\MyClass\Foo', 'nullable_type' => false, ], @@ -431,6 +469,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -445,6 +484,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -455,6 +495,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -465,6 +506,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => 'miXed', 'nullable_type' => false, ], @@ -475,6 +517,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '?mixed', 'nullable_type' => true, ], @@ -485,6 +528,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '?namespace\Name', 'nullable_type' => true, ], @@ -495,6 +539,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'int|float', 'nullable_type' => false, ], @@ -505,6 +550,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'MyClassA|\Package\MyClassB', 'nullable_type' => false, ], @@ -515,6 +561,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'array|bool|int|float|NULL|object|string', 'nullable_type' => false, ], @@ -525,6 +572,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => false, + 'is_readonly' => false, 'type' => 'false|mixed|self|parent|iterable|Resource', 'nullable_type' => false, ], @@ -535,6 +583,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, // Missing static, but that's OK as not an allowed syntax. 'type' => 'callable||void', 'nullable_type' => false, @@ -546,6 +595,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '?int|float', 'nullable_type' => true, ], @@ -556,6 +606,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'null', 'nullable_type' => false, ], @@ -566,6 +617,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'false', 'nullable_type' => false, ], @@ -576,6 +628,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'bool|FALSE', 'nullable_type' => false, ], @@ -586,6 +639,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'object|ClassName', 'nullable_type' => false, ], @@ -596,6 +650,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'iterable|array|Traversable', 'nullable_type' => false, ], @@ -606,16 +661,73 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'int|string|INT', 'nullable_type' => false, ], ], + [ + '/* testPHP81Readonly */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => true, + 'type' => 'int', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP81ReadonlyWithNullableType */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => true, + 'type' => '?array', + 'nullable_type' => true, + ], + ], + [ + '/* testPHP81ReadonlyWithUnionType */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => true, + 'type' => 'string|int', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP81ReadonlyWithUnionTypeWithNull */', + [ + 'scope' => 'protected', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => true, + 'type' => 'string|null', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP81OnlyReadonlyWithUnionType */', + [ + 'scope' => 'public', + 'scope_specified' => false, + 'is_static' => false, + 'is_readonly' => true, + 'type' => 'string|int', + 'nullable_type' => false, + ], + ], [ '/* testPHP8PropertySingleAttribute */', [ 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'string', 'nullable_type' => false, ], @@ -626,6 +738,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '?int|float', 'nullable_type' => true, ], @@ -636,10 +749,55 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'mixed', 'nullable_type' => false, ], ], + [ + '/* testEnumProperty */', + [], + ], + [ + '/* testPHP81IntersectionTypes */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'type' => 'Foo&Bar', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP81MoreIntersectionTypes */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'type' => 'Foo&Bar&Baz', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP81IllegalIntersectionTypes */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'type' => 'int&string', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP81NulltableIntersectionType */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'type' => '?Foo&Bar', + 'nullable_type' => true, + ], + ], ]; }//end dataGetMemberProperties() @@ -681,6 +839,7 @@ public function dataNotClassProperty() ['/* testGlobalVariable */'], ['/* testNestedMethodParam 1 */'], ['/* testNestedMethodParam 2 */'], + ['/* testEnumMethodParamNotProperty */'], ]; }//end dataNotClassProperty() diff --git a/tests/Core/File/GetMethodParametersTest.inc b/tests/Core/File/GetMethodParametersTest.inc index ed1762e7d2..dc46549140 100644 --- a/tests/Core/File/GetMethodParametersTest.inc +++ b/tests/Core/File/GetMethodParametersTest.inc @@ -108,6 +108,11 @@ class ConstructorPropertyPromotionAndNormalParams { public function __construct(public int $promotedProp, ?int $normalArg) {} } +class ConstructorPropertyPromotionWithReadOnly { + /* testPHP81ConstructorPropertyPromotionWithReadOnly */ + public function __construct(public readonly ?int $promotedProp, readonly private string|bool &$promotedToo) {} +} + /* testPHP8ConstructorPropertyPromotionGlobalFunction */ // Intentional fatal error. Property promotion not allowed in non-constructor, but that's not the concern of this method. function globalFunction(private $x) {} @@ -140,3 +145,20 @@ class ParametersWithAttributes( &...$otherParam, ) {} } + +/* testPHP8IntersectionTypes */ +function intersectionTypes(Foo&Bar $obj1, Boo&Bar $obj2) {} + +/* testPHP81IntersectionTypesWithSpreadOperatorAndReference */ +function globalFunctionWithSpreadAndReference(Boo&Bar &$paramA, Foo&Bar ...$paramB) {} + +/* testPHP81MoreIntersectionTypes */ +function moreIntersectionTypes(MyClassA&\Package\MyClassB&\Package\MyClassC $var) {} + +/* testPHP81IllegalIntersectionTypes */ +// Intentional fatal error - simple types are not allowed with intersection types, but that's not the concern of the method. +$closure = function (string&int $numeric_string) {}; + +/* testPHP81NullableIntersectionTypes */ +// Intentional fatal error - nullability is not allowed with intersection types, but that's not the concern of the method. +$closure = function (?Foo&Bar $object) {}; diff --git a/tests/Core/File/GetMethodParametersTest.php b/tests/Core/File/GetMethodParametersTest.php index e7692a7153..ba4d754485 100644 --- a/tests/Core/File/GetMethodParametersTest.php +++ b/tests/Core/File/GetMethodParametersTest.php @@ -62,173 +62,173 @@ public function testArrayHint() /** - * Verify type hint parsing. + * Verify variable. * * @return void */ - public function testTypeHint() + public function testVariable() { $expected = []; $expected[0] = [ - 'name' => '$var1', - 'content' => 'foo $var1', - 'has_attributes' => false, - 'pass_by_reference' => false, - 'variable_length' => false, - 'type_hint' => 'foo', - 'nullable_type' => false, - ]; - - $expected[1] = [ - 'name' => '$var2', - 'content' => 'bar $var2', + 'name' => '$var', + 'content' => '$var', 'has_attributes' => false, 'pass_by_reference' => false, 'variable_length' => false, - 'type_hint' => 'bar', + 'type_hint' => '', 'nullable_type' => false, ]; $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); - }//end testTypeHint() + }//end testVariable() /** - * Verify self type hint parsing. + * Verify default value parsing with a single function param. * * @return void */ - public function testSelfTypeHint() + public function testSingleDefaultValue() { $expected = []; $expected[0] = [ - 'name' => '$var', - 'content' => 'self $var', + 'name' => '$var1', + 'content' => '$var1=self::CONSTANT', 'has_attributes' => false, + 'default' => 'self::CONSTANT', 'pass_by_reference' => false, 'variable_length' => false, - 'type_hint' => 'self', + 'type_hint' => '', 'nullable_type' => false, ]; $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); - }//end testSelfTypeHint() + }//end testSingleDefaultValue() /** - * Verify nullable type hint parsing. + * Verify default value parsing. * * @return void */ - public function testNullableTypeHint() + public function testDefaultValues() { $expected = []; $expected[0] = [ 'name' => '$var1', - 'content' => '?int $var1', + 'content' => '$var1=1', 'has_attributes' => false, + 'default' => '1', 'pass_by_reference' => false, 'variable_length' => false, - 'type_hint' => '?int', - 'nullable_type' => true, + 'type_hint' => '', + 'nullable_type' => false, ]; - $expected[1] = [ 'name' => '$var2', - 'content' => '?\bar $var2', + 'content' => "\$var2='value'", 'has_attributes' => false, + 'default' => "'value'", 'pass_by_reference' => false, 'variable_length' => false, - 'type_hint' => '?\bar', - 'nullable_type' => true, + 'type_hint' => '', + 'nullable_type' => false, ]; $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); - }//end testNullableTypeHint() + }//end testDefaultValues() /** - * Verify variable. + * Verify type hint parsing. * * @return void */ - public function testVariable() + public function testTypeHint() { $expected = []; $expected[0] = [ - 'name' => '$var', - 'content' => '$var', + 'name' => '$var1', + 'content' => 'foo $var1', 'has_attributes' => false, 'pass_by_reference' => false, 'variable_length' => false, - 'type_hint' => '', + 'type_hint' => 'foo', + 'nullable_type' => false, + ]; + + $expected[1] = [ + 'name' => '$var2', + 'content' => 'bar $var2', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => 'bar', 'nullable_type' => false, ]; $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); - }//end testVariable() + }//end testTypeHint() /** - * Verify default value parsing with a single function param. + * Verify self type hint parsing. * * @return void */ - public function testSingleDefaultValue() + public function testSelfTypeHint() { $expected = []; $expected[0] = [ - 'name' => '$var1', - 'content' => '$var1=self::CONSTANT', + 'name' => '$var', + 'content' => 'self $var', 'has_attributes' => false, - 'default' => 'self::CONSTANT', 'pass_by_reference' => false, 'variable_length' => false, - 'type_hint' => '', + 'type_hint' => 'self', 'nullable_type' => false, ]; $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); - }//end testSingleDefaultValue() + }//end testSelfTypeHint() /** - * Verify default value parsing. + * Verify nullable type hint parsing. * * @return void */ - public function testDefaultValues() + public function testNullableTypeHint() { $expected = []; $expected[0] = [ 'name' => '$var1', - 'content' => '$var1=1', + 'content' => '?int $var1', 'has_attributes' => false, - 'default' => '1', 'pass_by_reference' => false, 'variable_length' => false, - 'type_hint' => '', - 'nullable_type' => false, + 'type_hint' => '?int', + 'nullable_type' => true, ]; + $expected[1] = [ 'name' => '$var2', - 'content' => "\$var2='value'", + 'content' => '?\bar $var2', 'has_attributes' => false, - 'default' => "'value'", 'pass_by_reference' => false, 'variable_length' => false, - 'type_hint' => '', - 'nullable_type' => false, + 'type_hint' => '?\bar', + 'nullable_type' => true, ]; $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); - }//end testDefaultValues() + }//end testNullableTypeHint() /** @@ -696,6 +696,7 @@ public function testPHP8ConstructorPropertyPromotionNoTypes() 'type_hint' => '', 'nullable_type' => false, 'property_visibility' => 'public', + 'property_readonly' => false, ]; $expected[1] = [ 'name' => '$y', @@ -707,6 +708,7 @@ public function testPHP8ConstructorPropertyPromotionNoTypes() 'type_hint' => '', 'nullable_type' => false, 'property_visibility' => 'protected', + 'property_readonly' => false, ]; $expected[2] = [ 'name' => '$z', @@ -718,6 +720,7 @@ public function testPHP8ConstructorPropertyPromotionNoTypes() 'type_hint' => '', 'nullable_type' => false, 'property_visibility' => 'private', + 'property_readonly' => false, ]; $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); @@ -742,6 +745,7 @@ public function testPHP8ConstructorPropertyPromotionWithTypes() 'type_hint' => 'float|int', 'nullable_type' => false, 'property_visibility' => 'protected', + 'property_readonly' => false, ]; $expected[1] = [ 'name' => '$y', @@ -753,6 +757,7 @@ public function testPHP8ConstructorPropertyPromotionWithTypes() 'type_hint' => '?string', 'nullable_type' => true, 'property_visibility' => 'public', + 'property_readonly' => false, ]; $expected[2] = [ 'name' => '$z', @@ -763,6 +768,7 @@ public function testPHP8ConstructorPropertyPromotionWithTypes() 'type_hint' => 'mixed', 'nullable_type' => false, 'property_visibility' => 'private', + 'property_readonly' => false, ]; $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); @@ -787,6 +793,7 @@ public function testPHP8ConstructorPropertyPromotionAndNormalParam() 'type_hint' => 'int', 'nullable_type' => false, 'property_visibility' => 'public', + 'property_readonly' => false, ]; $expected[1] = [ 'name' => '$normalArg', @@ -803,6 +810,42 @@ public function testPHP8ConstructorPropertyPromotionAndNormalParam() }//end testPHP8ConstructorPropertyPromotionAndNormalParam() + /** + * Verify recognition of PHP8 constructor with property promotion using PHP 8.1 readonly keyword. + * + * @return void + */ + public function testPHP81ConstructorPropertyPromotionWithReadOnly() + { + $expected = []; + $expected[0] = [ + 'name' => '$promotedProp', + 'content' => 'public readonly ?int $promotedProp', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => '?int', + 'nullable_type' => true, + 'property_visibility' => 'public', + 'property_readonly' => true, + ]; + $expected[1] = [ + 'name' => '$promotedToo', + 'content' => 'readonly private string|bool &$promotedToo', + 'has_attributes' => false, + 'pass_by_reference' => true, + 'variable_length' => false, + 'type_hint' => 'string|bool', + 'nullable_type' => false, + 'property_visibility' => 'private', + 'property_readonly' => true, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81ConstructorPropertyPromotionWithReadOnly() + + /** * Verify behaviour when a non-constructor function uses PHP 8 property promotion syntax. * @@ -948,6 +991,139 @@ public function testParameterAttributesInFunctionDeclaration() }//end testParameterAttributesInFunctionDeclaration() + /** + * Verify recognition of PHP8.1 intersection type declaration. + * + * @return void + */ + public function testPHP8IntersectionTypes() + { + $expected = []; + $expected[0] = [ + 'name' => '$obj1', + 'content' => 'Foo&Bar $obj1', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => 'Foo&Bar', + 'nullable_type' => false, + ]; + $expected[1] = [ + 'name' => '$obj2', + 'content' => 'Boo&Bar $obj2', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => 'Boo&Bar', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8IntersectionTypes() + + + /** + * Verify recognition of PHP8 intersection type declaration when the variable has either a spread operator or a reference. + * + * @return void + */ + public function testPHP81IntersectionTypesWithSpreadOperatorAndReference() + { + $expected = []; + $expected[0] = [ + 'name' => '$paramA', + 'content' => 'Boo&Bar &$paramA', + 'has_attributes' => false, + 'pass_by_reference' => true, + 'variable_length' => false, + 'type_hint' => 'Boo&Bar', + 'nullable_type' => false, + ]; + $expected[1] = [ + 'name' => '$paramB', + 'content' => 'Foo&Bar ...$paramB', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => true, + 'type_hint' => 'Foo&Bar', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81IntersectionTypesWithSpreadOperatorAndReference() + + + /** + * Verify recognition of PHP8.1 intersection type declaration with more types. + * + * @return void + */ + public function testPHP81MoreIntersectionTypes() + { + $expected = []; + $expected[0] = [ + 'name' => '$var', + 'content' => 'MyClassA&\Package\MyClassB&\Package\MyClassC $var', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => 'MyClassA&\Package\MyClassB&\Package\MyClassC', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81MoreIntersectionTypes() + + + /** + * Verify recognition of PHP8.1 intersection type declaration with illegal simple types. + * + * @return void + */ + public function testPHP81IllegalIntersectionTypes() + { + $expected = []; + $expected[0] = [ + 'name' => '$numeric_string', + 'content' => 'string&int $numeric_string', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => 'string&int', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81IllegalIntersectionTypes() + + + /** + * Verify recognition of PHP8.1 intersection type declaration with (illegal) nullability. + * + * @return void + */ + public function testPHP81NullableIntersectionTypes() + { + $expected = []; + $expected[0] = [ + 'name' => '$object', + 'content' => '?Foo&Bar $object', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => '?Foo&Bar', + 'nullable_type' => true, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81NullableIntersectionTypes() + + /** * Test helper. * diff --git a/tests/Core/File/GetMethodPropertiesTest.inc b/tests/Core/File/GetMethodPropertiesTest.inc index 3e9682df6e..0c592369a5 100644 --- a/tests/Core/File/GetMethodPropertiesTest.inc +++ b/tests/Core/File/GetMethodPropertiesTest.inc @@ -33,7 +33,7 @@ class MyClass { /* testMessyNullableReturnMethod */ public function myFunction() /* comment - */ : + */ : /* comment */ ? //comment array {} @@ -126,3 +126,27 @@ interface FooBar { /* testPHP8DuplicateTypeInUnionWhitespaceAndComment */ // Intentional fatal error - duplicate types are not allowed in union types, but that's not the concern of the method. function duplicateTypeInUnion(): int | /*comment*/ string | INT {} + +/* testPHP81NeverType */ +function never(): never {} + +/* testPHP81NullableNeverType */ +// Intentional fatal error - nullability is not allowed with never, but that's not the concern of the method. +function nullableNever(): ?never {} + +/* testPHP8IntersectionTypes */ +function intersectionTypes(): Foo&Bar {} + +/* testPHP81MoreIntersectionTypes */ +function moreIntersectionTypes(): MyClassA&\Package\MyClassB&\Package\MyClassC {} + +/* testPHP81IntersectionArrowFunction */ +$fn = fn($var): MyClassA&\Package\MyClassB => $var; + +/* testPHP81IllegalIntersectionTypes */ +// Intentional fatal error - simple types are not allowed with intersection types, but that's not the concern of the method. +$closure = function (): string&int {}; + +/* testPHP81NullableIntersectionTypes */ +// Intentional fatal error - nullability is not allowed with intersection types, but that's not the concern of the method. +$closure = function (): ?Foo&Bar {}; diff --git a/tests/Core/File/GetMethodPropertiesTest.php b/tests/Core/File/GetMethodPropertiesTest.php index d912d6b1ce..66f4eea3ea 100644 --- a/tests/Core/File/GetMethodPropertiesTest.php +++ b/tests/Core/File/GetMethodPropertiesTest.php @@ -728,6 +728,167 @@ public function testPHP8DuplicateTypeInUnionWhitespaceAndComment() }//end testPHP8DuplicateTypeInUnionWhitespaceAndComment() + /** + * Verify recognition of PHP8.1 type "never". + * + * @return void + */ + public function testPHP81NeverType() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'never', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81NeverType() + + + /** + * Verify recognition of PHP8.1 type "never" with (illegal) nullability. + * + * @return void + */ + public function testPHP81NullableNeverType() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => '?never', + 'nullable_return_type' => true, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81NullableNeverType() + + + /** + * Verify recognition of PHP8.1 intersection type declaration. + * + * @return void + */ + public function testPHP8IntersectionTypes() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'Foo&Bar', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8IntersectionTypes() + + + /** + * Verify recognition of PHP8.1 intersection type declaration with more types. + * + * @return void + */ + public function testPHP81MoreIntersectionTypes() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'MyClassA&\Package\MyClassB&\Package\MyClassC', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81MoreIntersectionTypes() + + + /** + * Verify recognition of PHP8.1 intersection type declaration in arrow function. + * + * @return void + */ + public function testPHP81IntersectionArrowFunction() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'MyClassA&\Package\MyClassB', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81IntersectionArrowFunction() + + + /** + * Verify recognition of PHP8.1 intersection type declaration with illegal simple types. + * + * @return void + */ + public function testPHP81IllegalIntersectionTypes() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'string&int', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81IllegalIntersectionTypes() + + + /** + * Verify recognition of PHP8.1 intersection type declaration with (illegal) nullability. + * + * @return void + */ + public function testPHP81NullableIntersectionTypes() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => '?Foo&Bar', + 'nullable_return_type' => true, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81NullableIntersectionTypes() + + /** * Test helper. * diff --git a/tests/Core/File/IsReferenceTest.inc b/tests/Core/File/IsReferenceTest.inc index cd40ed3ba7..f71e2639d8 100644 --- a/tests/Core/File/IsReferenceTest.inc +++ b/tests/Core/File/IsReferenceTest.inc @@ -12,7 +12,7 @@ $a = [ $something & $somethingElse ]; $a = [ $first, $something & self::$somethingElse ]; /* testBitwiseAndD */ -$a = array $first, $something & $somethingElse ); +$a = array( $first, $something & $somethingElse ); /* testBitwiseAndE */ $a = [ 'a' => $first, 'b' => $something & $somethingElse ]; @@ -51,7 +51,7 @@ function myFunction(array &$one) {} $closure = function (\MyClass &$one) {}; /* testFunctionPassByReferenceG */ -$closure = function myFunc($param, &...$moreParams) {}; +$closure = function ($param, &...$moreParams) {}; /* testForeachValueByReference */ foreach( $array as $key => &$value ) {} diff --git a/tests/Core/Tokenizer/AttributesTest.php b/tests/Core/Tokenizer/AttributesTest.php index b10a1efb95..8ac826f2f6 100644 --- a/tests/Core/Tokenizer/AttributesTest.php +++ b/tests/Core/Tokenizer/AttributesTest.php @@ -10,7 +10,6 @@ namespace PHP_CodeSniffer\Tests\Core\Tokenizer; use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; -use PHP_CodeSniffer\Util\Tokens; class AttributesTest extends AbstractMethodUnitTest { @@ -289,7 +288,7 @@ public function testAttributeAndLineComment() /** - * Test that attribute followed by a line comment is parsed correctly. + * Test that attributes on function declaration parameters are parsed correctly. * * @param string $testMarker The comment which prefaces the target token in the test file. * @param int $position The token position (starting from T_FUNCTION) of T_ATTRIBUTE token. diff --git a/tests/Core/Tokenizer/BackfillEnumTest.inc b/tests/Core/Tokenizer/BackfillEnumTest.inc new file mode 100644 index 0000000000..82bfe24fd5 --- /dev/null +++ b/tests/Core/Tokenizer/BackfillEnumTest.inc @@ -0,0 +1,95 @@ +enum = 'foo'; + } +} + +/* testEnumUsedAsFunctionName */ +function enum() +{ +} + +/* testDeclarationContainingComment */ +enum /* comment */ Name +{ + case SOME_CASE; +} + +enum /* testEnumUsedAsEnumName */ Enum +{ +} + +/* testEnumUsedAsNamespaceName */ +namespace Enum; +/* testEnumUsedAsPartOfNamespaceName */ +namespace My\Enum\Collection; +/* testEnumUsedInObjectInitialization */ +$obj = new Enum; +/* testEnumAsFunctionCall */ +$var = enum($a, $b); +/* testEnumAsFunctionCallWithNamespace */ +var = namespace\enum(); +/* testClassConstantFetchWithEnumAsClassName */ +echo Enum::CONSTANT; +/* testClassConstantFetchWithEnumAsConstantName */ +echo ClassName::ENUM; + +/* testParseErrorMissingName */ +enum { + case SOME_CASE; +} + +/* testParseErrorLiveCoding */ +// This must be the last test in the file. +enum diff --git a/tests/Core/Tokenizer/BackfillEnumTest.php b/tests/Core/Tokenizer/BackfillEnumTest.php new file mode 100644 index 0000000000..33cff3a2c7 --- /dev/null +++ b/tests/Core/Tokenizer/BackfillEnumTest.php @@ -0,0 +1,229 @@ + + * @copyright 2021 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class BackfillEnumTest extends AbstractMethodUnitTest +{ + + + /** + * Test that the "enum" keyword is tokenized as such. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $testContent The token content to look for. + * @param int $openerOffset Offset to find expected scope opener. + * @param int $closerOffset Offset to find expected scope closer. + * + * @dataProvider dataEnums + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testEnums($testMarker, $testContent, $openerOffset, $closerOffset) + { + $tokens = self::$phpcsFile->getTokens(); + + $enum = $this->getTargetToken($testMarker, [T_ENUM, T_STRING], $testContent); + + $this->assertSame(T_ENUM, $tokens[$enum]['code']); + $this->assertSame('T_ENUM', $tokens[$enum]['type']); + + $this->assertArrayHasKey('scope_condition', $tokens[$enum]); + $this->assertArrayHasKey('scope_opener', $tokens[$enum]); + $this->assertArrayHasKey('scope_closer', $tokens[$enum]); + + $this->assertSame($enum, $tokens[$enum]['scope_condition']); + + $scopeOpener = $tokens[$enum]['scope_opener']; + $scopeCloser = $tokens[$enum]['scope_closer']; + + $expectedScopeOpener = ($enum + $openerOffset); + $expectedScopeCloser = ($enum + $closerOffset); + + $this->assertSame($expectedScopeOpener, $scopeOpener); + $this->assertArrayHasKey('scope_condition', $tokens[$scopeOpener]); + $this->assertArrayHasKey('scope_opener', $tokens[$scopeOpener]); + $this->assertArrayHasKey('scope_closer', $tokens[$scopeOpener]); + $this->assertSame($enum, $tokens[$scopeOpener]['scope_condition']); + $this->assertSame($scopeOpener, $tokens[$scopeOpener]['scope_opener']); + $this->assertSame($scopeCloser, $tokens[$scopeOpener]['scope_closer']); + + $this->assertSame($expectedScopeCloser, $scopeCloser); + $this->assertArrayHasKey('scope_condition', $tokens[$scopeCloser]); + $this->assertArrayHasKey('scope_opener', $tokens[$scopeCloser]); + $this->assertArrayHasKey('scope_closer', $tokens[$scopeCloser]); + $this->assertSame($enum, $tokens[$scopeCloser]['scope_condition']); + $this->assertSame($scopeOpener, $tokens[$scopeCloser]['scope_opener']); + $this->assertSame($scopeCloser, $tokens[$scopeCloser]['scope_closer']); + + }//end testEnums() + + + /** + * Data provider. + * + * @see testEnums() + * + * @return array + */ + public function dataEnums() + { + return [ + [ + '/* testPureEnum */', + 'enum', + 4, + 12, + ], + [ + '/* testBackedIntEnum */', + 'enum', + 7, + 29, + ], + [ + '/* testBackedStringEnum */', + 'enum', + 8, + 30, + ], + [ + '/* testComplexEnum */', + 'enum', + 11, + 72, + ], + [ + '/* testEnumWithEnumAsClassName */', + 'enum', + 6, + 7, + ], + [ + '/* testEnumIsCaseInsensitive */', + 'EnUm', + 4, + 5, + ], + [ + '/* testDeclarationContainingComment */', + 'enum', + 6, + 14, + ], + ]; + + }//end dataEnums() + + + /** + * Test that "enum" when not used as the keyword is still tokenized as `T_STRING`. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $testContent The token content to look for. + * + * @dataProvider dataNotEnums + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testNotEnums($testMarker, $testContent) + { + $tokens = self::$phpcsFile->getTokens(); + + $target = $this->getTargetToken($testMarker, [T_ENUM, T_STRING], $testContent); + $this->assertSame(T_STRING, $tokens[$target]['code']); + $this->assertSame('T_STRING', $tokens[$target]['type']); + + }//end testNotEnums() + + + /** + * Data provider. + * + * @see testNotEnums() + * + * @return array + */ + public function dataNotEnums() + { + return [ + [ + '/* testEnumAsClassNameAfterEnumKeyword */', + 'Enum', + ], + [ + '/* testEnumUsedAsClassName */', + 'Enum', + ], + [ + '/* testEnumUsedAsClassConstantName */', + 'ENUM', + ], + [ + '/* testEnumUsedAsMethodName */', + 'enum', + ], + [ + '/* testEnumUsedAsPropertyName */', + 'enum', + ], + [ + '/* testEnumUsedAsFunctionName */', + 'enum', + ], + [ + '/* testEnumUsedAsEnumName */', + 'Enum', + ], + [ + '/* testEnumUsedAsNamespaceName */', + 'Enum', + ], + [ + '/* testEnumUsedAsPartOfNamespaceName */', + 'Enum', + ], + [ + '/* testEnumUsedInObjectInitialization */', + 'Enum', + ], + [ + '/* testEnumAsFunctionCall */', + 'enum', + ], + [ + '/* testEnumAsFunctionCallWithNamespace */', + 'enum', + ], + [ + '/* testClassConstantFetchWithEnumAsClassName */', + 'Enum', + ], + [ + '/* testClassConstantFetchWithEnumAsConstantName */', + 'ENUM', + ], + [ + '/* testParseErrorMissingName */', + 'enum', + ], + [ + '/* testParseErrorLiveCoding */', + 'enum', + ], + ]; + + }//end dataNotEnums() + + +}//end class diff --git a/tests/Core/Tokenizer/BackfillExplicitOctalNotationTest.inc b/tests/Core/Tokenizer/BackfillExplicitOctalNotationTest.inc new file mode 100644 index 0000000000..154d489506 --- /dev/null +++ b/tests/Core/Tokenizer/BackfillExplicitOctalNotationTest.inc @@ -0,0 +1,28 @@ + + * @copyright 2019 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class BackfillExplicitOctalNotationTest extends AbstractMethodUnitTest +{ + + + /** + * Test that explicitly-defined octal values are tokenized as a single number and not as a number and a string. + * + * @param string $marker The comment which prefaces the target token in the test file. + * @param string $value The expected content of the token + * @param int|string $nextToken The expected next token. + * @param string $nextContent The expected content of the next token. + * + * @dataProvider dataExplicitOctalNotation + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testExplicitOctalNotation($marker, $value, $nextToken, $nextContent) + { + $tokens = self::$phpcsFile->getTokens(); + + $number = $this->getTargetToken($marker, [T_LNUMBER]); + + $this->assertSame($value, $tokens[$number]['content'], 'Content of integer token does not match expectation'); + + $this->assertSame($nextToken, $tokens[($number + 1)]['code'], 'Next token is not the expected type, but '.$tokens[($number + 1)]['type']); + $this->assertSame($nextContent, $tokens[($number + 1)]['content'], 'Next token did not have the expected contents'); + + }//end testExplicitOctalNotation() + + + /** + * Data provider. + * + * @see testExplicitOctalNotation() + * + * @return array + */ + public function dataExplicitOctalNotation() + { + return [ + [ + 'marker' => '/* testExplicitOctal */', + 'value' => '0o137041', + 'nextToken' => T_SEMICOLON, + 'nextContent' => ';', + ], + [ + 'marker' => '/* testExplicitOctalCapitalised */', + 'value' => '0O137041', + 'nextToken' => T_SEMICOLON, + 'nextContent' => ';', + ], + [ + 'marker' => '/* testExplicitOctalWithNumericSeparator */', + 'value' => '0o137_041', + 'nextToken' => T_SEMICOLON, + 'nextContent' => ';', + ], + [ + 'marker' => '/* testInvalid1 */', + 'value' => '0', + 'nextToken' => T_STRING, + 'nextContent' => 'o_137', + ], + [ + 'marker' => '/* testInvalid2 */', + 'value' => '0', + 'nextToken' => T_STRING, + 'nextContent' => 'O_41', + ], + [ + 'marker' => '/* testInvalid3 */', + 'value' => '0', + 'nextToken' => T_STRING, + 'nextContent' => 'o91', + ], + [ + 'marker' => '/* testInvalid4 */', + 'value' => '0O2', + 'nextToken' => T_LNUMBER, + 'nextContent' => '82', + ], + [ + 'marker' => '/* testInvalid5 */', + 'value' => '0o2', + 'nextToken' => T_LNUMBER, + 'nextContent' => '8_2', + ], + [ + 'marker' => '/* testInvalid6 */', + 'value' => '0o2', + 'nextToken' => T_STRING, + 'nextContent' => '_82', + ], + ]; + + }//end dataExplicitOctalNotation() + + +}//end class diff --git a/tests/Core/Tokenizer/BackfillNumericSeparatorTest.inc b/tests/Core/Tokenizer/BackfillNumericSeparatorTest.inc index eef53f593b..d8559705c3 100644 --- a/tests/Core/Tokenizer/BackfillNumericSeparatorTest.inc +++ b/tests/Core/Tokenizer/BackfillNumericSeparatorTest.inc @@ -34,6 +34,12 @@ $foo = 0b0101_1111; /* testOctal */ $foo = 0137_041; +/* testExplicitOctal */ +$foo = 0o137_041; + +/* testExplicitOctalCapitalised */ +$foo = 0O137_041; + /* testIntMoreThanMax */ $foo = 10_223_372_036_854_775_807; @@ -71,6 +77,12 @@ $testValue = 107_925_284 .88; /* testInvalid10 */ $testValue = 107_925_284/*comment*/.88; +/* testInvalid11 */ +$foo = 0o_137; + +/* testInvalid12 */ +$foo = 0O_41; + /* * Ensure that legitimate calculations are not touched by the backfill. */ diff --git a/tests/Core/Tokenizer/BackfillNumericSeparatorTest.php b/tests/Core/Tokenizer/BackfillNumericSeparatorTest.php index ee4275a214..645088fd6c 100644 --- a/tests/Core/Tokenizer/BackfillNumericSeparatorTest.php +++ b/tests/Core/Tokenizer/BackfillNumericSeparatorTest.php @@ -132,6 +132,20 @@ public function dataTestBackfill() 'value' => '0137_041', ], ], + [ + [ + 'marker' => '/* testExplicitOctal */', + 'type' => 'T_LNUMBER', + 'value' => '0o137_041', + ], + ], + [ + [ + 'marker' => '/* testExplicitOctalCapitalised */', + 'type' => 'T_LNUMBER', + 'value' => '0O137_041', + ], + ], [ [ 'marker' => '/* testIntMoreThanMax */', @@ -322,6 +336,32 @@ public function dataNoBackfill() ], ], ], + [ + '/* testInvalid11 */', + [ + [ + 'code' => T_LNUMBER, + 'content' => '0', + ], + [ + 'code' => T_STRING, + 'content' => 'o_137', + ], + ], + ], + [ + '/* testInvalid12 */', + [ + [ + 'code' => T_LNUMBER, + 'content' => '0', + ], + [ + 'code' => T_STRING, + 'content' => 'O_41', + ], + ], + ], [ '/* testCalc1 */', [ diff --git a/tests/Core/Tokenizer/BackfillReadonlyTest.inc b/tests/Core/Tokenizer/BackfillReadonlyTest.inc new file mode 100644 index 0000000000..eaf0b4b3cc --- /dev/null +++ b/tests/Core/Tokenizer/BackfillReadonlyTest.inc @@ -0,0 +1,100 @@ +readonly = 'foo'; + + /* testReadonlyPropertyInTernaryOperator */ + $isReadonly = $this->readonly ? true : false; + } +} + +/* testReadonlyUsedAsFunctionName */ +function readonly() +{ +} + +/* testReadonlyUsedAsNamespaceName */ +namespace Readonly; +/* testReadonlyUsedAsPartOfNamespaceName */ +namespace My\Readonly\Collection; +/* testReadonlyAsFunctionCall */ +$var = readonly($a, $b); +/* testClassConstantFetchWithReadonlyAsConstantName */ +echo ClassName::READONLY; + +/* testReadonlyUsedAsFunctionCallWithSpaceBetweenKeywordAndParens */ +$var = readonly /* comment */ (); + +/* testParseErrorLiveCoding */ +// This must be the last test in the file. +readonly diff --git a/tests/Core/Tokenizer/BackfillReadonlyTest.php b/tests/Core/Tokenizer/BackfillReadonlyTest.php new file mode 100644 index 0000000000..dddc18ebc2 --- /dev/null +++ b/tests/Core/Tokenizer/BackfillReadonlyTest.php @@ -0,0 +1,236 @@ + + * @copyright 2021 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class BackfillReadonlyTest extends AbstractMethodUnitTest +{ + + + /** + * Test that the "readonly" keyword is tokenized as such. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $testContent The token content to look for. + * + * @dataProvider dataReadonly + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testReadonly($testMarker, $testContent) + { + $tokens = self::$phpcsFile->getTokens(); + + $target = $this->getTargetToken($testMarker, [T_READONLY, T_STRING], $testContent); + $this->assertSame(T_READONLY, $tokens[$target]['code']); + $this->assertSame('T_READONLY', $tokens[$target]['type']); + + }//end testReadonly() + + + /** + * Data provider. + * + * @see testReadonly() + * + * @return array + */ + public function dataReadonly() + { + return [ + [ + '/* testReadonlyProperty */', + 'readonly', + ], + [ + '/* testVarReadonlyProperty */', + 'readonly', + ], + [ + '/* testReadonlyVarProperty */', + 'readonly', + ], + [ + '/* testStaticReadonlyProperty */', + 'readonly', + ], + [ + '/* testReadonlyStaticProperty */', + 'readonly', + ], + [ + '/* testConstReadonlyProperty */', + 'readonly', + ], + [ + '/* testReadonlyPropertyWithoutType */', + 'readonly', + ], + [ + '/* testPublicReadonlyProperty */', + 'readonly', + ], + [ + '/* testProtectedReadonlyProperty */', + 'readonly', + ], + [ + '/* testPrivateReadonlyProperty */', + 'readonly', + ], + [ + '/* testPublicReadonlyPropertyWithReadonlyFirst */', + 'readonly', + ], + [ + '/* testProtectedReadonlyPropertyWithReadonlyFirst */', + 'readonly', + ], + [ + '/* testPrivateReadonlyPropertyWithReadonlyFirst */', + 'readonly', + ], + [ + '/* testReadonlyWithCommentsInDeclaration */', + 'readonly', + ], + [ + '/* testReadonlyWithNullableProperty */', + 'readonly', + ], + [ + '/* testReadonlyNullablePropertyWithUnionTypeHintAndNullFirst */', + 'readonly', + ], + [ + '/* testReadonlyNullablePropertyWithUnionTypeHintAndNullLast */', + 'readonly', + ], + [ + '/* testReadonlyPropertyWithArrayTypeHint */', + 'readonly', + ], + [ + '/* testReadonlyPropertyWithSelfTypeHint */', + 'readonly', + ], + [ + '/* testReadonlyPropertyWithParentTypeHint */', + 'readonly', + ], + [ + '/* testReadonlyPropertyWithFullyQualifiedTypeHint */', + 'readonly', + ], + [ + '/* testReadonlyIsCaseInsensitive */', + 'ReAdOnLy', + ], + [ + '/* testReadonlyConstructorPropertyPromotion */', + 'readonly', + ], + [ + '/* testReadonlyConstructorPropertyPromotionWithReference */', + 'ReadOnly', + ], + [ + '/* testReadonlyPropertyInAnonymousClass */', + 'readonly', + ], + [ + '/* testReadonlyUsedAsFunctionCallWithSpaceBetweenKeywordAndParens */', + 'readonly', + ], + [ + '/* testParseErrorLiveCoding */', + 'readonly', + ], + ]; + + }//end dataReadonly() + + + /** + * Test that "readonly" when not used as the keyword is still tokenized as `T_STRING`. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $testContent The token content to look for. + * + * @dataProvider dataNotReadonly + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testNotReadonly($testMarker, $testContent) + { + $tokens = self::$phpcsFile->getTokens(); + + $target = $this->getTargetToken($testMarker, [T_READONLY, T_STRING], $testContent); + $this->assertSame(T_STRING, $tokens[$target]['code']); + $this->assertSame('T_STRING', $tokens[$target]['type']); + + }//end testNotReadonly() + + + /** + * Data provider. + * + * @see testNotReadonly() + * + * @return array + */ + public function dataNotReadonly() + { + return [ + [ + '/* testReadonlyUsedAsClassConstantName */', + 'READONLY', + ], + [ + '/* testReadonlyUsedAsMethodName */', + 'readonly', + ], + [ + '/* testReadonlyUsedAsPropertyName */', + 'readonly', + ], + [ + '/* testReadonlyPropertyInTernaryOperator */', + 'readonly', + ], + [ + '/* testReadonlyUsedAsFunctionName */', + 'readonly', + ], + [ + '/* testReadonlyUsedAsNamespaceName */', + 'Readonly', + ], + [ + '/* testReadonlyUsedAsPartOfNamespaceName */', + 'Readonly', + ], + [ + '/* testReadonlyAsFunctionCall */', + 'readonly', + ], + [ + '/* testClassConstantFetchWithReadonlyAsConstantName */', + 'READONLY', + ], + ]; + + }//end dataNotReadonly() + + +}//end class diff --git a/tests/Core/Tokenizer/BitwiseOrTest.inc b/tests/Core/Tokenizer/BitwiseOrTest.inc index 2af0ecae98..bfdbdc18c1 100644 --- a/tests/Core/Tokenizer/BitwiseOrTest.inc +++ b/tests/Core/Tokenizer/BitwiseOrTest.inc @@ -33,6 +33,21 @@ class TypeUnion /* testTypeUnionPropertyFullyQualified */ public \Fully\Qualified\NameA|\Fully\Qualified\NameB $fullyQual; + /* testTypeUnionPropertyWithReadOnlyKeyword */ + protected readonly string|null $array; + + /* testTypeUnionPropertyWithStaticAndReadOnlyKeywords */ + static readonly string|null $array; + + /* testTypeUnionPropertyWithVarAndReadOnlyKeywords */ + var readonly string|null $array; + + /* testTypeUnionPropertyWithReadOnlyKeywordFirst */ + readonly protected string|null $array; + + /* testTypeUnionPropertyWithOnlyReadOnlyKeyword */ + readonly string|null $nullableString; + public function paramTypes( /* testTypeUnionParam1 */ int|float $paramA /* testBitwiseOrParamDefaultValue */ = CONSTANT_A | CONSTANT_B, diff --git a/tests/Core/Tokenizer/BitwiseOrTest.php b/tests/Core/Tokenizer/BitwiseOrTest.php index d4a27bdc33..d56e7340aa 100644 --- a/tests/Core/Tokenizer/BitwiseOrTest.php +++ b/tests/Core/Tokenizer/BitwiseOrTest.php @@ -105,6 +105,11 @@ public function dataTypeUnion() ['/* testTypeUnionPropertyNamespaceRelative */'], ['/* testTypeUnionPropertyPartiallyQualified */'], ['/* testTypeUnionPropertyFullyQualified */'], + ['/* testTypeUnionPropertyWithReadOnlyKeyword */'], + ['/* testTypeUnionPropertyWithReadOnlyKeywordFirst */'], + ['/* testTypeUnionPropertyWithStaticAndReadOnlyKeywords */'], + ['/* testTypeUnionPropertyWithVarAndReadOnlyKeywords */'], + ['/* testTypeUnionPropertyWithOnlyReadOnlyKeyword */'], ['/* testTypeUnionParam1 */'], ['/* testTypeUnionParam2 */'], ['/* testTypeUnionParam3 */'], diff --git a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc new file mode 100644 index 0000000000..82fe564382 --- /dev/null +++ b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc @@ -0,0 +1,227 @@ + 'a', + 2 => 'b', + /* testMatchDefaultIsKeyword */ default => 'default', +}; + +$closure = /* testFnIsKeyword */ fn () => 'string'; + +function () { + /* testYieldIsKeyword */ yield $f; + /* testYieldFromIsKeyword */ yield from someFunction(); +}; + +/* testDeclareIsKeyword */ declare(ticks=1): +/* testEndDeclareIsKeyword */ enddeclare; + +if (true /* testAndIsKeyword */ and false /* testOrIsKeyword */ or null /* testXorIsKeyword */ xor 0) { + +} + +$anonymousClass = new /* testAnonymousClassIsKeyword */ class {}; +$anonymousClass2 = new class /* testExtendsInAnonymousClassIsKeyword */ extends SomeParent {}; +$anonymousClass3 = new class /* testImplementsInAnonymousClassIsKeyword */ implements SomeInterface {}; + +$instantiated1 = new /* testClassInstantiationParentIsKeyword */ parent(); +$instantiated2 = new /* testClassInstantiationSelfIsKeyword */ SELF; +$instantiated3 = new /* testClassInstantiationStaticIsKeyword */ static($param); + +class Foo extends /* testNamespaceInNameIsKeyword */ namespace\Exception +{} + +function /* testKeywordAfterFunctionShouldBeString */ eval() {} +function /* testKeywordAfterFunctionByRefShouldBeString */ &switch() {} diff --git a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php new file mode 100644 index 0000000000..a747e573c2 --- /dev/null +++ b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php @@ -0,0 +1,509 @@ + + * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; +use PHP_CodeSniffer\Util\Tokens; + +class ContextSensitiveKeywordsTest extends AbstractMethodUnitTest +{ + + + /** + * Test that context sensitive keyword is tokenized as string when it should be string. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @dataProvider dataStrings + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testStrings($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $token = $this->getTargetToken($testMarker, (Tokens::$contextSensitiveKeywords + [T_STRING])); + + $this->assertSame(T_STRING, $tokens[$token]['code']); + $this->assertSame('T_STRING', $tokens[$token]['type']); + + }//end testStrings() + + + /** + * Data provider. + * + * @see testStrings() + * + * @return array + */ + public function dataStrings() + { + return [ + ['/* testAbstract */'], + ['/* testArray */'], + ['/* testAs */'], + ['/* testBreak */'], + ['/* testCallable */'], + ['/* testCase */'], + ['/* testCatch */'], + ['/* testClass */'], + ['/* testClone */'], + ['/* testConst */'], + ['/* testContinue */'], + ['/* testDeclare */'], + ['/* testDefault */'], + ['/* testDo */'], + ['/* testEcho */'], + ['/* testElse */'], + ['/* testElseIf */'], + ['/* testEmpty */'], + ['/* testEndDeclare */'], + ['/* testEndFor */'], + ['/* testEndForeach */'], + ['/* testEndIf */'], + ['/* testEndSwitch */'], + ['/* testEndWhile */'], + ['/* testEnum */'], + ['/* testEval */'], + ['/* testExit */'], + ['/* testExtends */'], + ['/* testFinal */'], + ['/* testFinally */'], + ['/* testFn */'], + ['/* testFor */'], + ['/* testForeach */'], + ['/* testFunction */'], + ['/* testGlobal */'], + ['/* testGoto */'], + ['/* testIf */'], + ['/* testImplements */'], + ['/* testInclude */'], + ['/* testIncludeOnce */'], + ['/* testInstanceOf */'], + ['/* testInsteadOf */'], + ['/* testInterface */'], + ['/* testIsset */'], + ['/* testList */'], + ['/* testMatch */'], + ['/* testNamespace */'], + ['/* testNew */'], + ['/* testParent */'], + ['/* testPrint */'], + ['/* testPrivate */'], + ['/* testProtected */'], + ['/* testPublic */'], + ['/* testReadonly */'], + ['/* testRequire */'], + ['/* testRequireOnce */'], + ['/* testReturn */'], + ['/* testSelf */'], + ['/* testStatic */'], + ['/* testSwitch */'], + ['/* testThrows */'], + ['/* testTrait */'], + ['/* testTry */'], + ['/* testUnset */'], + ['/* testUse */'], + ['/* testVar */'], + ['/* testWhile */'], + ['/* testYield */'], + ['/* testYieldFrom */'], + ['/* testAnd */'], + ['/* testOr */'], + ['/* testXor */'], + + ['/* testKeywordAfterNamespaceShouldBeString */'], + ['/* testNamespaceNameIsString1 */'], + ['/* testNamespaceNameIsString2 */'], + ['/* testNamespaceNameIsString3 */'], + + ['/* testKeywordAfterFunctionShouldBeString */'], + ['/* testKeywordAfterFunctionByRefShouldBeString */'], + ]; + + }//end dataStrings() + + + /** + * Test that context sensitive keyword is tokenized as keyword when it should be keyword. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $expectedTokenType The expected token type. + * + * @dataProvider dataKeywords + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testKeywords($testMarker, $expectedTokenType) + { + $tokens = self::$phpcsFile->getTokens(); + + $token = $this->getTargetToken($testMarker, (Tokens::$contextSensitiveKeywords + [T_ANON_CLASS, T_MATCH_DEFAULT, T_PARENT, T_SELF, T_STRING])); + + $this->assertSame(constant($expectedTokenType), $tokens[$token]['code']); + $this->assertSame($expectedTokenType, $tokens[$token]['type']); + + }//end testKeywords() + + + /** + * Data provider. + * + * @see testKeywords() + * + * @return array + */ + public function dataKeywords() + { + return [ + [ + '/* testNamespaceIsKeyword */', + 'T_NAMESPACE', + ], + [ + '/* testAbstractIsKeyword */', + 'T_ABSTRACT', + ], + [ + '/* testClassIsKeyword */', + 'T_CLASS', + ], + [ + '/* testExtendsIsKeyword */', + 'T_EXTENDS', + ], + [ + '/* testImplementsIsKeyword */', + 'T_IMPLEMENTS', + ], + [ + '/* testUseIsKeyword */', + 'T_USE', + ], + [ + '/* testInsteadOfIsKeyword */', + 'T_INSTEADOF', + ], + [ + '/* testAsIsKeyword */', + 'T_AS', + ], + [ + '/* testConstIsKeyword */', + 'T_CONST', + ], + [ + '/* testPrivateIsKeyword */', + 'T_PRIVATE', + ], + [ + '/* testProtectedIsKeyword */', + 'T_PROTECTED', + ], + [ + '/* testPublicIsKeyword */', + 'T_PUBLIC', + ], + [ + '/* testVarIsKeyword */', + 'T_VAR', + ], + [ + '/* testStaticIsKeyword */', + 'T_STATIC', + ], + [ + '/* testReadonlyIsKeyword */', + 'T_READONLY', + ], + [ + '/* testFinalIsKeyword */', + 'T_FINAL', + ], + [ + '/* testFunctionIsKeyword */', + 'T_FUNCTION', + ], + [ + '/* testCallableIsKeyword */', + 'T_CALLABLE', + ], + [ + '/* testSelfIsKeyword */', + 'T_SELF', + ], + [ + '/* testParentIsKeyword */', + 'T_PARENT', + ], + [ + '/* testReturnIsKeyword */', + 'T_RETURN', + ], + + [ + '/* testInterfaceIsKeyword */', + 'T_INTERFACE', + ], + [ + '/* testTraitIsKeyword */', + 'T_TRAIT', + ], + [ + '/* testEnumIsKeyword */', + 'T_ENUM', + ], + + [ + '/* testNewIsKeyword */', + 'T_NEW', + ], + [ + '/* testInstanceOfIsKeyword */', + 'T_INSTANCEOF', + ], + [ + '/* testCloneIsKeyword */', + 'T_CLONE', + ], + + [ + '/* testIfIsKeyword */', + 'T_IF', + ], + [ + '/* testEmptyIsKeyword */', + 'T_EMPTY', + ], + [ + '/* testElseIfIsKeyword */', + 'T_ELSEIF', + ], + [ + '/* testElseIsKeyword */', + 'T_ELSE', + ], + [ + '/* testEndIfIsKeyword */', + 'T_ENDIF', + ], + + [ + '/* testForIsKeyword */', + 'T_FOR', + ], + [ + '/* testEndForIsKeyword */', + 'T_ENDFOR', + ], + + [ + '/* testForeachIsKeyword */', + 'T_FOREACH', + ], + [ + '/* testEndForeachIsKeyword */', + 'T_ENDFOREACH', + ], + + [ + '/* testSwitchIsKeyword */', + 'T_SWITCH', + ], + [ + '/* testCaseIsKeyword */', + 'T_CASE', + ], + [ + '/* testDefaultIsKeyword */', + 'T_DEFAULT', + ], + [ + '/* testEndSwitchIsKeyword */', + 'T_ENDSWITCH', + ], + [ + '/* testBreakIsKeyword */', + 'T_BREAK', + ], + [ + '/* testContinueIsKeyword */', + 'T_CONTINUE', + ], + + [ + '/* testDoIsKeyword */', + 'T_DO', + ], + [ + '/* testWhileIsKeyword */', + 'T_WHILE', + ], + [ + '/* testEndWhileIsKeyword */', + 'T_ENDWHILE', + ], + + [ + '/* testTryIsKeyword */', + 'T_TRY', + ], + [ + '/* testThrowIsKeyword */', + 'T_THROW', + ], + [ + '/* testCatchIsKeyword */', + 'T_CATCH', + ], + [ + '/* testFinallyIsKeyword */', + 'T_FINALLY', + ], + + [ + '/* testGlobalIsKeyword */', + 'T_GLOBAL', + ], + [ + '/* testEchoIsKeyword */', + 'T_ECHO', + ], + [ + '/* testPrintIsKeyword */', + 'T_PRINT', + ], + [ + '/* testDieIsKeyword */', + 'T_EXIT', + ], + [ + '/* testEvalIsKeyword */', + 'T_EVAL', + ], + [ + '/* testExitIsKeyword */', + 'T_EXIT', + ], + [ + '/* testIssetIsKeyword */', + 'T_ISSET', + ], + [ + '/* testUnsetIsKeyword */', + 'T_UNSET', + ], + + [ + '/* testIncludeIsKeyword */', + 'T_INCLUDE', + ], + [ + '/* testIncludeOnceIsKeyword */', + 'T_INCLUDE_ONCE', + ], + [ + '/* testRequireIsKeyword */', + 'T_REQUIRE', + ], + [ + '/* testRequireOnceIsKeyword */', + 'T_REQUIRE_ONCE', + ], + + [ + '/* testListIsKeyword */', + 'T_LIST', + ], + [ + '/* testGotoIsKeyword */', + 'T_GOTO', + ], + [ + '/* testMatchIsKeyword */', + 'T_MATCH', + ], + [ + '/* testMatchDefaultIsKeyword */', + 'T_MATCH_DEFAULT', + ], + [ + '/* testFnIsKeyword */', + 'T_FN', + ], + + [ + '/* testYieldIsKeyword */', + 'T_YIELD', + ], + [ + '/* testYieldFromIsKeyword */', + 'T_YIELD_FROM', + ], + + [ + '/* testDeclareIsKeyword */', + 'T_DECLARE', + ], + [ + '/* testEndDeclareIsKeyword */', + 'T_ENDDECLARE', + ], + + [ + '/* testAndIsKeyword */', + 'T_LOGICAL_AND', + ], + [ + '/* testOrIsKeyword */', + 'T_LOGICAL_OR', + ], + [ + '/* testXorIsKeyword */', + 'T_LOGICAL_XOR', + ], + + [ + '/* testAnonymousClassIsKeyword */', + 'T_ANON_CLASS', + ], + [ + '/* testExtendsInAnonymousClassIsKeyword */', + 'T_EXTENDS', + ], + [ + '/* testImplementsInAnonymousClassIsKeyword */', + 'T_IMPLEMENTS', + ], + [ + '/* testClassInstantiationParentIsKeyword */', + 'T_PARENT', + ], + [ + '/* testClassInstantiationSelfIsKeyword */', + 'T_SELF', + ], + [ + '/* testClassInstantiationStaticIsKeyword */', + 'T_STATIC', + ], + [ + '/* testNamespaceInNameIsKeyword */', + 'T_NAMESPACE', + ], + ]; + + }//end dataKeywords() + + +}//end class diff --git a/tests/Core/Tokenizer/DoubleQuotedStringTest.inc b/tests/Core/Tokenizer/DoubleQuotedStringTest.inc new file mode 100644 index 0000000000..62535b1e41 --- /dev/null +++ b/tests/Core/Tokenizer/DoubleQuotedStringTest.inc @@ -0,0 +1,52 @@ +bar"; +/* testProperty2 */ +"{$foo->bar}"; + +/* testMethod1 */ +"{$foo->bar()}"; + +/* testClosure1 */ +"{$foo()}"; + +/* testChain1 */ +"{$foo['bar']->baz()()}"; + +/* testVariableVar1 */ +"${$bar}"; +/* testVariableVar2 */ +"${(foo)}"; +/* testVariableVar3 */ +"${foo->bar}"; + +/* testNested1 */ +"${foo["${bar}"]}"; +/* testNested2 */ +"${foo["${bar['baz']}"]}"; +/* testNested3 */ +"${foo->{$baz}}"; +/* testNested4 */ +"${foo->{${'a'}}}"; +/* testNested5 */ +"${foo->{"${'a'}"}}"; + +/* testParseError */ +"${foo["${bar diff --git a/tests/Core/Tokenizer/DoubleQuotedStringTest.php b/tests/Core/Tokenizer/DoubleQuotedStringTest.php new file mode 100644 index 0000000000..cc9fe49ec4 --- /dev/null +++ b/tests/Core/Tokenizer/DoubleQuotedStringTest.php @@ -0,0 +1,136 @@ + + * @copyright 2022 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class DoubleQuotedStringTest extends AbstractMethodUnitTest +{ + + + /** + * Test that double quoted strings contain the complete string. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $expectedContent The expected content of the double quoted string. + * + * @dataProvider dataDoubleQuotedString + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testDoubleQuotedString($testMarker, $expectedContent) + { + $tokens = self::$phpcsFile->getTokens(); + + $target = $this->getTargetToken($testMarker, T_DOUBLE_QUOTED_STRING); + $this->assertSame($expectedContent, $tokens[$target]['content']); + + }//end testDoubleQuotedString() + + + /** + * Data provider. + * + * @see testDoubleQuotedString() + * + * @return array + */ + public function dataDoubleQuotedString() + { + return [ + [ + 'testMarker' => '/* testSimple1 */', + 'expectedContent' => '"$foo"', + ], + [ + 'testMarker' => '/* testSimple2 */', + 'expectedContent' => '"{$foo}"', + ], + [ + 'testMarker' => '/* testSimple3 */', + 'expectedContent' => '"${foo}"', + ], + [ + 'testMarker' => '/* testDIM1 */', + 'expectedContent' => '"$foo[bar]"', + ], + [ + 'testMarker' => '/* testDIM2 */', + 'expectedContent' => '"{$foo[\'bar\']}"', + ], + [ + 'testMarker' => '/* testDIM3 */', + 'expectedContent' => '"${foo[\'bar\']}"', + ], + [ + 'testMarker' => '/* testProperty1 */', + 'expectedContent' => '"$foo->bar"', + ], + [ + 'testMarker' => '/* testProperty2 */', + 'expectedContent' => '"{$foo->bar}"', + ], + [ + 'testMarker' => '/* testMethod1 */', + 'expectedContent' => '"{$foo->bar()}"', + ], + [ + 'testMarker' => '/* testClosure1 */', + 'expectedContent' => '"{$foo()}"', + ], + [ + 'testMarker' => '/* testChain1 */', + 'expectedContent' => '"{$foo[\'bar\']->baz()()}"', + ], + [ + 'testMarker' => '/* testVariableVar1 */', + 'expectedContent' => '"${$bar}"', + ], + [ + 'testMarker' => '/* testVariableVar2 */', + 'expectedContent' => '"${(foo)}"', + ], + [ + 'testMarker' => '/* testVariableVar3 */', + 'expectedContent' => '"${foo->bar}"', + ], + [ + 'testMarker' => '/* testNested1 */', + 'expectedContent' => '"${foo["${bar}"]}"', + ], + [ + 'testMarker' => '/* testNested2 */', + 'expectedContent' => '"${foo["${bar[\'baz\']}"]}"', + ], + [ + 'testMarker' => '/* testNested3 */', + 'expectedContent' => '"${foo->{$baz}}"', + ], + [ + 'testMarker' => '/* testNested4 */', + 'expectedContent' => '"${foo->{${\'a\'}}}"', + ], + [ + 'testMarker' => '/* testNested5 */', + 'expectedContent' => '"${foo->{"${\'a\'}"}}"', + ], + [ + 'testMarker' => '/* testParseError */', + 'expectedContent' => '"${foo["${bar +', + ], + ]; + + }//end dataDoubleQuotedString() + + +}//end class diff --git a/tests/Core/Tokenizer/EnumCaseTest.inc b/tests/Core/Tokenizer/EnumCaseTest.inc new file mode 100644 index 0000000000..13b87242e1 --- /dev/null +++ b/tests/Core/Tokenizer/EnumCaseTest.inc @@ -0,0 +1,95 @@ + + * @copyright 2021 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class EnumCaseTest extends AbstractMethodUnitTest +{ + + + /** + * Test that the enum "case" is converted to T_ENUM_CASE. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @dataProvider dataEnumCases + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::recurseScopeMap + * + * @return void + */ + public function testEnumCases($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $enumCase = $this->getTargetToken($testMarker, [T_ENUM_CASE, T_CASE]); + + $this->assertSame(T_ENUM_CASE, $tokens[$enumCase]['code']); + $this->assertSame('T_ENUM_CASE', $tokens[$enumCase]['type']); + + $this->assertArrayNotHasKey('scope_condition', $tokens[$enumCase], 'Scope condition is set'); + $this->assertArrayNotHasKey('scope_opener', $tokens[$enumCase], 'Scope opener is set'); + $this->assertArrayNotHasKey('scope_closer', $tokens[$enumCase], 'Scope closer is set'); + + }//end testEnumCases() + + + /** + * Data provider. + * + * @see testEnumCases() + * + * @return array + */ + public function dataEnumCases() + { + return [ + ['/* testPureEnumCase */'], + ['/* testBackingIntegerEnumCase */'], + ['/* testBackingStringEnumCase */'], + ['/* testEnumCaseInComplexEnum */'], + ['/* testEnumCaseIsCaseInsensitive */'], + ['/* testEnumCaseAfterSwitch */'], + ['/* testEnumCaseAfterSwitchWithEndSwitch */'], + ]; + + }//end dataEnumCases() + + + /** + * Test that "case" that is not enum case is still tokenized as `T_CASE`. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @dataProvider dataNotEnumCases + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::recurseScopeMap + * + * @return void + */ + public function testNotEnumCases($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $case = $this->getTargetToken($testMarker, [T_ENUM_CASE, T_CASE]); + + $this->assertSame(T_CASE, $tokens[$case]['code']); + $this->assertSame('T_CASE', $tokens[$case]['type']); + + $this->assertArrayHasKey('scope_condition', $tokens[$case], 'Scope condition is not set'); + $this->assertArrayHasKey('scope_opener', $tokens[$case], 'Scope opener is not set'); + $this->assertArrayHasKey('scope_closer', $tokens[$case], 'Scope closer is not set'); + + }//end testNotEnumCases() + + + /** + * Data provider. + * + * @see testNotEnumCases() + * + * @return array + */ + public function dataNotEnumCases() + { + return [ + ['/* testCaseWithSemicolonIsNotEnumCase */'], + ['/* testCaseWithConstantIsNotEnumCase */'], + ['/* testCaseWithConstantAndIdenticalIsNotEnumCase */'], + ['/* testCaseWithAssigmentToConstantIsNotEnumCase */'], + ['/* testIsNotEnumCaseIsCaseInsensitive */'], + ['/* testCaseInSwitchWhenCreatingEnumInSwitch1 */'], + ['/* testCaseInSwitchWhenCreatingEnumInSwitch2 */'], + ]; + + }//end dataNotEnumCases() + + + /** + * Test that "case" that is not enum case is still tokenized as `T_CASE`. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @dataProvider dataKeywordAsEnumCaseNameShouldBeString + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testKeywordAsEnumCaseNameShouldBeString($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $enumCaseName = $this->getTargetToken($testMarker, [T_STRING, T_INTERFACE, T_TRAIT, T_ENUM, T_FUNCTION, T_FALSE, T_DEFAULT, T_ARRAY]); + + $this->assertSame(T_STRING, $tokens[$enumCaseName]['code']); + $this->assertSame('T_STRING', $tokens[$enumCaseName]['type']); + + }//end testKeywordAsEnumCaseNameShouldBeString() + + + /** + * Data provider. + * + * @see testKeywordAsEnumCaseNameShouldBeString() + * + * @return array + */ + public function dataKeywordAsEnumCaseNameShouldBeString() + { + return [ + ['/* testKeywordAsEnumCaseNameShouldBeString1 */'], + ['/* testKeywordAsEnumCaseNameShouldBeString2 */'], + ['/* testKeywordAsEnumCaseNameShouldBeString3 */'], + ['/* testKeywordAsEnumCaseNameShouldBeString4 */'], + ]; + + }//end dataKeywordAsEnumCaseNameShouldBeString() + + +}//end class diff --git a/tests/Core/Tokenizer/GotoLabelTest.inc b/tests/Core/Tokenizer/GotoLabelTest.inc index f6920f54fa..12df5d296b 100644 --- a/tests/Core/Tokenizer/GotoLabelTest.inc +++ b/tests/Core/Tokenizer/GotoLabelTest.inc @@ -51,3 +51,6 @@ switch (true) { // Do something. break; } + +/* testNotGotoDeclarationEnumWithType */ +enum Suit: string implements Colorful, CardGame {} diff --git a/tests/Core/Tokenizer/GotoLabelTest.php b/tests/Core/Tokenizer/GotoLabelTest.php index fa6f8cfb47..0f937cc8b0 100644 --- a/tests/Core/Tokenizer/GotoLabelTest.php +++ b/tests/Core/Tokenizer/GotoLabelTest.php @@ -163,6 +163,10 @@ public function dataNotAGotoDeclaration() '/* testNotGotoDeclarationGlobalConstantInTernary */', 'CONST_B', ], + [ + '/* testNotGotoDeclarationEnumWithType */', + 'Suit', + ], ]; }//end dataNotAGotoDeclaration() diff --git a/tests/Core/Tokenizer/HeredocNowdocCloserTest.inc b/tests/Core/Tokenizer/HeredocNowdocCloserTest.inc new file mode 100644 index 0000000000..a800980b8e --- /dev/null +++ b/tests/Core/Tokenizer/HeredocNowdocCloserTest.inc @@ -0,0 +1,43 @@ + + * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Config; +use PHP_CodeSniffer\Ruleset; +use PHP_CodeSniffer\Files\DummyFile; +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +/** + * Heredoc/nowdoc closer token test. + * + * @requires PHP 7.3 + */ +class HeredocNowdocCloserTest extends AbstractMethodUnitTest +{ + + + /** + * Initialize & tokenize \PHP_CodeSniffer\Files\File with code from the test case file. + * + * {@internal This is a near duplicate of the original method. Only difference is that + * tab replacement is enabled for this test.} + * + * @return void + */ + public static function setUpBeforeClass() + { + $config = new Config(); + $config->standards = ['PSR1']; + $config->tabWidth = 4; + + $ruleset = new Ruleset($config); + + // Default to a file with the same name as the test class. Extension is property based. + $relativeCN = str_replace(__NAMESPACE__, '', get_called_class()); + $relativePath = str_replace('\\', DIRECTORY_SEPARATOR, $relativeCN); + $pathToTestFile = realpath(__DIR__).$relativePath.'.'.static::$fileExtension; + + // Make sure the file gets parsed correctly based on the file type. + $contents = 'phpcs_input_file: '.$pathToTestFile.PHP_EOL; + $contents .= file_get_contents($pathToTestFile); + + self::$phpcsFile = new DummyFile($contents, $ruleset, $config); + self::$phpcsFile->process(); + + }//end setUpBeforeClass() + + + /** + * Verify that leading (indent) whitespace in a heredoc/nowdoc closer token get the tab replacement treatment. + * + * @param string $testMarker The comment prefacing the target token. + * @param array $expected Expectations for the token array. + * + * @dataProvider dataHeredocNowdocCloserTabReplacement + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap + * + * @return void + */ + public function testHeredocNowdocCloserTabReplacement($testMarker, $expected) + { + $tokens = self::$phpcsFile->getTokens(); + + $closer = $this->getTargetToken($testMarker, [T_END_HEREDOC, T_END_NOWDOC]); + + foreach ($expected as $key => $value) { + if ($key === 'orig_content' && $value === null) { + $this->assertArrayNotHasKey($key, $tokens[$closer], "Unexpected 'orig_content' key found in the token array."); + continue; + } + + $this->assertArrayHasKey($key, $tokens[$closer], "Key $key not found in the token array."); + $this->assertSame($value, $tokens[$closer][$key], "Value for key $key does not match expectation."); + } + + }//end testHeredocNowdocCloserTabReplacement() + + + /** + * Data provider. + * + * @see testHeredocNowdocCloserTabReplacement() + * + * @return array + */ + public function dataHeredocNowdocCloserTabReplacement() + { + return [ + [ + 'testMarker' => '/* testHeredocCloserNoIndent */', + 'expected' => [ + 'length' => 3, + 'content' => 'EOD', + 'orig_content' => null, + ], + ], + [ + 'testMarker' => '/* testNowdocCloserNoIndent */', + 'expected' => [ + 'length' => 3, + 'content' => 'EOD', + 'orig_content' => null, + ], + ], + [ + 'testMarker' => '/* testHeredocCloserSpaceIndent */', + 'expected' => [ + 'length' => 7, + 'content' => ' END', + 'orig_content' => null, + ], + ], + [ + 'testMarker' => '/* testNowdocCloserSpaceIndent */', + 'expected' => [ + 'length' => 8, + 'content' => ' END', + 'orig_content' => null, + ], + ], + [ + 'testMarker' => '/* testHeredocCloserTabIndent */', + 'expected' => [ + 'length' => 8, + 'content' => ' END', + 'orig_content' => ' END', + ], + ], + [ + 'testMarker' => '/* testNowdocCloserTabIndent */', + 'expected' => [ + 'length' => 7, + 'content' => ' END', + 'orig_content' => ' END', + ], + ], + ]; + + }//end dataHeredocNowdocCloserTabReplacement() + + +}//end class diff --git a/tests/Core/Tokenizer/HeredocStringTest.inc b/tests/Core/Tokenizer/HeredocStringTest.inc new file mode 100644 index 0000000000..ae43e24a59 --- /dev/null +++ b/tests/Core/Tokenizer/HeredocStringTest.inc @@ -0,0 +1,193 @@ +bar +EOD; + +/* testProperty2 */ +$heredoc = <<<"EOD" +{$foo->bar} +EOD; + +/* testMethod1 */ +$heredoc = <<bar()} +EOD; + +/* testClosure1 */ +$heredoc = <<<"EOD" +{$foo()} +EOD; + +/* testChain1 */ +$heredoc = <<baz()()} +EOD; + +/* testVariableVar1 */ +$heredoc = <<<"EOD" +${$bar} +EOD; + +/* testVariableVar2 */ +$heredoc = <<bar} +EOD; + +/* testNested1 */ +$heredoc = <<{$baz}} +EOD; + +/* testNested4 */ +$heredoc = <<<"EOD" +${foo->{${'a'}}} +EOD; + +/* testNested5 */ +$heredoc = <<{"${'a'}"}} +EOD; + +/* testSimple1Wrapped */ +$heredoc = <<bar Something +EOD; + +/* testProperty2Wrapped */ +$heredoc = <<<"EOD" +Do {$foo->bar} Something +EOD; + +/* testMethod1Wrapped */ +$heredoc = <<bar()} Something +EOD; + +/* testClosure1Wrapped */ +$heredoc = <<<"EOD" +Do {$foo()} Something +EOD; + +/* testChain1Wrapped */ +$heredoc = <<baz()()} Something +EOD; + +/* testVariableVar1Wrapped */ +$heredoc = <<<"EOD" +Do ${$bar} Something +EOD; + +/* testVariableVar2Wrapped */ +$heredoc = <<bar} Something +EOD; + +/* testNested1Wrapped */ +$heredoc = <<{$baz}} Something +EOD; + +/* testNested4Wrapped */ +$heredoc = <<<"EOD" +Do ${foo->{${'a'}}} Something +EOD; + +/* testNested5Wrapped */ +$heredoc = <<{"${'a'}"}} Something +EOD; diff --git a/tests/Core/Tokenizer/HeredocStringTest.php b/tests/Core/Tokenizer/HeredocStringTest.php new file mode 100644 index 0000000000..2c808be906 --- /dev/null +++ b/tests/Core/Tokenizer/HeredocStringTest.php @@ -0,0 +1,153 @@ + + * @copyright 2022 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class HeredocStringTest extends AbstractMethodUnitTest +{ + + + /** + * Test that heredoc strings contain the complete interpolated string. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $expectedContent The expected content of the heredoc string. + * + * @dataProvider dataHeredocString + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testHeredocString($testMarker, $expectedContent) + { + $tokens = self::$phpcsFile->getTokens(); + + $target = $this->getTargetToken($testMarker, T_HEREDOC); + $this->assertSame($expectedContent."\n", $tokens[$target]['content']); + + }//end testHeredocString() + + + /** + * Test that heredoc strings contain the complete interpolated string when combined with other texts. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $expectedContent The expected content of the heredoc string. + * + * @dataProvider dataHeredocString + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testHeredocStringWrapped($testMarker, $expectedContent) + { + $tokens = self::$phpcsFile->getTokens(); + + $testMarker = substr($testMarker, 0, -3).'Wrapped */'; + $target = $this->getTargetToken($testMarker, T_HEREDOC); + $this->assertSame('Do '.$expectedContent." Something\n", $tokens[$target]['content']); + + }//end testHeredocStringWrapped() + + + /** + * Data provider. + * + * @see testHeredocString() + * + * @return array + */ + public function dataHeredocString() + { + return [ + [ + 'testMarker' => '/* testSimple1 */', + 'expectedContent' => '$foo', + ], + [ + 'testMarker' => '/* testSimple2 */', + 'expectedContent' => '{$foo}', + ], + [ + 'testMarker' => '/* testSimple3 */', + 'expectedContent' => '${foo}', + ], + [ + 'testMarker' => '/* testDIM1 */', + 'expectedContent' => '$foo[bar]', + ], + [ + 'testMarker' => '/* testDIM2 */', + 'expectedContent' => '{$foo[\'bar\']}', + ], + [ + 'testMarker' => '/* testDIM3 */', + 'expectedContent' => '${foo[\'bar\']}', + ], + [ + 'testMarker' => '/* testProperty1 */', + 'expectedContent' => '$foo->bar', + ], + [ + 'testMarker' => '/* testProperty2 */', + 'expectedContent' => '{$foo->bar}', + ], + [ + 'testMarker' => '/* testMethod1 */', + 'expectedContent' => '{$foo->bar()}', + ], + [ + 'testMarker' => '/* testClosure1 */', + 'expectedContent' => '{$foo()}', + ], + [ + 'testMarker' => '/* testChain1 */', + 'expectedContent' => '{$foo[\'bar\']->baz()()}', + ], + [ + 'testMarker' => '/* testVariableVar1 */', + 'expectedContent' => '${$bar}', + ], + [ + 'testMarker' => '/* testVariableVar2 */', + 'expectedContent' => '${(foo)}', + ], + [ + 'testMarker' => '/* testVariableVar3 */', + 'expectedContent' => '${foo->bar}', + ], + [ + 'testMarker' => '/* testNested1 */', + 'expectedContent' => '${foo["${bar}"]}', + ], + [ + 'testMarker' => '/* testNested2 */', + 'expectedContent' => '${foo["${bar[\'baz\']}"]}', + ], + [ + 'testMarker' => '/* testNested3 */', + 'expectedContent' => '${foo->{$baz}}', + ], + [ + 'testMarker' => '/* testNested4 */', + 'expectedContent' => '${foo->{${\'a\'}}}', + ], + [ + 'testMarker' => '/* testNested5 */', + 'expectedContent' => '${foo->{"${\'a\'}"}}', + ], + ]; + + }//end dataHeredocString() + + +}//end class diff --git a/tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc b/tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc index d05d27d951..b9c0df24d4 100644 --- a/tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc +++ b/tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc @@ -208,6 +208,9 @@ foobar(elseif: $value, /* testReservedKeywordElseif2 */ elseif: $value); /* testReservedKeywordEmpty1 */ foobar(empty: $value, /* testReservedKeywordEmpty2 */ empty: $value); +/* testReservedKeywordEnum1 */ +foobar(enum: $value, /* testReservedKeywordEnum2 */ enum: $value); + /* testReservedKeywordEnddeclare1 */ foobar(enddeclare: $value, /* testReservedKeywordEnddeclare2 */ enddeclare: $value); @@ -310,6 +313,9 @@ foobar(protected: $value, /* testReservedKeywordProtected2 */ protected: $value) /* testReservedKeywordPublic1 */ foobar(public: $value, /* testReservedKeywordPublic2 */ public: $value); +/* testReservedKeywordReadonly1 */ +foobar(readonly: $value, /* testReservedKeywordReadonly2 */ readonly: $value); + /* testReservedKeywordRequire1 */ foobar(require: $value, /* testReservedKeywordRequire2 */ require: $value); @@ -396,3 +402,6 @@ foobar(parent: $value, /* testReservedKeywordParent2 */ parent: $value); /* testReservedKeywordSelf1 */ foobar(self: $value, /* testReservedKeywordSelf2 */ self: $value); + +/* testReservedKeywordNever1 */ +foobar(never: $value, /* testReservedKeywordNever2 */ never: $value); diff --git a/tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.php b/tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.php index 52845ac72c..cc57637c6f 100644 --- a/tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.php +++ b/tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.php @@ -776,6 +776,7 @@ public function dataReservedKeywordsAsName() 'endif', 'endswitch', 'endwhile', + 'enum', 'eval', 'exit', 'extends', @@ -804,6 +805,7 @@ public function dataReservedKeywordsAsName() 'private', 'protected', 'public', + 'readonly', 'require', 'require_once', 'return', @@ -831,6 +833,7 @@ public function dataReservedKeywordsAsName() 'resource', 'mixed', 'numeric', + 'never', // Not reserved keyword, but do have their own token in PHPCS. 'parent', diff --git a/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc b/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc index e2d61bb664..38e5a47d53 100644 --- a/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc +++ b/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc @@ -13,7 +13,7 @@ interface FooBar extends namespace\BarFoo {} function foo() : namespace\Baz {} /* testClosureReturnType */ -$closure = function () : namespace\Baz {} +$closure = function () : namespace\Baz {}; /* testArrowFunctionReturnType */ $fn = fn() : namespace\Baz => new namespace\Baz; diff --git a/tests/Core/Tokenizer/ShortArrayTest.inc b/tests/Core/Tokenizer/ShortArrayTest.inc index 54065f213d..60b23a51cc 100644 --- a/tests/Core/Tokenizer/ShortArrayTest.inc +++ b/tests/Core/Tokenizer/ShortArrayTest.inc @@ -92,6 +92,20 @@ echo [1, 2, 3][0]; /* testArrayWithinFunctionCall */ $var = functionCall([$x, $y]); +if ( true ) { + /* testShortListDeclarationAfterBracedControlStructure */ + [ $a ] = [ 'hi' ]; +} + +if ( true ) + /* testShortListDeclarationAfterNonBracedControlStructure */ + [ $a ] = [ 'hi' ]; + +if ( true ) : + /* testShortListDeclarationAfterAlternativeControlStructure */ + [ $a ] = [ 'hi' ]; +endif; + /* testLiveCoding */ // Intentional parse error. This has to be the last test in the file. $array = [ diff --git a/tests/Core/Tokenizer/ShortArrayTest.php b/tests/Core/Tokenizer/ShortArrayTest.php index 0484d4fcb0..1d97894f59 100644 --- a/tests/Core/Tokenizer/ShortArrayTest.php +++ b/tests/Core/Tokenizer/ShortArrayTest.php @@ -52,29 +52,29 @@ public function testSquareBrackets($testMarker) public function dataSquareBrackets() { return [ - ['/* testArrayAccess1 */'], - ['/* testArrayAccess2 */'], - ['/* testArrayAssignment */'], - ['/* testFunctionCallDereferencing */'], - ['/* testMethodCallDereferencing */'], - ['/* testStaticMethodCallDereferencing */'], - ['/* testPropertyDereferencing */'], - ['/* testPropertyDereferencingWithInaccessibleName */'], - ['/* testStaticPropertyDereferencing */'], - ['/* testStringDereferencing */'], - ['/* testStringDereferencingDoubleQuoted */'], - ['/* testConstantDereferencing */'], - ['/* testClassConstantDereferencing */'], - ['/* testMagicConstantDereferencing */'], - ['/* testArrayAccessCurlyBraces */'], - ['/* testArrayLiteralDereferencing */'], - ['/* testShortArrayLiteralDereferencing */'], - ['/* testClassMemberDereferencingOnInstantiation1 */'], - ['/* testClassMemberDereferencingOnInstantiation2 */'], - ['/* testClassMemberDereferencingOnClone */'], - ['/* testNullsafeMethodCallDereferencing */'], - ['/* testInterpolatedStringDereferencing */'], - ['/* testLiveCoding */'], + 'array access 1' => ['/* testArrayAccess1 */'], + 'array access 2' => ['/* testArrayAccess2 */'], + 'array assignment' => ['/* testArrayAssignment */'], + 'function call dereferencing' => ['/* testFunctionCallDereferencing */'], + 'method call dereferencing' => ['/* testMethodCallDereferencing */'], + 'static method call dereferencing' => ['/* testStaticMethodCallDereferencing */'], + 'property dereferencing' => ['/* testPropertyDereferencing */'], + 'property dereferencing with inaccessable name' => ['/* testPropertyDereferencingWithInaccessibleName */'], + 'static property dereferencing' => ['/* testStaticPropertyDereferencing */'], + 'string dereferencing single quotes' => ['/* testStringDereferencing */'], + 'string dereferencing double quotes' => ['/* testStringDereferencingDoubleQuoted */'], + 'global constant dereferencing' => ['/* testConstantDereferencing */'], + 'class constant dereferencing' => ['/* testClassConstantDereferencing */'], + 'magic constant dereferencing' => ['/* testMagicConstantDereferencing */'], + 'array access with curly braces' => ['/* testArrayAccessCurlyBraces */'], + 'array literal dereferencing' => ['/* testArrayLiteralDereferencing */'], + 'short array literal dereferencing' => ['/* testShortArrayLiteralDereferencing */'], + 'class member dereferencing on instantiation 1' => ['/* testClassMemberDereferencingOnInstantiation1 */'], + 'class member dereferencing on instantiation 2' => ['/* testClassMemberDereferencingOnInstantiation2 */'], + 'class member dereferencing on clone' => ['/* testClassMemberDereferencingOnClone */'], + 'nullsafe method call dereferencing' => ['/* testNullsafeMethodCallDereferencing */'], + 'interpolated string dereferencing' => ['/* testInterpolatedStringDereferencing */'], + 'live coding' => ['/* testLiveCoding */'], ]; }//end dataSquareBrackets() @@ -117,13 +117,16 @@ public function testShortArrays($testMarker) public function dataShortArrays() { return [ - ['/* testShortArrayDeclarationEmpty */'], - ['/* testShortArrayDeclarationWithOneValue */'], - ['/* testShortArrayDeclarationWithMultipleValues */'], - ['/* testShortArrayDeclarationWithDereferencing */'], - ['/* testShortListDeclaration */'], - ['/* testNestedListDeclaration */'], - ['/* testArrayWithinFunctionCall */'], + 'short array empty' => ['/* testShortArrayDeclarationEmpty */'], + 'short array with value' => ['/* testShortArrayDeclarationWithOneValue */'], + 'short array with values' => ['/* testShortArrayDeclarationWithMultipleValues */'], + 'short array with dereferencing' => ['/* testShortArrayDeclarationWithDereferencing */'], + 'short list' => ['/* testShortListDeclaration */'], + 'short list nested' => ['/* testNestedListDeclaration */'], + 'short array within function call' => ['/* testArrayWithinFunctionCall */'], + 'short list after braced control structure' => ['/* testShortListDeclarationAfterBracedControlStructure */'], + 'short list after non-braced control structure' => ['/* testShortListDeclarationAfterNonBracedControlStructure */'], + 'short list after alternative control structure' => ['/* testShortListDeclarationAfterAlternativeControlStructure */'], ]; }//end dataShortArrays() diff --git a/tests/Core/Tokenizer/TypeIntersectionTest.inc b/tests/Core/Tokenizer/TypeIntersectionTest.inc new file mode 100644 index 0000000000..abf9b85ba8 --- /dev/null +++ b/tests/Core/Tokenizer/TypeIntersectionTest.inc @@ -0,0 +1,120 @@ + $param & $int; + +/* testTypeIntersectionArrowReturnType */ +$arrowWithReturnType = fn ($param) : Foo&Bar => $param * 10; + +/* testBitwiseAndInArrayKey */ +$array = array( + A & B => /* testBitwiseAndInArrayValue */ B & C +); + +/* testBitwiseAndInShortArrayKey */ +$array = [ + A & B => /* testBitwiseAndInShortArrayValue */ B & C +]; + +/* testBitwiseAndNonArrowFnFunctionCall */ +$obj->fn($something & $else); + +/* testBitwiseAnd6 */ +function &fn(/* testTypeIntersectionNonArrowFunctionDeclaration */ Foo&Bar $something) {} + +/* testTypeIntersectionWithInvalidTypes */ +function (int&string $var) {}; + +/* testLiveCoding */ +// Intentional parse error. This has to be the last test in the file. +return function( Foo& diff --git a/tests/Core/Tokenizer/TypeIntersectionTest.php b/tests/Core/Tokenizer/TypeIntersectionTest.php new file mode 100644 index 0000000000..2170021933 --- /dev/null +++ b/tests/Core/Tokenizer/TypeIntersectionTest.php @@ -0,0 +1,138 @@ + + * @author Jaroslav Hanslík + * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class TypeIntersectionTest extends AbstractMethodUnitTest +{ + + + /** + * Test that non-intersection type bitwise and tokens are still tokenized as bitwise and. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @dataProvider dataBitwiseAnd + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testBitwiseAnd($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $opener = $this->getTargetToken($testMarker, [T_BITWISE_AND, T_TYPE_INTERSECTION]); + $this->assertSame(T_BITWISE_AND, $tokens[$opener]['code']); + $this->assertSame('T_BITWISE_AND', $tokens[$opener]['type']); + + }//end testBitwiseAnd() + + + /** + * Data provider. + * + * @see testBitwiseAnd() + * + * @return array + */ + public function dataBitwiseAnd() + { + return [ + ['/* testBitwiseAnd1 */'], + ['/* testBitwiseAnd2 */'], + ['/* testBitwiseAndPropertyDefaultValue */'], + ['/* testBitwiseAndParamDefaultValue */'], + ['/* testBitwiseAnd3 */'], + ['/* testBitwiseAnd4 */'], + ['/* testBitwiseAnd5 */'], + ['/* testBitwiseAndClosureParamDefault */'], + ['/* testBitwiseAndArrowParamDefault */'], + ['/* testBitwiseAndArrowExpression */'], + ['/* testBitwiseAndInArrayKey */'], + ['/* testBitwiseAndInArrayValue */'], + ['/* testBitwiseAndInShortArrayKey */'], + ['/* testBitwiseAndInShortArrayValue */'], + ['/* testBitwiseAndNonArrowFnFunctionCall */'], + ['/* testBitwiseAnd6 */'], + ['/* testLiveCoding */'], + ]; + + }//end dataBitwiseAnd() + + + /** + * Test that bitwise and tokens when used as part of a intersection type are tokenized as `T_TYPE_INTERSECTION`. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @dataProvider dataTypeIntersection + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testTypeIntersection($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $opener = $this->getTargetToken($testMarker, [T_BITWISE_AND, T_TYPE_INTERSECTION]); + $this->assertSame(T_TYPE_INTERSECTION, $tokens[$opener]['code']); + $this->assertSame('T_TYPE_INTERSECTION', $tokens[$opener]['type']); + + }//end testTypeIntersection() + + + /** + * Data provider. + * + * @see testTypeIntersection() + * + * @return array + */ + public function dataTypeIntersection() + { + return [ + ['/* testTypeIntersectionPropertySimple */'], + ['/* testTypeIntersectionPropertyReverseModifierOrder */'], + ['/* testTypeIntersectionPropertyMulti1 */'], + ['/* testTypeIntersectionPropertyMulti2 */'], + ['/* testTypeIntersectionPropertyMulti3 */'], + ['/* testTypeIntersectionPropertyNamespaceRelative */'], + ['/* testTypeIntersectionPropertyPartiallyQualified */'], + ['/* testTypeIntersectionPropertyFullyQualified */'], + ['/* testTypeIntersectionPropertyWithReadOnlyKeyword */'], + ['/* testTypeIntersectionParam1 */'], + ['/* testTypeIntersectionParam2 */'], + ['/* testTypeIntersectionParam3 */'], + ['/* testTypeIntersectionParamNamespaceRelative */'], + ['/* testTypeIntersectionParamPartiallyQualified */'], + ['/* testTypeIntersectionParamFullyQualified */'], + ['/* testTypeIntersectionReturnType */'], + ['/* testTypeIntersectionConstructorPropertyPromotion */'], + ['/* testTypeIntersectionAbstractMethodReturnType1 */'], + ['/* testTypeIntersectionAbstractMethodReturnType2 */'], + ['/* testTypeIntersectionReturnTypeNamespaceRelative */'], + ['/* testTypeIntersectionReturnPartiallyQualified */'], + ['/* testTypeIntersectionReturnFullyQualified */'], + ['/* testTypeIntersectionClosureParamIllegalNullable */'], + ['/* testTypeIntersectionWithReference */'], + ['/* testTypeIntersectionWithSpreadOperator */'], + ['/* testTypeIntersectionClosureReturn */'], + ['/* testTypeIntersectionArrowParam */'], + ['/* testTypeIntersectionArrowReturnType */'], + ['/* testTypeIntersectionNonArrowFunctionDeclaration */'], + ['/* testTypeIntersectionWithInvalidTypes */'], + ]; + + }//end dataTypeIntersection() + + +}//end class