diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 5002127b6e..dad38714f6 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -13,36 +13,79 @@ 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@v2
+
+ - 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@v2
+ 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@v2
+ 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:
# - custom_ini: Whether to run with specific custom ini settings to hit very specific
# code conditions.
- # - experimental: Whether the build is "allowed to fail".
matrix:
- php: ['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0']
+ php: ['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2']
custom_ini: [false]
- experimental: [false]
include:
# Builds running the basic tests with different PHP ini settings.
- php: '5.5'
custom_ini: true
- experimental: false
- php: '7.0'
custom_ini: true
- experimental: false
-
- # Nightly.
- - php: '8.1'
- custom_ini: false
- experimental: true
name: "PHP: ${{ matrix.php }} ${{ matrix.custom_ini && ' with custom ini settings' || '' }}"
- continue-on-error: ${{ matrix.experimental }}
+ continue-on-error: ${{ matrix.php == '8.2' }}
steps:
- name: Checkout code
@@ -54,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=E_ALL, display_errors=On, date.timezone=Australia/Sydney, short_open_tag=On, asp_tags=On'
+ echo '::set-output name=PHP_INI::error_reporting=-1, display_errors=On, date.timezone=Australia/Sydney, short_open_tag=On, asp_tags=On'
elif [[ ${{ matrix.custom_ini }} == true && "${{ matrix.php }}" == '7.0' ]]; then
- echo '::set-output name=PHP_INI::phar.readonly=Off, error_reporting=E_ALL, display_errors=On, date.timezone=Australia/Sydney, short_open_tag=On'
+ echo '::set-output name=PHP_INI::error_reporting=-1, display_errors=On, date.timezone=Australia/Sydney, short_open_tag=On'
else
- echo '::set-output name=PHP_INI::phar.readonly=Off, error_reporting=E_ALL, display_errors=On'
+ echo '::set-output name=PHP_INI::error_reporting=-1, display_errors=On'
fi
- name: Install PHP
@@ -72,12 +115,12 @@ jobs:
# Install dependencies and handle caching in one go.
# @link https://github.com/marketplace/actions/install-composer-dependencies
- name: Install Composer dependencies - normal
- if: ${{ matrix.php < 8.0 }}
+ if: ${{ matrix.php < '8.0' }}
uses: "ramsey/composer-install@v1"
# 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 }}
+ if: ${{ matrix.php >= '8.0' }}
uses: "ramsey/composer-install@v1"
with:
composer-options: --ignore-platform-reqs
@@ -88,37 +131,39 @@ jobs:
run: php bin/phpcs --config-set php_path php
- name: 'PHPUnit: run the tests'
- if: ${{ matrix.php != 8.1 }}
+ if: ${{ matrix.php != '8.1' && matrix.php != '8.2' }}
run: vendor/bin/phpunit tests/AllTests.php
# We need to ignore the config file so that PHPUnit doesn't try to read it.
# The config file causes an error on PHP 8.1+ with PHPunit 7, but it's not needed here anyway
# as we can pass all required settings in the phpunit command.
- - name: 'PHPUnit: run the tests on PHP nightly'
- if: ${{ matrix.php == 8.1 }}
+ - name: 'PHPUnit: run the tests on PHP > 8.0'
+ if: ${{ matrix.php == '8.1' || matrix.php == '8.2' }}
run: vendor/bin/phpunit tests/AllTests.php --no-configuration --bootstrap=tests/bootstrap.php --dont-report-useless-tests
- name: 'PHPCS: check code style without cache, no parallel'
- if: ${{ matrix.custom_ini == false && matrix.php != 7.4 }}
+ if: ${{ matrix.custom_ini == false && matrix.php != '7.4' }}
run: php bin/phpcs --no-cache --parallel=1
- name: 'PHPCS: check code style to show results in PR'
- if: ${{ matrix.custom_ini == false && matrix.php == 7.4 }}
+ if: ${{ matrix.custom_ini == false && matrix.php == '7.4' }}
continue-on-error: true
run: php bin/phpcs --no-cache --parallel=1 --report-full --report-checkstyle=./phpcs-report.xml
- name: Show PHPCS results in PR
- if: ${{ matrix.custom_ini == false && matrix.php == 7.4 }}
+ if: ${{ matrix.custom_ini == false && matrix.php == '7.4' }}
run: cs2pr ./phpcs-report.xml
- name: 'Composer: validate config'
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@v2
+ 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/package.xml b/package.xml
index f4244c4442..3836361538 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-04-09
-
+ 2021-12-13
+
- 3.6.1
- 3.6.1
+ 3.7.0
+ 3.7.0
stable
@@ -26,52 +26,21 @@ http://pear.php.net/dtd/package-2.0.xsd">
BSD 3-Clause License
- - PHPCS annotations can now be specified using hash-style comments
- -- Previously, only slash-style and block-style comments could be used to do things like disable errors
- -- Thanks to Juliette Reinders Folmer for the patch
- - Fixed an issue where some sniffs would not run on PHP files that only used the short echo tag
- -- The following sniffs were affected:
- --- Generic.Files.ExecutableFile
- --- Generic.Files.LowercasedFilename
- --- Generic.Files.LineEndings
- --- Generic.Files.EndFileNewline
- --- Generic.Files.EndFileNoNewline
- --- Generic.PHP.ClosingPHPTag
- --- Generic.PHP.Syntax
- --- Generic.VersionControl.GitMergeConflict
- --- Generic.WhiteSpace.DisallowSpaceIndent
- --- Generic.WhiteSpace.DisallowTabIndent
- -- Thanks to Juliette Reinders Folmer for the patch
- - Generic.NamingConventions.ConstructorName no longer throws deprecation notices on PHP 8.1
- -- Thanks to Juliette Reinders Folmer for the patch
- - Squiz.Commenting.BlockComment now correctly applies rules for block comments after a short echo tag
- -- Thanks to Juliette Reinders Folmer for the patch
- - Fixed bug #3294 : Bug in attribute tokenization when content contains PHP end token or attribute closer on new line
- -- Thanks to Alessandro Chitolina for the patch
- -- Thanks to Juliette Reinders Folmer for the tests
- - Fixed bug #3296 : PSR2.ControlStructures.SwitchDeclaration takes phpcs:ignore as content of case body
- - Fixed bug #3297 : PSR2.ControlStructures.SwitchDeclaration.TerminatingComment does not handle try/finally blocks
- -- Thanks to Juliette Reinders Folmer for the patch
- - Fixed bug #3302 : PHP 8.0 | Tokenizer/PHP: bugfix for union types using namespace operator
- -- Thanks to Juliette Reinders Folmer for the patch
- - Fixed bug #3303 : findStartOfStatement() doesn't work with T_OPEN_TAG_WITH_ECHO
- - Fixed bug #3316 : Arrow function not tokenized correctly when using null in union type
- - Fixed bug #3317 : Problem with how phpcs handles ignored files when running in parallel
- -- Thanks to Emil Andersson for the patch
- - Fixed bug #3324 : PHPCS hangs processing some nested arrow functions inside a function call
- - Fixed bug #3326 : Generic.Formatting.MultipleStatementAlignment error with const DEFAULT
- -- Thanks to Juliette Reinders Folmer for the patch
- - Fixed bug #3340 : Ensure interface and trait names are always tokenized as T_STRING
- -- Thanks to Juliette Reinders Folmer for the patch
- - Fixed bug #3342 : PSR12/Squiz/PEAR standards all error on promoted properties with docblocks
- -- Thanks to Juliette Reinders Folmer for the patch
- - Fixed bug #3345 : IF statement with no braces and double catch turned into syntax error by auto-fixer
- -- Thanks to Juliette Reinders Folmer for the patch
- - Fixed bug #3352 : PSR2.ControlStructures.SwitchDeclaration can remove comments on the same line as the case statement while fixing
- -- Thanks to Juliette Reinders Folmer for the patch
- - Fixed bug #3357 : Generic.Functions.OpeningFunctionBraceBsdAllman removes return type when additional lines are present
- -- Thanks to Juliette Reinders Folmer for the patch
- - Fixed bug #3362 : Generic.WhiteSpace.ScopeIndent false positive for arrow functions inside arrays
+ - 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
+ - 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
+ - Support for new PHP 8.1 readonly keyword has been added to the following sniffs:
+ -- Generic.PHP.LowerCaseKeyword
+ -- PSR2.Classes.PropertyDeclaration
+ -- Squiz.Commenting.BlockCommentS
+ -- Squiz.Commenting.DocCommentAlignment
+ -- Squiz.Commenting.VariableComment
+ -- Thanks to Juliette Reinders Folmer for the patches
+ - 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
@@ -155,8 +124,12 @@ http://pear.php.net/dtd/package-2.0.xsd">
+
+
+
+
@@ -177,6 +150,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
+
+
@@ -1145,6 +1120,7 @@ http://pear.php.net/dtd/package-2.0.xsd">
+
@@ -1186,6 +1162,9 @@ http://pear.php.net/dtd/package-2.0.xsd">
+
+
+
@@ -1640,6 +1619,7 @@ http://pear.php.net/dtd/package-2.0.xsd">
+
@@ -2103,8 +2083,12 @@ http://pear.php.net/dtd/package-2.0.xsd">
+
+
+
+
@@ -2125,6 +2109,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
+
+
@@ -2191,8 +2177,12 @@ http://pear.php.net/dtd/package-2.0.xsd">
+
+
+
+
@@ -2213,6 +2203,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
+
+
@@ -2230,6 +2222,128 @@ http://pear.php.net/dtd/package-2.0.xsd">
+
+
+ 3.6.2
+ 3.6.2
+
+
+ stable
+ stable
+
+ 2021-12-13
+ BSD 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
+
+
+
+
+ 3.6.1
+ 3.6.1
+
+
+ stable
+ stable
+
+ 2021-10-11
+ BSD License
+
+ - PHPCS annotations can now be specified using hash-style comments
+ -- Previously, only slash-style and block-style comments could be used to do things like disable errors
+ -- Thanks to Juliette Reinders Folmer for the patch
+ - Fixed an issue where some sniffs would not run on PHP files that only used the short echo tag
+ -- The following sniffs were affected:
+ --- Generic.Files.ExecutableFile
+ --- Generic.Files.LowercasedFilename
+ --- Generic.Files.LineEndings
+ --- Generic.Files.EndFileNewline
+ --- Generic.Files.EndFileNoNewline
+ --- Generic.PHP.ClosingPHPTag
+ --- Generic.PHP.Syntax
+ --- Generic.VersionControl.GitMergeConflict
+ --- Generic.WhiteSpace.DisallowSpaceIndent
+ --- Generic.WhiteSpace.DisallowTabIndent
+ -- Thanks to Juliette Reinders Folmer for the patch
+ - The new PHP 8.1 tokenisation for ampersands has been reverted to use the existing PHP_CodeSniffer method
+ -- The PHP 8.1 tokens T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG and T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG are unsued
+ -- Ampersands continue to be tokenized as T_BITWISE_AND for all PHP versions
+ -- Thanks to Juliette Reinders Folmer and Anna Filina for the patch
+ - File::getMethodParameters() no longer incorrectly returns argument attributes in the type hint array index
+ -- A new has_attributes array index is available and set to TRUE if the argument has attributes defined
+ -- Thanks to Juliette Reinders Folmer for the patch
+ - Generic.NamingConventions.ConstructorName no longer throws deprecation notices on PHP 8.1
+ -- Thanks to Juliette Reinders Folmer for the patch
+ - Squiz.Commenting.BlockComment now correctly applies rules for block comments after a short echo tag
+ -- Thanks to Juliette Reinders Folmer for the patch
+ - Fixed false positives when using attributes in the following sniffs:
+ -- PEAR.Commenting.FunctionComment
+ -- Squiz.Commenting.InlineComment
+ -- Squiz.Commenting.BlockComment
+ -- Squiz.Commenting.VariableComment
+ -- Squiz.WhiteSpace.MemberVarSpacing
+ -- Thanks to Juliette Reinders Folmer for the patch
+ - Fixed bug #3294 : Bug in attribute tokenization when content contains PHP end token or attribute closer on new line
+ -- Thanks to Alessandro Chitolina for the patch
+ -- Thanks to Juliette Reinders Folmer for the tests
+ - Fixed bug #3296 : PSR2.ControlStructures.SwitchDeclaration takes phpcs:ignore as content of case body
+ - Fixed bug #3297 : PSR2.ControlStructures.SwitchDeclaration.TerminatingComment does not handle try/finally blocks
+ -- Thanks to Juliette Reinders Folmer for the patch
+ - Fixed bug #3302 : PHP 8.0 | Tokenizer/PHP: bugfix for union types using namespace operator
+ -- Thanks to Juliette Reinders Folmer for the patch
+ - Fixed bug #3303 : findStartOfStatement() doesn't work with T_OPEN_TAG_WITH_ECHO
+ - Fixed bug #3316 : Arrow function not tokenized correctly when using null in union type
+ - Fixed bug #3317 : Problem with how phpcs handles ignored files when running in parallel
+ -- Thanks to Emil Andersson for the patch
+ - Fixed bug #3324 : PHPCS hangs processing some nested arrow functions inside a function call
+ - Fixed bug #3326 : Generic.Formatting.MultipleStatementAlignment error with const DEFAULT
+ -- Thanks to Juliette Reinders Folmer for the patch
+ - Fixed bug #3333 : Squiz.Objects.ObjectInstantiation: null coalesce operators are not recognized as assignment
+ -- Thanks to Juliette Reinders Folmer for the patch
+ - Fixed bug #3340 : Ensure interface and trait names are always tokenized as T_STRING
+ -- Thanks to Juliette Reinders Folmer for the patch
+ - Fixed bug #3342 : PSR12/Squiz/PEAR standards all error on promoted properties with docblocks
+ -- Thanks to Juliette Reinders Folmer for the patch
+ - Fixed bug #3345 : IF statement with no braces and double catch turned into syntax error by auto-fixer
+ -- Thanks to Juliette Reinders Folmer for the patch
+ - Fixed bug #3352 : PSR2.ControlStructures.SwitchDeclaration can remove comments on the same line as the case statement while fixing
+ -- Thanks to Juliette Reinders Folmer for the patch
+ - Fixed bug #3357 : Generic.Functions.OpeningFunctionBraceBsdAllman removes return type when additional lines are present
+ -- Thanks to Juliette Reinders Folmer for the patch
+ - Fixed bug #3362 : Generic.WhiteSpace.ScopeIndent false positive for arrow functions inside arrays
+ - Fixed bug #3384 : Squiz.Commenting.FileComment.SpacingAfterComment false positive on empty file
+ - Fixed bug #3394 : Fix PHP 8.1 auto_detect_line_endings deprecation notice
+ - Fixed bug #3400 : PHP 8.1: prevent deprecation notices about missing return types
+ -- Thanks to Juliette Reinders Folmer for the patch
+ - Fixed bug #3424 : PHPCS fails when using PHP 8 Constructor property promotion with attributes
+ -- Thanks to Juliette Reinders Folmer for the patch
+ - Fixed bug #3425 : PHP 8.1 | Runner::processChildProcs(): fix passing null to non-nullable bug
+ -- Thanks to Juliette Reinders Folmer for the patch
+ - Fixed bug #3445 : Nullable parameter after attribute incorrectly tokenized as ternary operator
+ -- Thanks to Juliette Reinders Folmer for the patch
+
+
3.6.0
@@ -2422,8 +2536,8 @@ http://pear.php.net/dtd/package-2.0.xsd">
- The PHP 8.0 T_NULLSAFE_OBJECT_OPERATOR token has been made available for older versions
-- Existing sniffs that check for T_OBJECT_OPERATOR have been modified to apply the same rules for the nullsafe object operator
-- Thanks to Juliette Reinders Folmer for the patch
- - The new method of PHP 8.0 tokenizing for namespaced names has been revert to thr pre 8.0 method
- -- This maintains backwards compatible for existing sniffs on PHP 8.0
+ - The new method of PHP 8.0 tokenizing for namespaced names has been reverted to the pre 8.0 method
+ -- This maintains backwards compatibility for existing sniffs on PHP 8.0
-- This change will be removed in PHPCS 4.0 as the PHP 8.0 tokenizing method will be backported for pre 8.0 versions
-- Thanks to Juliette Reinders Folmer for the patch
- Added support for changes to the way PHP 8.0 tokenizes hash comments
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 106b1c50b9..4e7b2b6204 100644
--- a/src/Config.php
+++ b/src/Config.php
@@ -14,6 +14,7 @@
use PHP_CodeSniffer\Exceptions\DeepExitException;
use PHP_CodeSniffer\Exceptions\RuntimeException;
+use PHP_CodeSniffer\Util\Common;
/**
* Stores the configuration used to run PHPCS and PHPCBF.
@@ -79,7 +80,7 @@ class Config
*
* @var string
*/
- const VERSION = '3.6.1';
+ const VERSION = '3.7.0';
/**
* Package stability; either stable, beta or alpha.
@@ -363,7 +364,7 @@ public function __construct(array $cliArgs=[], $dieOnUnknownArg=true)
$lastDir = $currentDir;
$currentDir = dirname($currentDir);
- } while ($currentDir !== '.' && $currentDir !== $lastDir && @is_readable($currentDir) === true);
+ } while ($currentDir !== '.' && $currentDir !== $lastDir && Common::isReadable($currentDir) === true);
}//end if
if (defined('STDIN') === false
@@ -459,7 +460,7 @@ public function setCommandLineValues($args)
/**
* Restore default values for all possible command line arguments.
*
- * @return array
+ * @return void
*/
public function restoreDefaults()
{
@@ -1656,7 +1657,7 @@ public static function getAllConfigData()
return [];
}
- if (is_readable($configFile) === false) {
+ if (Common::isReadable($configFile) === false) {
$error = 'ERROR: Config file '.$configFile.' is not readable'.PHP_EOL.PHP_EOL;
throw new DeepExitException($error, 3);
}
diff --git a/src/Files/DummyFile.php b/src/Files/DummyFile.php
index 3275bf0920..601430301d 100644
--- a/src/Files/DummyFile.php
+++ b/src/Files/DummyFile.php
@@ -38,7 +38,7 @@ public function __construct($content, Ruleset $ruleset, Config $config)
// This is done by including: phpcs_input_file: [file path]
// as the first line of content.
$path = 'STDIN';
- if ($content !== null) {
+ if ($content !== '') {
if (substr($content, 0, 17) === 'phpcs_input_file:') {
$eolPos = strpos($content, $this->eolChar);
$filename = trim(substr($content, 17, ($eolPos - 17)));
diff --git a/src/Files/File.php b/src/Files/File.php
index e4056bc8f8..cfe6f52af3 100644
--- a/src/Files/File.php
+++ b/src/Files/File.php
@@ -1283,6 +1283,7 @@ public function getDeclarationName($stackPtr)
* 'name' => '$var', // The variable name.
* 'token' => integer, // The stack pointer to the variable name.
* 'content' => string, // The full content of the variable definition.
+ * 'has_attributes' => boolean, // Does the parameter have one or more attributes attached ?
* 'pass_by_reference' => boolean, // Is the variable passed by reference?
* 'reference_token' => integer, // The stack pointer to the reference operator
* // or FALSE if the param is not passed by reference.
@@ -1355,6 +1356,7 @@ public function getMethodParameters($stackPtr)
$defaultStart = null;
$equalToken = null;
$paramCount = 0;
+ $hasAttributes = false;
$passByReference = false;
$referenceToken = false;
$variableLength = false;
@@ -1373,18 +1375,25 @@ public function getMethodParameters($stackPtr)
if (isset($this->tokens[$i]['parenthesis_opener']) === true) {
// Don't do this if it's the close parenthesis for the method.
if ($i !== $this->tokens[$i]['parenthesis_closer']) {
- $i = ($this->tokens[$i]['parenthesis_closer'] + 1);
+ $i = $this->tokens[$i]['parenthesis_closer'];
+ continue;
}
}
if (isset($this->tokens[$i]['bracket_opener']) === true) {
- // Don't do this if it's the close parenthesis for the method.
if ($i !== $this->tokens[$i]['bracket_closer']) {
- $i = ($this->tokens[$i]['bracket_closer'] + 1);
+ $i = $this->tokens[$i]['bracket_closer'];
+ continue;
}
}
switch ($this->tokens[$i]['code']) {
+ case T_ATTRIBUTE:
+ $hasAttributes = true;
+
+ // Skip to the end of the attribute.
+ $i = $this->tokens[$i]['attribute_closer'];
+ break;
case T_BITWISE_AND:
if ($defaultStart === null) {
$passByReference = true;
@@ -1501,6 +1510,7 @@ public function getMethodParameters($stackPtr)
$vars[$paramCount]['default_equal_token'] = $equalToken;
}
+ $vars[$paramCount]['has_attributes'] = $hasAttributes;
$vars[$paramCount]['pass_by_reference'] = $passByReference;
$vars[$paramCount]['reference_token'] = $referenceToken;
$vars[$paramCount]['variable_length'] = $variableLength;
@@ -1526,6 +1536,7 @@ public function getMethodParameters($stackPtr)
$paramStart = ($i + 1);
$defaultStart = null;
$equalToken = null;
+ $hasAttributes = false;
$passByReference = false;
$referenceToken = false;
$variableLength = false;
@@ -1800,6 +1811,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;
@@ -1807,6 +1819,7 @@ public function getMemberProperties($stackPtr)
$scope = 'public';
$scopeSpecified = false;
$isStatic = false;
+ $isReadonly = false;
$startOfStatement = $this->findPrevious(
[
@@ -1839,6 +1852,9 @@ public function getMemberProperties($stackPtr)
case T_STATIC:
$isStatic = true;
break;
+ case T_READONLY:
+ $isReadonly = true;
+ break;
}
}//end for
@@ -1890,6 +1906,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,
diff --git a/src/Files/FileList.php b/src/Files/FileList.php
index e889fc3d7e..66833a3ee4 100644
--- a/src/Files/FileList.php
+++ b/src/Files/FileList.php
@@ -16,6 +16,7 @@
use PHP_CodeSniffer\Ruleset;
use PHP_CodeSniffer\Config;
use PHP_CodeSniffer\Exceptions\DeepExitException;
+use ReturnTypeWillChange;
class FileList implements \Iterator, \Countable
{
@@ -169,6 +170,7 @@ private function getFilterClass()
*
* @return void
*/
+ #[ReturnTypeWillChange]
public function rewind()
{
reset($this->files);
@@ -181,6 +183,7 @@ public function rewind()
*
* @return \PHP_CodeSniffer\Files\File
*/
+ #[ReturnTypeWillChange]
public function current()
{
$path = key($this->files);
@@ -198,6 +201,7 @@ public function current()
*
* @return void
*/
+ #[ReturnTypeWillChange]
public function key()
{
return key($this->files);
@@ -210,6 +214,7 @@ public function key()
*
* @return void
*/
+ #[ReturnTypeWillChange]
public function next()
{
next($this->files);
@@ -222,6 +227,7 @@ public function next()
*
* @return boolean
*/
+ #[ReturnTypeWillChange]
public function valid()
{
if (current($this->files) === false) {
@@ -238,6 +244,7 @@ public function valid()
*
* @return integer
*/
+ #[ReturnTypeWillChange]
public function count()
{
return $this->numFiles;
diff --git a/src/Filters/Filter.php b/src/Filters/Filter.php
index 5bed499b25..a1246a2c52 100644
--- a/src/Filters/Filter.php
+++ b/src/Filters/Filter.php
@@ -12,6 +12,7 @@
use PHP_CodeSniffer\Util;
use PHP_CodeSniffer\Ruleset;
use PHP_CodeSniffer\Config;
+use ReturnTypeWillChange;
class Filter extends \RecursiveFilterIterator
{
@@ -89,6 +90,7 @@ public function __construct($iterator, $basedir, Config $config, Ruleset $rulese
*
* @return bool
*/
+ #[ReturnTypeWillChange]
public function accept()
{
$filePath = $this->current();
@@ -130,6 +132,7 @@ public function accept()
*
* @return \RecursiveIterator
*/
+ #[ReturnTypeWillChange]
public function getChildren()
{
$filterClass = get_called_class();
diff --git a/src/Fixer.php b/src/Fixer.php
index 1bea0555d4..b8dc05b16e 100644
--- a/src/Fixer.php
+++ b/src/Fixer.php
@@ -421,6 +421,7 @@ public function endChangeset()
}
$this->changeset = [];
+ return true;
}//end endChangeset()
diff --git a/src/Runner.php b/src/Runner.php
index c6f2c92edf..253ec91f9f 100644
--- a/src/Runner.php
+++ b/src/Runner.php
@@ -232,7 +232,7 @@ public function runPHPCBF()
/**
* Exits if the minimum requirements of PHP_CodeSniffer are not met.
*
- * @return array
+ * @return void
* @throws \PHP_CodeSniffer\Exceptions\DeepExitException If the requirements are not met.
*/
public function checkRequirements()
@@ -291,7 +291,7 @@ public function init()
// Ensure this option is enabled or else line endings will not always
// be detected properly for files created on a Mac with the /r line ending.
- ini_set('auto_detect_line_endings', true);
+ @ini_set('auto_detect_line_endings', true);
// Disable the PCRE JIT as this caused issues with parallel running.
ini_set('pcre.jit', false);
@@ -732,7 +732,7 @@ private function processChildProcs($childProcs)
if (isset($childOutput) === false) {
// The child process died, so the run has failed.
- $file = new DummyFile(null, $this->ruleset, $this->config);
+ $file = new DummyFile('', $this->ruleset, $this->config);
$file->setErrorCounts(1, 0, 0, 0);
$this->printProgress($file, $totalBatches, $numProcessed);
$success = false;
@@ -756,7 +756,7 @@ private function processChildProcs($childProcs)
}
// Fake a processed file so we can print progress output for the batch.
- $file = new DummyFile(null, $this->ruleset, $this->config);
+ $file = new DummyFile('', $this->ruleset, $this->config);
$file->setErrorCounts(
$childOutput['totalErrors'],
$childOutput['totalWarnings'],
diff --git a/src/Standards/Generic/Sniffs/Formatting/MultipleStatementAlignmentSniff.php b/src/Standards/Generic/Sniffs/Formatting/MultipleStatementAlignmentSniff.php
index b1b3cc64e3..802e594485 100644
--- a/src/Standards/Generic/Sniffs/Formatting/MultipleStatementAlignmentSniff.php
+++ b/src/Standards/Generic/Sniffs/Formatting/MultipleStatementAlignmentSniff.php
@@ -80,25 +80,6 @@ public function register()
*/
public function process(File $phpcsFile, $stackPtr)
{
- $tokens = $phpcsFile->getTokens();
-
- // Ignore assignments used in a condition, like an IF or FOR.
- if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) {
- // If the parenthesis is on the same line as the assignment,
- // then it should be ignored as it is specifically being grouped.
- $parens = $tokens[$stackPtr]['nested_parenthesis'];
- $lastParen = array_pop($parens);
- if ($tokens[$lastParen]['line'] === $tokens[$stackPtr]['line']) {
- return;
- }
-
- foreach ($tokens[$stackPtr]['nested_parenthesis'] as $start => $end) {
- if (isset($tokens[$start]['parenthesis_owner']) === true) {
- return;
- }
- }
- }
-
$lastAssign = $this->checkAlignment($phpcsFile, $stackPtr);
return ($lastAssign + 1);
@@ -120,6 +101,23 @@ public function checkAlignment($phpcsFile, $stackPtr, $end=null)
{
$tokens = $phpcsFile->getTokens();
+ // Ignore assignments used in a condition, like an IF or FOR or closure param defaults.
+ if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) {
+ // If the parenthesis is on the same line as the assignment,
+ // then it should be ignored as it is specifically being grouped.
+ $parens = $tokens[$stackPtr]['nested_parenthesis'];
+ $lastParen = array_pop($parens);
+ if ($tokens[$lastParen]['line'] === $tokens[$stackPtr]['line']) {
+ return $stackPtr;
+ }
+
+ foreach ($tokens[$stackPtr]['nested_parenthesis'] as $start => $end) {
+ if (isset($tokens[$start]['parenthesis_owner']) === true) {
+ return $stackPtr;
+ }
+ }
+ }
+
$assignments = [];
$prevAssign = null;
$lastLine = $tokens[$stackPtr]['line'];
diff --git a/src/Standards/Generic/Sniffs/Metrics/CyclomaticComplexitySniff.php b/src/Standards/Generic/Sniffs/Metrics/CyclomaticComplexitySniff.php
index df70df221f..18100c2d67 100644
--- a/src/Standards/Generic/Sniffs/Metrics/CyclomaticComplexitySniff.php
+++ b/src/Standards/Generic/Sniffs/Metrics/CyclomaticComplexitySniff.php
@@ -71,15 +71,18 @@ 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_DO => true,
- T_ELSEIF => 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,
];
$complexity = 1;
diff --git a/src/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php b/src/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php
index e10047b64e..2ddf314a02 100644
--- a/src/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php
+++ b/src/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php
@@ -82,6 +82,7 @@ public function register()
T_PRIVATE,
T_PROTECTED,
T_PUBLIC,
+ T_READONLY,
T_REQUIRE,
T_REQUIRE_ONCE,
T_RETURN,
diff --git a/src/Standards/Generic/Tests/Arrays/DisallowLongArraySyntaxUnitTest.php b/src/Standards/Generic/Tests/Arrays/DisallowLongArraySyntaxUnitTest.php
index 0297681061..af1d9c9a86 100644
--- a/src/Standards/Generic/Tests/Arrays/DisallowLongArraySyntaxUnitTest.php
+++ b/src/Standards/Generic/Tests/Arrays/DisallowLongArraySyntaxUnitTest.php
@@ -35,7 +35,6 @@ public function getErrorList($testFile='')
6 => 1,
7 => 1,
12 => 1,
- 13 => 1,
];
case 'DisallowLongArraySyntaxUnitTest.2.inc':
return [
diff --git a/src/Standards/Generic/Tests/Formatting/MultipleStatementAlignmentUnitTest.inc b/src/Standards/Generic/Tests/Formatting/MultipleStatementAlignmentUnitTest.inc
index e42225e89a..ec71d4b670 100644
--- a/src/Standards/Generic/Tests/Formatting/MultipleStatementAlignmentUnitTest.inc
+++ b/src/Standards/Generic/Tests/Formatting/MultipleStatementAlignmentUnitTest.inc
@@ -478,3 +478,27 @@ class Test
protected static $thisIsAReallyLongVariableName = [];
}
+
+// Issue #3460.
+function issue3460_invalid() {
+ $a = static function ($variables = false) use ($foo) {
+ return $variables;
+ };
+ $b = $a;
+}
+
+function issue3460_valid() {
+ $a = static function ($variables = false) use ($foo) {
+ return $variables;
+ };
+ $b = $a;
+}
+
+function makeSureThatAssignmentWithinClosureAreStillHandled() {
+ $a = static function ($variables = []) use ($temp) {
+ $a = 'foo';
+ $bar = 'bar';
+ $longer = 'longer';
+ return $variables;
+ };
+}
diff --git a/src/Standards/Generic/Tests/Formatting/MultipleStatementAlignmentUnitTest.inc.fixed b/src/Standards/Generic/Tests/Formatting/MultipleStatementAlignmentUnitTest.inc.fixed
index 5d5516d1fa..137a8ef9af 100644
--- a/src/Standards/Generic/Tests/Formatting/MultipleStatementAlignmentUnitTest.inc.fixed
+++ b/src/Standards/Generic/Tests/Formatting/MultipleStatementAlignmentUnitTest.inc.fixed
@@ -478,3 +478,27 @@ class Test
protected static $thisIsAReallyLongVariableName = [];
}
+
+// Issue #3460.
+function issue3460_invalid() {
+ $a = static function ($variables = false) use ($foo) {
+ return $variables;
+ };
+ $b = $a;
+}
+
+function issue3460_valid() {
+ $a = static function ($variables = false) use ($foo) {
+ return $variables;
+ };
+ $b = $a;
+}
+
+function makeSureThatAssignmentWithinClosureAreStillHandled() {
+ $a = static function ($variables = []) use ($temp) {
+ $a = 'foo';
+ $bar = 'bar';
+ $longer = 'longer';
+ return $variables;
+ };
+}
diff --git a/src/Standards/Generic/Tests/Formatting/MultipleStatementAlignmentUnitTest.php b/src/Standards/Generic/Tests/Formatting/MultipleStatementAlignmentUnitTest.php
index eef66a5d04..23f2f9a780 100644
--- a/src/Standards/Generic/Tests/Formatting/MultipleStatementAlignmentUnitTest.php
+++ b/src/Standards/Generic/Tests/Formatting/MultipleStatementAlignmentUnitTest.php
@@ -118,6 +118,9 @@ public function getWarningList($testFile='MultipleStatementAlignmentUnitTest.inc
442 => 1,
443 => 1,
454 => 1,
+ 487 => 1,
+ 499 => 1,
+ 500 => 1,
];
break;
case 'MultipleStatementAlignmentUnitTest.js':
diff --git a/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.inc b/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.inc
index 151ffed6a1..f6c6bb757b 100644
--- a/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.inc
+++ b/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.inc
@@ -79,9 +79,11 @@ function complexityTwenty()
switch ($condition) {
case '1':
- if ($condition) {
- } else if ($cond) {
- }
+ do {
+ if ($condition) {
+ } else if ($cond) {
+ }
+ } while ($cond);
break;
case '2':
while ($cond) {
@@ -116,9 +118,11 @@ function complexityTwenty()
function complexityTwentyOne()
{
while ($condition === true) {
- if ($condition) {
- } else if ($cond) {
- }
+ do {
+ if ($condition) {
+ } else if ($cond) {
+ }
+ } while ($cond);
}
switch ($condition) {
@@ -157,4 +161,276 @@ function complexityTwentyOne()
}
}
+
+function complexityTenWithTernaries()
+{
+ $value1 = (empty($condition1)) ? $value1A : $value1B;
+ $value2 = (empty($condition2)) ? $value2A : $value2B;
+
+ switch ($condition) {
+ case '1':
+ if ($condition) {
+ } else if ($cond) {
+ }
+ break;
+ case '2':
+ while ($cond) {
+ echo 'hi';
+ }
+ break;
+ case '3':
+ break;
+ default:
+ break;
+ }
+}
+
+
+function complexityElevenWithTernaries()
+{
+ $value1 = (empty($condition1)) ? $value1A : $value1B;
+ $value2 = (empty($condition2)) ? $value2A : $value2B;
+ $value3 = (empty($condition3)) ? $value3A : $value3B;
+
+ switch ($condition) {
+ case '1':
+ if ($condition) {
+ } else if ($cond) {
+ }
+ break;
+ case '2':
+ while ($cond) {
+ echo 'hi';
+ }
+ break;
+ case '3':
+ break;
+ default:
+ break;
+ }
+}
+
+
+function complexityTenWithNestedTernaries()
+{
+ $value1 = true ? $value1A : false ? $value1B : $value1C;
+
+ switch ($condition) {
+ case '1':
+ if ($condition) {
+ } else if ($cond) {
+ }
+ break;
+ case '2':
+ while ($cond) {
+ echo 'hi';
+ }
+ break;
+ case '3':
+ break;
+ default:
+ break;
+ }
+}
+
+
+function complexityElevenWithNestedTernaries()
+{
+ $value1 = (empty($condition1)) ? $value1A : $value1B;
+ $value2 = true ? $value2A : false ? $value2B : $value2C;
+
+ switch ($condition) {
+ case '1':
+ if ($condition) {
+ } else if ($cond) {
+ }
+ break;
+ case '2':
+ while ($cond) {
+ echo 'hi';
+ }
+ break;
+ case '3':
+ break;
+ default:
+ break;
+ }
+}
+
+
+function complexityTenWithNullCoalescence()
+{
+ $value1 = $value1A ?? $value1B;
+ $value2 = $value2A ?? $value2B;
+
+ switch ($condition) {
+ case '1':
+ if ($condition) {
+ } else if ($cond) {
+ }
+ break;
+ case '2':
+ while ($cond) {
+ echo 'hi';
+ }
+ break;
+ case '3':
+ break;
+ default:
+ break;
+ }
+}
+
+
+function complexityElevenWithNullCoalescence()
+{
+ $value1 = $value1A ?? $value1B;
+ $value2 = $value2A ?? $value2B;
+ $value3 = $value3A ?? $value3B;
+
+ switch ($condition) {
+ case '1':
+ if ($condition) {
+ } else if ($cond) {
+ }
+ break;
+ case '2':
+ while ($cond) {
+ echo 'hi';
+ }
+ break;
+ case '3':
+ break;
+ default:
+ break;
+ }
+}
+
+
+function complexityTenWithNestedNullCoalescence()
+{
+ $value1 = $value1A ?? $value1B ?? $value1C;
+
+ switch ($condition) {
+ case '1':
+ if ($condition) {
+ } else if ($cond) {
+ }
+ break;
+ case '2':
+ while ($cond) {
+ echo 'hi';
+ }
+ break;
+ case '3':
+ break;
+ default:
+ break;
+ }
+}
+
+
+function complexityElevenWithNestedNullCoalescence()
+{
+ $value1 = $value1A ?? $value1B;
+ $value2 = $value2A ?? $value2B ?? $value2C;
+
+ switch ($condition) {
+ case '1':
+ if ($condition) {
+ } else if ($cond) {
+ }
+ break;
+ case '2':
+ while ($cond) {
+ echo 'hi';
+ }
+ break;
+ case '3':
+ break;
+ default:
+ break;
+ }
+}
+
+
+function complexityTenWithNullCoalescenceAssignment()
+{
+ $value1 ??= $default1;
+ $value2 ??= $default2;
+
+ switch ($condition) {
+ case '1':
+ if ($condition) {
+ } else if ($cond) {
+ }
+ break;
+ case '2':
+ while ($cond) {
+ echo 'hi';
+ }
+ break;
+ case '3':
+ break;
+ default:
+ break;
+ }
+}
+
+
+function complexityElevenWithNullCoalescenceAssignment()
+{
+ $value1 ??= $default1;
+ $value2 ??= $default2;
+ $value3 ??= $default3;
+
+ switch ($condition) {
+ case '1':
+ if ($condition) {
+ } else if ($cond) {
+ }
+ break;
+ case '2':
+ while ($cond) {
+ echo 'hi';
+ }
+ break;
+ case '3':
+ break;
+ default:
+ break;
+ }
+}
+
+
+function complexityFiveWithMatch()
+{
+ return match(strtolower(substr($monthName, 0, 3))){
+ 'apr', 'jun', 'sep', 'nov' => 30,
+ 'jan', 'mar', 'may', 'jul', 'aug', 'oct', 'dec' => 31,
+ 'feb' => is_leap_year($year) ? 29 : 28,
+ default => throw new InvalidArgumentException("Invalid month"),
+ }
+}
+
+
+function complexityFourteenWithMatch()
+{
+ return match(strtolower(substr($monthName, 0, 3))) {
+ 'jan' => 31,
+ 'feb' => is_leap_year($year) ? 29 : 28,
+ 'mar' => 31,
+ 'apr' => 30,
+ 'may' => 31,
+ 'jun' => 30,
+ 'jul' => 31,
+ 'aug' => 31,
+ 'sep' => 30,
+ 'oct' => 31,
+ 'nov' => 30,
+ 'dec' => 31,
+ default => throw new InvalidArgumentException("Invalid month"),
+ };
+}
+
?>
diff --git a/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.php b/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.php
index e8099317cd..92635fc44d 100644
--- a/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.php
+++ b/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.php
@@ -25,7 +25,7 @@ class CyclomaticComplexityUnitTest extends AbstractSniffUnitTest
*/
public function getErrorList()
{
- return [116 => 1];
+ return [118 => 1];
}//end getErrorList()
@@ -41,8 +41,14 @@ public function getErrorList()
public function getWarningList()
{
return [
- 45 => 1,
- 72 => 1,
+ 45 => 1,
+ 72 => 1,
+ 189 => 1,
+ 237 => 1,
+ 285 => 1,
+ 333 => 1,
+ 381 => 1,
+ 417 => 1,
];
}//end getWarningList()
diff --git a/src/Standards/Generic/Tests/NamingConventions/InterfaceNameSuffixUnitTest.php b/src/Standards/Generic/Tests/NamingConventions/InterfaceNameSuffixUnitTest.php
index b4d9696f91..be23f7a656 100644
--- a/src/Standards/Generic/Tests/NamingConventions/InterfaceNameSuffixUnitTest.php
+++ b/src/Standards/Generic/Tests/NamingConventions/InterfaceNameSuffixUnitTest.php
@@ -10,7 +10,7 @@
use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest;
-class InterfaceSuffixNameUnitTest extends AbstractSniffUnitTest
+class InterfaceNameSuffixUnitTest extends AbstractSniffUnitTest
{
diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc
index 8d003c3bcc..6c3e3f9a21 100644
--- a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc
+++ b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc
@@ -35,5 +35,9 @@ $r = Match ($x) {
DEFAULT, => 3,
};
+class Reading {
+ Public READOnly int $var;
+}
+
__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..1c8550387b 100644
--- a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc.fixed
+++ b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc.fixed
@@ -35,5 +35,9 @@ $r = match ($x) {
default, => 3,
};
+class Reading {
+ public readonly int $var;
+}
+
__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..bf859e7fb9 100644
--- a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.php
+++ b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.php
@@ -40,6 +40,7 @@ public function getErrorList()
31 => 1,
32 => 1,
35 => 1,
+ 39 => 2,
];
}//end getErrorList()
diff --git a/src/Standards/PEAR/Sniffs/Commenting/FunctionCommentSniff.php b/src/Standards/PEAR/Sniffs/Commenting/FunctionCommentSniff.php
index 3c3dbfc408..408856fc5a 100644
--- a/src/Standards/PEAR/Sniffs/Commenting/FunctionCommentSniff.php
+++ b/src/Standards/PEAR/Sniffs/Commenting/FunctionCommentSniff.php
@@ -68,11 +68,25 @@ public function process(File $phpcsFile, $stackPtr)
return;
}
- $tokens = $phpcsFile->getTokens();
- $ignore = Tokens::$methodPrefixes;
- $ignore[] = T_WHITESPACE;
+ $tokens = $phpcsFile->getTokens();
+ $ignore = Tokens::$methodPrefixes;
+ $ignore[T_WHITESPACE] = T_WHITESPACE;
+
+ for ($commentEnd = ($stackPtr - 1); $commentEnd >= 0; $commentEnd--) {
+ if (isset($ignore[$tokens[$commentEnd]['code']]) === true) {
+ continue;
+ }
+
+ if ($tokens[$commentEnd]['code'] === T_ATTRIBUTE_END
+ && isset($tokens[$commentEnd]['attribute_opener']) === true
+ ) {
+ $commentEnd = $tokens[$commentEnd]['attribute_opener'];
+ continue;
+ }
+
+ break;
+ }
- $commentEnd = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true);
if ($tokens[$commentEnd]['code'] === T_COMMENT) {
// Inline comments might just be closing comments for
// control structures or functions instead of function comments
@@ -106,8 +120,19 @@ public function process(File $phpcsFile, $stackPtr)
}
if ($tokens[$commentEnd]['line'] !== ($tokens[$stackPtr]['line'] - 1)) {
- $error = 'There must be no blank lines after the function comment';
- $phpcsFile->addError($error, $commentEnd, 'SpacingAfter');
+ for ($i = ($commentEnd + 1); $i < $stackPtr; $i++) {
+ if ($tokens[$i]['column'] !== 1) {
+ continue;
+ }
+
+ if ($tokens[$i]['code'] === T_WHITESPACE
+ && $tokens[$i]['line'] !== $tokens[($i + 1)]['line']
+ ) {
+ $error = 'There must be no blank lines after the function comment';
+ $phpcsFile->addError($error, $commentEnd, 'SpacingAfter');
+ break;
+ }
+ }
}
$commentStart = $tokens[$commentEnd]['comment_opener'];
diff --git a/src/Standards/PEAR/Sniffs/Functions/FunctionDeclarationSniff.php b/src/Standards/PEAR/Sniffs/Functions/FunctionDeclarationSniff.php
index 1924f9c4df..bd59a7cd90 100644
--- a/src/Standards/PEAR/Sniffs/Functions/FunctionDeclarationSniff.php
+++ b/src/Standards/PEAR/Sniffs/Functions/FunctionDeclarationSniff.php
@@ -460,14 +460,14 @@ public function processArgumentList($phpcsFile, $stackPtr, $indent, $type='funct
if ($tokens[$i]['code'] === T_WHITESPACE
&& $tokens[$i]['line'] !== $tokens[($i + 1)]['line']
) {
- // This is an empty line, so don't check the indent.
- $foundIndent = $expectedIndent;
-
$error = 'Blank lines are not allowed in a multi-line '.$type.' declaration';
$fix = $phpcsFile->addFixableError($error, $i, 'EmptyLine');
if ($fix === true) {
$phpcsFile->fixer->replaceToken($i, '');
}
+
+ // This is an empty line, so don't check the indent.
+ continue;
} else if ($tokens[$i]['code'] === T_WHITESPACE) {
$foundIndent = $tokens[$i]['length'];
} else if ($tokens[$i]['code'] === T_DOC_COMMENT_WHITESPACE) {
@@ -507,6 +507,13 @@ public function processArgumentList($phpcsFile, $stackPtr, $indent, $type='funct
$lastLine = $tokens[$i]['line'];
continue;
}
+
+ if ($tokens[$i]['code'] === T_ATTRIBUTE) {
+ // Skip attributes as they have their own indentation rules.
+ $i = $tokens[$i]['attribute_closer'];
+ $lastLine = $tokens[$i]['line'];
+ continue;
+ }
}//end for
}//end processArgumentList()
diff --git a/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc b/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc
index 0e935eadf6..5c3295fd60 100644
--- a/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc
+++ b/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc
@@ -429,3 +429,50 @@ public function ignored() {
}
// phpcs:set PEAR.Commenting.FunctionComment specialMethods[] __construct,__destruct
+
+class Something implements JsonSerializable {
+ /**
+ * Single attribute.
+ *
+ * @return mixed
+ */
+ #[ReturnTypeWillChange]
+ public function jsonSerialize() {}
+
+ /**
+ * Multiple attributes.
+ *
+ * @return Something
+ */
+ #[AttributeA]
+ #[AttributeB]
+ public function methodName() {}
+
+ /**
+ * Blank line between docblock and attribute.
+ *
+ * @return mixed
+ */
+
+ #[ReturnTypeWillChange]
+ public function blankLineDetectionA() {}
+
+ /**
+ * Blank line between attribute and function declaration.
+ *
+ * @return mixed
+ */
+ #[ReturnTypeWillChange]
+
+ public function blankLineDetectionB() {}
+
+ /**
+ * Blank line between both docblock and attribute and attribute and function declaration.
+ *
+ * @return mixed
+ */
+
+ #[ReturnTypeWillChange]
+
+ public function blankLineDetectionC() {}
+}
diff --git a/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc.fixed b/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc.fixed
index 29588134d6..751b09c665 100644
--- a/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc.fixed
+++ b/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc.fixed
@@ -429,3 +429,50 @@ public function ignored() {
}
// phpcs:set PEAR.Commenting.FunctionComment specialMethods[] __construct,__destruct
+
+class Something implements JsonSerializable {
+ /**
+ * Single attribute.
+ *
+ * @return mixed
+ */
+ #[ReturnTypeWillChange]
+ public function jsonSerialize() {}
+
+ /**
+ * Multiple attributes.
+ *
+ * @return Something
+ */
+ #[AttributeA]
+ #[AttributeB]
+ public function methodName() {}
+
+ /**
+ * Blank line between docblock and attribute.
+ *
+ * @return mixed
+ */
+
+ #[ReturnTypeWillChange]
+ public function blankLineDetectionA() {}
+
+ /**
+ * Blank line between attribute and function declaration.
+ *
+ * @return mixed
+ */
+ #[ReturnTypeWillChange]
+
+ public function blankLineDetectionB() {}
+
+ /**
+ * Blank line between both docblock and attribute and attribute and function declaration.
+ *
+ * @return mixed
+ */
+
+ #[ReturnTypeWillChange]
+
+ public function blankLineDetectionC() {}
+}
diff --git a/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.php b/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.php
index a7b35e60a1..734ff73e50 100644
--- a/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.php
+++ b/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.php
@@ -70,6 +70,9 @@ public function getErrorList()
364 => 1,
406 => 1,
417 => 1,
+ 455 => 1,
+ 464 => 1,
+ 473 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc b/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc
index 0003ca0bda..02e0a20dbf 100644
--- a/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc
+++ b/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc
@@ -372,3 +372,49 @@ private string $private,
) {
}
}
+
+class ConstructorPropertyPromotionMultiLineAttributesOK
+{
+ public function __construct(
+ #[ORM\ManyToOne(
+ Something: true,
+ SomethingElse: 'text',
+ )]
+ #[Groups([
+ 'ArrayEntry',
+ 'Another.ArrayEntry',
+ ])]
+ #[MoreGroups(
+ [
+ 'ArrayEntry',
+ 'Another.ArrayEntry',
+ ]
+ )]
+ private Type $property
+ ) {
+ // Do something.
+ }
+}
+
+class ConstructorPropertyPromotionMultiLineAttributesIncorrectIndent
+{
+ public function __construct(
+ #[ORM\ManyToOne(
+ Something: true,
+ SomethingElse: 'text',
+ )]
+ #[Groups([
+ 'ArrayEntry',
+ 'Another.ArrayEntry',
+ ])]
+ #[MoreGroups(
+ [
+ 'ArrayEntry',
+ 'Another.ArrayEntry',
+ ]
+ )]
+ private Type $property
+ ) {
+ // Do something.
+ }
+}
diff --git a/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc.fixed b/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc.fixed
index 0f8d39db06..0d67e9f758 100644
--- a/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc.fixed
+++ b/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc.fixed
@@ -370,3 +370,49 @@ class ConstructorPropertyPromotionMultiLineDocblockAndAttributeIncorrectIndent
) {
}
}
+
+class ConstructorPropertyPromotionMultiLineAttributesOK
+{
+ public function __construct(
+ #[ORM\ManyToOne(
+ Something: true,
+ SomethingElse: 'text',
+ )]
+ #[Groups([
+ 'ArrayEntry',
+ 'Another.ArrayEntry',
+ ])]
+ #[MoreGroups(
+ [
+ 'ArrayEntry',
+ 'Another.ArrayEntry',
+ ]
+ )]
+ private Type $property
+ ) {
+ // Do something.
+ }
+}
+
+class ConstructorPropertyPromotionMultiLineAttributesIncorrectIndent
+{
+ public function __construct(
+ #[ORM\ManyToOne(
+ Something: true,
+ SomethingElse: 'text',
+ )]
+ #[Groups([
+ 'ArrayEntry',
+ 'Another.ArrayEntry',
+ ])]
+ #[MoreGroups(
+ [
+ 'ArrayEntry',
+ 'Another.ArrayEntry',
+ ]
+ )]
+ private Type $property
+ ) {
+ // Do something.
+ }
+}
diff --git a/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.php b/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.php
index 161d2b34f5..01ab3e8482 100644
--- a/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.php
+++ b/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.php
@@ -97,6 +97,8 @@ public function getErrorList($testFile='FunctionDeclarationUnitTest.inc')
369 => 1,
370 => 1,
371 => 1,
+ 402 => 1,
+ 406 => 1,
];
} else {
$errors = [
diff --git a/src/Standards/PSR12/Sniffs/Classes/ClassInstantiationSniff.php b/src/Standards/PSR12/Sniffs/Classes/ClassInstantiationSniff.php
index 804ecfe1c7..e4ebd576a3 100644
--- a/src/Standards/PSR12/Sniffs/Classes/ClassInstantiationSniff.php
+++ b/src/Standards/PSR12/Sniffs/Classes/ClassInstantiationSniff.php
@@ -63,6 +63,14 @@ public function process(File $phpcsFile, $stackPtr)
continue;
}
+ // Skip over potential attributes for anonymous classes.
+ if ($tokens[$i]['code'] === T_ATTRIBUTE
+ && isset($tokens[$i]['attribute_closer']) === true
+ ) {
+ $i = $tokens[$i]['attribute_closer'];
+ continue;
+ }
+
if ($tokens[$i]['code'] === T_OPEN_SQUARE_BRACKET
|| $tokens[$i]['code'] === T_OPEN_CURLY_BRACKET
) {
@@ -72,7 +80,7 @@ public function process(File $phpcsFile, $stackPtr)
$classNameEnd = $i;
break;
- }
+ }//end for
if ($classNameEnd === null) {
return;
@@ -88,6 +96,11 @@ public function process(File $phpcsFile, $stackPtr)
return;
}
+ if ($classNameEnd === $stackPtr) {
+ // Failed to find the class name.
+ return;
+ }
+
$error = 'Parentheses must be used when instantiating a new class';
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'MissingParentheses');
if ($fix === true) {
diff --git a/src/Standards/PSR12/Sniffs/Classes/OpeningBraceSpaceSniff.php b/src/Standards/PSR12/Sniffs/Classes/OpeningBraceSpaceSniff.php
new file mode 100644
index 0000000000..83ffda4df0
--- /dev/null
+++ b/src/Standards/PSR12/Sniffs/Classes/OpeningBraceSpaceSniff.php
@@ -0,0 +1,80 @@
+
+ * @copyright 2006-2019 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Standards\PSR12\Sniffs\Classes;
+
+use PHP_CodeSniffer\Files\File;
+use PHP_CodeSniffer\Sniffs\Sniff;
+use PHP_CodeSniffer\Util\Tokens;
+
+class OpeningBraceSpaceSniff implements Sniff
+{
+
+
+ /**
+ * Returns an array of tokens this test wants to listen for.
+ *
+ * @return array
+ */
+ public function register()
+ {
+ return Tokens::$ooScopeTokens;
+
+ }//end register()
+
+
+ /**
+ * Processes this test, when one of its tokens is encountered.
+ *
+ * @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
+ */
+ public function process(File $phpcsFile, $stackPtr)
+ {
+ $tokens = $phpcsFile->getTokens();
+ if (isset($tokens[$stackPtr]['scope_opener']) === false) {
+ return;
+ }
+
+ $opener = $tokens[$stackPtr]['scope_opener'];
+ $next = $phpcsFile->findNext(T_WHITESPACE, ($opener + 1), null, true);
+ if ($next === false
+ || $tokens[$next]['line'] <= ($tokens[$opener]['line'] + 1)
+ ) {
+ return;
+ }
+
+ $error = 'Opening brace must not be followed by a blank line';
+ $fix = $phpcsFile->addFixableError($error, $opener, 'Found');
+ if ($fix === false) {
+ return;
+ }
+
+ $phpcsFile->fixer->beginChangeset();
+ for ($i = ($opener + 1); $i < $next; $i++) {
+ if ($tokens[$i]['line'] === $tokens[$opener]['line']) {
+ continue;
+ }
+
+ if ($tokens[$i]['line'] === $tokens[$next]['line']) {
+ break;
+ }
+
+ $phpcsFile->fixer->replaceToken($i, '');
+ }
+
+ $phpcsFile->fixer->endChangeset();
+
+ }//end process()
+
+
+}//end class
diff --git a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc
index ca0dc3614a..d933ee273e 100644
--- a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc
+++ b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc
@@ -36,3 +36,9 @@ $a = new ${$varHoldingClassName};
$class = new $obj?->classname();
$class = new $obj?->classname;
$class = new ${$obj?->classname};
+
+// Issue 3456.
+// Anon classes should be skipped, even when there is an attribute between the new and the class keywords.
+$anonWithAttribute = new #[SomeAttribute('summary')] class {
+ public const SOME_STUFF = 'foo';
+};
diff --git a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc.fixed b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc.fixed
index a4d209cc16..02e3544fa6 100644
--- a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc.fixed
+++ b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc.fixed
@@ -36,3 +36,9 @@ $a = new ${$varHoldingClassName}();
$class = new $obj?->classname();
$class = new $obj?->classname();
$class = new ${$obj?->classname}();
+
+// Issue 3456.
+// Anon classes should be skipped, even when there is an attribute between the new and the class keywords.
+$anonWithAttribute = new #[SomeAttribute('summary')] class {
+ public const SOME_STUFF = 'foo';
+};
diff --git a/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.inc b/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.inc
new file mode 100644
index 0000000000..509c48c316
--- /dev/null
+++ b/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.inc
@@ -0,0 +1,49 @@
+
+ * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
+ * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
+ */
+
+namespace PHP_CodeSniffer\Standards\PSR12\Tests\Classes;
+
+use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest;
+
+class OpeningBraceSpaceUnitTest extends AbstractSniffUnitTest
+{
+
+
+ /**
+ * Returns the lines where errors should occur.
+ *
+ * The key of the array should represent the line number and the value
+ * should represent the number of errors that should occur on that line.
+ *
+ * @return array
+ */
+ public function getErrorList()
+ {
+ return [
+ 10 => 1,
+ 18 => 1,
+ 24 => 1,
+ 34 => 1,
+ 41 => 1,
+ ];
+
+ }//end getErrorList()
+
+
+ /**
+ * Returns the lines where warnings should occur.
+ *
+ * The key of the array should represent the line number and the value
+ * should represent the number of warnings that should occur on that line.
+ *
+ * @return array
+ */
+ public function getWarningList()
+ {
+ return [];
+
+ }//end getWarningList()
+
+
+}//end class
diff --git a/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc b/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc
index 9ae9432118..c067e6a2a8 100644
--- a/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc
+++ b/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc
@@ -63,3 +63,15 @@ $fn = fn(array & $one) => 1;
$fn = static fn(DateTime $a, DateTime $b): int => -($a->getTimestamp() <=> $b->getTimestamp());
function issue3267(string|int ...$values) {}
+
+function setDefault(#[ImportValue(
+ constraints: [
+ [
+ Assert\Type::class,
+ ['type' => 'bool'],
+ ],
+ ]
+ )] ?bool $value = null): void
+ {
+ // Do something
+ }
diff --git a/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc.fixed b/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc.fixed
index 504ae43e5f..76764291fa 100644
--- a/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc.fixed
+++ b/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc.fixed
@@ -63,3 +63,15 @@ $fn = fn(array & $one) => 1;
$fn = static fn(DateTime $a, DateTime $b): int => -($a->getTimestamp() <=> $b->getTimestamp());
function issue3267(string|int ...$values) {}
+
+function setDefault(#[ImportValue(
+ constraints: [
+ [
+ Assert\Type::class,
+ ['type' => 'bool'],
+ ],
+ ]
+ )] ?bool $value = null): void
+ {
+ // Do something
+ }
diff --git a/src/Standards/PSR12/ruleset.xml b/src/Standards/PSR12/ruleset.xml
index 1467004353..ce8b71a756 100644
--- a/src/Standards/PSR12/ruleset.xml
+++ b/src/Standards/PSR12/ruleset.xml
@@ -123,6 +123,7 @@
+
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/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/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/Commenting/BlockCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/BlockCommentSniff.php
index 93b60adaad..d59fe1e113 100644
--- a/src/Standards/Squiz/Sniffs/Commenting/BlockCommentSniff.php
+++ b/src/Standards/Squiz/Sniffs/Commenting/BlockCommentSniff.php
@@ -69,8 +69,18 @@ public function process(File $phpcsFile, $stackPtr)
// If this is a function/class/interface doc block comment, skip it.
// We are only interested in inline doc block comments.
if ($tokens[$stackPtr]['code'] === T_DOC_COMMENT_OPEN_TAG) {
- $nextToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
- $ignore = [
+ $nextToken = $stackPtr;
+ do {
+ $nextToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextToken + 1), null, true);
+ if ($tokens[$nextToken]['code'] === T_ATTRIBUTE) {
+ $nextToken = $tokens[$nextToken]['attribute_closer'];
+ continue;
+ }
+
+ break;
+ } while (true);
+
+ $ignore = [
T_CLASS => true,
T_INTERFACE => true,
T_TRAIT => true,
@@ -83,6 +93,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/DocCommentAlignmentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/DocCommentAlignmentSniff.php
index 2624bc2246..b557ae24cd 100644
--- a/src/Standards/Squiz/Sniffs/Commenting/DocCommentAlignmentSniff.php
+++ b/src/Standards/Squiz/Sniffs/Commenting/DocCommentAlignmentSniff.php
@@ -74,6 +74,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 73eb31b71f..2685854769 100644
--- a/src/Standards/Squiz/Sniffs/Commenting/FileCommentSniff.php
+++ b/src/Standards/Squiz/Sniffs/Commenting/FileCommentSniff.php
@@ -127,7 +127,7 @@ public function process(File $phpcsFile, $stackPtr)
// Exactly one blank line after the file comment.
$next = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), null, true);
- if ($tokens[$next]['line'] !== ($tokens[$commentEnd]['line'] + 2)) {
+ if ($next !== false && $tokens[$next]['line'] !== ($tokens[$commentEnd]['line'] + 2)) {
$error = 'There must be exactly one blank line after the file comment';
$phpcsFile->addError($error, $commentEnd, 'SpacingAfterComment');
}
diff --git a/src/Standards/Squiz/Sniffs/Commenting/FunctionCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/FunctionCommentSniff.php
index eeed382952..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) {
@@ -758,6 +760,8 @@ protected function checkInheritdoc(File $phpcsFile, $stackPtr, $commentStart)
}
}
+ return false;
+
}//end checkInheritdoc()
diff --git a/src/Standards/Squiz/Sniffs/Commenting/InlineCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/InlineCommentSniff.php
index 09b4ee2528..7d7ee40e96 100644
--- a/src/Standards/Squiz/Sniffs/Commenting/InlineCommentSniff.php
+++ b/src/Standards/Squiz/Sniffs/Commenting/InlineCommentSniff.php
@@ -59,12 +59,16 @@ public function process(File $phpcsFile, $stackPtr)
// We are only interested in inline doc block comments, which are
// not allowed.
if ($tokens[$stackPtr]['code'] === T_DOC_COMMENT_OPEN_TAG) {
- $nextToken = $phpcsFile->findNext(
- Tokens::$emptyTokens,
- ($stackPtr + 1),
- null,
- true
- );
+ $nextToken = $stackPtr;
+ do {
+ $nextToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextToken + 1), null, true);
+ if ($tokens[$nextToken]['code'] === T_ATTRIBUTE) {
+ $nextToken = $tokens[$nextToken]['attribute_closer'];
+ continue;
+ }
+
+ break;
+ } while (true);
$ignore = [
T_CLASS,
diff --git a/src/Standards/Squiz/Sniffs/Commenting/VariableCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/VariableCommentSniff.php
index 7b9fc933ad..32e89789a7 100644
--- a/src/Standards/Squiz/Sniffs/Commenting/VariableCommentSniff.php
+++ b/src/Standards/Squiz/Sniffs/Commenting/VariableCommentSniff.php
@@ -30,18 +30,33 @@ public function processMemberVar(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$ignore = [
- T_PUBLIC,
- T_PRIVATE,
- T_PROTECTED,
- T_VAR,
- T_STATIC,
- T_WHITESPACE,
- T_STRING,
- T_NS_SEPARATOR,
- T_NULLABLE,
+ T_PUBLIC => T_PUBLIC,
+ T_PRIVATE => T_PRIVATE,
+ 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,
+ T_NULLABLE => T_NULLABLE,
];
- $commentEnd = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true);
+ for ($commentEnd = ($stackPtr - 1); $commentEnd >= 0; $commentEnd--) {
+ if (isset($ignore[$tokens[$commentEnd]['code']]) === true) {
+ continue;
+ }
+
+ if ($tokens[$commentEnd]['code'] === T_ATTRIBUTE_END
+ && isset($tokens[$commentEnd]['attribute_opener']) === true
+ ) {
+ $commentEnd = $tokens[$commentEnd]['attribute_opener'];
+ continue;
+ }
+
+ break;
+ }
+
if ($commentEnd === false
|| ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG
&& $tokens[$commentEnd]['code'] !== T_COMMENT)
diff --git a/src/Standards/Squiz/Sniffs/Objects/ObjectInstantiationSniff.php b/src/Standards/Squiz/Sniffs/Objects/ObjectInstantiationSniff.php
index ea4970dbf8..84facf0540 100644
--- a/src/Standards/Squiz/Sniffs/Objects/ObjectInstantiationSniff.php
+++ b/src/Standards/Squiz/Sniffs/Objects/ObjectInstantiationSniff.php
@@ -48,21 +48,37 @@ public function process(File $phpcsFile, $stackPtr)
$prev = $phpcsFile->findPrevious($allowedTokens, ($stackPtr - 1), null, true);
$allowedTokens = [
- T_EQUAL => true,
- T_DOUBLE_ARROW => true,
- T_FN_ARROW => true,
- T_MATCH_ARROW => true,
- T_THROW => true,
- T_RETURN => true,
- T_INLINE_THEN => true,
- T_INLINE_ELSE => true,
+ T_EQUAL => T_EQUAL,
+ T_COALESCE_EQUAL => T_COALESCE_EQUAL,
+ T_DOUBLE_ARROW => T_DOUBLE_ARROW,
+ T_FN_ARROW => T_FN_ARROW,
+ T_MATCH_ARROW => T_MATCH_ARROW,
+ T_THROW => T_THROW,
+ T_RETURN => T_RETURN,
];
- if (isset($allowedTokens[$tokens[$prev]['code']]) === false) {
- $error = 'New objects must be assigned to a variable';
- $phpcsFile->addError($error, $stackPtr, 'NotAssigned');
+ if (isset($allowedTokens[$tokens[$prev]['code']]) === true) {
+ return;
}
+ $ternaryLikeTokens = [
+ T_COALESCE => true,
+ T_INLINE_THEN => true,
+ T_INLINE_ELSE => true,
+ ];
+
+ // For ternary like tokens, walk a little further back to see if it is preceded by
+ // one of the allowed tokens (within the same statement).
+ if (isset($ternaryLikeTokens[$tokens[$prev]['code']]) === true) {
+ $hasAllowedBefore = $phpcsFile->findPrevious($allowedTokens, ($prev - 1), null, false, null, true);
+ if ($hasAllowedBefore !== false) {
+ return;
+ }
+ }
+
+ $error = 'New objects must be assigned to a variable';
+ $phpcsFile->addError($error, $stackPtr, 'NotAssigned');
+
}//end process()
diff --git a/src/Standards/Squiz/Sniffs/WhiteSpace/MemberVarSpacingSniff.php b/src/Standards/Squiz/Sniffs/WhiteSpace/MemberVarSpacingSniff.php
index f0c84fb8e2..0ece1acad2 100644
--- a/src/Standards/Squiz/Sniffs/WhiteSpace/MemberVarSpacingSniff.php
+++ b/src/Standards/Squiz/Sniffs/WhiteSpace/MemberVarSpacingSniff.php
@@ -55,11 +55,26 @@ protected function processMemberVar(File $phpcsFile, $stackPtr)
$endOfStatement = $phpcsFile->findNext(T_SEMICOLON, ($stackPtr + 1), null, false, null, true);
- $ignore = $validPrefixes;
- $ignore[] = T_WHITESPACE;
+ $ignore = $validPrefixes;
+ $ignore[T_WHITESPACE] = T_WHITESPACE;
$start = $startOfStatement;
- $prev = $phpcsFile->findPrevious($ignore, ($startOfStatement - 1), null, true);
+ for ($prev = ($startOfStatement - 1); $prev >= 0; $prev--) {
+ if (isset($ignore[$tokens[$prev]['code']]) === true) {
+ continue;
+ }
+
+ if ($tokens[$prev]['code'] === T_ATTRIBUTE_END
+ && isset($tokens[$prev]['attribute_opener']) === true
+ ) {
+ $prev = $tokens[$prev]['attribute_opener'];
+ $start = $prev;
+ continue;
+ }
+
+ break;
+ }
+
if (isset(Tokens::$commentTokens[$tokens[$prev]['code']]) === true) {
// Assume the comment belongs to the member var if it is on a line by itself.
$prevContent = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prev - 1), null, true);
@@ -67,28 +82,48 @@ protected function processMemberVar(File $phpcsFile, $stackPtr)
// Check the spacing, but then skip it.
$foundLines = ($tokens[$startOfStatement]['line'] - $tokens[$prev]['line'] - 1);
if ($foundLines > 0) {
- $error = 'Expected 0 blank lines after member var comment; %s found';
- $data = [$foundLines];
- $fix = $phpcsFile->addFixableError($error, $prev, 'AfterComment', $data);
- if ($fix === true) {
- $phpcsFile->fixer->beginChangeset();
- // Inline comments have the newline included in the content but
- // docblock do not.
- if ($tokens[$prev]['code'] === T_COMMENT) {
- $phpcsFile->fixer->replaceToken($prev, rtrim($tokens[$prev]['content']));
+ for ($i = ($prev + 1); $i < $startOfStatement; $i++) {
+ if ($tokens[$i]['column'] !== 1) {
+ continue;
}
- for ($i = ($prev + 1); $i <= $startOfStatement; $i++) {
- if ($tokens[$i]['line'] === $tokens[$startOfStatement]['line']) {
- break;
- }
-
- $phpcsFile->fixer->replaceToken($i, '');
- }
-
- $phpcsFile->fixer->addNewline($prev);
- $phpcsFile->fixer->endChangeset();
- }
+ if ($tokens[$i]['code'] === T_WHITESPACE
+ && $tokens[$i]['line'] !== $tokens[($i + 1)]['line']
+ ) {
+ $error = 'Expected 0 blank lines after member var comment; %s found';
+ $data = [$foundLines];
+ $fix = $phpcsFile->addFixableError($error, $prev, 'AfterComment', $data);
+ if ($fix === true) {
+ $phpcsFile->fixer->beginChangeset();
+ // Inline comments have the newline included in the content but
+ // docblocks do not.
+ if ($tokens[$prev]['code'] === T_COMMENT) {
+ $phpcsFile->fixer->replaceToken($prev, rtrim($tokens[$prev]['content']));
+ }
+
+ for ($i = ($prev + 1); $i <= $startOfStatement; $i++) {
+ if ($tokens[$i]['line'] === $tokens[$startOfStatement]['line']) {
+ break;
+ }
+
+ // Remove the newline after the docblock, and any entirely
+ // empty lines before the member var.
+ if ($tokens[$i]['code'] === T_WHITESPACE
+ && $tokens[$i]['line'] === $tokens[$prev]['line']
+ || ($tokens[$i]['column'] === 1
+ && $tokens[$i]['line'] !== $tokens[($i + 1)]['line'])
+ ) {
+ $phpcsFile->fixer->replaceToken($i, '');
+ }
+ }
+
+ $phpcsFile->fixer->addNewline($prev);
+ $phpcsFile->fixer->endChangeset();
+ }//end if
+
+ break;
+ }//end if
+ }//end for
}//end if
$start = $prev;
@@ -106,7 +141,7 @@ protected function processMemberVar(File $phpcsFile, $stackPtr)
$first = $tokens[$start]['comment_opener'];
} else {
$first = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($start - 1), null, true);
- $first = $phpcsFile->findNext(Tokens::$commentTokens, ($first + 1));
+ $first = $phpcsFile->findNext(array_merge(Tokens::$commentTokens, [T_ATTRIBUTE]), ($first + 1));
}
// Determine if this is the first member var.
diff --git a/src/Standards/Squiz/Sniffs/WhiteSpace/ScopeClosingBraceSniff.php b/src/Standards/Squiz/Sniffs/WhiteSpace/ScopeClosingBraceSniff.php
index f68613932d..fd03875347 100644
--- a/src/Standards/Squiz/Sniffs/WhiteSpace/ScopeClosingBraceSniff.php
+++ b/src/Standards/Squiz/Sniffs/WhiteSpace/ScopeClosingBraceSniff.php
@@ -65,11 +65,20 @@ public function process(File $phpcsFile, $stackPtr)
// Check that the closing brace is on it's own line.
$lastContent = $phpcsFile->findPrevious([T_INLINE_HTML, T_WHITESPACE, T_OPEN_TAG], ($scopeEnd - 1), $scopeStart, true);
- if ($tokens[$lastContent]['line'] === $tokens[$scopeEnd]['line']) {
+ for ($lineStart = $scopeEnd; $tokens[$lineStart]['column'] > 1; $lineStart--);
+
+ if ($tokens[$lastContent]['line'] === $tokens[$scopeEnd]['line']
+ || ($tokens[$lineStart]['code'] === T_INLINE_HTML
+ && trim($tokens[$lineStart]['content']) !== '')
+ ) {
$error = 'Closing brace must be on a line by itself';
$fix = $phpcsFile->addFixableError($error, $scopeEnd, 'ContentBefore');
if ($fix === true) {
- $phpcsFile->fixer->addNewlineBefore($scopeEnd);
+ if ($tokens[$lastContent]['line'] === $tokens[$scopeEnd]['line']) {
+ $phpcsFile->fixer->addNewlineBefore($scopeEnd);
+ } else {
+ $phpcsFile->fixer->addNewlineBefore(($lineStart + 1));
+ }
}
return;
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/Commenting/BlockCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc
index daf50fa382..b25de27679 100644
--- a/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc
@@ -272,3 +272,30 @@ $contentToEcho
* No blank line allowed above the comment if it's the first non-empty token after a PHP open tag.
*/
$contentToEcho
+
+/**
+ * Comment should be ignored, even though there is an attribute between the docblock and the class declaration.
+ */
+
+#[AttributeA]
+
+final class MyClass
+{
+ /**
+ * Comment should be ignored, even though there is an attribute between the docblock and the function declaration
+ */
+ #[AttributeA]
+ #[AttributeB]
+ final public function test() {}
+}
+
+/**
+ * Comment should be ignored.
+ */
+abstract class MyClass
+{
+ /**
+ * Comment should be ignored.
+ */
+ readonly public string $prop;
+}
diff --git a/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc.fixed
index e402160f02..e1e821bf56 100644
--- a/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc.fixed
@@ -274,3 +274,30 @@ $contentToEcho
* No blank line allowed above the comment if it's the first non-empty token after a PHP open tag.
*/
$contentToEcho
+
+/**
+ * Comment should be ignored, even though there is an attribute between the docblock and the class declaration.
+ */
+
+#[AttributeA]
+
+final class MyClass
+{
+ /**
+ * Comment should be ignored, even though there is an attribute between the docblock and the function declaration
+ */
+ #[AttributeA]
+ #[AttributeB]
+ final public function test() {}
+}
+
+/**
+ * Comment should be ignored.
+ */
+abstract class MyClass
+{
+ /**
+ * Comment should be ignored.
+ */
+ readonly public string $prop;
+}
diff --git a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc
index e7d880d682..5de613da22 100644
--- a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc
@@ -77,6 +77,14 @@ class MyClass2
var $x;
}
+abstract class MyClass
+{
+ /**
+* Property comment
+ */
+ readonly public string $prop;
+}
+
/** ************************************************************************
* 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..7395d2fab4 100644
--- a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc.fixed
@@ -77,6 +77,14 @@ class MyClass2
var $x;
}
+abstract class MyClass
+{
+ /**
+ * Property comment
+ */
+ readonly public string $prop;
+}
+
/** ************************************************************************
* Example with no errors.
**************************************************************************/
diff --git a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.php
index 974951ce42..a862d4a819 100644
--- a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.php
+++ b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.php
@@ -45,6 +45,8 @@ public function getErrorList($testFile='DocCommentAlignmentUnitTest.inc')
if ($testFile === 'DocCommentAlignmentUnitTest.inc') {
$errors[75] = 1;
+ $errors[83] = 1;
+ $errors[84] = 1;
}
return $errors;
diff --git a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.8.inc b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.8.inc
new file mode 100644
index 0000000000..5ef90f2ad1
--- /dev/null
+++ b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.8.inc
@@ -0,0 +1,9 @@
+
+ * @copyright 2010-2014 Squiz Pty Ltd (ABN 77 084 670 600)
+ */
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 377db023d9..1b97af0b92 100644
--- a/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc
@@ -149,6 +149,22 @@ if ($foo) {
// another comment here.
$foo++;
+/**
+ * Comment should be ignored, even though there is an attribute between the docblock and the class declaration.
+ */
+
+#[AttributeA]
+
+final class MyClass
+{
+ /**
+ * Comment should be ignored, even though there is an attribute between the docblock and the function declaration
+ */
+ #[AttributeA]
+ #[AttributeB]
+ final public function test() {}
+}
+
/*
* 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 975143f2c5..6b66624176 100644
--- a/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc.fixed
@@ -142,6 +142,22 @@ if ($foo) {
// another comment here.
$foo++;
+/**
+ * Comment should be ignored, even though there is an attribute between the docblock and the class declaration.
+ */
+
+#[AttributeA]
+
+final class MyClass
+{
+ /**
+ * Comment should be ignored, even though there is an attribute between the docblock and the function declaration
+ */
+ #[AttributeA]
+ #[AttributeB]
+ final public function test() {}
+}
+
/*
* 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 65f4389bdc..36efc443bf 100644
--- a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc
@@ -363,3 +363,42 @@ class Foo
var int $noComment = 1;
}
+
+class HasAttributes
+{
+ /**
+ * Short description of the member variable.
+ *
+ * @var array
+ */
+ #[ORM\Id]#[ORM\Column("integer")]
+ private $id;
+
+ /**
+ * Short description of the member variable.
+ *
+ * @var array
+ */
+ #[ORM\GeneratedValue]
+ #[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 ca0b052e35..5c652f5402 100644
--- a/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/Commenting/VariableCommentUnitTest.inc.fixed
@@ -363,3 +363,42 @@ class Foo
var int $noComment = 1;
}
+
+class HasAttributes
+{
+ /**
+ * Short description of the member variable.
+ *
+ * @var array
+ */
+ #[ORM\Id]#[ORM\Column("integer")]
+ private $id;
+
+ /**
+ * Short description of the member variable.
+ *
+ * @var array
+ */
+ #[ORM\GeneratedValue]
+ #[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/Objects/ObjectInstantiationUnitTest.inc b/src/Standards/Squiz/Tests/Objects/ObjectInstantiationUnitTest.inc
index f58af275cd..41c881289d 100644
--- a/src/Standards/Squiz/Tests/Objects/ObjectInstantiationUnitTest.inc
+++ b/src/Standards/Squiz/Tests/Objects/ObjectInstantiationUnitTest.inc
@@ -23,6 +23,24 @@ function returnMatch() {
}
}
+// Issue 3333.
+$time2 ??= new \DateTime();
+$time3 = $time1 ?? new \DateTime();
+$time3 = $time1 ?? $time2 ?? new \DateTime();
+
+function_call($time1 ?? new \DateTime());
+$return = function_call($time1 ?? new \DateTime()); // False negative depending on interpretation of the sniff.
+
+function returnViaTernary() {
+ return ($y == false ) ? ($x === true ? new Foo : new Bar) : new FooBar;
+}
+
+function nonAssignmentTernary() {
+ if (($x ? new Foo() : new Bar) instanceof FooBar) {
+ // Do something.
+ }
+}
+
// Intentional parse error. This must be the last test in the file.
function new
?>
diff --git a/src/Standards/Squiz/Tests/Objects/ObjectInstantiationUnitTest.php b/src/Standards/Squiz/Tests/Objects/ObjectInstantiationUnitTest.php
index fa32521c85..f9979fa29f 100644
--- a/src/Standards/Squiz/Tests/Objects/ObjectInstantiationUnitTest.php
+++ b/src/Standards/Squiz/Tests/Objects/ObjectInstantiationUnitTest.php
@@ -26,8 +26,10 @@ class ObjectInstantiationUnitTest extends AbstractSniffUnitTest
public function getErrorList()
{
return [
- 5 => 1,
- 8 => 1,
+ 5 => 1,
+ 8 => 1,
+ 31 => 1,
+ 39 => 2,
];
}//end getErrorList()
diff --git a/src/Standards/Squiz/Tests/PHP/CommentedOutCodeUnitTest.php b/src/Standards/Squiz/Tests/PHP/CommentedOutCodeUnitTest.php
index d51f23ca3b..36c556d8c2 100644
--- a/src/Standards/Squiz/Tests/PHP/CommentedOutCodeUnitTest.php
+++ b/src/Standards/Squiz/Tests/PHP/CommentedOutCodeUnitTest.php
@@ -49,7 +49,6 @@ public function getWarningList($testFile='CommentedOutCodeUnitTest.inc')
8 => 1,
15 => 1,
19 => 1,
- 35 => 1,
87 => 1,
91 => 1,
97 => 1,
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc b/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc
index fd7c6e34fc..038072dfe0 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc
+++ b/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc
@@ -332,3 +332,36 @@ class CommentedOutCodeAtStartOfClassNoBlankLine {
*/
public $property = true;
}
+
+class HasAttributes
+{
+ /**
+ * Short description of the member variable.
+ *
+ * @var array
+ */
+
+ #[ORM\Id]#[ORM\Column("integer")]
+
+ private $id;
+
+
+ /**
+ * Short description of the member variable.
+ *
+ * @var array
+ */
+ #[ORM\GeneratedValue]
+
+ #[ORM\Column(ORM\Column::T_INTEGER)]
+ protected $height;
+
+ #[SingleAttribute]
+ protected $propertySingle;
+
+ #[FirstAttribute]
+ #[SecondAttribute]
+ protected $propertyDouble;
+ #[ThirdAttribute]
+ protected $propertyWithoutSpacing;
+}
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc.fixed b/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc.fixed
index b6ebcc9ab1..3cb2ca3a48 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc.fixed
@@ -319,3 +319,34 @@ class CommentedOutCodeAtStartOfClassNoBlankLine {
*/
public $property = true;
}
+
+class HasAttributes
+{
+
+ /**
+ * Short description of the member variable.
+ *
+ * @var array
+ */
+ #[ORM\Id]#[ORM\Column("integer")]
+ private $id;
+
+ /**
+ * Short description of the member variable.
+ *
+ * @var array
+ */
+ #[ORM\GeneratedValue]
+ #[ORM\Column(ORM\Column::T_INTEGER)]
+ protected $height;
+
+ #[SingleAttribute]
+ protected $propertySingle;
+
+ #[FirstAttribute]
+ #[SecondAttribute]
+ protected $propertyDouble;
+
+ #[ThirdAttribute]
+ protected $propertyWithoutSpacing;
+}
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.php b/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.php
index 08a11bca41..9b4066811a 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.php
+++ b/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.php
@@ -57,6 +57,11 @@ public function getErrorList()
288 => 1,
292 => 1,
333 => 1,
+ 342 => 1,
+ 346 => 1,
+ 353 => 1,
+ 357 => 1,
+ 366 => 1,
];
}//end getErrorList()
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc b/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc
index b71c0be729..ecd80b5504 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc
+++ b/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc
@@ -114,3 +114,9 @@ $match = match ($test) {
1 => 'a',
2 => 'b'
};
+
+?>
+
+
+
+
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc.fixed b/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc.fixed
index b5d87d3d56..a30dfd0e6f 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc.fixed
+++ b/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc.fixed
@@ -116,3 +116,10 @@ $match = match ($test) {
1 => 'a',
2 => 'b'
};
+
+?>
+
+
+
+
+
diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.php b/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.php
index 668332562e..0df8603f86 100644
--- a/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.php
+++ b/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.php
@@ -33,6 +33,7 @@ public function getErrorList()
102 => 1,
111 => 1,
116 => 1,
+ 122 => 1,
];
}//end getErrorList()
diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php
index 2730b03150..5b7abf1cb9 100644
--- a/src/Tokenizers/PHP.php
+++ b/src/Tokenizers/PHP.php
@@ -393,6 +393,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,
@@ -646,6 +647,48 @@ 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')
+ ) {
+ $finalTokens[$newStackPtr] = [
+ 'code' => T_LNUMBER,
+ 'type' => 'T_LNUMBER',
+ 'content' => $token[1] .= $tokens[($stackPtr + 1)][1],
+ ];
+ $stackPtr++;
+ $newStackPtr++;
+ continue;
+ }
+
+ /*
+ PHP 8.1 introduced two dedicated tokens for the & character.
+ Retokenizing both of these to T_BITWISE_AND, which is the
+ token PHPCS already tokenized them as.
+ */
+
+ if ($tokenIsArray === true
+ && ($token[0] === T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG
+ || $token[0] === T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG)
+ ) {
+ $finalTokens[$newStackPtr] = [
+ 'code' => T_BITWISE_AND,
+ 'type' => 'T_BITWISE_AND',
+ 'content' => $token[1],
+ ];
+ $newStackPtr++;
+ continue;
+ }
+
/*
If this is a double quoted string, PHP will tokenize the whole
thing which causes problems with the scope map when braces are
@@ -1311,6 +1354,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
@@ -1557,7 +1601,7 @@ protected function tokenize($string)
&& isset(Util\Tokens::$emptyTokens[$tokenType]) === false
) {
// Found the previous non-empty token.
- if ($tokenType === ':' || $tokenType === ',') {
+ if ($tokenType === ':' || $tokenType === ',' || $tokenType === T_ATTRIBUTE_END) {
$newToken['code'] = T_NULLABLE;
$newToken['type'] = 'T_NULLABLE';
@@ -1667,7 +1711,8 @@ protected function tokenize($string)
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
+ || (isset(Util\Tokens::$emptyTokens[$tokens[$x][0]]) === false
+ && $tokens[$x][1] !== '&')
) {
// Non-empty content.
break;
@@ -1776,23 +1821,6 @@ function return types. We want to keep the parenthesis map clean,
break;
}//end for
-
- // Any T_ARRAY tokens we find between here and the next
- // token that can't be part of the return type, need to be
- // converted to T_STRING tokens.
- for ($x; $x < $numTokens; $x++) {
- if ((is_array($tokens[$x]) === false && $tokens[$x] !== '|')
- || (is_array($tokens[$x]) === true && isset($allowed[$tokens[$x][0]]) === false)
- ) {
- break;
- } else if (is_array($tokens[$x]) === true && $tokens[$x][0] === T_ARRAY) {
- $tokens[$x][0] = T_STRING;
-
- if (PHP_CODESNIFFER_VERBOSITY > 1) {
- echo "\t\t* token $x changed from T_ARRAY to T_STRING".PHP_EOL;
- }
- }
- }
}//end if
}//end if
}//end if
@@ -2055,20 +2083,25 @@ function return types. We want to keep the parenthesis map clean,
}
}//end if
- // This is a special condition for T_ARRAY tokens used for
- // type hinting function arguments as being arrays. We want to keep
- // the parenthesis map clean, so let's tag these tokens as
+ // This is a special condition for T_ARRAY tokens used for anything else
+ // but array declarations, like type hinting function arguments as
+ // being arrays.
+ // We want to keep the parenthesis map clean, so let's tag these tokens as
// T_STRING.
if ($newToken['code'] === T_ARRAY) {
- for ($i = $stackPtr; $i < $numTokens; $i++) {
- if ($tokens[$i] === '(') {
- break;
- } else if ($tokens[$i][0] === T_VARIABLE) {
- $newToken['code'] = T_STRING;
- $newToken['type'] = 'T_STRING';
+ for ($i = ($stackPtr + 1); $i < $numTokens; $i++) {
+ if (is_array($tokens[$i]) === false
+ || isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === false
+ ) {
+ // Non-empty content.
break;
}
}
+
+ if ($tokens[$i] !== '(' && $i !== $numTokens) {
+ $newToken['code'] = T_STRING;
+ $newToken['type'] = 'T_STRING';
+ }
}
// This is a special case when checking PHP 5.5+ code in PHP < 5.5
@@ -2677,7 +2710,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;
@@ -2804,6 +2838,77 @@ protected function processAdditional()
$this->tokens[$x]['code'] = T_STRING;
$this->tokens[$x]['type'] = 'T_STRING';
}
+ } else if ($this->tokens[$i]['code'] === T_READONLY
+ || ($this->tokens[$i]['code'] === T_STRING
+ && strtolower($this->tokens[$i]['content']) === 'readonly')
+ ) {
+ /*
+ Adds "readonly" keyword support:
+ PHP < 8.1: Converts T_STRING to T_READONLY
+ PHP >= 8.1: Converts some T_READONLY to T_STRING because token_get_all()
+ without the TOKEN_PARSE flag cannot distinguish between them in some situations.
+ */
+
+ $allowedAfter = [
+ T_STRING => T_STRING,
+ T_NS_SEPARATOR => T_NS_SEPARATOR,
+ T_NAME_FULLY_QUALIFIED => T_NAME_FULLY_QUALIFIED,
+ T_NAME_RELATIVE => T_NAME_RELATIVE,
+ T_NAME_QUALIFIED => T_NAME_QUALIFIED,
+ T_TYPE_UNION => T_TYPE_UNION,
+ T_BITWISE_OR => T_BITWISE_OR,
+ T_BITWISE_AND => T_BITWISE_AND,
+ T_ARRAY => T_ARRAY,
+ T_CALLABLE => T_CALLABLE,
+ T_SELF => T_SELF,
+ T_PARENT => T_PARENT,
+ T_NULL => T_FALSE,
+ T_NULLABLE => T_NULLABLE,
+ T_STATIC => T_STATIC,
+ T_PUBLIC => T_PUBLIC,
+ T_PROTECTED => T_PROTECTED,
+ T_PRIVATE => T_PRIVATE,
+ T_VAR => T_VAR,
+ ];
+
+ $shouldBeReadonly = true;
+
+ for ($x = ($i + 1); $x < $numTokens; $x++) {
+ if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true) {
+ continue;
+ }
+
+ if ($this->tokens[$x]['code'] === T_VARIABLE
+ || $this->tokens[$x]['code'] === T_CONST
+ ) {
+ break;
+ }
+
+ if (isset($allowedAfter[$this->tokens[$x]['code']]) === false) {
+ $shouldBeReadonly = false;
+ break;
+ }
+ }
+
+ if ($this->tokens[$i]['code'] === T_STRING && $shouldBeReadonly === true) {
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ $line = $this->tokens[$i]['line'];
+ echo "\t* token $i on line $line changed from T_STRING to T_READONLY".PHP_EOL;
+ }
+
+ $this->tokens[$i]['code'] = T_READONLY;
+ $this->tokens[$i]['type'] = 'T_READONLY';
+ } else if ($this->tokens[$i]['code'] === T_READONLY && $shouldBeReadonly === false) {
+ if (PHP_CODESNIFFER_VERBOSITY > 1) {
+ $line = $this->tokens[$i]['line'];
+ echo "\t* token $i on line $line changed from T_READONLY to T_STRING".PHP_EOL;
+ }
+
+ $this->tokens[$i]['code'] = T_STRING;
+ $this->tokens[$i]['type'] = 'T_STRING';
+ }
+
+ continue;
}//end if
if (($this->tokens[$i]['code'] !== T_CASE
diff --git a/src/Tokenizers/Tokenizer.php b/src/Tokenizers/Tokenizer.php
index 4c5d391340..c79323ccd3 100644
--- a/src/Tokenizers/Tokenizer.php
+++ b/src/Tokenizers/Tokenizer.php
@@ -638,25 +638,13 @@ public function replaceTabsInToken(&$token, $prefix=' ', $padding=' ', $tabWidth
}
// Process the tab that comes after the content.
- $lastCurrColumn = $currColumn;
$tabNum++;
// Move the pointer to the next tab stop.
- if (($currColumn % $tabWidth) === 0) {
- // This is the first tab, and we are already at a
- // tab stop, so this tab counts as a single space.
- $currColumn++;
- } else {
- $currColumn++;
- while (($currColumn % $tabWidth) !== 0) {
- $currColumn++;
- }
-
- $currColumn++;
- }
-
- $length += ($currColumn - $lastCurrColumn);
- $newContent .= $prefix.str_repeat($padding, ($currColumn - $lastCurrColumn - 1));
+ $pad = ($tabWidth - ($currColumn + $tabWidth - 1) % $tabWidth);
+ $currColumn += $pad;
+ $length += $pad;
+ $newContent .= $prefix.str_repeat($padding, ($pad - 1));
}//end foreach
}//end if
diff --git a/src/Util/Common.php b/src/Util/Common.php
index e60eec1df2..204f44de14 100644
--- a/src/Util/Common.php
+++ b/src/Util/Common.php
@@ -60,11 +60,11 @@ public static function isPharFile($path)
*/
public static function isReadable($path)
{
- if (is_readable($path) === true) {
+ if (@is_readable($path) === true) {
return true;
}
- if (file_exists($path) === true && is_file($path) === true) {
+ if (@file_exists($path) === true && @is_file($path) === true) {
$f = @fopen($path, 'rb');
if (fclose($f) === true) {
return true;
diff --git a/src/Util/Timing.php b/src/Util/Timing.php
index cf27dcfe36..95ee85216d 100644
--- a/src/Util/Timing.php
+++ b/src/Util/Timing.php
@@ -64,7 +64,7 @@ public static function printRunTime($force=false)
if ($time > 60000) {
$mins = floor($time / 60000);
- $secs = round((($time % 60000) / 1000), 2);
+ $secs = round((fmod($time, 60000) / 1000), 2);
$time = $mins.' mins';
if ($secs !== 0) {
$time .= ", $secs secs";
diff --git a/src/Util/Tokens.php b/src/Util/Tokens.php
index 56afd27919..0bc1747275 100644
--- a/src/Util/Tokens.php
+++ b/src/Util/Tokens.php
@@ -154,6 +154,19 @@
define('T_ATTRIBUTE', 'PHPCS_T_ATTRIBUTE');
}
+// Some PHP 8.1 tokens, replicated for lower versions.
+if (defined('T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG') === false) {
+ define('T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG', 'PHPCS_T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG');
+}
+
+if (defined('T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG') === false) {
+ 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');
+}
+
// 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');
diff --git a/tests/Core/File/GetMemberPropertiesTest.inc b/tests/Core/File/GetMemberPropertiesTest.inc
index ea47e9fc57..e156099382 100644
--- a/tests/Core/File/GetMemberPropertiesTest.inc
+++ b/tests/Core/File/GetMemberPropertiesTest.inc
@@ -239,6 +239,11 @@ $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;
+
+ /* testPHP81NotReadonly */
+ private string $notReadonly;
+ /* testPHP81Readonly */
+ public readonly int $readonly;
};
$anon = class {
diff --git a/tests/Core/File/GetMemberPropertiesTest.php b/tests/Core/File/GetMemberPropertiesTest.php
index 0af0437932..82764b01e5 100644
--- a/tests/Core/File/GetMemberPropertiesTest.php
+++ b/tests/Core/File/GetMemberPropertiesTest.php
@@ -610,6 +610,28 @@ public function dataGetMemberProperties()
'nullable_type' => false,
],
],
+ [
+ '/* testPHP81NotReadonly */',
+ [
+ 'scope' => 'private',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => false,
+ 'type' => 'string',
+ 'nullable_type' => false,
+ ],
+ ],
+ [
+ '/* testPHP81Readonly */',
+ [
+ 'scope' => 'public',
+ 'scope_specified' => true,
+ 'is_static' => false,
+ 'is_readonly' => true,
+ 'type' => 'int',
+ 'nullable_type' => false,
+ ],
+ ],
[
'/* testPHP8PropertySingleAttribute */',
[
diff --git a/tests/Core/File/GetMethodParametersTest.inc b/tests/Core/File/GetMethodParametersTest.inc
index b78301d309..ed1762e7d2 100644
--- a/tests/Core/File/GetMethodParametersTest.inc
+++ b/tests/Core/File/GetMethodParametersTest.inc
@@ -120,3 +120,23 @@ abstract class ConstructorPropertyPromotionAbstractMethod {
// 3. The callable type is not supported for properties, but that's not the concern of this method.
abstract public function __construct(public callable $y, private ...$x);
}
+
+/* testCommentsInParameter */
+function commentsInParams(
+ // Leading comment.
+ ?MyClass /*-*/ & /*-*/.../*-*/ $param /*-*/ = /*-*/ 'default value' . /*-*/ 'second part' // Trailing comment.
+) {}
+
+/* testParameterAttributesInFunctionDeclaration */
+class ParametersWithAttributes(
+ public function __construct(
+ #[\MyExample\MyAttribute] private string $constructorPropPromTypedParamSingleAttribute,
+ #[MyAttr([1, 2])]
+ Type|false
+ $typedParamSingleAttribute,
+ #[MyAttribute(1234), MyAttribute(5678)] ?int $nullableTypedParamMultiAttribute,
+ #[WithoutArgument] #[SingleArgument(0)] $nonTypedParamTwoAttributes,
+ #[MyAttribute(array("key" => "value"))]
+ &...$otherParam,
+ ) {}
+}
diff --git a/tests/Core/File/GetMethodParametersTest.php b/tests/Core/File/GetMethodParametersTest.php
index 253b806215..e7692a7153 100644
--- a/tests/Core/File/GetMethodParametersTest.php
+++ b/tests/Core/File/GetMethodParametersTest.php
@@ -26,6 +26,7 @@ public function testPassByReference()
$expected[0] = [
'name' => '$var',
'content' => '&$var',
+ 'has_attributes' => false,
'pass_by_reference' => true,
'variable_length' => false,
'type_hint' => '',
@@ -48,6 +49,7 @@ public function testArrayHint()
$expected[0] = [
'name' => '$var',
'content' => 'array $var',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'array',
@@ -70,6 +72,7 @@ public function testTypeHint()
$expected[0] = [
'name' => '$var1',
'content' => 'foo $var1',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'foo',
@@ -79,6 +82,7 @@ public function testTypeHint()
$expected[1] = [
'name' => '$var2',
'content' => 'bar $var2',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'bar',
@@ -101,6 +105,7 @@ public function testSelfTypeHint()
$expected[0] = [
'name' => '$var',
'content' => 'self $var',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'self',
@@ -123,6 +128,7 @@ public function testNullableTypeHint()
$expected[0] = [
'name' => '$var1',
'content' => '?int $var1',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => '?int',
@@ -132,6 +138,7 @@ public function testNullableTypeHint()
$expected[1] = [
'name' => '$var2',
'content' => '?\bar $var2',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => '?\bar',
@@ -154,6 +161,7 @@ public function testVariable()
$expected[0] = [
'name' => '$var',
'content' => '$var',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => '',
@@ -176,6 +184,7 @@ public function testSingleDefaultValue()
$expected[0] = [
'name' => '$var1',
'content' => '$var1=self::CONSTANT',
+ 'has_attributes' => false,
'default' => 'self::CONSTANT',
'pass_by_reference' => false,
'variable_length' => false,
@@ -199,6 +208,7 @@ public function testDefaultValues()
$expected[0] = [
'name' => '$var1',
'content' => '$var1=1',
+ 'has_attributes' => false,
'default' => '1',
'pass_by_reference' => false,
'variable_length' => false,
@@ -208,6 +218,7 @@ public function testDefaultValues()
$expected[1] = [
'name' => '$var2',
'content' => "\$var2='value'",
+ 'has_attributes' => false,
'default' => "'value'",
'pass_by_reference' => false,
'variable_length' => false,
@@ -232,6 +243,7 @@ public function testBitwiseAndConstantExpressionDefaultValue()
'name' => '$a',
'content' => '$a = 10 & 20',
'default' => '10 & 20',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => '',
@@ -254,6 +266,7 @@ public function testArrowFunction()
$expected[0] = [
'name' => '$a',
'content' => 'int $a',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'int',
@@ -263,6 +276,7 @@ public function testArrowFunction()
$expected[1] = [
'name' => '$b',
'content' => '...$b',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => true,
'type_hint' => '',
@@ -285,6 +299,7 @@ public function testPHP8MixedTypeHint()
$expected[0] = [
'name' => '$var1',
'content' => 'mixed &...$var1',
+ 'has_attributes' => false,
'pass_by_reference' => true,
'variable_length' => true,
'type_hint' => 'mixed',
@@ -307,6 +322,7 @@ public function testPHP8MixedTypeHintNullable()
$expected[0] = [
'name' => '$var1',
'content' => '?Mixed $var1',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => '?Mixed',
@@ -329,6 +345,7 @@ public function testNamespaceOperatorTypeHint()
$expected[0] = [
'name' => '$var1',
'content' => '?namespace\Name $var1',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => '?namespace\Name',
@@ -351,6 +368,7 @@ public function testPHP8UnionTypesSimple()
$expected[0] = [
'name' => '$number',
'content' => 'int|float $number',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'int|float',
@@ -359,6 +377,7 @@ public function testPHP8UnionTypesSimple()
$expected[1] = [
'name' => '$obj',
'content' => 'self|parent &...$obj',
+ 'has_attributes' => false,
'pass_by_reference' => true,
'variable_length' => true,
'type_hint' => 'self|parent',
@@ -381,6 +400,7 @@ public function testPHP8UnionTypesWithSpreadOperatorAndReference()
$expected[0] = [
'name' => '$paramA',
'content' => 'float|null &$paramA',
+ 'has_attributes' => false,
'pass_by_reference' => true,
'variable_length' => false,
'type_hint' => 'float|null',
@@ -389,6 +409,7 @@ public function testPHP8UnionTypesWithSpreadOperatorAndReference()
$expected[1] = [
'name' => '$paramB',
'content' => 'string|int ...$paramB',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => true,
'type_hint' => 'string|int',
@@ -412,6 +433,7 @@ public function testPHP8UnionTypesSimpleWithBitwiseOrInDefault()
'name' => '$var',
'content' => 'int|float $var = CONSTANT_A | CONSTANT_B',
'default' => 'CONSTANT_A | CONSTANT_B',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'int|float',
@@ -434,6 +456,7 @@ public function testPHP8UnionTypesTwoClasses()
$expected[0] = [
'name' => '$var',
'content' => 'MyClassA|\Package\MyClassB $var',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'MyClassA|\Package\MyClassB',
@@ -456,6 +479,7 @@ public function testPHP8UnionTypesAllBaseTypes()
$expected[0] = [
'name' => '$var',
'content' => 'array|bool|callable|int|float|null|object|string $var',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'array|bool|callable|int|float|null|object|string',
@@ -478,6 +502,7 @@ public function testPHP8UnionTypesAllPseudoTypes()
$expected[0] = [
'name' => '$var',
'content' => 'false|mixed|self|parent|iterable|Resource $var',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'false|mixed|self|parent|iterable|Resource',
@@ -500,6 +525,7 @@ public function testPHP8UnionTypesNullable()
$expected[0] = [
'name' => '$number',
'content' => '?int|float $number',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => '?int|float',
@@ -523,6 +549,7 @@ public function testPHP8PseudoTypeNull()
'name' => '$var',
'content' => 'null $var = null',
'default' => 'null',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'null',
@@ -546,6 +573,7 @@ public function testPHP8PseudoTypeFalse()
'name' => '$var',
'content' => 'false $var = false',
'default' => 'false',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'false',
@@ -569,6 +597,7 @@ public function testPHP8PseudoTypeFalseAndBool()
'name' => '$var',
'content' => 'bool|false $var = false',
'default' => 'false',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'bool|false',
@@ -591,6 +620,7 @@ public function testPHP8ObjectAndClass()
$expected[0] = [
'name' => '$var',
'content' => 'object|ClassName $var',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'object|ClassName',
@@ -613,6 +643,7 @@ public function testPHP8PseudoTypeIterableAndArray()
$expected[0] = [
'name' => '$var',
'content' => 'iterable|array|Traversable $var',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'iterable|array|Traversable',
@@ -635,6 +666,7 @@ public function testPHP8DuplicateTypeInUnionWhitespaceAndComment()
$expected[0] = [
'name' => '$var',
'content' => 'int | string /*comment*/ | INT $var',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'int|string|INT',
@@ -658,6 +690,7 @@ public function testPHP8ConstructorPropertyPromotionNoTypes()
'name' => '$x',
'content' => 'public $x = 0.0',
'default' => '0.0',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => '',
@@ -668,6 +701,7 @@ public function testPHP8ConstructorPropertyPromotionNoTypes()
'name' => '$y',
'content' => 'protected $y = \'\'',
'default' => "''",
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => '',
@@ -678,6 +712,7 @@ public function testPHP8ConstructorPropertyPromotionNoTypes()
'name' => '$z',
'content' => 'private $z = null',
'default' => 'null',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => '',
@@ -701,6 +736,7 @@ public function testPHP8ConstructorPropertyPromotionWithTypes()
$expected[0] = [
'name' => '$x',
'content' => 'protected float|int $x',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'float|int',
@@ -711,6 +747,7 @@ public function testPHP8ConstructorPropertyPromotionWithTypes()
'name' => '$y',
'content' => 'public ?string &$y = \'test\'',
'default' => "'test'",
+ 'has_attributes' => false,
'pass_by_reference' => true,
'variable_length' => false,
'type_hint' => '?string',
@@ -720,6 +757,7 @@ public function testPHP8ConstructorPropertyPromotionWithTypes()
$expected[2] = [
'name' => '$z',
'content' => 'private mixed $z',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'mixed',
@@ -743,6 +781,7 @@ public function testPHP8ConstructorPropertyPromotionAndNormalParam()
$expected[0] = [
'name' => '$promotedProp',
'content' => 'public int $promotedProp',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'int',
@@ -752,6 +791,7 @@ public function testPHP8ConstructorPropertyPromotionAndNormalParam()
$expected[1] = [
'name' => '$normalArg',
'content' => '?int $normalArg',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => '?int',
@@ -774,6 +814,7 @@ public function testPHP8ConstructorPropertyPromotionGlobalFunction()
$expected[0] = [
'name' => '$x',
'content' => 'private $x',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => '',
@@ -797,6 +838,7 @@ public function testPHP8ConstructorPropertyPromotionAbstractMethod()
$expected[0] = [
'name' => '$y',
'content' => 'public callable $y',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => false,
'type_hint' => 'callable',
@@ -806,6 +848,7 @@ public function testPHP8ConstructorPropertyPromotionAbstractMethod()
$expected[1] = [
'name' => '$x',
'content' => 'private ...$x',
+ 'has_attributes' => false,
'pass_by_reference' => false,
'variable_length' => true,
'type_hint' => '',
@@ -818,6 +861,93 @@ public function testPHP8ConstructorPropertyPromotionAbstractMethod()
}//end testPHP8ConstructorPropertyPromotionAbstractMethod()
+ /**
+ * Verify and document behaviour when there are comments within a parameter declaration.
+ *
+ * @return void
+ */
+ public function testCommentsInParameter()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$param',
+ 'content' => '// Leading comment.
+ ?MyClass /*-*/ & /*-*/.../*-*/ $param /*-*/ = /*-*/ \'default value\' . /*-*/ \'second part\' // Trailing comment.',
+ 'has_attributes' => false,
+ 'pass_by_reference' => true,
+ 'variable_length' => true,
+ 'type_hint' => '?MyClass',
+ 'nullable_type' => true,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testCommentsInParameter()
+
+
+ /**
+ * Verify behaviour when parameters have attributes attached.
+ *
+ * @return void
+ */
+ public function testParameterAttributesInFunctionDeclaration()
+ {
+ $expected = [];
+ $expected[0] = [
+ 'name' => '$constructorPropPromTypedParamSingleAttribute',
+ 'content' => '#[\MyExample\MyAttribute] private string $constructorPropPromTypedParamSingleAttribute',
+ 'has_attributes' => true,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'string',
+ 'nullable_type' => false,
+ 'property_visibility' => 'private',
+ ];
+ $expected[1] = [
+ 'name' => '$typedParamSingleAttribute',
+ 'content' => '#[MyAttr([1, 2])]
+ Type|false
+ $typedParamSingleAttribute',
+ 'has_attributes' => true,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => 'Type|false',
+ 'nullable_type' => false,
+ ];
+ $expected[2] = [
+ 'name' => '$nullableTypedParamMultiAttribute',
+ 'content' => '#[MyAttribute(1234), MyAttribute(5678)] ?int $nullableTypedParamMultiAttribute',
+ 'has_attributes' => true,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => '?int',
+ 'nullable_type' => true,
+ ];
+ $expected[3] = [
+ 'name' => '$nonTypedParamTwoAttributes',
+ 'content' => '#[WithoutArgument] #[SingleArgument(0)] $nonTypedParamTwoAttributes',
+ 'has_attributes' => true,
+ 'pass_by_reference' => false,
+ 'variable_length' => false,
+ 'type_hint' => '',
+ 'nullable_type' => false,
+ ];
+ $expected[4] = [
+ 'name' => '$otherParam',
+ 'content' => '#[MyAttribute(array("key" => "value"))]
+ &...$otherParam',
+ 'has_attributes' => true,
+ 'pass_by_reference' => true,
+ 'variable_length' => true,
+ 'type_hint' => '',
+ 'nullable_type' => false,
+ ];
+
+ $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected);
+
+ }//end testParameterAttributesInFunctionDeclaration()
+
+
/**
* Test helper.
*
diff --git a/tests/Core/Tokenizer/ArrayKeywordTest.inc b/tests/Core/Tokenizer/ArrayKeywordTest.inc
new file mode 100644
index 0000000000..ce5c553cf6
--- /dev/null
+++ b/tests/Core/Tokenizer/ArrayKeywordTest.inc
@@ -0,0 +1,35 @@
+ 10);
+
+/* testArrayWithComment */
+$var = Array /*comment*/ (1 => 10);
+
+/* testNestingArray */
+$var = array(
+ /* testNestedArray */
+ array(
+ 'key' => 'value',
+
+ /* testClosureReturnType */
+ 'closure' => function($a) use($global) : Array {},
+ ),
+);
+
+/* testFunctionDeclarationParamType */
+function foo(array $a) {}
+
+/* testFunctionDeclarationReturnType */
+function foo($a) : int|array|null {}
+
+class Bar {
+ /* testClassConst */
+ const ARRAY = [];
+
+ /* testClassMethod */
+ public function array() {}
+}
diff --git a/tests/Core/Tokenizer/ArrayKeywordTest.php b/tests/Core/Tokenizer/ArrayKeywordTest.php
new file mode 100644
index 0000000000..237258a62a
--- /dev/null
+++ b/tests/Core/Tokenizer/ArrayKeywordTest.php
@@ -0,0 +1,170 @@
+
+ * @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 ArrayKeywordTest extends AbstractMethodUnitTest
+{
+
+
+ /**
+ * Test that the array keyword is correctly tokenized as `T_ARRAY`.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param string $testContent Optional. The token content to look for.
+ *
+ * @dataProvider dataArrayKeyword
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createTokenMap
+ *
+ * @return void
+ */
+ public function testArrayKeyword($testMarker, $testContent='array')
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $token = $this->getTargetToken($testMarker, [T_ARRAY, T_STRING], $testContent);
+ $tokenArray = $tokens[$token];
+
+ $this->assertSame(T_ARRAY, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_ARRAY (code)');
+ $this->assertSame('T_ARRAY', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_ARRAY (type)');
+
+ $this->assertArrayHasKey('parenthesis_owner', $tokenArray, 'Parenthesis owner is not set');
+ $this->assertArrayHasKey('parenthesis_opener', $tokenArray, 'Parenthesis opener is not set');
+ $this->assertArrayHasKey('parenthesis_closer', $tokenArray, 'Parenthesis closer is not set');
+
+ }//end testArrayKeyword()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testArrayKeyword()
+ *
+ * @return array
+ */
+ public function dataArrayKeyword()
+ {
+ return [
+ 'empty array' => ['/* testEmptyArray */'],
+ 'array with space before parenthesis' => ['/* testArrayWithSpace */'],
+ 'array with comment before parenthesis' => [
+ '/* testArrayWithComment */',
+ 'Array',
+ ],
+ 'nested: outer array' => ['/* testNestingArray */'],
+ 'nested: inner array' => ['/* testNestedArray */'],
+ ];
+
+ }//end dataArrayKeyword()
+
+
+ /**
+ * Test that the array keyword when used in a type declaration is correctly tokenized as `T_STRING`.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param string $testContent Optional. The token content to look for.
+ *
+ * @dataProvider dataArrayType
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createTokenMap
+ *
+ * @return void
+ */
+ public function testArrayType($testMarker, $testContent='array')
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $token = $this->getTargetToken($testMarker, [T_ARRAY, T_STRING], $testContent);
+ $tokenArray = $tokens[$token];
+
+ $this->assertSame(T_STRING, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_STRING (code)');
+ $this->assertSame('T_STRING', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_STRING (type)');
+
+ $this->assertArrayNotHasKey('parenthesis_owner', $tokenArray, 'Parenthesis owner is set');
+ $this->assertArrayNotHasKey('parenthesis_opener', $tokenArray, 'Parenthesis opener is set');
+ $this->assertArrayNotHasKey('parenthesis_closer', $tokenArray, 'Parenthesis closer is set');
+
+ }//end testArrayType()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testArrayType()
+ *
+ * @return array
+ */
+ public function dataArrayType()
+ {
+ return [
+ 'closure return type' => [
+ '/* testClosureReturnType */',
+ 'Array',
+ ],
+ 'function param type' => ['/* testFunctionDeclarationParamType */'],
+ 'function union return type' => ['/* testFunctionDeclarationReturnType */'],
+ ];
+
+ }//end dataArrayType()
+
+
+ /**
+ * Verify that the retokenization of `T_ARRAY` tokens to `T_STRING` is handled correctly
+ * for tokens with the contents 'array' which aren't in actual fact the array keyword.
+ *
+ * @param string $testMarker The comment prefacing the target token.
+ * @param string $testContent The token content to look for.
+ *
+ * @dataProvider dataNotArrayKeyword
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createTokenMap
+ *
+ * @return void
+ */
+ public function testNotArrayKeyword($testMarker, $testContent='array')
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $token = $this->getTargetToken($testMarker, [T_ARRAY, T_STRING], $testContent);
+ $tokenArray = $tokens[$token];
+
+ $this->assertSame(T_STRING, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_STRING (code)');
+ $this->assertSame('T_STRING', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_STRING (type)');
+
+ $this->assertArrayNotHasKey('parenthesis_owner', $tokenArray, 'Parenthesis owner is set');
+ $this->assertArrayNotHasKey('parenthesis_opener', $tokenArray, 'Parenthesis opener is set');
+ $this->assertArrayNotHasKey('parenthesis_closer', $tokenArray, 'Parenthesis closer is set');
+
+ }//end testNotArrayKeyword()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testNotArrayKeyword()
+ *
+ * @return array
+ */
+ public function dataNotArrayKeyword()
+ {
+ return [
+ 'class-constant-name' => [
+ '/* testClassConst */',
+ 'ARRAY',
+ ],
+ 'class-method-name' => ['/* testClassMethod */'],
+ ];
+
+ }//end dataNotArrayKeyword()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/BackfillExplicitOctalNotationTest.inc b/tests/Core/Tokenizer/BackfillExplicitOctalNotationTest.inc
new file mode 100644
index 0000000000..7c5e8c089a
--- /dev/null
+++ b/tests/Core/Tokenizer/BackfillExplicitOctalNotationTest.inc
@@ -0,0 +1,7 @@
+
+ * @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 array $testData The data required for the specific test case.
+ *
+ * @dataProvider dataExplicitOctalNotation
+ * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize
+ *
+ * @return void
+ */
+ public function testExplicitOctalNotation($testData)
+ {
+ $tokens = self::$phpcsFile->getTokens();
+
+ $number = $this->getTargetToken($testData['marker'], [T_LNUMBER]);
+
+ $this->assertSame(constant($testData['type']), $tokens[$number]['code']);
+ $this->assertSame($testData['type'], $tokens[$number]['type']);
+ $this->assertSame($testData['value'], $tokens[$number]['content']);
+
+ }//end testExplicitOctalNotation()
+
+
+ /**
+ * Data provider.
+ *
+ * @see testExplicitOctalNotation()
+ *
+ * @return array
+ */
+ public function dataExplicitOctalNotation()
+ {
+ return [
+ [
+ [
+ 'marker' => '/* testExplicitOctal */',
+ 'type' => 'T_LNUMBER',
+ 'value' => '0o137041',
+ ],
+ ],
+ [
+ [
+ 'marker' => '/* testExplicitOctalCapitalised */',
+ 'type' => 'T_LNUMBER',
+ 'value' => '0O137041',
+ ],
+ ],
+ ];
+
+ }//end dataExplicitOctalNotation()
+
+
+}//end class
diff --git a/tests/Core/Tokenizer/BackfillNumericSeparatorTest.inc b/tests/Core/Tokenizer/BackfillNumericSeparatorTest.inc
index eef53f593b..66f1a9a02a 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;
diff --git a/tests/Core/Tokenizer/BackfillNumericSeparatorTest.php b/tests/Core/Tokenizer/BackfillNumericSeparatorTest.php
index ee4275a214..27efdec5af 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 */',
diff --git a/tests/Core/Tokenizer/BackfillReadonlyTest.inc b/tests/Core/Tokenizer/BackfillReadonlyTest.inc
new file mode 100644
index 0000000000..ab7c16c321
--- /dev/null
+++ b/tests/Core/Tokenizer/BackfillReadonlyTest.inc
@@ -0,0 +1,97 @@
+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;
+
+/* 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..d347417143
--- /dev/null
+++ b/tests/Core/Tokenizer/BackfillReadonlyTest.php
@@ -0,0 +1,232 @@
+
+ * @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',
+ ],
+ [
+ '/* 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..0baca8c5c2 100644
--- a/tests/Core/Tokenizer/BitwiseOrTest.inc
+++ b/tests/Core/Tokenizer/BitwiseOrTest.inc
@@ -33,6 +33,9 @@ class TypeUnion
/* testTypeUnionPropertyFullyQualified */
public \Fully\Qualified\NameA|\Fully\Qualified\NameB $fullyQual;
+ /* testTypeUnionPropertyWithReadOnlyKeyword */
+ protected readonly string|null $array;
+
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..64b5c32986 100644
--- a/tests/Core/Tokenizer/BitwiseOrTest.php
+++ b/tests/Core/Tokenizer/BitwiseOrTest.php
@@ -105,6 +105,7 @@ public function dataTypeUnion()
['/* testTypeUnionPropertyNamespaceRelative */'],
['/* testTypeUnionPropertyPartiallyQualified */'],
['/* testTypeUnionPropertyFullyQualified */'],
+ ['/* testTypeUnionPropertyWithReadOnlyKeyword */'],
['/* testTypeUnionParam1 */'],
['/* testTypeUnionParam2 */'],
['/* testTypeUnionParam3 */'],