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 */'],