diff --git a/.gitattributes b/.gitattributes index 56dcc5dc74..2807459b5e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,14 @@ -.travis.yml export-ignore +.cspell.json export-ignore +.gitattributes export-ignore +.github/ export-ignore +.gitignore export-ignore package.xml export-ignore +phpcs.xml.dist export-ignore +phpstan.neon export-ignore phpunit.xml.dist export-ignore -php5-testingConfig.ini export-ignore -php7-testingConfig.ini export-ignore +scripts/ export-ignore + +# Declare files that should always have CRLF line endings on checkout. +*WinTest.inc text eol=crlf +*WinTest.php text eol=crlf +src/Standards/Generic/Tests/Files/LineEndingsUnitTest*.inc text eol=crlf diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000..5036ca4dcd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,66 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: ['Status: triage', 'Type: bug'] +assignees: '' + +--- + + + +## Describe the bug +A clear and concise description of what the bug is. + +### Code sample +```php +echo "A short code snippet that can be used to reproduce the bug. Do NOT paste screenshots of code!"; +``` + +### Custom ruleset +```xml + + + If you are using a custom ruleset, please enter it here. + +``` + +### To reproduce +Steps to reproduce the behavior: +1. Create a file called `test.php` with the code sample above... +2. Run `phpcs test.php ...` +3. See error message displayed +``` +PHPCS output here +``` + +## Expected behavior +A clear and concise description of what you expected to happen. + +## Versions (please complete the following information) + +| | | +|-|-| +| Operating System | [e.g., Windows 10, MacOS 10.15] +| PHP version | [e.g., 7.2, 7.4] +| PHP_CodeSniffer version | [e.g., 3.5.5, master] +| Standard | [e.g., PSR2, PSR12, Squiz, custom] +| Install type | [e.g. Composer (global/local), PHAR, PEAR, git clone, other (please expand)] + +## Additional context +Add any other context about the problem here. + +## Please confirm: + +- [ ] I have searched the issue list and am not opening a duplicate issue. +- [ ] I confirm that this bug is a bug in PHP_CodeSniffer and not in one of the external standards. +- [ ] I have verified the issue still exists in the `master` branch of PHP_CodeSniffer. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..4afe79d762 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,51 @@ + + + + +## Description + + + +### Suggested changelog entry + + + +### Related issues/external references + +Fixes # + + +## Types of changes + +- [ ] Bug fix _(non-breaking change which fixes an issue)_ +- [ ] New feature _(non-breaking change which adds functionality)_ +- [ ] Breaking change _(fix or feature that would cause existing functionality to change)_ + - [ ] This change is only breaking for integrators, not for external standards or end-users. +- [ ] Documentation improvement + + +## PR checklist + +- [ ] I have checked there is no other PR open for the same change. +- [ ] I have read the [Contribution Guidelines](.github/CONTRIBUTING.md). +- [ ] I grant the project the right to include and distribute the code under the BSD-3-Clause license (and I have the right to grant these rights). +- [ ] I have added tests to cover my changes. +- [ ] I have verified that the code complies with the projects coding standards. +- [ ] [Required for new sniffs] I have added XML documentation for the sniff. +- [ ] [Required for new files] I have added any new files to the `package.xml` file. + + diff --git a/.github/workflows/manage-labels.yml b/.github/workflows/manage-labels.yml new file mode 100644 index 0000000000..f2b074e8c2 --- /dev/null +++ b/.github/workflows/manage-labels.yml @@ -0,0 +1,59 @@ +name: Remove outdated labels + +on: + # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target + issues: + types: + - closed + pull_request_target: + types: + - closed + +jobs: + on-issue-close: + runs-on: ubuntu-latest + if: github.repository_owner == 'squizlabs' && github.event.issue.state == 'closed' + + name: Clean up labels on issue close + + steps: + - uses: mondeja/remove-labels-gh-action@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + labels: | + Status: awaiting feedback + Status: close candidate + Status: needs investigation + Status: triage + + on-pr-merge: + runs-on: ubuntu-latest + if: github.repository_owner == 'squizlabs' && github.event.pull_request.merged == true + + name: Clean up labels on PR merge + + steps: + - uses: mondeja/remove-labels-gh-action@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + labels: | + Status: awaiting feedback + Status: close candidate + Status: needs investigation + Status: triage + + on-pr-close: + runs-on: ubuntu-latest + if: github.repository_owner == 'squizlabs' && github.event_name == 'pull_request_target' && github.event.pull_request.merged == false + + name: Clean up labels on PR close + + steps: + - uses: mondeja/remove-labels-gh-action@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + labels: | + Status: awaiting feedback + Status: close candidate + Status: needs investigation + Status: triage diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000000..19d45d1ee4 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,42 @@ +name: PHPStan + +on: + # Run on all pushes and on all pull requests. + # Prevent the build from running when there are only irrelevant changes. + push: + paths-ignore: + - '**.md' + pull_request: + paths-ignore: + - '**.md' + # Allow manually triggering the workflow. + workflow_dispatch: + +jobs: + phpstan: + name: "PHP: 7.4 | PHPStan" + + runs-on: "ubuntu-latest" + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + coverage: none + tools: phpstan + + # Install dependencies and handle caching in one go. + # Dependencies need to be installed to make sure the PHPUnit classes are recognized. + # @link https://github.com/marketplace/actions/install-composer-dependencies + - name: Install Composer dependencies + uses: "ramsey/composer-install@v2" + with: + # Bust the cache at least once a month - output format: YYYY-MM. + custom-cache-suffix: $(date -u "+%Y-%m") + + - name: Run PHPStan + run: phpstan analyse --configuration=phpstan.neon diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000..53bd6bf782 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,173 @@ +name: Test + +on: + # Run on all pushes and on all pull requests. + # Prevent the build from running when there are only irrelevant changes. + push: + paths-ignore: + - '**.md' + pull_request: + paths-ignore: + - '**.md' + # Allow manually triggering the workflow. + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + php: ['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] + + name: "Build Phar on PHP: ${{ matrix.php }}" + + continue-on-error: ${{ matrix.php == '8.2' }} + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + ini-values: phar.readonly=Off, error_reporting=-1, display_errors=On + + - name: Build the phar + run: php scripts/build-phar.php + + - name: Upload the PHPCS phar + uses: actions/upload-artifact@v3 + if: ${{ success() && matrix.php == '8.0' }} + with: + name: phpcs-phar + path: ./phpcs.phar + if-no-files-found: error + retention-days: 28 + + - name: Upload the PHPCBF phar + uses: actions/upload-artifact@v3 + if: ${{ success() && matrix.php == '8.0' }} + with: + name: phpcbf-phar + path: ./phpcbf.phar + if-no-files-found: error + retention-days: 28 + + # Both the below only check a few files which are rarely changed and therefore unlikely to have issues. + # This test is about testing that the phars are functional, *not* about whether the code style complies. + - name: 'PHPCS: check code style using the Phar file to test the Phar is functional' + run: php phpcs.phar ./scripts + + - name: 'PHPCBF: fix code style using the Phar file to test the Phar is functional' + run: php phpcbf.phar ./scripts + + test: + runs-on: ubuntu-latest + needs: build + + strategy: + # Keys: + # - custom_ini: Whether to run with specific custom ini settings to hit very specific + # code conditions. + matrix: + 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] + + include: + # Builds running the basic tests with different PHP ini settings. + - php: '5.5' + custom_ini: true + - php: '7.0' + custom_ini: true + + name: "PHP: ${{ matrix.php }} ${{ matrix.custom_ini && ' with custom ini settings' || '' }}" + + continue-on-error: ${{ matrix.php == '8.2' }} + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup ini config + id: set_ini + run: | + # 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 'PHP_INI=error_reporting=-1, display_errors=On, date.timezone=Australia/Sydney, short_open_tag=On, asp_tags=On' >> $GITHUB_OUTPUT + elif [[ ${{ matrix.custom_ini }} == true && "${{ matrix.php }}" == '7.0' ]]; then + echo 'PHP_INI=error_reporting=-1, display_errors=On, date.timezone=Australia/Sydney, short_open_tag=On' >> $GITHUB_OUTPUT + else + echo 'PHP_INI=error_reporting=-1, display_errors=On' >> $GITHUB_OUTPUT + fi + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + ini-values: ${{ steps.set_ini.outputs.PHP_INI }} + coverage: none + tools: cs2pr + + # 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' }} + uses: "ramsey/composer-install@v2" + with: + # Bust the cache at least once a month - output format: YYYY-MM. + custom-cache-suffix: $(date -u "+%Y-%m") + + # For PHP 8.0+, we need to install with ignore platform reqs as PHPUnit 7 is still used. + - name: Install Composer dependencies - with ignore platform + if: ${{ matrix.php >= '8.0' }} + uses: "ramsey/composer-install@v2" + with: + composer-options: --ignore-platform-reqs + custom-cache-suffix: $(date -u "+%Y-%m") + + # Note: The code style check is run multiple times against every PHP version + # as it also acts as an integration test. + - name: 'PHPCS: set the path to PHP' + run: php bin/phpcs --config-set php_path php + + - name: 'PHPUnit: run the tests' + 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 > 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' }} + 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' }} + 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' }} + run: cs2pr ./phpcs-report.xml + + - name: 'Composer: validate config' + if: ${{ matrix.custom_ini == false }} + run: composer validate --no-check-all --strict + + - name: Download the PHPCS phar + uses: actions/download-artifact@v3 + with: + name: phpcs-phar + + # This test specifically tests that the Phar which will be released works correctly on all PHP versions. + - name: 'PHPCS: check code style using the Phar file' + if: ${{ matrix.custom_ini == false }} + run: php phpcs.phar diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000000..887b9c2b7b --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,80 @@ +name: Validate + +on: + # Run on all pushes and on all pull requests. + # Prevent the build from running when there are only irrelevant changes. + push: + paths-ignore: + - '**.md' + pull_request: + paths-ignore: + - '**.md' + # Allow manually triggering the workflow. + workflow_dispatch: + +jobs: + checkxml: + name: Check XML files + runs-on: ubuntu-latest + + env: + XMLLINT_INDENT: ' ' + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Install xmllint + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y libxml2-utils + + - name: Retrieve XML Schema + run: curl -O https://www.w3.org/2012/04/XMLSchema.xsd + + # Show XML violations inline in the file diff. + # @link https://github.com/marketplace/actions/xmllint-problem-matcher + - uses: korelstar/xmllint-problem-matcher@v1 + + # Validate the XML ruleset files. + # @link http://xmlsoft.org/xmllint.html + - name: Validate rulesets against schema + run: xmllint --noout --schema phpcs.xsd ./src/Standards/*/ruleset.xml + + # Validate the XSD file. + # @link http://xmlsoft.org/xmllint.html + - name: Validate XSD against schema + run: xmllint --noout --schema ./XMLSchema.xsd ./phpcs.xsd + + # Check the code-style consistency of the XML files. + - name: Check XML code style + run: | + diff -B ./phpcs.xml.dist <(xmllint --format "./phpcs.xml.dist") + diff -B ./src/Standards/Generic/ruleset.xml <(xmllint --format "./src/Standards/Generic/ruleset.xml") + diff -B ./src/Standards/MySource/ruleset.xml <(xmllint --format "./src/Standards/MySource/ruleset.xml") + diff -B ./src/Standards/PEAR/ruleset.xml <(xmllint --format "./src/Standards/PEAR/ruleset.xml") + diff -B ./src/Standards/PSR1/ruleset.xml <(xmllint --format "./src/Standards/PSR1/ruleset.xml") + diff -B ./src/Standards/PSR2/ruleset.xml <(xmllint --format "./src/Standards/PSR2/ruleset.xml") + diff -B ./src/Standards/PSR12/ruleset.xml <(xmllint --format "./src/Standards/PSR12/ruleset.xml") + diff -B ./src/Standards/Squiz/ruleset.xml <(xmllint --format "./src/Standards/Squiz/ruleset.xml") + diff -B ./src/Standards/Zend/ruleset.xml <(xmllint --format "./src/Standards/Zend/ruleset.xml") + + pear: + name: "PHP: 7.4 | PEAR package validation" + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + coverage: none + + - name: Validate the PEAR package file contents + run: php scripts/validate-pear-package.php + + - name: Validate the PEAR package + run: pear package-validate package.xml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 66e4286d6a..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,93 +0,0 @@ -sudo: false -language: php -dist: trusty - -matrix: - fast_finish: true - include: - - php: 5.4 - - php: 5.5 - - php: 5.5 - env: CUSTOM_INI=1 XMLLINT=1 - addons: - apt: - packages: - - libxml2-utils - - php: 5.6 - - php: 7.0 - - php: 7.0 - env: CUSTOM_INI=1 PEAR_VALIDATE=1 - - php: 7.1 - - php: 7.2 - env: PHPUNIT_INCOMPAT=1 - - php: 7.3 - env: PHPUNIT_INCOMPAT=1 - - php: 7.4 - env: PHPUNIT_INCOMPAT=1 - - php: 7.4 - env: PHPSTAN=1 PHPUNIT_INCOMPAT=1 - addons: - apt: - packages: - - libonig-dev - # Nightly is PHP 8.0 since Feb 2019. - - php: nightly - env: PHPUNIT_INCOMPAT=1 - addons: - apt: - packages: - - libonig-dev - - allow_failures: - - php: 7.4 - env: PHPSTAN=1 PHPUNIT_INCOMPAT=1 - - php: nightly - -before_install: - - export XMLLINT_INDENT=" " - # Speed up build time by disabling Xdebug when its not needed. - - phpenv config-rm xdebug.ini || echo 'No xdebug config.' - # PHPUnit 8.x is not (yet) supported, so prevent issues with Travis images using it. - - | - if [[ $PHPUNIT_INCOMPAT == "1" && $TRAVIS_PHP_VERSION != "nightly" ]]; then - composer install - elif [[ $PHPUNIT_INCOMPAT == "1" && $TRAVIS_PHP_VERSION == "nightly" ]]; then - // Allow installing "incompatible" PHPUnit version on PHP 8/nightly. - composer install --ignore-platform-reqs - fi - -before_script: - - if [[ $CUSTOM_INI == "1" && ${TRAVIS_PHP_VERSION:0:1} == "5" ]]; then phpenv config-add php5-testingConfig.ini; fi - - if [[ $CUSTOM_INI == "1" ]] && [[ ${TRAVIS_PHP_VERSION:0:1} == "7" || $TRAVIS_PHP_VERSION == "nightly" ]]; then phpenv config-add php7-testingConfig.ini; fi - -script: - - php bin/phpcs --config-set php_path php - - | - if [[ $PHPUNIT_INCOMPAT != "1" ]]; then - phpunit tests/AllTests.php - else - vendor/bin/phpunit tests/AllTests.php - fi - - if [[ $CUSTOM_INI != "1" ]]; then php bin/phpcs --no-cache --parallel=1; fi - - if [[ $CUSTOM_INI != "1" && $TRAVIS_PHP_VERSION != "nightly" ]]; then pear package-validate package.xml; fi - - if [[ $PEAR_VALIDATE == "1" ]]; then php scripts/validate-pear-package.php; fi - - if [[ $CUSTOM_INI != "1" ]]; then composer validate --no-check-all --strict; fi - - if [[ $CUSTOM_INI != "1" ]]; then php scripts/build-phar.php; fi - - if [[ $CUSTOM_INI != "1" ]]; then php phpcs.phar; fi - # Validate the xml ruleset files. - # @link http://xmlsoft.org/xmllint.html - - if [[ $XMLLINT == "1" ]]; then xmllint --noout --schema phpcs.xsd ./src/Standards/*/ruleset.xml; fi - - if [[ $XMLLINT == "1" ]]; then curl -O https://www.w3.org/2012/04/XMLSchema.xsd; fi - - if [[ $XMLLINT == "1" ]]; then xmllint --noout --schema ./XMLSchema.xsd ./phpcs.xsd; fi - # Check the code-style consistency of the xml files. - - if [[ $XMLLINT == "1" ]]; then diff -B ./phpcs.xml.dist <(xmllint --format "./phpcs.xml.dist"); fi - - if [[ $XMLLINT == "1" ]]; then diff -B ./src/Standards/Generic/ruleset.xml <(xmllint --format "./src/Standards/Generic/ruleset.xml"); fi - - if [[ $XMLLINT == "1" ]]; then diff -B ./src/Standards/MySource/ruleset.xml <(xmllint --format "./src/Standards/MySource/ruleset.xml"); fi - - if [[ $XMLLINT == "1" ]]; then diff -B ./src/Standards/PEAR/ruleset.xml <(xmllint --format "./src/Standards/PEAR/ruleset.xml"); fi - - if [[ $XMLLINT == "1" ]]; then diff -B ./src/Standards/PSR1/ruleset.xml <(xmllint --format "./src/Standards/PSR1/ruleset.xml"); fi - - if [[ $XMLLINT == "1" ]]; then diff -B ./src/Standards/PSR2/ruleset.xml <(xmllint --format "./src/Standards/PSR2/ruleset.xml"); fi - - if [[ $XMLLINT == "1" ]]; then diff -B ./src/Standards/PSR12/ruleset.xml <(xmllint --format "./src/Standards/PSR12/ruleset.xml"); fi - - if [[ $XMLLINT == "1" ]]; then diff -B ./src/Standards/Squiz/ruleset.xml <(xmllint --format "./src/Standards/Squiz/ruleset.xml"); fi - - if [[ $XMLLINT == "1" ]]; then diff -B ./src/Standards/Zend/ruleset.xml <(xmllint --format "./src/Standards/Zend/ruleset.xml"); fi - # Run PHPStan - - if [[ $PHPSTAN == "1" ]]; then composer require --dev phpstan/phpstan && php vendor/bin/phpstan analyse --configuration=phpstan.neon; fi diff --git a/CodeSniffer.conf.dist b/CodeSniffer.conf.dist index 62dc395cf8..f95058faa7 100644 --- a/CodeSniffer.conf.dist +++ b/CodeSniffer.conf.dist @@ -5,5 +5,5 @@ 'show_warnings' => '0', 'show_progress' => '1', 'report_width' => '120', -) +); ?> diff --git a/README.md b/README.md index 7452f7f101..25c51c7cbf 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,18 @@ +> [!WARNING] +> This repository has been abandoned. Its successor is [PHPCSStandards/PHP_CodeSniffer](https://github.com/PHPCSStandards/PHP_CodeSniffer/) +> +> See issue [#3932](https://github.com/squizlabs/PHP_CodeSniffer/issues/3932) for more information. +> + + ## About PHP_CodeSniffer is a set of two PHP scripts; the main `phpcs` script that tokenizes PHP, JavaScript and CSS files to detect violations of a defined coding standard, and a second `phpcbf` script to automatically correct coding standard violations. PHP_CodeSniffer is an essential development tool that ensures your code remains clean and consistent. -[![Build Status](https://travis-ci.org/squizlabs/PHP_CodeSniffer.svg?branch=phpcs-fixer)](https://travis-ci.org/squizlabs/PHP_CodeSniffer) [![Code consistency](http://squizlabs.github.io/PHP_CodeSniffer/analysis/squizlabs/PHP_CodeSniffer/grade.svg)](http://squizlabs.github.io/PHP_CodeSniffer/analysis/squizlabs/PHP_CodeSniffer) [![Join the chat at https://gitter.im/squizlabs/PHP_CodeSniffer](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/squizlabs/PHP_CodeSniffer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Build Status](https://github.com/squizlabs/PHP_CodeSniffer/workflows/Validate/badge.svg?branch=master)](https://github.com/squizlabs/PHP_CodeSniffer/actions) +[![Build Status](https://github.com/squizlabs/PHP_CodeSniffer/workflows/Test/badge.svg?branch=master)](https://github.com/squizlabs/PHP_CodeSniffer/actions) +[![Code consistency](http://squizlabs.github.io/PHP_CodeSniffer/analysis/squizlabs/PHP_CodeSniffer/grade.svg)](http://squizlabs.github.io/PHP_CodeSniffer/analysis/squizlabs/PHP_CodeSniffer) +[![Join the chat at https://gitter.im/squizlabs/PHP_CodeSniffer](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/squizlabs/PHP_CodeSniffer?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ## Requirements @@ -30,9 +40,9 @@ php phpcbf.phar -h ### Composer If you use Composer, you can install PHP_CodeSniffer system-wide with the following command: - - composer global require "squizlabs/php_codesniffer=*" - +```bash +composer global require "squizlabs/php_codesniffer=*" +``` Make sure you have the composer bin dir in your PATH. The default value is `~/.composer/vendor/bin/`, but you can check the value that you need to use by running `composer global config bin-dir --absolute`. Or alternatively, include a dependency for `squizlabs/php_codesniffer` in your `composer.json` file. For example: @@ -46,47 +56,48 @@ Or alternatively, include a dependency for `squizlabs/php_codesniffer` in your ` ``` You will then be able to run PHP_CodeSniffer from the vendor bin directory: - - ./vendor/bin/phpcs -h - ./vendor/bin/phpcbf -h - +```bash +./vendor/bin/phpcs -h +./vendor/bin/phpcbf -h +``` ### Phive If you use Phive, you can install PHP_CodeSniffer as a project tool using the following commands: - - phive install phpcs - phive install phpcbf - +```bash +phive install phpcs +phive install phpcbf +``` You will then be able to run PHP_CodeSniffer from the tools directory: - - ./tools/phpcs -h - ./tools/phpcbf -h - +```bash +./tools/phpcs -h +./tools/phpcbf -h +``` ### PEAR If you use PEAR, you can install PHP_CodeSniffer using the PEAR installer. This will make the `phpcs` and `phpcbf` commands immediately available for use. To install PHP_CodeSniffer using the PEAR installer, first ensure you have [installed PEAR](http://pear.php.net/manual/en/installation.getting.php) and then run the following command: - - pear install PHP_CodeSniffer - +```bash +pear install PHP_CodeSniffer +``` ### Git Clone You can also download the PHP_CodeSniffer source and run the `phpcs` and `phpcbf` commands directly from the Git clone: - - git clone https://github.com/squizlabs/PHP_CodeSniffer.git - cd PHP_CodeSniffer - php bin/phpcs -h - php bin/phpcbf -h - +```bash +git clone https://github.com/squizlabs/PHP_CodeSniffer.git +cd PHP_CodeSniffer +php bin/phpcs -h +php bin/phpcbf -h +``` ## Getting Started The default coding standard used by PHP_CodeSniffer is the PEAR coding standard. To check a file against the PEAR coding standard, simply specify the file's location: - - $ phpcs /path/to/code/myfile.php - +```bash +phpcs /path/to/code/myfile.php +``` Or if you wish to check an entire directory you can specify the directory location instead of a file. - - $ phpcs /path/to/code-directory - -If you wish to check your code against the PSR-2 coding standard, use the `--standard` command line argument: - - $ phpcs --standard=PSR2 /path/to/code-directory +```bash +phpcs /path/to/code-directory +``` +If you wish to check your code against the PSR-12 coding standard, use the `--standard` command line argument: +```bash +phpcs --standard=PSR12 /path/to/code-directory +``` If PHP_CodeSniffer finds any coding standard errors, a report will be shown after running the command. diff --git a/autoload.php b/autoload.php index 165ff1a6a0..0dcf1b4c81 100644 --- a/autoload.php +++ b/autoload.php @@ -160,14 +160,58 @@ public static function loadFile($path) return self::$loadedClasses[$path]; } - $classes = get_declared_classes(); - $interfaces = get_declared_interfaces(); - $traits = get_declared_traits(); + $classesBeforeLoad = [ + 'classes' => get_declared_classes(), + 'interfaces' => get_declared_interfaces(), + 'traits' => get_declared_traits(), + ]; include $path; - $className = null; - $newClasses = array_reverse(array_diff(get_declared_classes(), $classes)); + $classesAfterLoad = [ + 'classes' => get_declared_classes(), + 'interfaces' => get_declared_interfaces(), + 'traits' => get_declared_traits(), + ]; + + $className = self::determineLoadedClass($classesBeforeLoad, $classesAfterLoad); + + self::$loadedClasses[$path] = $className; + self::$loadedFiles[$className] = $path; + return self::$loadedClasses[$path]; + + }//end loadFile() + + + /** + * Determine which class was loaded based on the before and after lists of loaded classes. + * + * @param array $classesBeforeLoad The classes/interfaces/traits before the file was included. + * @param array $classesAfterLoad The classes/interfaces/traits after the file was included. + * + * @return string The fully qualified name of the class in the loaded file. + */ + public static function determineLoadedClass($classesBeforeLoad, $classesAfterLoad) + { + $className = null; + + $newClasses = array_diff($classesAfterLoad['classes'], $classesBeforeLoad['classes']); + if (PHP_VERSION_ID < 70400) { + $newClasses = array_reverse($newClasses); + } + + // Since PHP 7.4 get_declared_classes() does not guarantee any order, making + // it impossible to use order to determine which is the parent an which is the child. + // Let's reduce the list of candidates by removing all the classes known to be "parents". + // That way, at the end, only the "main" class just included will remain. + $newClasses = array_reduce( + $newClasses, + function ($remaining, $current) { + return array_diff($remaining, class_parents($current)); + }, + $newClasses + ); + foreach ($newClasses as $name) { if (isset(self::$loadedFiles[$name]) === false) { $className = $name; @@ -176,7 +220,7 @@ public static function loadFile($path) } if ($className === null) { - $newClasses = array_reverse(array_diff(get_declared_interfaces(), $interfaces)); + $newClasses = array_reverse(array_diff($classesAfterLoad['interfaces'], $classesBeforeLoad['interfaces'])); foreach ($newClasses as $name) { if (isset(self::$loadedFiles[$name]) === false) { $className = $name; @@ -186,7 +230,7 @@ public static function loadFile($path) } if ($className === null) { - $newClasses = array_reverse(array_diff(get_declared_traits(), $traits)); + $newClasses = array_reverse(array_diff($classesAfterLoad['traits'], $classesBeforeLoad['traits'])); foreach ($newClasses as $name) { if (isset(self::$loadedFiles[$name]) === false) { $className = $name; @@ -195,11 +239,9 @@ public static function loadFile($path) } } - self::$loadedClasses[$path] = $className; - self::$loadedFiles[$className] = $path; - return self::$loadedClasses[$path]; + return $className; - }//end loadFile() + }//end determineLoadedClass() /** diff --git a/composer.json b/composer.json index 7605a5df91..37f41a0b80 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,8 @@ "type": "library", "keywords": [ "phpcs", - "standards" + "standards", + "static analysis" ], "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", "license": "BSD-3-Clause", diff --git a/licence.txt b/licence.txt index f95432c87b..9f95b67713 100644 --- a/licence.txt +++ b/licence.txt @@ -8,7 +8,7 @@ modification, are permitted provided that the following conditions are met: * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Squiz Pty Ltd nor the + * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/package.xml b/package.xml index 4d2105b94e..4120579390 100644 --- a/package.xml +++ b/package.xml @@ -14,11 +14,11 @@ http://pear.php.net/dtd/package-2.0.xsd"> gsherwood@squiz.net yes - 2020-04-17 - + 2022-06-18 + - 3.5.6 - 3.5.6 + 3.8.0 + 3.8.0 stable @@ -26,19 +26,94 @@ http://pear.php.net/dtd/package-2.0.xsd"> BSD 3-Clause License - - File::getMethodProperties() now detects the PHP 8.0 static return type - -- Thanks to Juliette Reinders Folmer for the patch - - The cache is no longer used if the list of loaded PHP extensions changes - -- Thanks to Juliette Reinders Folmer for the patch - - Squiz.Scope.StaticThisUsage now detects usage of $this inside closures and arrow Functions - -- Thanks to Michał Bundyra for the patch - - Fixed bug #2877 : PEAR.Functions.FunctionCallSignature false positive for array of functions - -- Thanks to Vincent Langlet for the patch - - Fixed bug #2888 : PSR12.Files.FileHeader blank line error with multiple namespaces in one file - - Fixed bug #2926 : phpcs hangs when using arrow functions that return heredoc - - Fixed bug #2943 : Redundant semicolon added to a file when fixing PSR2.Files.ClosingTag.NotAllowed - - Fixed bug #2977 : File::isReference() does not detect return by reference for closures - -- Thanks to Juliette Reinders Folmer for the patch + - Changes have been made to the way PHPCS handles invalid sniff properties being set in a custom ruleset + -- Fixes PHP 8.2 deprecation notices for properties set in a (custom) ruleset for complete standards/complete sniff categories + -- Invalid sniff properties set for individual sniffs will now result in an error and halt the execution of PHPCS + --- A descriptive error message is provided to allow users to fix their ruleset + -- Sniff properties set for complete standards/complete sniff categories will now only be set on sniffs which explicitly support the property + --- The property will be silently ignored for those sniffs which do not support the property + -- For sniff developers, it is strongly recommended for sniffs to explicitly declare any user-adjustable public properties + --- If dynamic properties need to be supported for a sniff, either declare the magic __set()/__get()/__isset()/__unset() methods on the sniff or let the sniff extend stdClass + --- Note: The #[\AllowDynamicProperties] attribute will have no effect for properties which are being set in rulesets. + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - The third parameter for the Ruleset::setSniffProperty() method has been changed to expect an array + -- Sniff developers/integrators of PHPCS may need to make some small adjustments to allow for this change + -- Existing code will continue to work but will throw a deprecation error + -- The backwards compatiblity layer will be removed in PHPCS 4.0 + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - When using auto report width (the default) a value of 80 columns will be used if an auto width cannot be determined + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Sniff error messages are now more informative to help bugs get reported to the correct project + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Added support for readonly classes to File::getClassProperties() through a new is_readonly array index in the return value + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Added support for readonly class to a number of sniffs + -- Generic.CodeAnalysis.UnnecessaryFinalModifier + -- PEAR.Commenting.ClassComment + -- PEAR.Commenting.FileComment + -- PSR2.Classes.ClassDeclaration + -- Squiz.Classes.ClassDeclaration + -- Squiz.Classes.LowercaseClassKeywords + -- Squiz.Commenting.ClassComment + -- Squiz.Commenting.DocCommentAlignment + -- Squiz.Commenting.FileComment + -- Squiz.Commenting.InlineComment + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - PSR2.Classes.PropertyDeclaration now enforces that the readonly modifier comes after the visibility modifier + - PSR2 and PSR12 do not have documented rules for this as they pre-date the readonly modifier + - PSR-PER has been used to confirm the order of this keyword so it can be applied to PSR2 and PSR12 correctly + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Squiz.Commenting.FunctionComment: new ParamNameUnexpectedAmpersandPrefix error for parameters annotated as passed by reference while the parameter is not passed by reference + -- Thanks to Dan Wallis (@fredden) for the patch + - PEAR.Commenting.FunctionComment + Squiz.Commenting.FunctionComment: the SpacingAfter error can now be auto-fixed. + -- Thanks to Dan Wallis (@fredden) for the patch + - Squiz.PHP.InnerFunctions sniff no longer reports on OO methods for OO structures declared within a function or closure + -- Thanks to @Daimona for the patch + - Documentation has been added for the following sniffs: + -- PSR2.Files.ClosingTag + -- PSR2.Methods.FunctionCallSignature + -- PSR2.Methods.FunctionClosingBrace + -- Thanks to Atsushi Okui (@blue32a) for the patch + - Fixed bug #3557 : Squiz.Arrays.ArrayDeclaration will now ignore PHP 7.4 array unpacking when determining whether an array is associative + -- Thanks to Volker Dusch (@edorian) for the patch + - Fixed bug #3717 : Squiz.Commenting.FunctionComment: fixed false positive for InvalidNoReturn when type is never + -- Thanks to Choraimy Kroonstuiver (@axlon) for the patch + - Fixed bug #3722 : Potential "Uninitialized string offset 1" in octal notation backfill + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3728 : PHP 8.2 | PSR1/SideEffects: allow for readonly classes + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3770 : Squiz/NonExecutableCode: prevent false positives for switching between PHP and HTML + -- Thanks to Dan Wallis (@fredden) for the patch + - Fixed bug #3776 : Generic/JSHint: error when JSHint is not available + -- Thanks to Dan Wallis (@fredden) for the patch + - Fixed bug #3777 : Squiz/NonExecutableCode: slew of bug fixes, mostly related to modern PHP + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3779 : Squiz/LowercasePHPFunctions + Generic/ForbiddenFunctions: bug fix for class names in attributes + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3785 : Squiz.Commenting.FunctionComment: potential "Uninitialized string offset 0" when a type contains a duplicate pipe symbol + -- Thanks to Dan Wallis (@fredden) for the patch + - Fixed bug #3787 : PEAR/Squiz/[MultiLine]FunctionDeclaration: allow for PHP 8.1 new in initializers + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3789 : Incorrect tokenization for ternary operator with match inside of it + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3797 : Tokenizer/PHP: more context sensitive keyword fixes + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3801 : File::getMethodParameters(): allow for readonly promoted properties without visibility + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3805 : Generic/FunctionCallArgumentSpacing: prevent fixer conflict over PHP 7.3+ trailing comma's in function calls + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3806 : Squiz.PHP.InnerFunctions sniff now correctly reports inner functions declared within a closure + -- Thanks to @Daimona for the patch + - Fixed bug #3809 : GitBlame report was broken when passing a basepath + -- Thanks to Chris (@datengraben) for the patch + - Fixed bug #3813 : Squiz.Commenting.FunctionComment: false positive for parameter name mismatch on parameters annotated as passed by reference + -- Thanks to Dan Wallis (@fredden) for the patch + - Fixed bug #3816 : PSR12/FileHeader: bug fix - false positives on PHP 8.2+ readonly classes + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3877 : Filter names can be case-sensitive. The -h help text will now display the correct case for the available filters + -- Thanks to @simonsan for the patch + - Fixed bug #3906 : Tokenizer/CSS: fixed a bug related to the unsupported slash comment syntax + -- Thanks to Dan Wallis (@fredden) for the patch @@ -70,6 +145,20 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + + + + + + + + + + + @@ -77,6 +166,10 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + @@ -93,6 +186,16 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + + + + + + + @@ -100,6 +203,15 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + + + + + + @@ -109,10 +221,58 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -257,8 +417,11 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + @@ -362,8 +525,11 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + @@ -507,6 +673,11 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + + @@ -528,9 +699,16 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + + + + @@ -542,8 +720,10 @@ http://pear.php.net/dtd/package-2.0.xsd"> - - + + + + @@ -552,7 +732,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> - + + @@ -607,10 +788,16 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + + + @@ -621,7 +808,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> - + + @@ -661,7 +849,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> - + + @@ -681,6 +870,7 @@ http://pear.php.net/dtd/package-2.0.xsd"> + @@ -696,13 +886,19 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + - - + + + + + + @@ -896,7 +1092,10 @@ http://pear.php.net/dtd/package-2.0.xsd"> - + + + + @@ -985,6 +1184,7 @@ http://pear.php.net/dtd/package-2.0.xsd"> + @@ -1000,6 +1200,10 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + @@ -1032,6 +1236,7 @@ http://pear.php.net/dtd/package-2.0.xsd"> + @@ -1073,6 +1278,9 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + @@ -1108,6 +1316,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + @@ -1166,9 +1376,12 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + @@ -1250,6 +1463,13 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + + + + @@ -1518,6 +1738,10 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + @@ -1571,6 +1795,7 @@ http://pear.php.net/dtd/package-2.0.xsd"> + @@ -1634,6 +1859,7 @@ http://pear.php.net/dtd/package-2.0.xsd"> + @@ -1720,6 +1946,7 @@ http://pear.php.net/dtd/package-2.0.xsd"> + @@ -1945,12 +2172,22 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + + + + + + + @@ -1968,15 +2205,76 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1998,12 +2296,22 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + + + + + + + @@ -2021,15 +2329,76 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2037,6 +2406,541 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + + 3.7.2 + 3.7.2 + + + stable + stable + + 2023-02-23 + BSD License + + - Newer versions of Composer will now suggest installing PHPCS using require-dev instead of require + -- Thanks to Gary Jones (@GaryJones) for the patch + - A custom Out Of Memory error will now be shown if PHPCS or PHPCBF run out of memory during a run + -- Error message provides actionable information about how to fix the problem and ensures the error is not silent + -- Thanks to Juliette Reinders Folmer (@jrfnl) and Alain Schlesser (@schlessera) for the patch + - Generic.PHP.LowerCaseType sniff now correctly examines types inside arrow functions + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Squiz.Formatting.OperatorBracket no longer reports false positives in match() structures + - Fixed bug #3616 : Squiz.PHP.DisallowComparisonAssignment false positive for PHP 8 match expression + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3618 : Generic.WhiteSpace.ArbitraryParenthesesSpacing false positive for return new parent() + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3632 : Short list not tokenized correctly in control structures without braces + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3639 : Tokenizer not applying tab replacement to heredoc/nowdoc closers + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3640 : Generic.WhiteSpace.DisallowTabIndent not reporting errors for PHP 7.3 flexible heredoc/nowdoc syntax + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3645 : PHPCS can show 0 exit code when running in parallel even if child process has fatal error + -- Thanks to Alex Panshin (@enl) for the patch + - Fixed bug #3653 : False positives for match() in OperatorSpacingSniff + -- Thanks to Jaroslav Hanslík (@kukulich) for the patch + - Fixed bug #3666 : PEAR.Functions.FunctionCallSignature incorrect indent fix when checking mixed HTML/PHP files + - Fixed bug #3668 : PSR12.Classes.ClassInstantiation.MissingParentheses false positive when instantiating parent classes + -- Similar issues also fixed in Generic.Functions.FunctionCallArgumentSpacing and Squiz.Formatting.OperatorBracket + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + - Fixed bug #3672 : Incorrect ScopeIndent.IncorrectExact report for match inside array literal + - Fixed bug #3694 : Generic.WhiteSpace.SpreadOperatorSpacingAfter does not ignore spread operator in PHP 8.1 first class callables + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + + + + + 3.7.1 + 3.7.1 + + + stable + stable + + 2022-06-18 + BSD License + + - Fixed bug #3609 : Methods/constants with name empty/isset/unset are always reported as error + -- Thanks to Juliette Reinders Folmer (@jrfnl) for the patch + + + + + 3.7.0 + 3.7.0 + + + stable + stable + + 2022-06-13 + BSD License + + - Added support for PHP 8.1 explicit octal notation + -- This new syntax has been backfilled for PHP versions less than 8.1 + -- Thanks to Mark Baker for the patch + -- Thanks to Juliette Reinders Folmer for additional fixes + - Added support for PHP 8.1 enums + -- This new syntax has been backfilled for PHP versions less than 8.1 + -- Includes a new T_ENUM_CASE token to represent the case statements inside an enum + -- Thanks to Jaroslav Hanslík for the patch + -- Thanks to Juliette Reinders Folmer for additional core and sniff support + - Added support for the PHP 8.1 readonly token + -- Tokenzing of the readonly keyword has been backfilled for PHP versions less than 8.1 + -- Thanks to Jaroslav Hanslík for the patch + - Added support for PHP 8.1 intersection types + -- Includes a new T_TYPE_INTERSECTION token to represent the ampersand character inside intersection types + -- Thanks to Jaroslav Hanslík for the patch + - File::getMethodParameters now supports the new PHP 8.1 readonly token + -- When constructor property promotion is used, a new property_readonly array index is included in the return value + --- This is a boolean value indicating if the property is readonly + -- If the readonly token is detected, a new readonly_token array index is included in the return value + --- This contains the token index of the readonly keyword + -- Thanks to Juliette Reinders Folmer for the patch + - Support for new PHP 8.1 readonly keyword has been added to the following sniffs: + -- Generic.PHP.LowerCaseKeyword + -- PSR2.Classes.PropertyDeclaration + -- Squiz.Commenting.BlockComment + -- Squiz.Commenting.DocCommentAlignment + -- Squiz.Commenting.VariableComment + -- Squiz.WhiteSpace.ScopeKeywordSpacing + -- Thanks to Juliette Reinders Folmer for the patches + - The parallel feature is now more efficient and runs faster in some situations due to improved process management + -- Thanks to Sergei Morozov for the patch + - The list of installed coding standards now has consistent ordering across all platforms + -- Thanks to Juliette Reinders Folmer for the patch + - Generic.PHP.UpperCaseConstant and Generic.PHP.LowerCaseConstant now ignore type declarations + -- These sniffs now only report errors for true/false/null when used as values + -- Thanks to Juliette Reinders Folmer for the patch + - Generic.PHP.LowerCaseType now supports the PHP 8.1 never type + -- Thanks to Jaroslav Hanslík for the patch + - Fixed bug #3502 : A match statement within an array produces Squiz.Arrays.ArrayDeclaration.NoKeySpecified + - Fixed bug #3503 : Squiz.Commenting.FunctionComment.ThrowsNoFullStop false positive when one line @throw + - Fixed bug #3505 : The nullsafe operator is not counted in Generic.Metrics.CyclomaticComplexity + -- Thanks to Mark Baker for the patch + - Fixed bug #3526 : PSR12.Properties.ConstantVisibility false positive when using public final const syntax + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed bug #3530 : Line indented incorrectly false positive when using match-expression inside switch case + - Fixed bug #3534 : Name of typed enum tokenized as T_GOTO_LABEL + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed bug #3546 : Tokenizer/PHP: bug fix - parent/static keywords in class instantiations + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed bug #3550 : False positive from PSR2.ControlStructures.SwitchDeclaration.TerminatingComment when using trailing comment + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed bug #3575: Squiz.Scope.MethodScope misses visibility keyword on previous line + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed bug #3604: Tokenizer/PHP: bug fix for double quoted strings using ${ + -- Thanks to Juliette Reinders Folmer for the patch + + + + + 3.6.2 + 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 + 3.6.0 + + + stable + stable + + 2021-04-09 + BSD License + + - Added support for PHP 8.0 union types + -- A new T_TYPE_UNION token is available to represent the pipe character + -- File::getMethodParameters(), getMethodProperties(), and getMemberProperties() will now return union types + -- Thanks to Juliette Reinders Folmer for the patch + - Added support for PHP 8.0 named function call arguments + -- A new T_PARAM_NAME token is available to represent the label with the name of the function argument in it + -- Thanks to Juliette Reinders Folmer for the patch + - Added support for PHP 8.0 attributes + -- The PHP-supplied T_ATTRIBUTE token marks the start of an attribute + -- A new T_ATTRIBUTE_END token is available to mark the end of an attribute + -- New attribute_owner and attribute_closer indexes are available in the tokens array for all tokens inside an attribute + -- Tokenizing of attributes has been backfilled for older PHP versions + -- The following sniffs have been updated to support attributes: + --- PEAR.Commenting.ClassComment + --- PEAR.Commenting.FileComment + --- PSR1.Files.SideEffects + --- PSR12.Files.FileHeader + --- Squiz.Commenting.ClassComment + --- Squiz.Commenting.FileComment + --- Squiz.WhiteSpace.FunctionSpacing + ---- Thanks to Vadim Borodavko for the patch + -- Thanks to Alessandro Chitolina for the patch + - Added support for PHP 8.0 dereferencing of text strings with interpolated variables + -- Thanks to Juliette Reinders Folmer for the patch + - Added support for PHP 8.0 match expressions + -- Match expressions are now tokenised with parenthesis and scope openers and closers + --- Sniffs can listen for the T_MATCH token to process match expressions + --- Note that the case and default statements inside match expressions do not have scopes set + -- A new T_MATCH_ARROW token is available to represent the arrows in match expressions + -- A new T_MATCH_DEFAULT token is available to represent the default keyword in match expressions + -- All tokenizing of match expressions has been backfilled for older PHP versions + -- The following sniffs have been updated to support match expressions: + --- Generic.CodeAnalysis.AssignmentInCondition + --- Generic.CodeAnalysis.EmptyPHPStatement + ---- Thanks to Vadim Borodavko for the patch + --- Generic.CodeAnalysis.EmptyStatement + --- Generic.PHP.LowerCaseKeyword + --- PEAR.ControlStructures.ControlSignature + --- PSR12.ControlStructures.BooleanOperatorPlacement + --- Squiz.Commenting.LongConditionClosingComment + --- Squiz.Commenting.PostStatementComment + --- Squiz.ControlStructures.LowercaseDeclaration + --- Squiz.ControlStructures.ControlSignature + --- Squiz.Formatting.OperatorBracket + --- Squiz.PHP.DisallowMultipleAssignments + --- Squiz.Objects.ObjectInstantiation + --- Squiz.WhiteSpace.ControlStructureSpacing + -- Thanks to Juliette Reinders Folmer for the patch + - The value of the T_FN_ARROW token has changed from "T_FN_ARROW" to "PHPCS_T_FN_ARROW" to avoid package conflicts + -- This will have no impact on custom sniffs unless they are specifically looking at the value of the T_FN_ARROW constant + -- If sniffs are just using constant to find arrow functions, they will continue to work without modification + -- Thanks to Juliette Reinders Folmer for the patch + - File::findStartOfStatement() now works correctly when passed the last token in a statement + - File::getMethodParameters() now supports PHP 8.0 constructor property promotion + -- Returned method params now include a "property_visibility" and "visibility_token" index if property promotion is detected + -- Thanks to Juliette Reinders Folmer for the patch + - File::getMethodProperties() now includes a "return_type_end_token" index in the return value + -- This indicates the last token in the return type, which is helpful when checking union types + -- Thanks to Juliette Reinders Folmer for the patch + - Include patterns are now ignored when processing STDIN + -- Previously, checks using include patterns were excluded when processing STDIN when no file path was provided via --stdin-path + -- Now, all include and exclude rules are ignored when no file path is provided, allowing all checks to run + -- If you want include and exclude rules enforced when checking STDIN, use --stdin-path to set the file path + -- Thanks to Juliette Reinders Folmer for the patch + - Spaces are now correctly escaped in the paths to external on Windows + -- Thanks to Juliette Reinders Folmer for the patch + - Added Generic.NamingConventions.AbstractClassNamePrefix to enforce that class names are prefixed with "Abstract" + -- Thanks to Anna Borzenko for the contribution + - Added Generic.NamingConventions.InterfaceNameSuffix to enforce that interface names are suffixed with "Interface" + -- Thanks to Anna Borzenko for the contribution + - Added Generic.NamingConventions.TraitNameSuffix to enforce that trait names are suffixed with "Trait" + -- Thanks to Anna Borzenko for the contribution + - Generic.CodeAnalysis.UnusedFunctionParameter can now be configured to ignore variable usage for specific type hints + -- This allows you to suppress warnings for some variables that are not required, but leave warnings for others + -- Set the ignoreTypeHints array property to a list of type hints to ignore + -- Thanks to Petr Bugyík for the patch + - Generic.Formatting.MultipleStatementAlignment can now align statements at the start of the assignment token + -- Previously, the sniff enforced that the values were aligned, even if this meant the assignment tokens were not + -- Now, the sniff can enforce that the assignment tokens are aligned, even if this means the values are not + -- Set the "alignAtEnd" sniff property to "false" to align the assignment tokens + -- The default remains at "true", so the assigned values are aligned + -- Thanks to John P. Bloch for the patch + - Generic.PHP.LowerCaseType now supports checking of typed properties + -- Thanks to Juliette Reinders Folmer for the patch + - Generic.PHP.LowerCaseType now supports checking of union types + -- Thanks to Juliette Reinders Folmer for the patch + - PEAR.Commenting.FunctionComment and Squiz.Commenting.FunctionComment sniffs can now ignore private and protected methods + -- Set the "minimumVisibility" sniff property to "protected" to ignore private methods + -- Set the "minimumVisibility" sniff property to "public" to ignore both private and protected methods + -- The default remains at "private", so all methods are checked + -- Thanks to Vincent Langlet for the patch + - PEAR.Commenting.FunctionComment and Squiz.Commenting.FunctionComment sniffs can now ignore return tags in any method + -- Previously, only __construct and __destruct were ignored + -- Set the list of method names to ignore in the "specialMethods" sniff property + -- The default remains at "__construct" and "__destruct" only + -- Thanks to Vincent Langlet for the patch + - PSR2.ControlStructures.SwitchDeclaration now supports nested switch statements where every branch terminates + -- Previously, if a CASE only contained a SWITCH and no direct terminating statement, a fall-through error was displayed + -- Now, the error is surpressed if every branch of the SWITCH has a terminating statement + -- Thanks to Vincent Langlet for the patch + - The PSR2.Methods.FunctionCallSignature.SpaceBeforeCloseBracket error message is now reported on the closing parenthesis token + -- Previously, the error was being reported on the function keyword, leading to confusing line numbers in the error report + - Squiz.Commenting.FunctionComment is now able to ignore function comments that are only inheritdoc statements + -- Set the skipIfInheritdoc sniff property to "true" to skip checking function comments if the content is only {@inhertidoc} + -- The default remains at "false", so these comments will continue to report errors + -- Thanks to Jess Myrbo for the patch + - Squiz.Commenting.FunctionComment now supports the PHP 8 mixed type + -- Thanks to Vadim Borodavko for the patch + - Squiz.PHP.NonExecutableCode now has improved handling of syntax errors + -- Thanks to Thiemo Kreuz for the patch + - Squiz.WhiteSpace.ScopeKeywordSpacing now checks spacing when using PHP 8.0 constructor property promotion + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed an issue that could occurr when checking files on network drives, such as with WSL2 on Windows 10 + -- This works around a long-standing PHP bug with is_readable() + -- Thanks to Michael S for the patch + - Fixed a number of false positives in the Squiz.PHP.DisallowMultipleAssignments sniff + -- Sniff no longer errors for default value assignments in arrow functions + -- Sniff no longer errors for assignments on first line of closure + -- Sniff no longer errors for assignments after a goto label + -- Thanks to Jaroslav Hanslík for the patch + - Fixed bug #2913 : Generic.WhiteSpace.ScopeIndent false positive when opening and closing tag on same line inside conditional + - Fixed bug #2992 : Enabling caching using a ruleset produces invalid cache files when using --sniffs and --exclude CLI args + - Fixed bug #3003 : Squiz.Formatting.OperatorBracket autofix incorrect when assignment used with null coalescing operator + - Fixed bug #3145 : Autoloading of sniff fails when multiple classes declared in same file + - Fixed bug #3157 : PSR2.ControlStructures.SwitchDeclaration.BreakIndent false positive when case keyword is not indented + - Fixed bug #3163 : Undefined index error with pre-commit hook using husky on PHP 7.4 + -- Thanks to Ismo Vuorinen for the patch + - Fixed bug #3165 : Squiz.PHP.DisallowComparisonAssignment false positive when comparison inside closure + - Fixed bug #3167 : Generic.WhiteSpace.ScopeIndent false positive when using PHP 8.0 constructor property promotion + - Fixed bug #3170 : Squiz.WhiteSpace.OperatorSpacing false positive when using negation with string concat + -- This also fixes the same issue in the PSR12.Operators.OperatorSpacing sniff + - Fixed bug #3177 : Incorrect tokenization of GOTO statements in mixed PHP/HTML files + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed bug #3184 : PSR2.Namespace.NamespaceDeclaration false positive on namespace operator + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed bug #3188 : Squiz.WhiteSpace.ScopeKeywordSpacing false positive for static return type + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed bug #3192 : findStartOfStatement doesn't work correctly inside switch + -- Thanks to Vincent Langlet for the patch + - Fixed bug #3195 : Generic.WhiteSpace.ScopeIndent confusing message when combination of tabs and spaces found + - Fixed bug #3197 : Squiz.NamingConventions.ValidVariableName does not use correct error code for all member vars + - Fixed bug #3219 : Generic.Formatting.MultipleStatementAlignment false positive for empty anonymous classes and closures + - Fixed bug #3258 : Squiz.Formatting.OperatorBracket duplicate error messages for unary minus + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed bug #3273 : Squiz.Functions.FunctionDeclarationArgumentSpacing reports line break as 0 spaces between parenthesis + - Fixed bug #3277 : Nullable static return typehint causes whitespace error + - Fixed bug #3284 : Unused parameter false positive when using array index in arrow function + + + + + 3.5.8 + 3.5.8 + + + stable + stable + + 2020-10-23 + BSD License + + - Reverted a change to the way include/exclude patterns are processed for STDIN content + -- This change is not backwards compatible and will be re-introduced in version 3.6.0 + + + + + 3.5.7 + 3.5.7 + + + stable + stable + + 2020-10-23 + BSD License + + - 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 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 + -- The existing PHP 5-7 behaviour has been replicated for version 8, so no sniff changes are required + -- Thanks to Juliette Reinders Folmer for the patch + - The autoloader has been changed to fix sniff class name detection issues that may occur when running on PHP 7.4+ + -- Thanks to Eloy Lafuente for the patch + - Running the unit tests now includes warnings in the found and fixable error code counts + -- Thanks to Juliette Reinders Folmer for the patch + - PSR12.ControlStructures.BooleanOperatorPlacement.FoundMixed error message is now more accurate when using the allowOnly setting + -- Thanks to Vincent Langlet for the patch + - PSR12.Functions.NullableTypeDeclaration now supports the PHP8 static return type + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed Squiz.Formatting.OperatorBracket false positive when exiting with a negative number + - Fixed Squiz.PHP.DisallowComparisonAssignment false positive for methods called on an object + - Fixed bug #2882 : Generic.Arrays.ArrayIndent can request close brace indent to be less than the statement indent level + - Fixed bug #2883 : Generic.WhiteSpace.ScopeIndent.Incorrect issue after NOWDOC + - Fixed bug #2975 : Undefined offset in PSR12.Functions.ReturnTypeDeclaration when checking function return type inside ternary + - Fixed bug #2988 : Undefined offset in Squiz.Strings.ConcatenationSpacing during live coding + -- Thanks to Thiemo Kreuz for the patch + - Fixed bug #2989 : Incorrect auto-fixing in Generic.ControlStructures.InlineControlStructure during live coding + -- Thanks to Thiemo Kreuz for the patch + - Fixed bug #3007 : Directory exclude pattern improperly excludes directories with names that start the same + -- Thanks to Steve Talbot for the patch + - Fixed bug #3043 : Squiz.WhiteSpace.OperatorSpacing false positive for negation in arrow function + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed bug #3049 : Incorrect error with arrow function and parameter passed as reference + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed bug #3053 : PSR2 incorrect fix when multiple use statements on same line do not have whitespace between them + - Fixed bug #3058 : Progress gets unaligned when 100% happens at the end of the available dots + - Fixed bug #3059 : Squiz.Arrays.ArrayDeclaration false positive when using type casting + -- Thanks to Sergei Morozov for the patch + - Fixed bug #3060 : Squiz.Arrays.ArrayDeclaration false positive for static functions + -- Thanks to Sergei Morozov for the patch + - Fixed bug #3065 : Should not fix Squiz.Arrays.ArrayDeclaration.SpaceBeforeComma if comment between element and comma + -- Thanks to Sergei Morozov for the patch + - Fixed bug #3066 : No support for namespace operator used in type declarations + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed bug #3075 : PSR12.ControlStructures.BooleanOperatorPlacement false positive when operator is the only content on line + - Fixed bug #3099 : Squiz.WhiteSpace.OperatorSpacing false positive when exiting with negative number + -- Thanks to Sergei Morozov for the patch + - Fixed bug #3102 : PSR12.Squiz.OperatorSpacing false positive for default values of arrow functions + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed bug #3124 : PSR-12 not reporting error for empty lines with only whitespace + - Fixed bug #3135 : Ignore annotations are broken on PHP 8.0 + -- Thanks to Juliette Reinders Folmer for the patch + + + + + 3.5.6 + 3.5.6 + + + stable + stable + + 2020-08-10 + BSD License + + - Added support for PHP 8.0 magic constant dereferencing + -- Thanks to Juliette Reinders Folmer for the patch + - Added support for changes to the way PHP 8.0 tokenizes comments + -- The existing PHP 5-7 behaviour has been replicated for version 8, so no sniff changes are required + -- Thanks to Juliette Reinders Folmer for the patch + - File::getMethodProperties() now detects the PHP 8.0 static return type + -- Thanks to Juliette Reinders Folmer for the patch + - The PHP 8.0 static return type is now supported for arrow functions + -- Thanks to Juliette Reinders Folmer for the patch + - The cache is no longer used if the list of loaded PHP extensions changes + -- Thanks to Juliette Reinders Folmer for the patch + - Generic.NamingConventions.CamelCapsFunctionName no longer reports __serialize and __unserialize as invalid names + -- Thanks to Filip Š for the patch + - PEAR.NamingConventions.ValidFunctionName no longer reports __serialize and __unserialize as invalid names + -- Thanks to Filip Š for the patch + - Squiz.Scope.StaticThisUsage now detects usage of $this inside closures and arrow Functions + -- Thanks to Michał Bundyra for the patch + - Fixed bug #2877 : PEAR.Functions.FunctionCallSignature false positive for array of functions + -- Thanks to Vincent Langlet for the patch + - Fixed bug #2888 : PSR12.Files.FileHeader blank line error with multiple namespaces in one file + - Fixed bug #2926 : phpcs hangs when using arrow functions that return heredoc + - Fixed bug #2943 : Redundant semicolon added to a file when fixing PSR2.Files.ClosingTag.NotAllowed + - Fixed bug #2967 : Markdown generator does not output headings correctly + -- Thanks to Petr Bugyík for the patch + - Fixed bug #2977 : File::isReference() does not detect return by reference for closures + -- Thanks to Juliette Reinders Folmer for the patch + - Fixed bug #2994 : Generic.Formatting.DisallowMultipleStatements false positive for FOR loop with no body + - Fixed bug #3033 : Error generated during tokenizing of goto statements on PHP 8 + -- Thanks to Juliette Reinders Folmer for the patch + + 3.5.5 diff --git a/php5-testingConfig.ini b/php5-testingConfig.ini deleted file mode 100644 index 4f0aa048a3..0000000000 --- a/php5-testingConfig.ini +++ /dev/null @@ -1,12 +0,0 @@ -; Defines the default timezone used by the date functions -; http://php.net/date.timezone -date.timezone = "Australia/Sydney" - -; This directive determines whether or not PHP will recognize code between -; tags as PHP source which should be processed as such. -; http://php.net/short-open-tag -short_open_tag = On - -; Allow ASP-style <% %> tags. -; http://php.net/asp-tags -asp_tags = On diff --git a/php7-testingConfig.ini b/php7-testingConfig.ini deleted file mode 100644 index ac008640d3..0000000000 --- a/php7-testingConfig.ini +++ /dev/null @@ -1,8 +0,0 @@ -; Defines the default timezone used by the date functions -; http://php.net/date.timezone -date.timezone = "Australia/Sydney" - -; This directive determines whether or not PHP will recognize code between -; tags as PHP source which should be processed as such. -; http://php.net/short-open-tag -short_open_tag = On diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 48cbdd8ff7..3c54610094 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -9,7 +9,8 @@ tests */src/Standards/*/Tests/*\.(inc|css|js)$ - */tests/Core/*/*Test\.(inc|css|js)$ + */tests/Core/*/*\.(inc|css|js)$ + */tests/Core/*/Fixtures/*\.php$ @@ -143,7 +144,12 @@ - tests/bootstrap.php + tests/bootstrap\.php + + + + + tests/Core/Tokenizer/StableCommentWhitespaceWinTest\.php diff --git a/phpstan.neon b/phpstan.neon index bfd91301f5..f0ab0473ed 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,7 +2,7 @@ parameters: level: 0 paths: - src - autoload_files: + bootstrapFiles: - tests/bootstrap.php ignoreErrors: - diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ff288b9151..34b4afcded 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,5 @@ - + tests/AllTests.php diff --git a/scripts/ValidatePEAR/ValidatePEARPackageXML.php b/scripts/ValidatePEAR/ValidatePEARPackageXML.php index 11d73bc87b..5c1d2d1f0f 100644 --- a/scripts/ValidatePEAR/ValidatePEARPackageXML.php +++ b/scripts/ValidatePEAR/ValidatePEARPackageXML.php @@ -232,10 +232,11 @@ protected function checkFileTag($tag, $currentDirectory='') $valid = false; } else { // Limited validation of the "role" tags. - if (strpos($name, 'Test.') !== false && $role !== 'test') { + if ((strpos($name, 'tests/') === 0 || strpos($name, 'Test.') !== false) && $role !== 'test') { echo "- Test files should have the role 'test'. Found: '$role' for file '{$name}'.".PHP_EOL; $valid = false; - } else if ((strpos($name, 'Standard.xml') !== false || strpos($name, 'Sniff.php') !== false) + } else if (strpos($name, 'tests/') !== 0 + && (strpos($name, 'Standard.xml') !== false || strpos($name, 'Sniff.php') !== false) && $role !== 'php' ) { echo "- Sniff files, including sniff documentation files should have the role 'php'. Found: '$role' for file '{$name}'.".PHP_EOL; 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 74198153cd..0320bdee87 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.5.6'; + const VERSION = '3.8.0'; /** * Package stability; either stable, beta or alpha. @@ -88,6 +89,13 @@ class Config */ const STABILITY = 'stable'; + /** + * Default report width when no report width is provided and 'auto' does not yield a valid width. + * + * @var int + */ + const DEFAULT_REPORT_WIDTH = 80; + /** * An array of settings that PHPCS and PHPCBF accept. * @@ -222,13 +230,20 @@ public function __set($name, $value) switch ($name) { case 'reportWidth' : // Support auto terminal width. - if ($value === 'auto' - && function_exists('shell_exec') === true - && preg_match('|\d+ (\d+)|', shell_exec('stty size 2>&1'), $matches) === 1 - ) { - $value = (int) $matches[1]; - } else { + if ($value === 'auto' && function_exists('shell_exec') === true) { + $dimensions = shell_exec('stty size 2>&1'); + if (is_string($dimensions) === true && preg_match('|\d+ (\d+)|', $dimensions, $matches) === 1) { + $value = (int) $matches[1]; + break; + } + } + + if (is_int($value) === true) { + $value = abs($value); + } else if (is_string($value) === true && preg_match('`^\d+$`', $value) === 1) { $value = (int) $value; + } else { + $value = self::DEFAULT_REPORT_WIDTH; } break; case 'standards' : @@ -363,11 +378,11 @@ 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 - || strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' + || stripos(PHP_OS, 'WIN') === 0 ) { return; } @@ -459,7 +474,7 @@ public function setCommandLineValues($args) /** * Restore default values for all possible command line arguments. * - * @return array + * @return void */ public function restoreDefaults() { @@ -689,7 +704,7 @@ public function processShortArgument($arg, $pos) /** - * Processes a long (--example) command line argument. + * Processes a long (--example) command-line argument. * * @param string $arg The command line argument. * @param int $pos The position of the argument on the command line. @@ -708,7 +723,7 @@ public function processLongArgument($arg, $pos) throw new DeepExitException($output, 0); case 'version': $output = 'PHP_CodeSniffer version '.self::VERSION.' ('.self::STABILITY.') '; - $output .= 'by Squiz (http://www.squiz.net)'.PHP_EOL; + $output .= 'by Squiz (https://www.squiz.net)'.PHP_EOL; throw new DeepExitException($output, 0); case 'colors': if (isset(self::$overriddenDefaults['colors']) === true) { @@ -1388,7 +1403,7 @@ public function printPHPCSUsage() echo ' e.g., module/php,es/js'.PHP_EOL; echo ' One or more files and/or directories to check'.PHP_EOL; echo ' A file containing a list of files and/or directories to check (one per line)'.PHP_EOL; - echo ' Use either the "gitmodified" or "gitstaged" filter,'.PHP_EOL; + echo ' Use either the "GitModified" or "GitStaged" filter,'.PHP_EOL; echo ' or specify the path to a custom filter class'.PHP_EOL; echo ' Use either the "HTML", "Markdown" or "Text" generator'.PHP_EOL; echo ' (forces documentation generation instead of checking)'.PHP_EOL; @@ -1450,7 +1465,7 @@ public function printPHPCBFUsage() echo ' e.g., module/php,es/js'.PHP_EOL; echo ' One or more files and/or directories to fix'.PHP_EOL; echo ' A file containing a list of files and/or directories to fix (one per line)'.PHP_EOL; - echo ' Use either the "gitmodified" or "gitstaged" filter,'.PHP_EOL; + echo ' Use either the "GitModified" or "GitStaged" filter,'.PHP_EOL; echo ' or specify the path to a custom filter class'.PHP_EOL; echo ' A comma separated list of patterns to ignore files and directories'.PHP_EOL; echo ' How many files should be fixed simultaneously (default is 1)'.PHP_EOL; @@ -1516,7 +1531,7 @@ public static function getExecutablePath($name) return self::$executablePaths[$name]; } - if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + if (stripos(PHP_OS, 'WIN') === 0) { $cmd = 'where '.escapeshellarg($name).' 2> nul'; } else { $cmd = 'which '.escapeshellarg($name).' 2> /dev/null'; @@ -1596,7 +1611,7 @@ public static function setConfigData($key, $value, $temp=false) if ($temp === false) { $output = '<'.'?php'."\n".' $phpCodeSnifferConfig = '; $output .= var_export($phpCodeSnifferConfig, true); - $output .= "\n?".'>'; + $output .= ";\n?".'>'; if (file_put_contents($configFile, $output) === false) { $error = 'ERROR: Config file '.$configFile.' could not be written'.PHP_EOL.PHP_EOL; @@ -1656,7 +1671,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 b18d4d445e..21cbeb2158 100644 --- a/src/Files/File.php +++ b/src/Files/File.php @@ -356,7 +356,7 @@ public function process() || $token['code'] === T_DOC_COMMENT_TAG || ($inTests === true && $token['code'] === T_INLINE_HTML)) ) { - $commentText = ltrim($this->tokens[$stackPtr]['content'], ' /*'); + $commentText = ltrim($this->tokens[$stackPtr]['content'], " \t/*#"); $commentTextLower = strtolower($commentText); if (strpos($commentText, '@codingStandards') !== false) { if (strpos($commentText, '@codingStandardsIgnoreFile') !== false) { @@ -379,9 +379,12 @@ public function process() if (isset($this->ruleset->sniffCodes[$parts[0]]) === true) { $listenerCode = array_shift($parts); $propertyCode = array_shift($parts); - $propertyValue = rtrim(implode(' ', $parts), " */\r\n"); + $settings = [ + 'value' => rtrim(implode(' ', $parts), " */\r\n"), + 'scope' => 'sniff', + ]; $listenerClass = $this->ruleset->sniffCodes[$listenerCode]; - $this->ruleset->setSniffProperty($listenerClass, $propertyCode, $propertyValue); + $this->ruleset->setSniffProperty($listenerClass, $propertyCode, $settings); } } } @@ -403,9 +406,12 @@ public function process() $listenerCode = $token['sniffCode']; if (isset($this->ruleset->sniffCodes[$listenerCode]) === true) { $propertyCode = $token['sniffProperty']; - $propertyValue = $token['sniffPropertyValue']; + $settings = [ + 'value' => $token['sniffPropertyValue'], + 'scope' => 'sniff', + ]; $listenerClass = $this->ruleset->sniffCodes[$listenerCode]; - $this->ruleset->setSniffProperty($listenerClass, $propertyCode, $propertyValue); + $this->ruleset->setSniffProperty($listenerClass, $propertyCode, $settings); } } }//end if @@ -442,30 +448,11 @@ public function process() continue; } - // If the file path matches one of our ignore patterns, skip it. - // While there is support for a type of each pattern - // (absolute or relative) we don't actually support it here. - foreach ($listenerData['ignore'] as $pattern) { - // We assume a / directory separator, as do the exclude rules - // most developers write, so we need a special case for any system - // that is different. - if (DIRECTORY_SEPARATOR === '\\') { - $pattern = str_replace('/', '\\\\', $pattern); - } - - $pattern = '`'.$pattern.'`i'; - if (preg_match($pattern, $this->path) === 1) { - $this->ignoredListeners[$class] = true; - continue(2); - } - } - - // If the file path does not match one of our include patterns, skip it. - // While there is support for a type of each pattern - // (absolute or relative) we don't actually support it here. - if (empty($listenerData['include']) === false) { - $included = false; - foreach ($listenerData['include'] as $pattern) { + if (trim($this->path, '\'"') !== 'STDIN') { + // If the file path matches one of our ignore patterns, skip it. + // While there is support for a type of each pattern + // (absolute or relative) we don't actually support it here. + foreach ($listenerData['ignore'] as $pattern) { // We assume a / directory separator, as do the exclude rules // most developers write, so we need a special case for any system // that is different. @@ -475,15 +462,36 @@ public function process() $pattern = '`'.$pattern.'`i'; if (preg_match($pattern, $this->path) === 1) { - $included = true; - break; + $this->ignoredListeners[$class] = true; + continue(2); } } - if ($included === false) { - $this->ignoredListeners[$class] = true; - continue; - } + // If the file path does not match one of our include patterns, skip it. + // While there is support for a type of each pattern + // (absolute or relative) we don't actually support it here. + if (empty($listenerData['include']) === false) { + $included = false; + foreach ($listenerData['include'] as $pattern) { + // We assume a / directory separator, as do the exclude rules + // most developers write, so we need a special case for any system + // that is different. + if (DIRECTORY_SEPARATOR === '\\') { + $pattern = str_replace('/', '\\\\', $pattern); + } + + $pattern = '`'.$pattern.'`i'; + if (preg_match($pattern, $this->path) === 1) { + $included = true; + break; + } + } + + if ($included === false) { + $this->ignoredListeners[$class] = true; + continue; + } + }//end if }//end if $this->activeListener = $class; @@ -965,59 +973,63 @@ protected function addMessage($error, $message, $line, $column, $code, $data, $s // Make sure we are not ignoring this file. $included = null; - foreach ($checkCodes as $checkCode) { - $patterns = null; - - if (isset($this->configCache['includePatterns'][$checkCode]) === true) { - $patterns = $this->configCache['includePatterns'][$checkCode]; - $excluding = false; - } else if (isset($this->configCache['ignorePatterns'][$checkCode]) === true) { - $patterns = $this->configCache['ignorePatterns'][$checkCode]; - $excluding = true; - } - - if ($patterns === null) { - continue; - } + if (trim($this->path, '\'"') === 'STDIN') { + $included = true; + } else { + foreach ($checkCodes as $checkCode) { + $patterns = null; + + if (isset($this->configCache['includePatterns'][$checkCode]) === true) { + $patterns = $this->configCache['includePatterns'][$checkCode]; + $excluding = false; + } else if (isset($this->configCache['ignorePatterns'][$checkCode]) === true) { + $patterns = $this->configCache['ignorePatterns'][$checkCode]; + $excluding = true; + } - foreach ($patterns as $pattern => $type) { - // While there is support for a type of each pattern - // (absolute or relative) we don't actually support it here. - $replacements = [ - '\\,' => ',', - '*' => '.*', - ]; - - // We assume a / directory separator, as do the exclude rules - // most developers write, so we need a special case for any system - // that is different. - if (DIRECTORY_SEPARATOR === '\\') { - $replacements['/'] = '\\\\'; + if ($patterns === null) { + continue; } - $pattern = '`'.strtr($pattern, $replacements).'`i'; - $matched = preg_match($pattern, $this->path); + foreach ($patterns as $pattern => $type) { + // While there is support for a type of each pattern + // (absolute or relative) we don't actually support it here. + $replacements = [ + '\\,' => ',', + '*' => '.*', + ]; - if ($matched === 0) { - if ($excluding === false && $included === null) { - // This file path is not being included. - $included = false; + // We assume a / directory separator, as do the exclude rules + // most developers write, so we need a special case for any system + // that is different. + if (DIRECTORY_SEPARATOR === '\\') { + $replacements['/'] = '\\\\'; } - continue; - } + $pattern = '`'.strtr($pattern, $replacements).'`i'; + $matched = preg_match($pattern, $this->path); - if ($excluding === true) { - // This file path is being excluded. - $this->ignoredCodes[$checkCode] = true; - return false; - } + if ($matched === 0) { + if ($excluding === false && $included === null) { + // This file path is not being included. + $included = false; + } - // This file path is being included. - $included = true; - break; + continue; + } + + if ($excluding === true) { + // This file path is being excluded. + $this->ignoredCodes[$checkCode] = true; + return false; + } + + // This file path is being included. + $included = true; + break; + }//end foreach }//end foreach - }//end foreach + }//end if if ($included === false) { // There were include rules set, but this file @@ -1217,7 +1229,7 @@ public function getFilename() /** - * Returns the declaration names for classes, interfaces, traits, and functions. + * Returns the declaration name for classes, interfaces, traits, enums, and functions. * * @param int $stackPtr The position of the declaration token which * declared the class, interface, trait, or function. @@ -1226,7 +1238,7 @@ public function getFilename() * or NULL if the function or class is anonymous. * @throws \PHP_CodeSniffer\Exceptions\RuntimeException If the specified token is not of type * T_FUNCTION, T_CLASS, T_ANON_CLASS, - * T_CLOSURE, T_TRAIT, or T_INTERFACE. + * T_CLOSURE, T_TRAIT, T_ENUM, or T_INTERFACE. */ public function getDeclarationName($stackPtr) { @@ -1240,8 +1252,9 @@ public function getDeclarationName($stackPtr) && $tokenCode !== T_CLASS && $tokenCode !== T_INTERFACE && $tokenCode !== T_TRAIT + && $tokenCode !== T_ENUM ) { - throw new RuntimeException('Token type "'.$this->tokens[$stackPtr]['type'].'" is not T_FUNCTION, T_CLASS, T_INTERFACE or T_TRAIT'); + throw new RuntimeException('Token type "'.$this->tokens[$stackPtr]['type'].'" is not T_FUNCTION, T_CLASS, T_INTERFACE, T_TRAIT or T_ENUM'); } if ($tokenCode === T_FUNCTION @@ -1277,6 +1290,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. @@ -1288,17 +1302,26 @@ public function getDeclarationName($stackPtr) * // or FALSE if there is no type hint. * 'type_hint_end_token' => integer, // The stack pointer to the end of the type hint * // or FALSE if there is no type hint. - * 'nullable_type' => boolean, // TRUE if the var type is nullable. + * 'nullable_type' => boolean, // TRUE if the type is preceded by the nullability + * // operator. * 'comma_token' => integer, // The stack pointer to the comma after the param * // or FALSE if this is the last param. * ) * * - * Parameters with default values have an additional array indexes of: + * Parameters with default values have additional array indexes of: * 'default' => string, // The full content of the default value. * 'default_token' => integer, // The stack pointer to the start of the default value. * 'default_equal_token' => integer, // The stack pointer to the equals sign. * + * Parameters declared using PHP 8 constructor property promotion, have these additional array indexes: + * 'property_visibility' => string, // The property visibility as declared. + * 'visibility_token' => integer|false, // The stack pointer to the visibility modifier token + * // or FALSE if the visibility is not explicitly declared. + * 'property_readonly' => boolean, // TRUE if the readonly keyword was found. + * 'readonly_token' => integer, // The stack pointer to the readonly modifier token. + * // This index will only be set if the property is readonly. + * * @param int $stackPtr The position in the stack of the function token * to acquire the parameters for. * @@ -1344,6 +1367,7 @@ public function getMethodParameters($stackPtr) $defaultStart = null; $equalToken = null; $paramCount = 0; + $hasAttributes = false; $passByReference = false; $referenceToken = false; $variableLength = false; @@ -1352,6 +1376,8 @@ public function getMethodParameters($stackPtr) $typeHintToken = false; $typeHintEndToken = false; $nullableType = false; + $visibilityToken = null; + $readonlyToken = null; for ($i = $paramStart; $i <= $closer; $i++) { // Check to see if this token has a parenthesis or bracket opener. If it does @@ -1361,18 +1387,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; @@ -1441,7 +1474,12 @@ public function getMethodParameters($stackPtr) $typeHintEndToken = $i; } break; + case T_NAMESPACE: case T_NS_SEPARATOR: + case T_TYPE_UNION: + case T_TYPE_INTERSECTION: + case T_FALSE: + case T_NULL: // Part of a type hint or default value. if ($defaultStart === null) { if ($typeHintToken === false) { @@ -1459,6 +1497,18 @@ public function getMethodParameters($stackPtr) $typeHintEndToken = $i; } break; + case T_PUBLIC: + case T_PROTECTED: + case T_PRIVATE: + if ($defaultStart === null) { + $visibilityToken = $i; + } + break; + case T_READONLY: + if ($defaultStart === null) { + $readonlyToken = $i; + } + break; case T_CLOSE_PARENTHESIS: case T_COMMA: // If it's null, then there must be no parameters for this @@ -1478,6 +1528,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; @@ -1487,6 +1538,22 @@ public function getMethodParameters($stackPtr) $vars[$paramCount]['type_hint_end_token'] = $typeHintEndToken; $vars[$paramCount]['nullable_type'] = $nullableType; + if ($visibilityToken !== null || $readonlyToken !== null) { + $vars[$paramCount]['property_visibility'] = 'public'; + $vars[$paramCount]['visibility_token'] = false; + $vars[$paramCount]['property_readonly'] = false; + + if ($visibilityToken !== null) { + $vars[$paramCount]['property_visibility'] = $this->tokens[$visibilityToken]['content']; + $vars[$paramCount]['visibility_token'] = $visibilityToken; + } + + if ($readonlyToken !== null) { + $vars[$paramCount]['property_readonly'] = true; + $vars[$paramCount]['readonly_token'] = $readonlyToken; + } + } + if ($this->tokens[$i]['code'] === T_COMMA) { $vars[$paramCount]['comma_token'] = $i; } else { @@ -1498,6 +1565,7 @@ public function getMethodParameters($stackPtr) $paramStart = ($i + 1); $defaultStart = null; $equalToken = null; + $hasAttributes = false; $passByReference = false; $referenceToken = false; $variableLength = false; @@ -1506,6 +1574,8 @@ public function getMethodParameters($stackPtr) $typeHintToken = false; $typeHintEndToken = false; $nullableType = false; + $visibilityToken = null; + $readonlyToken = null; $paramCount++; break; @@ -1527,16 +1597,19 @@ public function getMethodParameters($stackPtr) * The format of the return value is: * * array( - * 'scope' => 'public', // Public, private, or protected - * 'scope_specified' => true, // TRUE if the scope keyword was found. - * 'return_type' => '', // The return type of the method. - * 'return_type_token' => integer, // The stack pointer to the start of the return type - * // or FALSE if there is no return type. - * 'nullable_return_type' => false, // TRUE if the return type is nullable. - * 'is_abstract' => false, // TRUE if the abstract keyword was found. - * 'is_final' => false, // TRUE if the final keyword was found. - * 'is_static' => false, // TRUE if the static keyword was found. - * 'has_body' => false, // TRUE if the method has a body + * 'scope' => 'public', // Public, private, or protected + * 'scope_specified' => true, // TRUE if the scope keyword was found. + * 'return_type' => '', // The return type of the method. + * 'return_type_token' => integer, // The stack pointer to the start of the return type + * // or FALSE if there is no return type. + * 'return_type_end_token' => integer, // The stack pointer to the end of the return type + * // or FALSE if there is no return type. + * 'nullable_return_type' => false, // TRUE if the return type is preceded by the + * // nullability operator. + * 'is_abstract' => false, // TRUE if the abstract keyword was found. + * 'is_final' => false, // TRUE if the final keyword was found. + * 'is_static' => false, // TRUE if the static keyword was found. + * 'has_body' => false, // TRUE if the method has a body * ); * * @@ -1615,6 +1688,7 @@ public function getMethodProperties($stackPtr) $returnType = ''; $returnTypeToken = false; + $returnTypeEndToken = false; $nullableReturnType = false; $hasBody = true; @@ -1625,12 +1699,17 @@ public function getMethodProperties($stackPtr) } $valid = [ - T_STRING => T_STRING, - T_CALLABLE => T_CALLABLE, - T_SELF => T_SELF, - T_PARENT => T_PARENT, - T_STATIC => T_STATIC, - T_NS_SEPARATOR => T_NS_SEPARATOR, + T_STRING => T_STRING, + T_CALLABLE => T_CALLABLE, + T_SELF => T_SELF, + T_PARENT => T_PARENT, + T_STATIC => T_STATIC, + T_FALSE => T_FALSE, + T_NULL => T_NULL, + T_NAMESPACE => T_NAMESPACE, + T_NS_SEPARATOR => T_NS_SEPARATOR, + T_TYPE_UNION => T_TYPE_UNION, + T_TYPE_INTERSECTION => T_TYPE_INTERSECTION, ]; for ($i = $this->tokens[$stackPtr]['parenthesis_closer']; $i < $this->numTokens; $i++) { @@ -1650,9 +1729,10 @@ public function getMethodProperties($stackPtr) $returnTypeToken = $i; } - $returnType .= $this->tokens[$i]['content']; + $returnType .= $this->tokens[$i]['content']; + $returnTypeEndToken = $i; } - } + }//end for if ($this->tokens[$stackPtr]['code'] === T_FN) { $bodyToken = T_FN_ARROW; @@ -1669,15 +1749,16 @@ public function getMethodProperties($stackPtr) } return [ - 'scope' => $scope, - 'scope_specified' => $scopeSpecified, - 'return_type' => $returnType, - 'return_type_token' => $returnTypeToken, - 'nullable_return_type' => $nullableReturnType, - 'is_abstract' => $isAbstract, - 'is_final' => $isFinal, - 'is_static' => $isStatic, - 'has_body' => $hasBody, + 'scope' => $scope, + 'scope_specified' => $scopeSpecified, + 'return_type' => $returnType, + 'return_type_token' => $returnTypeToken, + 'return_type_end_token' => $returnTypeEndToken, + 'nullable_return_type' => $nullableReturnType, + 'is_abstract' => $isAbstract, + 'is_final' => $isFinal, + 'is_static' => $isStatic, + 'has_body' => $hasBody, ]; }//end getMethodProperties() @@ -1693,12 +1774,14 @@ public function getMethodProperties($stackPtr) * 'scope' => string, // Public, private, or protected. * 'scope_specified' => boolean, // TRUE if the scope was explicitly specified. * 'is_static' => boolean, // TRUE if the static keyword was found. + * 'is_readonly' => boolean, // TRUE if the readonly keyword was found. * 'type' => string, // The type of the var (empty if no type specified). * 'type_token' => integer, // The stack pointer to the start of the type * // or FALSE if there is no type. * 'type_end_token' => integer, // The stack pointer to the end of the type * // or FALSE if there is no type. - * 'nullable_type' => boolean, // TRUE if the type is nullable. + * 'nullable_type' => boolean, // TRUE if the type is preceded by the nullability + * // operator. * ); * * @@ -1724,23 +1807,26 @@ public function getMemberProperties($stackPtr) && $this->tokens[$ptr]['code'] !== T_TRAIT) ) { if (isset($this->tokens[$ptr]) === true - && $this->tokens[$ptr]['code'] === T_INTERFACE + && ($this->tokens[$ptr]['code'] === T_INTERFACE + || $this->tokens[$ptr]['code'] === T_ENUM) ) { - // T_VARIABLEs in interfaces can actually be method arguments - // but they wont be seen as being inside the method because there + // T_VARIABLEs in interfaces/enums can actually be method arguments + // but they won't be seen as being inside the method because there // are no scope openers and closers for abstract methods. If it is in // parentheses, we can be pretty sure it is a method argument. if (isset($this->tokens[$stackPtr]['nested_parenthesis']) === false || empty($this->tokens[$stackPtr]['nested_parenthesis']) === true ) { - $error = 'Possible parse error: interfaces may not include member vars'; - $this->addWarning($error, $stackPtr, 'Internal.ParseError.InterfaceHasMemberVar'); + $error = 'Possible parse error: %ss may not include member vars'; + $code = sprintf('Internal.ParseError.%sHasMemberVar', ucfirst($this->tokens[$ptr]['content'])); + $data = [strtolower($this->tokens[$ptr]['content'])]; + $this->addWarning($error, $stackPtr, $code, $data); return []; } } else { throw new RuntimeException('$stackPtr is not a class member var'); } - } + }//end if // Make sure it's not a method parameter. if (empty($this->tokens[$stackPtr]['nested_parenthesis']) === false) { @@ -1760,6 +1846,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; @@ -1767,12 +1854,14 @@ public function getMemberProperties($stackPtr) $scope = 'public'; $scopeSpecified = false; $isStatic = false; + $isReadonly = false; $startOfStatement = $this->findPrevious( [ T_SEMICOLON, T_OPEN_CURLY_BRACKET, T_CLOSE_CURLY_BRACKET, + T_ATTRIBUTE_END, ], ($stackPtr - 1) ); @@ -1798,6 +1887,9 @@ public function getMemberProperties($stackPtr) case T_STATIC: $isStatic = true; break; + case T_READONLY: + $isReadonly = true; + break; } }//end for @@ -1809,11 +1901,16 @@ public function getMemberProperties($stackPtr) if ($i < $stackPtr) { // We've found a type. $valid = [ - T_STRING => T_STRING, - T_CALLABLE => T_CALLABLE, - T_SELF => T_SELF, - T_PARENT => T_PARENT, - T_NS_SEPARATOR => T_NS_SEPARATOR, + T_STRING => T_STRING, + T_CALLABLE => T_CALLABLE, + T_SELF => T_SELF, + T_PARENT => T_PARENT, + T_FALSE => T_FALSE, + T_NULL => T_NULL, + T_NAMESPACE => T_NAMESPACE, + T_NS_SEPARATOR => T_NS_SEPARATOR, + T_TYPE_UNION => T_TYPE_UNION, + T_TYPE_INTERSECTION => T_TYPE_INTERSECTION, ]; for ($i; $i < $stackPtr; $i++) { @@ -1845,6 +1942,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, @@ -1862,6 +1960,7 @@ public function getMemberProperties($stackPtr) * array( * 'is_abstract' => false, // true if the abstract keyword was found. * 'is_final' => false, // true if the final keyword was found. + * 'is_readonly' => false, // true if the readonly keyword was found. * ); * * @@ -1881,6 +1980,7 @@ public function getClassProperties($stackPtr) $valid = [ T_FINAL => T_FINAL, T_ABSTRACT => T_ABSTRACT, + T_READONLY => T_READONLY, T_WHITESPACE => T_WHITESPACE, T_COMMENT => T_COMMENT, T_DOC_COMMENT => T_DOC_COMMENT, @@ -1888,6 +1988,7 @@ public function getClassProperties($stackPtr) $isAbstract = false; $isFinal = false; + $isReadonly = false; for ($i = ($stackPtr - 1); $i > 0; $i--) { if (isset($valid[$this->tokens[$i]['code']]) === false) { @@ -1902,12 +2003,17 @@ public function getClassProperties($stackPtr) case T_FINAL: $isFinal = true; break; + + case T_READONLY: + $isReadonly = true; + break; } }//end for return [ 'is_abstract' => $isAbstract, 'is_final' => $isFinal, + 'is_readonly' => $isReadonly, ]; }//end getClassProperties() @@ -1978,22 +2084,11 @@ public function isReference($stackPtr) $owner = $this->tokens[$this->tokens[$lastBracket]['parenthesis_owner']]; if ($owner['code'] === T_FUNCTION || $owner['code'] === T_CLOSURE + || $owner['code'] === T_FN ) { $params = $this->getMethodParameters($this->tokens[$lastBracket]['parenthesis_owner']); foreach ($params as $param) { - $varToken = $tokenAfter; - if ($param['variable_length'] === true) { - $varToken = $this->findNext( - (Util\Tokens::$emptyTokens + [T_ELLIPSIS]), - ($stackPtr + 1), - null, - true - ); - } - - if ($param['token'] === $varToken - && $param['pass_by_reference'] === true - ) { + if ($param['reference_token'] === $stackPtr) { // Function parameter declared to be passed by reference. return true; } @@ -2103,12 +2198,12 @@ public function getTokensAsString($start, $length, $origContent=false) * @param int|string|array $types The type(s) of tokens to search for. * @param int $start The position to start searching from in the * token stack. - * @param int $end The end position to fail if no token is found. + * @param int|null $end The end position to fail if no token is found. * if not specified or null, end will default to * the start of the token stack. * @param bool $exclude If true, find the previous token that is NOT of * the types specified in $types. - * @param string $value The value that the token(s) must be equal to. + * @param string|null $value The value that the token(s) must be equal to. * If value is omitted, tokens with any value will * be returned. * @param bool $local If true, tokens outside the current statement @@ -2184,12 +2279,12 @@ public function findPrevious( * @param int|string|array $types The type(s) of tokens to search for. * @param int $start The position to start searching from in the * token stack. - * @param int $end The end position to fail if no token is found. + * @param int|null $end The end position to fail if no token is found. * if not specified or null, end will default to * the end of the token stack. * @param bool $exclude If true, find the next token that is NOT of * a type specified in $types. - * @param string $value The value that the token(s) must be equal to. + * @param string|null $value The value that the token(s) must be equal to. * If value is omitted, tokens with any value will * be returned. * @param bool $local If true, tokens outside the current statement @@ -2250,27 +2345,104 @@ public function findNext( */ public function findStartOfStatement($start, $ignore=null) { - $endTokens = Util\Tokens::$blockOpeners; + $startTokens = Util\Tokens::$blockOpeners; + $startTokens[T_OPEN_SHORT_ARRAY] = true; + $startTokens[T_OPEN_TAG] = true; + $startTokens[T_OPEN_TAG_WITH_ECHO] = true; - $endTokens[T_COLON] = true; - $endTokens[T_COMMA] = true; - $endTokens[T_DOUBLE_ARROW] = true; - $endTokens[T_SEMICOLON] = true; - $endTokens[T_OPEN_TAG] = true; - $endTokens[T_CLOSE_TAG] = true; - $endTokens[T_OPEN_SHORT_ARRAY] = true; + $endTokens = [ + T_CLOSE_TAG => true, + T_COLON => true, + T_COMMA => true, + T_DOUBLE_ARROW => true, + T_MATCH_ARROW => true, + T_SEMICOLON => true, + ]; if ($ignore !== null) { $ignore = (array) $ignore; foreach ($ignore as $code) { - unset($endTokens[$code]); + if (isset($startTokens[$code]) === true) { + unset($startTokens[$code]); + } + + if (isset($endTokens[$code]) === true) { + unset($endTokens[$code]); + } } } + // If the start token is inside the case part of a match expression, + // find the start of the condition. If it's in the statement part, find + // the token that comes after the match arrow. + $matchExpression = $this->getCondition($start, T_MATCH); + if ($matchExpression !== false) { + for ($prevMatch = $start; $prevMatch > $this->tokens[$matchExpression]['scope_opener']; $prevMatch--) { + if ($prevMatch !== $start + && ($this->tokens[$prevMatch]['code'] === T_MATCH_ARROW + || $this->tokens[$prevMatch]['code'] === T_COMMA) + ) { + break; + } + + // Skip nested statements. + if (isset($this->tokens[$prevMatch]['bracket_opener']) === true + && $prevMatch === $this->tokens[$prevMatch]['bracket_closer'] + ) { + $prevMatch = $this->tokens[$prevMatch]['bracket_opener']; + } else if (isset($this->tokens[$prevMatch]['parenthesis_opener']) === true + && $prevMatch === $this->tokens[$prevMatch]['parenthesis_closer'] + ) { + $prevMatch = $this->tokens[$prevMatch]['parenthesis_opener']; + } + } + + if ($prevMatch <= $this->tokens[$matchExpression]['scope_opener']) { + // We're before the arrow in the first case. + $next = $this->findNext(Util\Tokens::$emptyTokens, ($this->tokens[$matchExpression]['scope_opener'] + 1), null, true); + if ($next === false) { + return $start; + } + + return $next; + } + + if ($this->tokens[$prevMatch]['code'] === T_COMMA) { + // We're before the arrow, but not in the first case. + $prevMatchArrow = $this->findPrevious(T_MATCH_ARROW, ($prevMatch - 1), $this->tokens[$matchExpression]['scope_opener']); + if ($prevMatchArrow === false) { + // We're before the arrow in the first case. + $next = $this->findNext(Util\Tokens::$emptyTokens, ($this->tokens[$matchExpression]['scope_opener'] + 1), null, true); + return $next; + } + + $end = $this->findEndOfStatement($prevMatchArrow); + $next = $this->findNext(Util\Tokens::$emptyTokens, ($end + 1), null, true); + return $next; + } + }//end if + $lastNotEmpty = $start; + // If we are starting at a token that ends a scope block, skip to + // the start and continue from there. + // If we are starting at a token that ends a statement, skip this + // token so we find the true start of the statement. + while (isset($endTokens[$this->tokens[$start]['code']]) === true + || (isset($this->tokens[$start]['scope_condition']) === true + && $start === $this->tokens[$start]['scope_closer']) + ) { + if (isset($this->tokens[$start]['scope_condition']) === true) { + $start = $this->tokens[$start]['scope_condition']; + } else { + $start--; + } + } + for ($i = $start; $i >= 0; $i--) { - if (isset($endTokens[$this->tokens[$i]['code']]) === true) { + if (isset($startTokens[$this->tokens[$i]['code']]) === true + || isset($endTokens[$this->tokens[$i]['code']]) === true + ) { // Found the end of the previous statement. return $lastNotEmpty; } @@ -2278,6 +2450,13 @@ public function findStartOfStatement($start, $ignore=null) if (isset($this->tokens[$i]['scope_opener']) === true && $i === $this->tokens[$i]['scope_closer'] && $this->tokens[$i]['code'] !== T_CLOSE_PARENTHESIS + && $this->tokens[$i]['code'] !== T_END_NOWDOC + && $this->tokens[$i]['code'] !== T_END_HEREDOC + && $this->tokens[$i]['code'] !== T_BREAK + && $this->tokens[$i]['code'] !== T_RETURN + && $this->tokens[$i]['code'] !== T_CONTINUE + && $this->tokens[$i]['code'] !== T_THROW + && $this->tokens[$i]['code'] !== T_EXIT ) { // Found the end of the previous scope block. return $lastNotEmpty; @@ -2292,7 +2471,12 @@ public function findStartOfStatement($start, $ignore=null) && $i === $this->tokens[$i]['parenthesis_closer'] ) { $i = $this->tokens[$i]['parenthesis_opener']; - } + } else if ($this->tokens[$i]['code'] === T_CLOSE_USE_GROUP) { + $start = $this->findPrevious(T_OPEN_USE_GROUP, ($i - 1)); + if ($start !== false) { + $i = $start; + } + }//end if if (isset(Util\Tokens::$emptyTokens[$this->tokens[$i]['code']]) === false) { $lastNotEmpty = $i; @@ -2334,6 +2518,33 @@ public function findEndOfStatement($start, $ignore=null) } } + // If the start token is inside the case part of a match expression, + // advance to the match arrow and continue looking for the + // end of the statement from there so that we skip over commas. + if ($this->tokens[$start]['code'] !== T_MATCH_ARROW) { + $matchExpression = $this->getCondition($start, T_MATCH); + if ($matchExpression !== false) { + $beforeArrow = true; + $prevMatchArrow = $this->findPrevious(T_MATCH_ARROW, ($start - 1), $this->tokens[$matchExpression]['scope_opener']); + if ($prevMatchArrow !== false) { + $prevComma = $this->findNext(T_COMMA, ($prevMatchArrow + 1), $start); + if ($prevComma === false) { + // No comma between this token and the last match arrow, + // so this token exists after the arrow and we can continue + // checking as normal. + $beforeArrow = false; + } + } + + if ($beforeArrow === true) { + $nextMatchArrow = $this->findNext(T_MATCH_ARROW, ($start + 1), $this->tokens[$matchExpression]['scope_closer']); + if ($nextMatchArrow !== false) { + $start = $nextMatchArrow; + } + } + }//end if + }//end if + $lastNotEmpty = $start; for ($i = $start; $i < $this->numTokens; $i++) { if ($i !== $start && isset($endTokens[$this->tokens[$i]['code']]) === true) { @@ -2494,7 +2705,7 @@ public function hasCondition($stackPtr, $types) * @param int $stackPtr The position of the token we are checking. * @param int|string $type The type of token to search for. * @param bool $first If TRUE, will return the matched condition - * furtherest away from the passed token. + * furthest away from the passed token. * If FALSE, will return the matched condition * closest to the passed token. * @@ -2582,11 +2793,11 @@ public function findExtendedClassName($stackPtr) /** - * Returns the names of the interfaces that the specified class implements. + * Returns the names of the interfaces that the specified class or enum implements. * * Returns FALSE on error or if there are no implemented interface names. * - * @param int $stackPtr The stack position of the class. + * @param int $stackPtr The stack position of the class or enum token. * * @return array|false */ @@ -2599,6 +2810,7 @@ public function findImplementedInterfaceNames($stackPtr) if ($this->tokens[$stackPtr]['code'] !== T_CLASS && $this->tokens[$stackPtr]['code'] !== T_ANON_CLASS + && $this->tokens[$stackPtr]['code'] !== T_ENUM ) { return false; } diff --git a/src/Files/FileList.php b/src/Files/FileList.php index 877b1c003e..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,10 +183,11 @@ public function rewind() * * @return \PHP_CodeSniffer\Files\File */ + #[ReturnTypeWillChange] public function current() { $path = key($this->files); - if ($this->files[$path] === null) { + if (isset($this->files[$path]) === false) { $this->files[$path] = new LocalFile($path, $this->ruleset, $this->config); } @@ -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/Files/LocalFile.php b/src/Files/LocalFile.php index 7c2c671d39..ca2e74ad39 100644 --- a/src/Files/LocalFile.php +++ b/src/Files/LocalFile.php @@ -12,6 +12,7 @@ use PHP_CodeSniffer\Ruleset; use PHP_CodeSniffer\Config; use PHP_CodeSniffer\Util\Cache; +use PHP_CodeSniffer\Util\Common; class LocalFile extends File { @@ -29,7 +30,7 @@ class LocalFile extends File public function __construct($path, Ruleset $ruleset, Config $config) { $this->path = trim($path); - if (is_readable($this->path) === false) { + if (Common::isReadable($this->path) === false) { parent::__construct($this->path, $ruleset, $config); $error = 'Error opening file; file no longer exists or you do not have access to read the file'; $this->addMessage(true, $error, 1, 1, 'Internal.LocalFile', [], 5, false); diff --git a/src/Filters/Filter.php b/src/Filters/Filter.php index fa7360f0cd..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(); @@ -218,7 +221,7 @@ protected function shouldIgnorePath($path) // Need to check this pattern for dirs as well as individual file paths. $this->ignoreFilePatterns[$pattern] = $type; - $pattern = substr($pattern, 0, -2); + $pattern = substr($pattern, 0, -2).'(?=/|$)'; $this->ignoreDirPatterns[$pattern] = $type; } else { // This is a file-specific pattern, so only need to check this diff --git a/src/Fixer.php b/src/Fixer.php index 897e14771b..b8dc05b16e 100644 --- a/src/Fixer.php +++ b/src/Fixer.php @@ -184,6 +184,9 @@ public function fixFile() } echo ']... '; + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo PHP_EOL; + } } if ($this->numFixes === 0 && $this->inConflict === false) { @@ -255,6 +258,10 @@ public function generateDiff($filePath=null, $colors=true) unlink($tempName); } + if ($diff === null) { + return ''; + } + if ($colors === false) { return $diff; } @@ -414,6 +421,7 @@ public function endChangeset() } $this->changeset = []; + return true; }//end endChangeset() @@ -743,7 +751,7 @@ public function addContentBefore($stackPtr, $content) * @param int $change The number of spaces to adjust the indent by * (positive or negative). * - * @return bool If the change was accepted. + * @return void */ public function changeCodeBlockIndent($start, $end, $change) { diff --git a/src/Generators/Markdown.php b/src/Generators/Markdown.php index b51b267bcb..9756bcf140 100644 --- a/src/Generators/Markdown.php +++ b/src/Generators/Markdown.php @@ -84,7 +84,7 @@ protected function printFooter() protected function processSniff(\DOMNode $doc) { $title = $this->getTitle($doc); - echo "## $title".PHP_EOL; + echo PHP_EOL."## $title".PHP_EOL; foreach ($doc->childNodes as $node) { if ($node->nodeName === 'standard') { diff --git a/src/Reports/Cbf.php b/src/Reports/Cbf.php index 25249e858d..c85353d988 100644 --- a/src/Reports/Cbf.php +++ b/src/Reports/Cbf.php @@ -28,10 +28,10 @@ class Cbf implements Report * and FALSE if it ignored the file. Returning TRUE indicates that the file and * its data should be counted in the grand totals. * - * @param array $report Prepared report data. - * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on. - * @param bool $showSources Show sources? - * @param int $width Maximum allowed line width. + * @param array $report Prepared report data. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on. + * @param bool $showSources Show sources? + * @param int $width Maximum allowed line width. * * @return bool * @throws \PHP_CodeSniffer\Exceptions\DeepExitException @@ -44,6 +44,9 @@ public function generateFileReport($report, File $phpcsFile, $showSources=false, ob_end_clean(); $startTime = microtime(true); echo "\t=> Fixing file: $errors/$errors violations remaining"; + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo PHP_EOL; + } } $fixed = $phpcsFile->fixer->fixFile(); diff --git a/src/Reports/Checkstyle.php b/src/Reports/Checkstyle.php index 06a78e19fa..313e086710 100644 --- a/src/Reports/Checkstyle.php +++ b/src/Reports/Checkstyle.php @@ -23,10 +23,10 @@ class Checkstyle implements Report * and FALSE if it ignored the file. Returning TRUE indicates that the file and * its data should be counted in the grand totals. * - * @param array $report Prepared report data. - * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on. - * @param bool $showSources Show sources? - * @param int $width Maximum allowed line width. + * @param array $report Prepared report data. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on. + * @param bool $showSources Show sources? + * @param int $width Maximum allowed line width. * * @return bool */ diff --git a/src/Reports/Code.php b/src/Reports/Code.php index c54c1e1a4e..4a7a27af82 100644 --- a/src/Reports/Code.php +++ b/src/Reports/Code.php @@ -23,10 +23,10 @@ class Code implements Report * and FALSE if it ignored the file. Returning TRUE indicates that the file and * its data should be counted in the grand totals. * - * @param array $report Prepared report data. - * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on. - * @param bool $showSources Show sources? - * @param int $width Maximum allowed line width. + * @param array $report Prepared report data. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on. + * @param bool $showSources Show sources? + * @param int $width Maximum allowed line width. * * @return bool */ @@ -225,7 +225,7 @@ public function generateFileReport($report, File $phpcsFile, $showSources=false, if (strpos($tokenContent, "\t") !== false) { $token = $tokens[$i]; $token['content'] = $tokenContent; - if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + if (stripos(PHP_OS, 'WIN') === 0) { $tab = "\000"; } else { $tab = "\033[30;1m»\033[0m"; diff --git a/src/Reports/Csv.php b/src/Reports/Csv.php index 6db7ecfc18..1f02edbd85 100644 --- a/src/Reports/Csv.php +++ b/src/Reports/Csv.php @@ -22,10 +22,10 @@ class Csv implements Report * and FALSE if it ignored the file. Returning TRUE indicates that the file and * its data should be counted in the grand totals. * - * @param array $report Prepared report data. - * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on. - * @param bool $showSources Show sources? - * @param int $width Maximum allowed line width. + * @param array $report Prepared report data. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on. + * @param bool $showSources Show sources? + * @param int $width Maximum allowed line width. * * @return bool */ diff --git a/src/Reports/Diff.php b/src/Reports/Diff.php index ce4b31fc08..68af0a0261 100644 --- a/src/Reports/Diff.php +++ b/src/Reports/Diff.php @@ -22,10 +22,10 @@ class Diff implements Report * and FALSE if it ignored the file. Returning TRUE indicates that the file and * its data should be counted in the grand totals. * - * @param array $report Prepared report data. - * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on. - * @param bool $showSources Show sources? - * @param int $width Maximum allowed line width. + * @param array $report Prepared report data. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on. + * @param bool $showSources Show sources? + * @param int $width Maximum allowed line width. * * @return bool */ diff --git a/src/Reports/Emacs.php b/src/Reports/Emacs.php index 3555f55418..cae9d22d85 100644 --- a/src/Reports/Emacs.php +++ b/src/Reports/Emacs.php @@ -22,10 +22,10 @@ class Emacs implements Report * and FALSE if it ignored the file. Returning TRUE indicates that the file and * its data should be counted in the grand totals. * - * @param array $report Prepared report data. - * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on. - * @param bool $showSources Show sources? - * @param int $width Maximum allowed line width. + * @param array $report Prepared report data. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on. + * @param bool $showSources Show sources? + * @param int $width Maximum allowed line width. * * @return bool */ diff --git a/src/Reports/Full.php b/src/Reports/Full.php index 084bc8aa58..5f6bcde80a 100644 --- a/src/Reports/Full.php +++ b/src/Reports/Full.php @@ -23,10 +23,10 @@ class Full implements Report * and FALSE if it ignored the file. Returning TRUE indicates that the file and * its data should be counted in the grand totals. * - * @param array $report Prepared report data. - * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on. - * @param bool $showSources Show sources? - * @param int $width Maximum allowed line width. + * @param array $report Prepared report data. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on. + * @param bool $showSources Show sources? + * @param int $width Maximum allowed line width. * * @return bool */ diff --git a/src/Reports/Gitblame.php b/src/Reports/Gitblame.php index 947f3d80b1..0919e0e0ba 100644 --- a/src/Reports/Gitblame.php +++ b/src/Reports/Gitblame.php @@ -70,7 +70,7 @@ protected function getBlameContent($filename) $cwd = getcwd(); chdir(dirname($filename)); - $command = 'git blame --date=short "'.$filename.'" 2>&1'; + $command = 'git blame --date=short "'.basename($filename).'" 2>&1'; $handle = popen($command, 'r'); if ($handle === false) { $error = 'ERROR: Could not execute "'.$command.'"'.PHP_EOL.PHP_EOL; @@ -78,7 +78,7 @@ protected function getBlameContent($filename) } $rawContent = stream_get_contents($handle); - fclose($handle); + pclose($handle); $blames = explode("\n", $rawContent); chdir($cwd); diff --git a/src/Reports/Hgblame.php b/src/Reports/Hgblame.php index b0af6643fd..f88a06836b 100644 --- a/src/Reports/Hgblame.php +++ b/src/Reports/Hgblame.php @@ -97,7 +97,7 @@ protected function getBlameContent($filename) } $rawContent = stream_get_contents($handle); - fclose($handle); + pclose($handle); $blames = explode("\n", $rawContent); chdir($cwd); diff --git a/src/Reports/Info.php b/src/Reports/Info.php index 3181bcc546..fa0698efdf 100644 --- a/src/Reports/Info.php +++ b/src/Reports/Info.php @@ -23,10 +23,10 @@ class Info implements Report * and FALSE if it ignored the file. Returning TRUE indicates that the file and * its data should be counted in the grand totals. * - * @param array $report Prepared report data. - * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on. - * @param bool $showSources Show sources? - * @param int $width Maximum allowed line width. + * @param array $report Prepared report data. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on. + * @param bool $showSources Show sources? + * @param int $width Maximum allowed line width. * * @return bool */ diff --git a/src/Reports/Json.php b/src/Reports/Json.php index 59d8f305e5..f7aedb30a5 100644 --- a/src/Reports/Json.php +++ b/src/Reports/Json.php @@ -23,10 +23,10 @@ class Json implements Report * and FALSE if it ignored the file. Returning TRUE indicates that the file and * its data should be counted in the grand totals. * - * @param array $report Prepared report data. - * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on. - * @param bool $showSources Show sources? - * @param int $width Maximum allowed line width. + * @param array $report Prepared report data. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on. + * @param bool $showSources Show sources? + * @param int $width Maximum allowed line width. * * @return bool */ diff --git a/src/Reports/Junit.php b/src/Reports/Junit.php index d3ede61d47..0b59604ae0 100644 --- a/src/Reports/Junit.php +++ b/src/Reports/Junit.php @@ -24,10 +24,10 @@ class Junit implements Report * and FALSE if it ignored the file. Returning TRUE indicates that the file and * its data should be counted in the grand totals. * - * @param array $report Prepared report data. - * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on. - * @param bool $showSources Show sources? - * @param int $width Maximum allowed line width. + * @param array $report Prepared report data. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on. + * @param bool $showSources Show sources? + * @param int $width Maximum allowed line width. * * @return bool */ diff --git a/src/Reports/Notifysend.php b/src/Reports/Notifysend.php index 5ffc5baf5d..71aed108ed 100644 --- a/src/Reports/Notifysend.php +++ b/src/Reports/Notifysend.php @@ -18,6 +18,7 @@ use PHP_CodeSniffer\Config; use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Util\Common; class Notifysend implements Report { @@ -58,7 +59,7 @@ public function __construct() { $path = Config::getExecutablePath('notifysend'); if ($path !== null) { - $this->path = escapeshellcmd($path); + $this->path = Common::escapeshellcmd($path); } $timeout = Config::getConfigData('notifysend_timeout'); @@ -87,10 +88,10 @@ public function __construct() * and FALSE if it ignored the file. Returning TRUE indicates that the file and * its data should be counted in the grand totals. * - * @param array $report Prepared report data. - * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on. - * @param bool $showSources Show sources? - * @param int $width Maximum allowed line width. + * @param array $report Prepared report data. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on. + * @param bool $showSources Show sources? + * @param int $width Maximum allowed line width. * * @return bool */ diff --git a/src/Reports/Report.php b/src/Reports/Report.php index 38f8a6298c..fd60185e94 100644 --- a/src/Reports/Report.php +++ b/src/Reports/Report.php @@ -22,10 +22,10 @@ interface Report * and FALSE if it ignored the file. Returning TRUE indicates that the file and * its data should be counted in the grand totals. * - * @param array $report Prepared report data. - * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on. - * @param bool $showSources Show sources? - * @param int $width Maximum allowed line width. + * @param array $report Prepared report data. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on. + * @param bool $showSources Show sources? + * @param int $width Maximum allowed line width. * * @return bool */ diff --git a/src/Reports/Source.php b/src/Reports/Source.php index ce8c3cfec1..44f0d8748e 100644 --- a/src/Reports/Source.php +++ b/src/Reports/Source.php @@ -23,10 +23,10 @@ class Source implements Report * and FALSE if it ignored the file. Returning TRUE indicates that the file and * its data should be counted in the grand totals. * - * @param array $report Prepared report data. - * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on. - * @param bool $showSources Show sources? - * @param int $width Maximum allowed line width. + * @param array $report Prepared report data. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on. + * @param bool $showSources Show sources? + * @param int $width Maximum allowed line width. * * @return bool */ diff --git a/src/Reports/Summary.php b/src/Reports/Summary.php index 8fe18e7640..cd5ca69476 100644 --- a/src/Reports/Summary.php +++ b/src/Reports/Summary.php @@ -23,10 +23,10 @@ class Summary implements Report * and FALSE if it ignored the file. Returning TRUE indicates that the file and * its data should be counted in the grand totals. * - * @param array $report Prepared report data. - * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on. - * @param bool $showSources Show sources? - * @param int $width Maximum allowed line width. + * @param array $report Prepared report data. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on. + * @param bool $showSources Show sources? + * @param int $width Maximum allowed line width. * * @return bool */ diff --git a/src/Reports/Svnblame.php b/src/Reports/Svnblame.php index f4719fe5dc..a7f65e154b 100644 --- a/src/Reports/Svnblame.php +++ b/src/Reports/Svnblame.php @@ -61,7 +61,7 @@ protected function getBlameContent($filename) } $rawContent = stream_get_contents($handle); - fclose($handle); + pclose($handle); $blames = explode("\n", $rawContent); diff --git a/src/Reports/VersionControl.php b/src/Reports/VersionControl.php index 0f414567dc..e8e399a7dd 100644 --- a/src/Reports/VersionControl.php +++ b/src/Reports/VersionControl.php @@ -31,10 +31,10 @@ abstract class VersionControl implements Report * and FALSE if it ignored the file. Returning TRUE indicates that the file and * its data should be counted in the grand totals. * - * @param array $report Prepared report data. - * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on. - * @param bool $showSources Show sources? - * @param int $width Maximum allowed line width. + * @param array $report Prepared report data. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on. + * @param bool $showSources Show sources? + * @param int $width Maximum allowed line width. * * @return bool */ diff --git a/src/Reports/Xml.php b/src/Reports/Xml.php index 066383debe..c748dca726 100644 --- a/src/Reports/Xml.php +++ b/src/Reports/Xml.php @@ -23,10 +23,10 @@ class Xml implements Report * and FALSE if it ignored the file. Returning TRUE indicates that the file and * its data should be counted in the grand totals. * - * @param array $report Prepared report data. - * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on. - * @param bool $showSources Show sources? - * @param int $width Maximum allowed line width. + * @param array $report Prepared report data. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being reported on. + * @param bool $showSources Show sources? + * @param int $width Maximum allowed line width. * * @return bool */ diff --git a/src/Ruleset.php b/src/Ruleset.php index 3220a18f22..5ba0d180fc 100644 --- a/src/Ruleset.php +++ b/src/Ruleset.php @@ -13,6 +13,7 @@ use PHP_CodeSniffer\Exceptions\RuntimeException; use PHP_CodeSniffer\Util; +use stdClass; class Ruleset { @@ -126,15 +127,9 @@ class Ruleset */ public function __construct(Config $config) { - // Ignore sniff restrictions if caching is on. - $restrictions = []; - $exclusions = []; - if ($config->cache === false) { - $restrictions = $config->sniffs; - $exclusions = $config->exclude; - } - $this->config = $config; + $restrictions = $config->sniffs; + $exclusions = $config->exclude; $sniffs = []; $standardPaths = []; @@ -200,6 +195,12 @@ public function __construct(Config $config) $sniffs = array_merge($sniffs, $this->processRuleset($standard)); }//end foreach + // Ignore sniff restrictions if caching is on. + if ($config->cache === true) { + $restrictions = []; + $exclusions = []; + } + $sniffRestrictions = []; foreach ($restrictions as $sniffCode) { $parts = explode('.', strtolower($sniffCode)); @@ -249,7 +250,12 @@ public function explain() // one last time and clear the output buffer. $sniffs[] = ''; - echo PHP_EOL."The $this->name standard contains $sniffCount sniffs".PHP_EOL; + $summaryLine = PHP_EOL."The $this->name standard contains 1 sniff".PHP_EOL; + if ($sniffCount !== 1) { + $summaryLine = str_replace('1 sniff', "$sniffCount sniffs", $summaryLine); + } + + echo $summaryLine; ob_start(); @@ -726,7 +732,7 @@ private function expandRulesetReference($ref, $rulesetDir, $depth=0) } else { // See if this is a whole standard being referenced. $path = Util\Standards::getInstalledStandardPath($ref); - if (Util\Common::isPharFile($path) === true && strpos($path, 'ruleset.xml') === false) { + if ($path !== null && Util\Common::isPharFile($path) === true && strpos($path, 'ruleset.xml') === false) { // If the ruleset exists inside the phar file, use it. if (file_exists($path.DIRECTORY_SEPARATOR.'ruleset.xml') === true) { $path .= DIRECTORY_SEPARATOR.'ruleset.xml'; @@ -955,6 +961,11 @@ private function processRule($rule, $newSniffs, $depth=0) if (isset($rule->properties) === true && $this->shouldProcessElement($rule->properties) === true ) { + $propertyScope = 'standard'; + if ($code === $ref || substr($ref, -9) === 'Sniff.php') { + $propertyScope = 'sniff'; + } + foreach ($rule->properties->property as $prop) { if ($this->shouldProcessElement($prop) === false) { continue; @@ -975,9 +986,9 @@ private function processRule($rule, $newSniffs, $depth=0) $values = []; if (isset($prop['extend']) === true && (string) $prop['extend'] === 'true' - && isset($this->ruleset[$code]['properties'][$name]) === true + && isset($this->ruleset[$code]['properties'][$name]['value']) === true ) { - $values = $this->ruleset[$code]['properties'][$name]; + $values = $this->ruleset[$code]['properties'][$name]['value']; } if (isset($prop->element) === true) { @@ -1012,7 +1023,10 @@ private function processRule($rule, $newSniffs, $depth=0) } }//end if - $this->ruleset[$code]['properties'][$name] = $values; + $this->ruleset[$code]['properties'][$name] = [ + 'value' => $values, + 'scope' => $propertyScope, + ]; if (PHP_CODESNIFFER_VERBOSITY > 1) { echo str_repeat("\t", $depth); echo "\t\t=> array property \"$name\" set to \"$printValue\""; @@ -1023,7 +1037,10 @@ private function processRule($rule, $newSniffs, $depth=0) echo PHP_EOL; } } else { - $this->ruleset[$code]['properties'][$name] = (string) $prop['value']; + $this->ruleset[$code]['properties'][$name] = [ + 'value' => (string) $prop['value'], + 'scope' => $propertyScope, + ]; if (PHP_CODESNIFFER_VERBOSITY > 1) { echo str_repeat("\t", $depth); echo "\t\t=> property \"$name\" set to \"".(string) $prop['value'].'"'; @@ -1213,8 +1230,8 @@ public function populateTokenListeners() // Set custom properties. if (isset($this->ruleset[$sniffCode]['properties']) === true) { - foreach ($this->ruleset[$sniffCode]['properties'] as $name => $value) { - $this->setSniffProperty($sniffClass, $name, $value); + foreach ($this->ruleset[$sniffCode]['properties'] as $name => $settings) { + $this->setSniffProperty($sniffClass, $name, $settings); } } @@ -1281,18 +1298,76 @@ public function populateTokenListeners() * * @param string $sniffClass The class name of the sniff. * @param string $name The name of the property to change. - * @param string $value The new value of the property. + * @param array $settings Array with the new value of the property and the scope of the property being set. * * @return void + * + * @throws \PHP_CodeSniffer\Exceptions\RuntimeException When attempting to set a non-existent property on a sniff + * which doesn't declare the property or explicitly supports + * dynamic properties. */ - public function setSniffProperty($sniffClass, $name, $value) + public function setSniffProperty($sniffClass, $name, $settings) { // Setting a property for a sniff we are not using. if (isset($this->sniffs[$sniffClass]) === false) { return; } - $name = trim($name); + $name = trim($name); + $propertyName = $name; + if (substr($propertyName, -2) === '[]') { + $propertyName = substr($propertyName, 0, -2); + } + + /* + * BC-compatibility layer for $settings using the pre-PHPCS 3.8.0 format. + * + * Prior to PHPCS 3.8.0, `$settings` was expected to only contain the new _value_ + * for the property (which could be an array). + * Since PHPCS 3.8.0, `$settings` is expected to be an array with two keys: 'scope' + * and 'value', where 'scope' indicates whether the property should be set to the given 'value' + * for one individual sniff or for all sniffs in a standard. + * + * This BC-layer is only for integrations with PHPCS which may call this method directly + * and will be removed in PHPCS 4.0.0. + */ + + if (is_array($settings) === false + || isset($settings['scope'], $settings['value']) === false + ) { + // This will be an "old" format value. + $settings = [ + 'value' => $settings, + 'scope' => 'standard', + ]; + + trigger_error( + __FUNCTION__.': the format of the $settings parameter has changed from (mixed) $value to array(\'scope\' => \'sniff|standard\', \'value\' => $value). Please update your integration code. See PR #3629 for more information.', + E_USER_DEPRECATED + ); + } + + $isSettable = false; + $sniffObject = $this->sniffs[$sniffClass]; + if (property_exists($sniffObject, $propertyName) === true + || ($sniffObject instanceof stdClass) === true + || method_exists($sniffObject, '__set') === true + ) { + $isSettable = true; + } + + if ($isSettable === false) { + if ($settings['scope'] === 'sniff') { + $notice = "Ruleset invalid. Property \"$propertyName\" does not exist on sniff "; + $notice .= array_search($sniffClass, $this->sniffCodes, true); + throw new RuntimeException($notice); + } + + return; + } + + $value = $settings['value']; + if (is_string($value) === true) { $value = trim($value); } @@ -1307,7 +1382,7 @@ public function setSniffProperty($sniffClass, $name, $value) } else if ($value === 'false') { $value = false; } else if (substr($name, -2) === '[]') { - $name = substr($name, 0, -2); + $name = $propertyName; $values = []; if ($value !== null) { foreach (explode(',', $value) as $val) { @@ -1323,7 +1398,7 @@ public function setSniffProperty($sniffClass, $name, $value) $value = $values; } - $this->sniffs[$sniffClass]->$name = $value; + $sniffObject->$name = $value; }//end setSniffProperty() diff --git a/src/Runner.php b/src/Runner.php index bbed3c9d34..2f21edf482 100644 --- a/src/Runner.php +++ b/src/Runner.php @@ -53,6 +53,8 @@ class Runner */ public function runPHPCS() { + $this->registerOutOfMemoryShutdownMessage('phpcs'); + try { Util\Timing::startTiming(); Runner::checkRequirements(); @@ -153,6 +155,8 @@ public function runPHPCS() */ public function runPHPCBF() { + $this->registerOutOfMemoryShutdownMessage('phpcbf'); + if (defined('PHP_CODESNIFFER_CBF') === false) { define('PHP_CODESNIFFER_CBF', true); } @@ -232,7 +236,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 +295,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); @@ -460,10 +464,7 @@ private function run() if ($pid === -1) { throw new RuntimeException('Failed to create child process'); } else if ($pid !== 0) { - $childProcs[] = [ - 'pid' => $pid, - 'out' => $childOutFilename, - ]; + $childProcs[$pid] = $childOutFilename; } else { // Move forward to the start of the batch. $todo->rewind(); @@ -487,6 +488,7 @@ private function run() $file = $todo->current(); if ($file->ignored === true) { + $todo->next(); continue; } @@ -535,7 +537,7 @@ private function run() $output .= ";\n?".'>'; file_put_contents($childOutFilename, $output); - exit($pid); + exit(); }//end if }//end for @@ -650,6 +652,39 @@ public function processFile($file) } } catch (\Exception $e) { $error = 'An error occurred during processing; checking has been aborted. The error message was: '.$e->getMessage(); + + // Determine which sniff caused the error. + $sniffStack = null; + $nextStack = null; + foreach ($e->getTrace() as $step) { + if (isset($step['file']) === false) { + continue; + } + + if (empty($sniffStack) === false) { + $nextStack = $step; + break; + } + + if (substr($step['file'], -9) === 'Sniff.php') { + $sniffStack = $step; + continue; + } + } + + if (empty($sniffStack) === false) { + if (empty($nextStack) === false + && isset($nextStack['class']) === true + && substr($nextStack['class'], -5) === 'Sniff' + ) { + $sniffCode = Common::getSniffCode($nextStack['class']); + } else { + $sniffCode = substr(strrchr(str_replace('\\', '/', $sniffStack['file']), '/'), 1); + } + + $error .= sprintf(PHP_EOL.'The error originated in the %s sniff on line %s.', $sniffCode, $sniffStack['line']); + } + $file->addErrorOnLine($error, 1, 'Internal.Exception'); }//end try @@ -718,54 +753,61 @@ private function processChildProcs($childProcs) $success = true; while (count($childProcs) > 0) { - foreach ($childProcs as $key => $procData) { - $res = pcntl_waitpid($procData['pid'], $status, WNOHANG); - if ($res === $procData['pid']) { - if (file_exists($procData['out']) === true) { - include $procData['out']; - - unlink($procData['out']); - unset($childProcs[$key]); - - $numProcessed++; - - if (isset($childOutput) === false) { - // The child process died, so the run has failed. - $file = new DummyFile(null, $this->ruleset, $this->config); - $file->setErrorCounts(1, 0, 0, 0); - $this->printProgress($file, $totalBatches, $numProcessed); - $success = false; - continue; - } + $pid = pcntl_waitpid(0, $status); + if ($pid <= 0) { + continue; + } - $this->reporter->totalFiles += $childOutput['totalFiles']; - $this->reporter->totalErrors += $childOutput['totalErrors']; - $this->reporter->totalWarnings += $childOutput['totalWarnings']; - $this->reporter->totalFixable += $childOutput['totalFixable']; - $this->reporter->totalFixed += $childOutput['totalFixed']; + $childProcessStatus = pcntl_wexitstatus($status); + if ($childProcessStatus !== 0) { + $success = false; + } - if (isset($debugOutput) === true) { - echo $debugOutput; - } + $out = $childProcs[$pid]; + unset($childProcs[$pid]); + if (file_exists($out) === false) { + continue; + } - if (isset($childCache) === true) { - foreach ($childCache as $path => $cache) { - Cache::set($path, $cache); - } - } + include $out; + unlink($out); - // Fake a processed file so we can print progress output for the batch. - $file = new DummyFile(null, $this->ruleset, $this->config); - $file->setErrorCounts( - $childOutput['totalErrors'], - $childOutput['totalWarnings'], - $childOutput['totalFixable'], - $childOutput['totalFixed'] - ); - $this->printProgress($file, $totalBatches, $numProcessed); - }//end if - }//end if - }//end foreach + $numProcessed++; + + if (isset($childOutput) === false) { + // The child process died, so the run has failed. + $file = new DummyFile('', $this->ruleset, $this->config); + $file->setErrorCounts(1, 0, 0, 0); + $this->printProgress($file, $totalBatches, $numProcessed); + $success = false; + continue; + } + + $this->reporter->totalFiles += $childOutput['totalFiles']; + $this->reporter->totalErrors += $childOutput['totalErrors']; + $this->reporter->totalWarnings += $childOutput['totalWarnings']; + $this->reporter->totalFixable += $childOutput['totalFixable']; + $this->reporter->totalFixed += $childOutput['totalFixed']; + + if (isset($debugOutput) === true) { + echo $debugOutput; + } + + if (isset($childCache) === true) { + foreach ($childCache as $path => $cache) { + Cache::set($path, $cache); + } + } + + // Fake a processed file so we can print progress output for the batch. + $file = new DummyFile('', $this->ruleset, $this->config); + $file->setErrorCounts( + $childOutput['totalErrors'], + $childOutput['totalWarnings'], + $childOutput['totalFixable'], + $childOutput['totalFixed'] + ); + $this->printProgress($file, $totalBatches, $numProcessed); }//end while return $success; @@ -874,7 +916,10 @@ public function printProgress(File $file, $numFiles, $numProcessed) $percent = round(($numProcessed / $numFiles) * 100); $padding = (strlen($numFiles) - strlen($numProcessed)); - if ($numProcessed === $numFiles && $numFiles > $numPerLine) { + if ($numProcessed === $numFiles + && $numFiles > $numPerLine + && ($numProcessed % $numPerLine) !== 0 + ) { $padding += ($numPerLine - ($numFiles - (floor($numFiles / $numPerLine) * $numPerLine))); } @@ -883,4 +928,42 @@ public function printProgress(File $file, $numFiles, $numProcessed) }//end printProgress() + /** + * Registers a PHP shutdown function to provide a more informative out of memory error. + * + * @param string $command The command which was used to initiate the PHPCS run. + * + * @return void + */ + private function registerOutOfMemoryShutdownMessage($command) + { + // Allocate all needed memory beforehand as much as possible. + $errorMsg = PHP_EOL.'The PHP_CodeSniffer "%1$s" command ran out of memory.'.PHP_EOL; + $errorMsg .= 'Either raise the "memory_limit" of PHP in the php.ini file or raise the memory limit at runtime'.PHP_EOL; + $errorMsg .= 'using `%1$s -d memory_limit=512M` (replace 512M with the desired memory limit).'.PHP_EOL; + $errorMsg = sprintf($errorMsg, $command); + $memoryError = 'Allowed memory size of'; + $errorArray = [ + 'type' => 42, + 'message' => 'Some random dummy string to take up memory and take up some more memory and some more', + 'file' => 'Another random string, which would be a filename this time. Should be relatively long to allow for deeply nested files', + 'line' => 31427, + ]; + + register_shutdown_function( + static function () use ( + $errorMsg, + $memoryError, + $errorArray + ) { + $errorArray = error_get_last(); + if (is_array($errorArray) === true && strpos($errorArray['message'], $memoryError) !== false) { + echo $errorMsg; + } + } + ); + + }//end registerOutOfMemoryShutdownMessage() + + }//end class diff --git a/src/Sniffs/AbstractArraySniff.php b/src/Sniffs/AbstractArraySniff.php index 141b9a133c..efe9969d8d 100644 --- a/src/Sniffs/AbstractArraySniff.php +++ b/src/Sniffs/AbstractArraySniff.php @@ -104,9 +104,9 @@ public function process(File $phpcsFile, $stackPtr) /** * Find next separator in array - either: comma or double arrow. * - * @param File $phpcsFile The current file being checked. - * @param int $ptr The position of current token. - * @param int $arrayEnd The token that ends the array definition. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The current file being checked. + * @param int $ptr The position of current token. + * @param int $arrayEnd The token that ends the array definition. * * @return int */ diff --git a/src/Sniffs/Sniff.php b/src/Sniffs/Sniff.php index c9d7daea82..3f0fb6a1cf 100644 --- a/src/Sniffs/Sniff.php +++ b/src/Sniffs/Sniff.php @@ -45,7 +45,7 @@ public function register(); * is found. * * The stackPtr variable indicates where in the stack the token was found. - * A sniff can acquire information this token, along with all the other + * A sniff can acquire information about this token, along with all the other * tokens within the stack by first acquiring the token stack: * * diff --git a/src/Standards/Generic/Docs/Classes/OpeningBraceSameLineStandard.xml b/src/Standards/Generic/Docs/Classes/OpeningBraceSameLineStandard.xml index 6226a3fff6..6fa08be7a4 100644 --- a/src/Standards/Generic/Docs/Classes/OpeningBraceSameLineStandard.xml +++ b/src/Standards/Generic/Docs/Classes/OpeningBraceSameLineStandard.xml @@ -15,6 +15,14 @@ class Foo { { +} + ]]> + + + + + { } ]]> diff --git a/src/Standards/Generic/Docs/Formatting/SpaceAfterNotStandard.xml b/src/Standards/Generic/Docs/Formatting/SpaceAfterNotStandard.xml index dd3e773118..aea863695e 100644 --- a/src/Standards/Generic/Docs/Formatting/SpaceAfterNotStandard.xml +++ b/src/Standards/Generic/Docs/Formatting/SpaceAfterNotStandard.xml @@ -10,13 +10,10 @@ if (! $someVar || ! $x instanceOf stdClass) {}; ]]> - + $someVar || !$x instanceOf stdClass) {}; - ]]> - - - $someVar || ! $x instanceOf stdClass) {}; ]]> diff --git a/src/Standards/Generic/Docs/NamingConventions/AbstractClassNamePrefixStandard.xml b/src/Standards/Generic/Docs/NamingConventions/AbstractClassNamePrefixStandard.xml new file mode 100644 index 0000000000..c30d26e9d5 --- /dev/null +++ b/src/Standards/Generic/Docs/NamingConventions/AbstractClassNamePrefixStandard.xml @@ -0,0 +1,23 @@ + + + + + + + AbstractBar +{ +} + ]]> + + + Bar +{ +} + ]]> + + + diff --git a/src/Standards/Generic/Docs/NamingConventions/InterfaceNameSuffixStandard.xml b/src/Standards/Generic/Docs/NamingConventions/InterfaceNameSuffixStandard.xml new file mode 100644 index 0000000000..0aa0c76e4d --- /dev/null +++ b/src/Standards/Generic/Docs/NamingConventions/InterfaceNameSuffixStandard.xml @@ -0,0 +1,23 @@ + + + + + + + BarInterface +{ +} + ]]> + + + Bar +{ +} + ]]> + + + diff --git a/src/Standards/Generic/Docs/NamingConventions/TraitNameSuffixStandard.xml b/src/Standards/Generic/Docs/NamingConventions/TraitNameSuffixStandard.xml new file mode 100644 index 0000000000..711867e451 --- /dev/null +++ b/src/Standards/Generic/Docs/NamingConventions/TraitNameSuffixStandard.xml @@ -0,0 +1,23 @@ + + + + + + + BarTrait +{ +} + ]]> + + + Bar +{ +} + ]]> + + + diff --git a/src/Standards/Generic/Docs/WhiteSpace/ArbitraryParenthesesSpacingStandard.xml b/src/Standards/Generic/Docs/WhiteSpace/ArbitraryParenthesesSpacingStandard.xml index 30e0def93c..338c838933 100644 --- a/src/Standards/Generic/Docs/WhiteSpace/ArbitraryParenthesesSpacingStandard.xml +++ b/src/Standards/Generic/Docs/WhiteSpace/ArbitraryParenthesesSpacingStandard.xml @@ -10,13 +10,10 @@ $a = (null !== $extra); ]]> - + - - - getTokens(); - $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $stackPtr, true); - $expectedIndent = ($tokens[$first]['column'] - 1 + $this->indent); + // Determine how far indented the entire array declaration should be. + $ignore = Tokens::$emptyTokens; + $ignore[] = T_DOUBLE_ARROW; + $ignore[] = T_COMMA; + $prev = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true); + $start = $phpcsFile->findStartOfStatement($prev); + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $start, true); + $baseIndent = ($tokens[$first]['column'] - 1); + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $stackPtr, true); + $startIndent = ($tokens[$first]['column'] - 1); + + // If the open brace is not indented to at least to the level of the start + // of the statement, the sniff will conflict with other sniffs trying to + // check indent levels because it's not valid. But we don't enforce exactly + // how far indented it should be. + if ($startIndent < $baseIndent) { + $error = 'Array open brace not indented correctly; expected at least %s spaces but found %s'; + $data = [ + $baseIndent, + $startIndent, + ]; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'OpenBraceIncorrect', $data); + if ($fix === true) { + $padding = str_repeat(' ', $baseIndent); + if ($startIndent === 0) { + $phpcsFile->fixer->addContentBefore($first, $padding); + } else { + $phpcsFile->fixer->replaceToken(($first - 1), $padding); + } + } + + return; + }//end if + + $expectedIndent = ($startIndent + $this->indent); foreach ($indices as $index) { if (isset($index['index_start']) === true) { diff --git a/src/Standards/Generic/Sniffs/Classes/DuplicateClassNameSniff.php b/src/Standards/Generic/Sniffs/Classes/DuplicateClassNameSniff.php index 8374bfd535..3243f8b7dc 100644 --- a/src/Standards/Generic/Sniffs/Classes/DuplicateClassNameSniff.php +++ b/src/Standards/Generic/Sniffs/Classes/DuplicateClassNameSniff.php @@ -53,6 +53,7 @@ public function process(File $phpcsFile, $stackPtr) T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, T_NAMESPACE, T_CLOSE_TAG, ]; diff --git a/src/Standards/Generic/Sniffs/Classes/OpeningBraceSameLineSniff.php b/src/Standards/Generic/Sniffs/Classes/OpeningBraceSameLineSniff.php index fb92d82c08..dfd925f918 100644 --- a/src/Standards/Generic/Sniffs/Classes/OpeningBraceSameLineSniff.php +++ b/src/Standards/Generic/Sniffs/Classes/OpeningBraceSameLineSniff.php @@ -27,6 +27,7 @@ public function register() T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, ]; }//end register() diff --git a/src/Standards/Generic/Sniffs/CodeAnalysis/AssignmentInConditionSniff.php b/src/Standards/Generic/Sniffs/CodeAnalysis/AssignmentInConditionSniff.php index 63e831949e..8788a912ea 100644 --- a/src/Standards/Generic/Sniffs/CodeAnalysis/AssignmentInConditionSniff.php +++ b/src/Standards/Generic/Sniffs/CodeAnalysis/AssignmentInConditionSniff.php @@ -60,6 +60,7 @@ public function register() T_SWITCH, T_CASE, T_WHILE, + T_MATCH, ]; }//end register() diff --git a/src/Standards/Generic/Sniffs/CodeAnalysis/EmptyPHPStatementSniff.php b/src/Standards/Generic/Sniffs/CodeAnalysis/EmptyPHPStatementSniff.php index a37d6cc68f..1c9e400019 100644 --- a/src/Standards/Generic/Sniffs/CodeAnalysis/EmptyPHPStatementSniff.php +++ b/src/Standards/Generic/Sniffs/CodeAnalysis/EmptyPHPStatementSniff.php @@ -72,7 +72,7 @@ public function process(File $phpcsFile, $stackPtr) } $scopeOwner = $tokens[$tokens[$prevNonEmpty]['scope_condition']]['code']; - if ($scopeOwner === T_CLOSURE || $scopeOwner === T_ANON_CLASS) { + if ($scopeOwner === T_CLOSURE || $scopeOwner === T_ANON_CLASS || $scopeOwner === T_MATCH) { return; } diff --git a/src/Standards/Generic/Sniffs/CodeAnalysis/EmptyStatementSniff.php b/src/Standards/Generic/Sniffs/CodeAnalysis/EmptyStatementSniff.php index d9248af39c..574bb0ba5a 100644 --- a/src/Standards/Generic/Sniffs/CodeAnalysis/EmptyStatementSniff.php +++ b/src/Standards/Generic/Sniffs/CodeAnalysis/EmptyStatementSniff.php @@ -50,6 +50,7 @@ public function register() T_IF, T_SWITCH, T_WHILE, + T_MATCH, ]; }//end register() diff --git a/src/Standards/Generic/Sniffs/CodeAnalysis/UnconditionalIfStatementSniff.php b/src/Standards/Generic/Sniffs/CodeAnalysis/UnconditionalIfStatementSniff.php index 94eab1f991..cc9958c586 100644 --- a/src/Standards/Generic/Sniffs/CodeAnalysis/UnconditionalIfStatementSniff.php +++ b/src/Standards/Generic/Sniffs/CodeAnalysis/UnconditionalIfStatementSniff.php @@ -63,7 +63,7 @@ public function process(File $phpcsFile, $stackPtr) $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; - // Skip for-loop without body. + // Skip if statement without body. if (isset($token['parenthesis_opener']) === false) { return; } diff --git a/src/Standards/Generic/Sniffs/CodeAnalysis/UnnecessaryFinalModifierSniff.php b/src/Standards/Generic/Sniffs/CodeAnalysis/UnnecessaryFinalModifierSniff.php index bed67c93c5..02ae2e7a32 100644 --- a/src/Standards/Generic/Sniffs/CodeAnalysis/UnnecessaryFinalModifierSniff.php +++ b/src/Standards/Generic/Sniffs/CodeAnalysis/UnnecessaryFinalModifierSniff.php @@ -24,7 +24,6 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; -use PHP_CodeSniffer\Util\Tokens; class UnnecessaryFinalModifierSniff implements Sniff { @@ -56,16 +55,13 @@ public function process(File $phpcsFile, $stackPtr) $tokens = $phpcsFile->getTokens(); $token = $tokens[$stackPtr]; - // Skip for-statements without body. + // Skip for statements without body. if (isset($token['scope_opener']) === false) { return; } - // Fetch previous token. - $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); - - // Skip for non final class. - if ($prev === false || $tokens[$prev]['code'] !== T_FINAL) { + if ($phpcsFile->getClassProperties($stackPtr)['is_final'] === false) { + // This class is not final so we don't need to check it. return; } @@ -77,6 +73,13 @@ public function process(File $phpcsFile, $stackPtr) $error = 'Unnecessary FINAL modifier in FINAL class'; $phpcsFile->addWarning($error, $next, 'Found'); } + + // Skip over the contents of functions as those can't contain the `final` keyword anyway. + if ($tokens[$next]['code'] === T_FUNCTION + && isset($tokens[$next]['scope_closer']) === true + ) { + $next = $tokens[$next]['scope_closer']; + } } }//end process() diff --git a/src/Standards/Generic/Sniffs/CodeAnalysis/UnusedFunctionParameterSniff.php b/src/Standards/Generic/Sniffs/CodeAnalysis/UnusedFunctionParameterSniff.php index 427f509cae..58aa29fe85 100644 --- a/src/Standards/Generic/Sniffs/CodeAnalysis/UnusedFunctionParameterSniff.php +++ b/src/Standards/Generic/Sniffs/CodeAnalysis/UnusedFunctionParameterSniff.php @@ -23,6 +23,13 @@ class UnusedFunctionParameterSniff implements Sniff { + /** + * The list of class type hints which will be ignored. + * + * @var array + */ + public $ignoreTypeHints = []; + /** * Returns an array of tokens this test wants to listen for. @@ -83,12 +90,24 @@ public function process(File $phpcsFile, $stackPtr) } foreach ($methodParams as $param) { + if (isset($param['property_visibility']) === true) { + // Ignore constructor property promotion. + continue; + } + $params[$param['name']] = $stackPtr; } $next = ++$token['scope_opener']; $end = --$token['scope_closer']; + // Check the end token for arrow functions as + // they can end at a content token due to not having + // a clearly defined closing token. + if ($token['code'] === T_FN) { + ++$end; + } + $foundContent = false; $validTokens = [ T_HEREDOC => T_HEREDOC, @@ -191,6 +210,10 @@ public function process(File $phpcsFile, $stackPtr) // If there is only one parameter and it is unused, no need for additional errorcode toggling logic. if ($methodParamsCount === 1) { foreach ($params as $paramName => $position) { + if (in_array($methodParams[0]['type_hint'], $this->ignoreTypeHints, true) === true) { + continue; + } + $data = [$paramName]; $phpcsFile->addWarning($error, $position, $errorCode, $data); } @@ -207,6 +230,7 @@ public function process(File $phpcsFile, $stackPtr) $errorInfo[$methodParams[$i]['name']] = [ 'position' => $params[$methodParams[$i]['name']], 'errorcode' => $errorCode.'BeforeLastUsed', + 'typehint' => $methodParams[$i]['type_hint'], ]; } } else { @@ -216,14 +240,19 @@ public function process(File $phpcsFile, $stackPtr) $errorInfo[$methodParams[$i]['name']] = [ 'position' => $params[$methodParams[$i]['name']], 'errorcode' => $errorCode.'AfterLastUsed', + 'typehint' => $methodParams[$i]['type_hint'], ]; } } - } + }//end for if (count($errorInfo) > 0) { $errorInfo = array_reverse($errorInfo); foreach ($errorInfo as $paramName => $info) { + if (in_array($info['typehint'], $this->ignoreTypeHints, true) === true) { + continue; + } + $data = [$paramName]; $phpcsFile->addWarning($error, $info['position'], $info['errorcode'], $data); } diff --git a/src/Standards/Generic/Sniffs/ControlStructures/InlineControlStructureSniff.php b/src/Standards/Generic/Sniffs/ControlStructures/InlineControlStructureSniff.php index d48a477ce1..a67a6a798c 100644 --- a/src/Standards/Generic/Sniffs/ControlStructures/InlineControlStructureSniff.php +++ b/src/Standards/Generic/Sniffs/ControlStructures/InlineControlStructureSniff.php @@ -142,8 +142,10 @@ public function process(File $phpcsFile, $stackPtr) return; } - if ($tokens[$nextNonEmpty]['code'] === T_COLON) { - // Alternative control structure. + if ($tokens[$nextNonEmpty]['code'] === T_OPEN_CURLY_BRACKET + || $tokens[$nextNonEmpty]['code'] === T_COLON + ) { + // T_CLOSE_CURLY_BRACKET missing, or alternative control structure with // T_END... missing. Either live coding, parse error or end // tag in short open tags and scan run with short_open_tag=Off. // Bow out completely as any further detection will be unreliable @@ -208,7 +210,10 @@ public function process(File $phpcsFile, $stackPtr) if (isset($tokens[$end]['scope_opener']) === true) { $type = $tokens[$end]['code']; $end = $tokens[$end]['scope_closer']; - if ($type === T_DO || $type === T_IF || $type === T_ELSEIF || $type === T_TRY) { + if ($type === T_DO + || $type === T_IF || $type === T_ELSEIF + || $type === T_TRY || $type === T_CATCH || $type === T_FINALLY + ) { $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($end + 1), null, true); if ($next === false) { break; @@ -225,15 +230,20 @@ public function process(File $phpcsFile, $stackPtr) continue; } + // Account for TRY... CATCH/FINALLY statements. + if (($type === T_TRY + || $type === T_CATCH + || $type === T_FINALLY) + && ($nextType === T_CATCH + || $nextType === T_FINALLY) + ) { + continue; + } + // Account for DO... WHILE conditions. if ($type === T_DO && $nextType === T_WHILE) { $end = $phpcsFile->findNext(T_SEMICOLON, ($next + 1)); } - - // Account for TRY... CATCH statements. - if ($type === T_TRY && $nextType === T_CATCH) { - $end = $tokens[$next]['scope_closer']; - } } else if ($type === T_CLOSURE) { // There should be a semicolon after the closing brace. $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($end + 1), null, true); diff --git a/src/Standards/Generic/Sniffs/Debug/CSSLintSniff.php b/src/Standards/Generic/Sniffs/Debug/CSSLintSniff.php index d299dc845c..81284787a2 100644 --- a/src/Standards/Generic/Sniffs/Debug/CSSLintSniff.php +++ b/src/Standards/Generic/Sniffs/Debug/CSSLintSniff.php @@ -12,6 +12,7 @@ use PHP_CodeSniffer\Config; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Common; class CSSLintSniff implements Sniff { @@ -54,7 +55,7 @@ public function process(File $phpcsFile, $stackPtr) $fileName = $phpcsFile->getFilename(); - $cmd = escapeshellcmd($csslintPath).' '.escapeshellarg($fileName).' 2>&1'; + $cmd = Common::escapeshellcmd($csslintPath).' '.escapeshellarg($fileName).' 2>&1'; exec($cmd, $output, $retval); if (is_array($output) === false) { diff --git a/src/Standards/Generic/Sniffs/Debug/ClosureLinterSniff.php b/src/Standards/Generic/Sniffs/Debug/ClosureLinterSniff.php index f6b6b729bc..19204718b0 100644 --- a/src/Standards/Generic/Sniffs/Debug/ClosureLinterSniff.php +++ b/src/Standards/Generic/Sniffs/Debug/ClosureLinterSniff.php @@ -12,6 +12,7 @@ use PHP_CodeSniffer\Config; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Common; class ClosureLinterSniff implements Sniff { @@ -71,7 +72,7 @@ public function process(File $phpcsFile, $stackPtr) $fileName = $phpcsFile->getFilename(); - $lintPath = escapeshellcmd($lintPath); + $lintPath = Common::escapeshellcmd($lintPath); $cmd = $lintPath.' --nosummary --notime --unix_mode '.escapeshellarg($fileName); exec($cmd, $output, $retval); diff --git a/src/Standards/Generic/Sniffs/Debug/ESLintSniff.php b/src/Standards/Generic/Sniffs/Debug/ESLintSniff.php index 081c3c1ebf..d11d347050 100644 --- a/src/Standards/Generic/Sniffs/Debug/ESLintSniff.php +++ b/src/Standards/Generic/Sniffs/Debug/ESLintSniff.php @@ -12,6 +12,7 @@ use PHP_CodeSniffer\Config; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Common; class ESLintSniff implements Sniff { @@ -76,7 +77,7 @@ public function process(File $phpcsFile, $stackPtr) $eslintOptions[] = '--config '.escapeshellarg($configFile); } - $cmd = escapeshellcmd(escapeshellarg($eslintPath).' '.implode(' ', $eslintOptions).' '.escapeshellarg($filename)); + $cmd = Common::escapeshellcmd(escapeshellarg($eslintPath).' '.implode(' ', $eslintOptions).' '.escapeshellarg($filename)); // Execute! exec($cmd, $stdout, $code); diff --git a/src/Standards/Generic/Sniffs/Debug/JSHintSniff.php b/src/Standards/Generic/Sniffs/Debug/JSHintSniff.php index 8e1f18bd0d..738ab67ebe 100644 --- a/src/Standards/Generic/Sniffs/Debug/JSHintSniff.php +++ b/src/Standards/Generic/Sniffs/Debug/JSHintSniff.php @@ -13,6 +13,7 @@ use PHP_CodeSniffer\Config; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Common; class JSHintSniff implements Sniff { @@ -51,15 +52,15 @@ public function process(File $phpcsFile, $stackPtr) { $rhinoPath = Config::getExecutablePath('rhino'); $jshintPath = Config::getExecutablePath('jshint'); - if ($rhinoPath === null && $jshintPath === null) { + if ($jshintPath === null) { return; } $fileName = $phpcsFile->getFilename(); - $jshintPath = escapeshellcmd($jshintPath); + $jshintPath = Common::escapeshellcmd($jshintPath); if ($rhinoPath !== null) { - $rhinoPath = escapeshellcmd($rhinoPath); + $rhinoPath = Common::escapeshellcmd($rhinoPath); $cmd = "$rhinoPath \"$jshintPath\" ".escapeshellarg($fileName); exec($cmd, $output, $retval); diff --git a/src/Standards/Generic/Sniffs/Files/EndFileNewlineSniff.php b/src/Standards/Generic/Sniffs/Files/EndFileNewlineSniff.php index ee52a1b975..d5375dc5c0 100644 --- a/src/Standards/Generic/Sniffs/Files/EndFileNewlineSniff.php +++ b/src/Standards/Generic/Sniffs/Files/EndFileNewlineSniff.php @@ -34,7 +34,10 @@ class EndFileNewlineSniff implements Sniff */ public function register() { - return [T_OPEN_TAG]; + return [ + T_OPEN_TAG, + T_OPEN_TAG_WITH_ECHO, + ]; }//end register() diff --git a/src/Standards/Generic/Sniffs/Files/EndFileNoNewlineSniff.php b/src/Standards/Generic/Sniffs/Files/EndFileNoNewlineSniff.php index 2c7fcd35bc..41c9c74991 100644 --- a/src/Standards/Generic/Sniffs/Files/EndFileNoNewlineSniff.php +++ b/src/Standards/Generic/Sniffs/Files/EndFileNoNewlineSniff.php @@ -34,7 +34,10 @@ class EndFileNoNewlineSniff implements Sniff */ public function register() { - return [T_OPEN_TAG]; + return [ + T_OPEN_TAG, + T_OPEN_TAG_WITH_ECHO, + ]; }//end register() diff --git a/src/Standards/Generic/Sniffs/Files/ExecutableFileSniff.php b/src/Standards/Generic/Sniffs/Files/ExecutableFileSniff.php index 51534bf377..e1213d5f64 100644 --- a/src/Standards/Generic/Sniffs/Files/ExecutableFileSniff.php +++ b/src/Standards/Generic/Sniffs/Files/ExecutableFileSniff.php @@ -23,7 +23,10 @@ class ExecutableFileSniff implements Sniff */ public function register() { - return [T_OPEN_TAG]; + return [ + T_OPEN_TAG, + T_OPEN_TAG_WITH_ECHO, + ]; }//end register() diff --git a/src/Standards/Generic/Sniffs/Files/LineEndingsSniff.php b/src/Standards/Generic/Sniffs/Files/LineEndingsSniff.php index 763e7863ca..845e1bcfc4 100644 --- a/src/Standards/Generic/Sniffs/Files/LineEndingsSniff.php +++ b/src/Standards/Generic/Sniffs/Files/LineEndingsSniff.php @@ -41,7 +41,10 @@ class LineEndingsSniff implements Sniff */ public function register() { - return [T_OPEN_TAG]; + return [ + T_OPEN_TAG, + T_OPEN_TAG_WITH_ECHO, + ]; }//end register() diff --git a/src/Standards/Generic/Sniffs/Files/LowercasedFilenameSniff.php b/src/Standards/Generic/Sniffs/Files/LowercasedFilenameSniff.php index 2f442b1123..a9fd4c5d44 100644 --- a/src/Standards/Generic/Sniffs/Files/LowercasedFilenameSniff.php +++ b/src/Standards/Generic/Sniffs/Files/LowercasedFilenameSniff.php @@ -23,7 +23,10 @@ class LowercasedFilenameSniff implements Sniff */ public function register() { - return [T_OPEN_TAG]; + return [ + T_OPEN_TAG, + T_OPEN_TAG_WITH_ECHO, + ]; }//end register() diff --git a/src/Standards/Generic/Sniffs/Files/OneClassPerFileSniff.php b/src/Standards/Generic/Sniffs/Files/OneClassPerFileSniff.php index 9bcc00c1c2..52d5d84c74 100644 --- a/src/Standards/Generic/Sniffs/Files/OneClassPerFileSniff.php +++ b/src/Standards/Generic/Sniffs/Files/OneClassPerFileSniff.php @@ -39,7 +39,13 @@ public function register() */ public function process(File $phpcsFile, $stackPtr) { - $nextClass = $phpcsFile->findNext($this->register(), ($stackPtr + 1)); + $tokens = $phpcsFile->getTokens(); + $start = ($stackPtr + 1); + if (isset($tokens[$stackPtr]['scope_closer']) === true) { + $start = ($tokens[$stackPtr]['scope_closer'] + 1); + } + + $nextClass = $phpcsFile->findNext($this->register(), $start); if ($nextClass !== false) { $error = 'Only one class is allowed in a file'; $phpcsFile->addError($error, $nextClass, 'MultipleFound'); diff --git a/src/Standards/Generic/Sniffs/Files/OneInterfacePerFileSniff.php b/src/Standards/Generic/Sniffs/Files/OneInterfacePerFileSniff.php index f7cfa8d30b..9a6f5bccd5 100644 --- a/src/Standards/Generic/Sniffs/Files/OneInterfacePerFileSniff.php +++ b/src/Standards/Generic/Sniffs/Files/OneInterfacePerFileSniff.php @@ -39,7 +39,13 @@ public function register() */ public function process(File $phpcsFile, $stackPtr) { - $nextInterface = $phpcsFile->findNext($this->register(), ($stackPtr + 1)); + $tokens = $phpcsFile->getTokens(); + $start = ($stackPtr + 1); + if (isset($tokens[$stackPtr]['scope_closer']) === true) { + $start = ($tokens[$stackPtr]['scope_closer'] + 1); + } + + $nextInterface = $phpcsFile->findNext($this->register(), $start); if ($nextInterface !== false) { $error = 'Only one interface is allowed in a file'; $phpcsFile->addError($error, $nextInterface, 'MultipleFound'); diff --git a/src/Standards/Generic/Sniffs/Files/OneObjectStructurePerFileSniff.php b/src/Standards/Generic/Sniffs/Files/OneObjectStructurePerFileSniff.php index d9d71b6996..4d417e069e 100644 --- a/src/Standards/Generic/Sniffs/Files/OneObjectStructurePerFileSniff.php +++ b/src/Standards/Generic/Sniffs/Files/OneObjectStructurePerFileSniff.php @@ -27,6 +27,7 @@ public function register() T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, ]; }//end register() @@ -43,7 +44,13 @@ public function register() */ public function process(File $phpcsFile, $stackPtr) { - $nextClass = $phpcsFile->findNext($this->register(), ($stackPtr + 1)); + $tokens = $phpcsFile->getTokens(); + $start = ($stackPtr + 1); + if (isset($tokens[$stackPtr]['scope_closer']) === true) { + $start = ($tokens[$stackPtr]['scope_closer'] + 1); + } + + $nextClass = $phpcsFile->findNext($this->register(), $start); if ($nextClass !== false) { $error = 'Only one object structure is allowed in a file'; $phpcsFile->addError($error, $nextClass, 'MultipleFound'); diff --git a/src/Standards/Generic/Sniffs/Files/OneTraitPerFileSniff.php b/src/Standards/Generic/Sniffs/Files/OneTraitPerFileSniff.php index dd97da85b4..7ae523f70e 100644 --- a/src/Standards/Generic/Sniffs/Files/OneTraitPerFileSniff.php +++ b/src/Standards/Generic/Sniffs/Files/OneTraitPerFileSniff.php @@ -39,7 +39,13 @@ public function register() */ public function process(File $phpcsFile, $stackPtr) { - $nextClass = $phpcsFile->findNext($this->register(), ($stackPtr + 1)); + $tokens = $phpcsFile->getTokens(); + $start = ($stackPtr + 1); + if (isset($tokens[$stackPtr]['scope_closer']) === true) { + $start = ($tokens[$stackPtr]['scope_closer'] + 1); + } + + $nextClass = $phpcsFile->findNext($this->register(), $start); if ($nextClass !== false) { $error = 'Only one trait is allowed in a file'; $phpcsFile->addError($error, $nextClass, 'MultipleFound'); diff --git a/src/Standards/Generic/Sniffs/Formatting/DisallowMultipleStatementsSniff.php b/src/Standards/Generic/Sniffs/Formatting/DisallowMultipleStatementsSniff.php index d67bb761e4..6f9ff9f917 100644 --- a/src/Standards/Generic/Sniffs/Formatting/DisallowMultipleStatementsSniff.php +++ b/src/Standards/Generic/Sniffs/Formatting/DisallowMultipleStatementsSniff.php @@ -59,16 +59,18 @@ public function process(File $phpcsFile, $stackPtr) } while ($tokens[$prev]['code'] === T_PHPCS_IGNORE); // Ignore multiple statements in a FOR condition. - if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) { - foreach ($tokens[$stackPtr]['nested_parenthesis'] as $bracket) { - if (isset($tokens[$bracket]['parenthesis_owner']) === false) { - // Probably a closure sitting inside a function call. - continue; - } - - $owner = $tokens[$bracket]['parenthesis_owner']; - if ($tokens[$owner]['code'] === T_FOR) { - return; + foreach ([$stackPtr, $prev] as $checkToken) { + if (isset($tokens[$checkToken]['nested_parenthesis']) === true) { + foreach ($tokens[$checkToken]['nested_parenthesis'] as $bracket) { + if (isset($tokens[$bracket]['parenthesis_owner']) === false) { + // Probably a closure sitting inside a function call. + continue; + } + + $owner = $tokens[$bracket]['parenthesis_owner']; + if ($tokens[$owner]['code'] === T_FOR) { + return; + } } } } diff --git a/src/Standards/Generic/Sniffs/Formatting/MultipleStatementAlignmentSniff.php b/src/Standards/Generic/Sniffs/Formatting/MultipleStatementAlignmentSniff.php index ab7caa9d48..802e594485 100644 --- a/src/Standards/Generic/Sniffs/Formatting/MultipleStatementAlignmentSniff.php +++ b/src/Standards/Generic/Sniffs/Formatting/MultipleStatementAlignmentSniff.php @@ -47,6 +47,13 @@ class MultipleStatementAlignmentSniff implements Sniff */ public $maxPadding = 1000; + /** + * Controls which side of the assignment token is used for alignment. + * + * @var boolean + */ + public $alignAtEnd = true; + /** * Returns an array of tokens this test wants to listen for. @@ -73,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); @@ -113,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']; @@ -140,11 +145,25 @@ public function checkAlignment($phpcsFile, $stackPtr, $end=null) break; } - if (isset($scopes[$tokens[$assign]['code']]) === true - && isset($tokens[$assign]['scope_opener']) === true + if (isset($tokens[$assign]['scope_opener']) === true && $tokens[$assign]['level'] === $tokens[$stackPtr]['level'] ) { - break; + if (isset($scopes[$tokens[$assign]['code']]) === true) { + // This type of scope indicates that the assignment block is over. + break; + } + + // Skip over the scope block because it is seen as part of the assignment block, + // but also process any assignment blocks that are inside as well. + $nextAssign = $phpcsFile->findNext($find, ($assign + 1), ($tokens[$assign]['scope_closer'] - 1)); + if ($nextAssign !== false) { + $assign = $this->checkAlignment($phpcsFile, $nextAssign); + } else { + $assign = $tokens[$assign]['scope_closer']; + } + + $lastCode = $assign; + continue; } if ($assign === $arrayEnd) { @@ -253,6 +272,10 @@ public function checkAlignment($phpcsFile, $stackPtr, $end=null) // padding length if they aligned with us. $varEnd = $tokens[($var + 1)]['column']; $assignLen = $tokens[$assign]['length']; + if ($this->alignAtEnd !== true) { + $assignLen = 1; + } + if ($assign !== $stackPtr) { if ($prevAssign === null) { // Processing an inner block but no assignments found. diff --git a/src/Standards/Generic/Sniffs/Functions/FunctionCallArgumentSpacingSniff.php b/src/Standards/Generic/Sniffs/Functions/FunctionCallArgumentSpacingSniff.php index ca9223814b..39fdae9526 100644 --- a/src/Standards/Generic/Sniffs/Functions/FunctionCallArgumentSpacingSniff.php +++ b/src/Standards/Generic/Sniffs/Functions/FunctionCallArgumentSpacingSniff.php @@ -30,6 +30,7 @@ public function register() T_UNSET, T_SELF, T_STATIC, + T_PARENT, T_VARIABLE, T_CLOSE_CURLY_BRACKET, T_CLOSE_PARENTHESIS, @@ -155,10 +156,13 @@ public function checkSpacing(File $phpcsFile, $stackPtr, $openBracket) }//end if if ($tokens[($nextSeparator + 1)]['code'] !== T_WHITESPACE) { - $error = 'No space found after comma in argument list'; - $fix = $phpcsFile->addFixableError($error, $nextSeparator, 'NoSpaceAfterComma'); - if ($fix === true) { - $phpcsFile->fixer->addContent($nextSeparator, ' '); + // Ignore trailing comma's after last argument as that's outside the scope of this sniff. + if (($nextSeparator + 1) !== $closeBracket) { + $error = 'No space found after comma in argument list'; + $fix = $phpcsFile->addFixableError($error, $nextSeparator, 'NoSpaceAfterComma'); + if ($fix === true) { + $phpcsFile->fixer->addContent($nextSeparator, ' '); + } } } else { // If there is a newline in the space, then they must be formatting diff --git a/src/Standards/Generic/Sniffs/Functions/OpeningFunctionBraceBsdAllmanSniff.php b/src/Standards/Generic/Sniffs/Functions/OpeningFunctionBraceBsdAllmanSniff.php index be1464d7ce..ff19526a9b 100644 --- a/src/Standards/Generic/Sniffs/Functions/OpeningFunctionBraceBsdAllmanSniff.php +++ b/src/Standards/Generic/Sniffs/Functions/OpeningFunctionBraceBsdAllmanSniff.php @@ -139,17 +139,31 @@ public function process(File $phpcsFile, $stackPtr) } else if ($lineDifference > 1) { $error = 'Opening brace should be on the line after the declaration; found %s blank line(s)'; $data = [($lineDifference - 1)]; - $fix = $phpcsFile->addFixableError($error, $openingBrace, 'BraceSpacing', $data); - if ($fix === true) { - for ($i = ($tokens[$stackPtr]['parenthesis_closer'] + 1); $i < $openingBrace; $i++) { - if ($tokens[$i]['line'] === $braceLine) { - $phpcsFile->fixer->addNewLineBefore($i); - break; + + $prevNonWs = $phpcsFile->findPrevious(T_WHITESPACE, ($openingBrace - 1), $closeBracket, true); + if ($prevNonWs !== $prev) { + // There must be a comment between the end of the function declaration and the open brace. + // Report, but don't fix. + $phpcsFile->addError($error, $openingBrace, 'BraceSpacing', $data); + } else { + $fix = $phpcsFile->addFixableError($error, $openingBrace, 'BraceSpacing', $data); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + for ($i = $openingBrace; $i > $prev; $i--) { + if ($tokens[$i]['line'] === $tokens[$openingBrace]['line']) { + if ($tokens[$i]['column'] === 1) { + $phpcsFile->fixer->addNewLineBefore($i); + } + + continue; + } + + $phpcsFile->fixer->replaceToken($i, ''); } - $phpcsFile->fixer->replaceToken($i, ''); + $phpcsFile->fixer->endChangeset(); } - } + }//end if }//end if $ignore = Tokens::$phpcsCommentTokens; diff --git a/src/Standards/Generic/Sniffs/Metrics/CyclomaticComplexitySniff.php b/src/Standards/Generic/Sniffs/Metrics/CyclomaticComplexitySniff.php index df70df221f..9bd0dff3de 100644 --- a/src/Standards/Generic/Sniffs/Metrics/CyclomaticComplexitySniff.php +++ b/src/Standards/Generic/Sniffs/Metrics/CyclomaticComplexitySniff.php @@ -71,15 +71,19 @@ public function process(File $phpcsFile, $stackPtr) // Predicate nodes for PHP. $find = [ - T_CASE => true, - T_DEFAULT => true, - T_CATCH => true, - T_IF => true, - T_FOR => true, - T_FOREACH => true, - T_WHILE => true, - T_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, + T_NULLSAFE_OBJECT_OPERATOR => true, ]; $complexity = 1; diff --git a/src/Standards/Generic/Sniffs/NamingConventions/AbstractClassNamePrefixSniff.php b/src/Standards/Generic/Sniffs/NamingConventions/AbstractClassNamePrefixSniff.php new file mode 100644 index 0000000000..3e3af830d3 --- /dev/null +++ b/src/Standards/Generic/Sniffs/NamingConventions/AbstractClassNamePrefixSniff.php @@ -0,0 +1,60 @@ + + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions; + +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; + +class AbstractClassNamePrefixSniff implements Sniff +{ + + + /** + * Registers the tokens that this sniff wants to listen for. + * + * @return int[] + */ + public function register() + { + return [T_CLASS]; + + }//end register() + + + /** + * Processes this sniff, 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) + { + if ($phpcsFile->getClassProperties($stackPtr)['is_abstract'] === false) { + // This class is not abstract so we don't need to check it. + return; + } + + $className = $phpcsFile->getDeclarationName($stackPtr); + if ($className === null) { + // We are not interested in anonymous classes. + return; + } + + $prefix = substr($className, 0, 8); + if (strtolower($prefix) !== 'abstract') { + $phpcsFile->addError('Abstract class names must be prefixed with "Abstract"; found "%s"', $stackPtr, 'Missing', [$className]); + } + + }//end process() + + +}//end class diff --git a/src/Standards/Generic/Sniffs/NamingConventions/CamelCapsFunctionNameSniff.php b/src/Standards/Generic/Sniffs/NamingConventions/CamelCapsFunctionNameSniff.php index 64e12689a9..b45112034a 100644 --- a/src/Standards/Generic/Sniffs/NamingConventions/CamelCapsFunctionNameSniff.php +++ b/src/Standards/Generic/Sniffs/NamingConventions/CamelCapsFunctionNameSniff.php @@ -23,21 +23,23 @@ class CamelCapsFunctionNameSniff extends AbstractScopeSniff * @var array */ protected $magicMethods = [ - 'construct' => true, - 'destruct' => true, - 'call' => true, - 'callstatic' => true, - 'get' => true, - 'set' => true, - 'isset' => true, - 'unset' => true, - 'sleep' => true, - 'wakeup' => true, - 'tostring' => true, - 'set_state' => true, - 'clone' => true, - 'invoke' => true, - 'debuginfo' => true, + 'construct' => true, + 'destruct' => true, + 'call' => true, + 'callstatic' => true, + 'get' => true, + 'set' => true, + 'isset' => true, + 'unset' => true, + 'sleep' => true, + 'wakeup' => true, + 'serialize' => true, + 'unserialize' => true, + 'tostring' => true, + 'invoke' => true, + 'set_state' => true, + 'clone' => true, + 'debuginfo' => true, ]; /** diff --git a/src/Standards/Generic/Sniffs/NamingConventions/ConstructorNameSniff.php b/src/Standards/Generic/Sniffs/NamingConventions/ConstructorNameSniff.php index b7f5427a22..a41960637c 100644 --- a/src/Standards/Generic/Sniffs/NamingConventions/ConstructorNameSniff.php +++ b/src/Standards/Generic/Sniffs/NamingConventions/ConstructorNameSniff.php @@ -66,7 +66,12 @@ protected function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScop return; } - $className = strtolower($phpcsFile->getDeclarationName($currScope)); + $className = $phpcsFile->getDeclarationName($currScope); + if (empty($className) === false) { + // Not an anonymous class. + $className = strtolower($className); + } + if ($className !== $this->currentClass) { $this->loadFunctionNamesInScope($phpcsFile, $currScope); $this->currentClass = $className; diff --git a/src/Standards/Generic/Sniffs/NamingConventions/InterfaceNameSuffixSniff.php b/src/Standards/Generic/Sniffs/NamingConventions/InterfaceNameSuffixSniff.php new file mode 100644 index 0000000000..c5dc34d489 --- /dev/null +++ b/src/Standards/Generic/Sniffs/NamingConventions/InterfaceNameSuffixSniff.php @@ -0,0 +1,54 @@ + + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions; + +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; + +class InterfaceNameSuffixSniff implements Sniff +{ + + + /** + * Registers the tokens that this sniff wants to listen for. + * + * @return int[] + */ + public function register() + { + return [T_INTERFACE]; + + }//end register() + + + /** + * Processes this sniff, 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) + { + $interfaceName = $phpcsFile->getDeclarationName($stackPtr); + if ($interfaceName === null) { + return; + } + + $suffix = substr($interfaceName, -9); + if (strtolower($suffix) !== 'interface') { + $phpcsFile->addError('Interface names must be suffixed with "Interface"; found "%s"', $stackPtr, 'Missing', [$interfaceName]); + } + + }//end process() + + +}//end class diff --git a/src/Standards/Generic/Sniffs/NamingConventions/TraitNameSuffixSniff.php b/src/Standards/Generic/Sniffs/NamingConventions/TraitNameSuffixSniff.php new file mode 100644 index 0000000000..4e3b211dff --- /dev/null +++ b/src/Standards/Generic/Sniffs/NamingConventions/TraitNameSuffixSniff.php @@ -0,0 +1,54 @@ + + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions; + +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; + +class TraitNameSuffixSniff implements Sniff +{ + + + /** + * Registers the tokens that this sniff wants to listen for. + * + * @return int[] + */ + public function register() + { + return [T_TRAIT]; + + }//end register() + + + /** + * Processes this sniff, 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) + { + $traitName = $phpcsFile->getDeclarationName($stackPtr); + if ($traitName === null) { + return; + } + + $suffix = substr($traitName, -5); + if (strtolower($suffix) !== 'trait') { + $phpcsFile->addError('Trait names must be suffixed with "Trait"; found "%s"', $stackPtr, 'Missing', [$traitName]); + } + + }//end process() + + +}//end class diff --git a/src/Standards/Generic/Sniffs/NamingConventions/UpperCaseConstantNameSniff.php b/src/Standards/Generic/Sniffs/NamingConventions/UpperCaseConstantNameSniff.php index e4173d98d0..db50eb56bb 100644 --- a/src/Standards/Generic/Sniffs/NamingConventions/UpperCaseConstantNameSniff.php +++ b/src/Standards/Generic/Sniffs/NamingConventions/UpperCaseConstantNameSniff.php @@ -83,6 +83,7 @@ public function process(File $phpcsFile, $stackPtr) $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); if ($tokens[$prev]['code'] === T_OBJECT_OPERATOR || $tokens[$prev]['code'] === T_DOUBLE_COLON + || $tokens[$prev]['code'] === T_NULLSAFE_OBJECT_OPERATOR ) { return; } diff --git a/src/Standards/Generic/Sniffs/PHP/ClosingPHPTagSniff.php b/src/Standards/Generic/Sniffs/PHP/ClosingPHPTagSniff.php index e59ed5ffa7..d03bf8abe1 100644 --- a/src/Standards/Generic/Sniffs/PHP/ClosingPHPTagSniff.php +++ b/src/Standards/Generic/Sniffs/PHP/ClosingPHPTagSniff.php @@ -23,7 +23,10 @@ class ClosingPHPTagSniff implements Sniff */ public function register() { - return [T_OPEN_TAG]; + return [ + T_OPEN_TAG, + T_OPEN_TAG_WITH_ECHO, + ]; }//end register() diff --git a/src/Standards/Generic/Sniffs/PHP/DisallowRequestSuperglobalSniff.php b/src/Standards/Generic/Sniffs/PHP/DisallowRequestSuperglobalSniff.php index d5c7ddb724..1c2b1aee93 100644 --- a/src/Standards/Generic/Sniffs/PHP/DisallowRequestSuperglobalSniff.php +++ b/src/Standards/Generic/Sniffs/PHP/DisallowRequestSuperglobalSniff.php @@ -31,8 +31,9 @@ public function register() /** * Processes this sniff, when one of its tokens is encountered. * - * @param File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the stack + * passed in $tokens. * * @return void */ diff --git a/src/Standards/Generic/Sniffs/PHP/ForbiddenFunctionsSniff.php b/src/Standards/Generic/Sniffs/PHP/ForbiddenFunctionsSniff.php index d8b8bcf498..8717189f0e 100644 --- a/src/Standards/Generic/Sniffs/PHP/ForbiddenFunctionsSniff.php +++ b/src/Standards/Generic/Sniffs/PHP/ForbiddenFunctionsSniff.php @@ -120,18 +120,19 @@ public function process(File $phpcsFile, $stackPtr) $tokens = $phpcsFile->getTokens(); $ignore = [ - T_DOUBLE_COLON => true, - T_OBJECT_OPERATOR => true, - T_FUNCTION => true, - T_CONST => true, - T_PUBLIC => true, - T_PRIVATE => true, - T_PROTECTED => true, - T_AS => true, - T_NEW => true, - T_INSTEADOF => true, - T_NS_SEPARATOR => true, - T_IMPLEMENTS => true, + T_DOUBLE_COLON => true, + T_OBJECT_OPERATOR => true, + T_NULLSAFE_OBJECT_OPERATOR => true, + T_FUNCTION => true, + T_CONST => true, + T_PUBLIC => true, + T_PRIVATE => true, + T_PROTECTED => true, + T_AS => true, + T_NEW => true, + T_INSTEADOF => true, + T_NS_SEPARATOR => true, + T_IMPLEMENTS => true, ]; $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); @@ -162,6 +163,11 @@ public function process(File $phpcsFile, $stackPtr) return; } + if (empty($tokens[$stackPtr]['nested_attributes']) === false) { + // Class instantiation in attribute, not function call. + return; + } + $function = strtolower($tokens[$stackPtr]['content']); $pattern = null; diff --git a/src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php b/src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php index 8b8c76e922..4376daa9dd 100644 --- a/src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php +++ b/src/Standards/Generic/Sniffs/PHP/LowerCaseConstantSniff.php @@ -11,6 +11,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Tokens; class LowerCaseConstantSniff implements Sniff { @@ -25,6 +26,17 @@ class LowerCaseConstantSniff implements Sniff 'JS', ]; + /** + * The tokens this sniff is targetting. + * + * @var array + */ + private $targets = [ + T_TRUE => T_TRUE, + T_FALSE => T_FALSE, + T_NULL => T_NULL, + ]; + /** * Returns an array of tokens this test wants to listen for. @@ -33,11 +45,14 @@ class LowerCaseConstantSniff implements Sniff */ public function register() { - return [ - T_TRUE, - T_FALSE, - T_NULL, - ]; + $targets = $this->targets; + + // Register function keywords to filter out type declarations. + $targets[] = T_FUNCTION; + $targets[] = T_CLOSURE; + $targets[] = T_FN; + + return $targets; }//end register() @@ -52,10 +67,89 @@ public function register() * @return void */ public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + // Handle function declarations separately as they may contain the keywords in type declarations. + if ($tokens[$stackPtr]['code'] === T_FUNCTION + || $tokens[$stackPtr]['code'] === T_CLOSURE + || $tokens[$stackPtr]['code'] === T_FN + ) { + if (isset($tokens[$stackPtr]['parenthesis_closer']) === false) { + return; + } + + $end = $tokens[$stackPtr]['parenthesis_closer']; + if (isset($tokens[$stackPtr]['scope_opener']) === true) { + $end = $tokens[$stackPtr]['scope_opener']; + } + + // Do a quick check if any of the targets exist in the declaration. + $found = $phpcsFile->findNext($this->targets, $tokens[$stackPtr]['parenthesis_opener'], $end); + if ($found === false) { + // Skip forward, no need to examine these tokens again. + return $end; + } + + // Handle the whole function declaration in one go. + $params = $phpcsFile->getMethodParameters($stackPtr); + foreach ($params as $param) { + if (isset($param['default_token']) === false) { + continue; + } + + $paramEnd = $param['comma_token']; + if ($param['comma_token'] === false) { + $paramEnd = $tokens[$stackPtr]['parenthesis_closer']; + } + + for ($i = $param['default_token']; $i < $paramEnd; $i++) { + if (isset($this->targets[$tokens[$i]['code']]) === true) { + $this->processConstant($phpcsFile, $i); + } + } + } + + // Skip over return type declarations. + return $end; + }//end if + + // Handle property declarations separately as they may contain the keywords in type declarations. + if (isset($tokens[$stackPtr]['conditions']) === true) { + $conditions = $tokens[$stackPtr]['conditions']; + $lastCondition = end($conditions); + if (isset(Tokens::$ooScopeTokens[$lastCondition]) === true) { + // This can only be an OO constant or property declaration as methods are handled above. + $equals = $phpcsFile->findPrevious(T_EQUAL, ($stackPtr - 1), null, false, null, true); + if ($equals !== false) { + $this->processConstant($phpcsFile, $stackPtr); + } + + return; + } + } + + // Handle everything else. + $this->processConstant($phpcsFile, $stackPtr); + + }//end process() + + + /** + * Processes a non-type declaration constant. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the + * stack passed in $tokens. + * + * @return void + */ + protected function processConstant(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); $keyword = $tokens[$stackPtr]['content']; $expected = strtolower($keyword); + if ($keyword !== $expected) { if ($keyword === strtoupper($keyword)) { $phpcsFile->recordMetric($stackPtr, 'PHP constant case', 'upper'); @@ -77,7 +171,7 @@ public function process(File $phpcsFile, $stackPtr) $phpcsFile->recordMetric($stackPtr, 'PHP constant case', 'lower'); } - }//end process() + }//end processConstant() }//end class diff --git a/src/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php b/src/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php index 26522b9a2a..e6b4917834 100644 --- a/src/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php +++ b/src/Standards/Generic/Sniffs/PHP/LowerCaseKeywordSniff.php @@ -11,7 +11,8 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; -use PHP_CodeSniffer\Util; +use PHP_CodeSniffer\Util\Common; +use PHP_CodeSniffer\Util\Tokens; class LowerCaseKeywordSniff implements Sniff { @@ -24,79 +25,21 @@ class LowerCaseKeywordSniff implements Sniff */ public function register() { - return [ - T_ABSTRACT, - T_ARRAY, - T_AS, - T_BREAK, - T_CALLABLE, - T_CASE, - T_CATCH, - T_CLASS, - T_CLONE, - T_CLOSURE, - T_CONST, - T_CONTINUE, - T_DECLARE, - T_DEFAULT, - T_DO, - T_ECHO, - T_ELSE, - T_ELSEIF, - T_EMPTY, - T_ENDDECLARE, - T_ENDFOR, - T_ENDFOREACH, - T_ENDIF, - T_ENDSWITCH, - T_ENDWHILE, - T_EVAL, - T_EXIT, - T_EXTENDS, - T_FINAL, - T_FINALLY, - T_FN, - T_FOR, - T_FOREACH, - T_FUNCTION, - T_GLOBAL, - T_GOTO, - T_IF, - T_IMPLEMENTS, - T_INCLUDE, - T_INCLUDE_ONCE, - T_INSTANCEOF, - T_INSTEADOF, - T_INTERFACE, - T_ISSET, - T_LIST, - T_LOGICAL_AND, - T_LOGICAL_OR, - T_LOGICAL_XOR, - T_NAMESPACE, - T_NEW, - T_PARENT, - T_PRINT, - T_PRIVATE, - T_PROTECTED, - T_PUBLIC, - T_REQUIRE, - T_REQUIRE_ONCE, - T_RETURN, - T_SELF, - T_STATIC, - T_SWITCH, - T_THROW, - T_TRAIT, - T_TRY, - T_UNSET, - T_USE, - T_VAR, - T_WHILE, - T_YIELD, - T_YIELD_FROM, + $targets = Tokens::$contextSensitiveKeywords; + $targets += [ + T_CLOSURE => T_CLOSURE, + T_EMPTY => T_EMPTY, + T_ENUM_CASE => T_ENUM_CASE, + T_EVAL => T_EVAL, + T_ISSET => T_ISSET, + T_MATCH_DEFAULT => T_MATCH_DEFAULT, + T_PARENT => T_PARENT, + T_SELF => T_SELF, + T_UNSET => T_UNSET, ]; + return $targets; + }//end register() @@ -120,7 +63,7 @@ public function process(File $phpcsFile, $stackPtr) $phpcsFile->recordMetric($stackPtr, 'PHP keyword case', 'mixed'); } - $messageKeyword = Util\Common::prepareForOutput($keyword); + $messageKeyword = Common::prepareForOutput($keyword); $error = 'PHP keywords must be lowercase; expected "%s" but found "%s"'; $data = [ diff --git a/src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php b/src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php index 27ef5ecac9..4d463f28fb 100644 --- a/src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php +++ b/src/Standards/Generic/Sniffs/PHP/LowerCaseTypeSniff.php @@ -9,6 +9,7 @@ namespace PHP_CodeSniffer\Standards\Generic\Sniffs\PHP; +use PHP_CodeSniffer\Exceptions\RuntimeException; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; use PHP_CodeSniffer\Util\Tokens; @@ -16,6 +17,30 @@ class LowerCaseTypeSniff implements Sniff { + /** + * Native types supported by PHP. + * + * @var array + */ + private $phpTypes = [ + 'self' => true, + 'parent' => true, + 'array' => true, + 'callable' => true, + 'bool' => true, + 'float' => true, + 'int' => true, + 'string' => true, + 'iterable' => true, + 'void' => true, + 'object' => true, + 'mixed' => true, + 'static' => true, + 'false' => true, + 'null' => true, + 'never' => true, + ]; + /** * Returns an array of tokens this test wants to listen for. @@ -27,6 +52,8 @@ public function register() $tokens = Tokens::$castTokens; $tokens[] = T_FUNCTION; $tokens[] = T_CLOSURE; + $tokens[] = T_FN; + $tokens[] = T_VARIABLE; return $tokens; }//end register() @@ -47,76 +74,85 @@ public function process(File $phpcsFile, $stackPtr) if (isset(Tokens::$castTokens[$tokens[$stackPtr]['code']]) === true) { // A cast token. - if (strtolower($tokens[$stackPtr]['content']) !== $tokens[$stackPtr]['content']) { - if ($tokens[$stackPtr]['content'] === strtoupper($tokens[$stackPtr]['content'])) { - $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'upper'); - } else { - $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'mixed'); - } + $this->processType( + $phpcsFile, + $stackPtr, + $tokens[$stackPtr]['content'], + 'PHP type casts must be lowercase; expected "%s" but found "%s"', + 'TypeCastFound' + ); + + return; + } + + /* + * Check property types. + */ - $error = 'PHP type casts must be lowercase; expected "%s" but found "%s"'; - $data = [ - strtolower($tokens[$stackPtr]['content']), - $tokens[$stackPtr]['content'], - ]; + if ($tokens[$stackPtr]['code'] === T_VARIABLE) { + try { + $props = $phpcsFile->getMemberProperties($stackPtr); + } catch (RuntimeException $e) { + // Not an OO property. + return; + } + + // Strip off potential nullable indication. + $type = ltrim($props['type'], '?'); - $fix = $phpcsFile->addFixableError($error, $stackPtr, 'TypeCastFound', $data); - if ($fix === true) { - $phpcsFile->fixer->replaceToken($stackPtr, strtolower($tokens[$stackPtr]['content'])); + if ($type !== '') { + $error = 'PHP property type declarations must be lowercase; expected "%s" but found "%s"'; + $errorCode = 'PropertyTypeFound'; + + if ($props['type_token'] === T_TYPE_INTERSECTION) { + // Intersection types don't support simple types. + } else if (strpos($type, '|') !== false) { + $this->processUnionType( + $phpcsFile, + $props['type_token'], + $props['type_end_token'], + $error, + $errorCode + ); + } else if (isset($this->phpTypes[strtolower($type)]) === true) { + $this->processType($phpcsFile, $props['type_token'], $type, $error, $errorCode); } - } else { - $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'lower'); - }//end if + } return; }//end if - $phpTypes = [ - 'self' => true, - 'parent' => true, - 'array' => true, - 'callable' => true, - 'bool' => true, - 'float' => true, - 'int' => true, - 'string' => true, - 'iterable' => true, - 'void' => true, - 'object' => true, - ]; + /* + * Check function return type. + */ $props = $phpcsFile->getMethodProperties($stackPtr); // Strip off potential nullable indication. - $returnType = ltrim($props['return_type'], '?'); - $returnTypeLower = strtolower($returnType); - - if ($returnType !== '' - && isset($phpTypes[$returnTypeLower]) === true - ) { - // A function return type. - if ($returnTypeLower !== $returnType) { - if ($returnType === strtoupper($returnType)) { - $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'upper'); - } else { - $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'mixed'); - } + $returnType = ltrim($props['return_type'], '?'); - $error = 'PHP return type declarations must be lowercase; expected "%s" but found "%s"'; - $token = $props['return_type_token']; - $data = [ - $returnTypeLower, - $returnType, - ]; + if ($returnType !== '') { + $error = 'PHP return type declarations must be lowercase; expected "%s" but found "%s"'; + $errorCode = 'ReturnTypeFound'; - $fix = $phpcsFile->addFixableError($error, $token, 'ReturnTypeFound', $data); - if ($fix === true) { - $phpcsFile->fixer->replaceToken($token, $returnTypeLower); - } - } else { - $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'lower'); - }//end if - }//end if + if ($props['return_type_token'] === T_TYPE_INTERSECTION) { + // Intersection types don't support simple types. + } else if (strpos($returnType, '|') !== false) { + $this->processUnionType( + $phpcsFile, + $props['return_type_token'], + $props['return_type_end_token'], + $error, + $errorCode + ); + } else if (isset($this->phpTypes[strtolower($returnType)]) === true) { + $this->processType($phpcsFile, $props['return_type_token'], $returnType, $error, $errorCode); + } + } + + /* + * Check function parameter types. + */ $params = $phpcsFile->getMethodParameters($stackPtr); if (empty($params) === true) { @@ -125,38 +161,116 @@ public function process(File $phpcsFile, $stackPtr) foreach ($params as $param) { // Strip off potential nullable indication. - $typeHint = ltrim($param['type_hint'], '?'); - $typeHintLower = strtolower($typeHint); - - if ($typeHint !== '' - && isset($phpTypes[$typeHintLower]) === true - ) { - // A function return type. - if ($typeHintLower !== $typeHint) { - if ($typeHint === strtoupper($typeHint)) { - $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'upper'); - } else { - $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'mixed'); - } - - $error = 'PHP parameter type declarations must be lowercase; expected "%s" but found "%s"'; - $token = $param['type_hint_token']; - $data = [ - $typeHintLower, - $typeHint, - ]; - - $fix = $phpcsFile->addFixableError($error, $token, 'ParamTypeFound', $data); - if ($fix === true) { - $phpcsFile->fixer->replaceToken($token, $typeHintLower); - } - } else { - $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'lower'); - }//end if - }//end if + $typeHint = ltrim($param['type_hint'], '?'); + + if ($typeHint !== '') { + $error = 'PHP parameter type declarations must be lowercase; expected "%s" but found "%s"'; + $errorCode = 'ParamTypeFound'; + + if ($param['type_hint_token'] === T_TYPE_INTERSECTION) { + // Intersection types don't support simple types. + } else if (strpos($typeHint, '|') !== false) { + $this->processUnionType( + $phpcsFile, + $param['type_hint_token'], + $param['type_hint_end_token'], + $error, + $errorCode + ); + } else if (isset($this->phpTypes[strtolower($typeHint)]) === true) { + $this->processType($phpcsFile, $param['type_hint_token'], $typeHint, $error, $errorCode); + } + } }//end foreach }//end process() + /** + * Processes a union type declaration. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $typeDeclStart The position of the start of the type token. + * @param int $typeDeclEnd The position of the end of the type token. + * @param string $error Error message template. + * @param string $errorCode The error code. + * + * @return void + */ + protected function processUnionType(File $phpcsFile, $typeDeclStart, $typeDeclEnd, $error, $errorCode) + { + $tokens = $phpcsFile->getTokens(); + $current = $typeDeclStart; + + do { + $endOfType = $phpcsFile->findNext(T_TYPE_UNION, $current, $typeDeclEnd); + if ($endOfType === false) { + // This must be the last type in the union. + $endOfType = ($typeDeclEnd + 1); + } + + $hasNsSep = $phpcsFile->findNext(T_NS_SEPARATOR, $current, $endOfType); + if ($hasNsSep !== false) { + // Multi-token class based type. Ignore. + $current = ($endOfType + 1); + continue; + } + + // Type consisting of a single token. + $startOfType = $phpcsFile->findNext(Tokens::$emptyTokens, $current, $endOfType, true); + if ($startOfType === false) { + // Parse error. + return; + } + + $type = $tokens[$startOfType]['content']; + if (isset($this->phpTypes[strtolower($type)]) === true) { + $this->processType($phpcsFile, $startOfType, $type, $error, $errorCode); + } + + $current = ($endOfType + 1); + } while ($current <= $typeDeclEnd); + + }//end processUnionType() + + + /** + * Processes a type cast or a singular type declaration. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the type token. + * @param string $type The type found. + * @param string $error Error message template. + * @param string $errorCode The error code. + * + * @return void + */ + protected function processType(File $phpcsFile, $stackPtr, $type, $error, $errorCode) + { + $typeLower = strtolower($type); + + if ($typeLower === $type) { + $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'lower'); + return; + } + + if ($type === strtoupper($type)) { + $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'upper'); + } else { + $phpcsFile->recordMetric($stackPtr, 'PHP type case', 'mixed'); + } + + $data = [ + $typeLower, + $type, + ]; + + $fix = $phpcsFile->addFixableError($error, $stackPtr, $errorCode, $data); + if ($fix === true) { + $phpcsFile->fixer->replaceToken($stackPtr, $typeLower); + } + + }//end processType() + + }//end class diff --git a/src/Standards/Generic/Sniffs/PHP/NoSilencedErrorsSniff.php b/src/Standards/Generic/Sniffs/PHP/NoSilencedErrorsSniff.php index de1912d221..14dfaa7216 100644 --- a/src/Standards/Generic/Sniffs/PHP/NoSilencedErrorsSniff.php +++ b/src/Standards/Generic/Sniffs/PHP/NoSilencedErrorsSniff.php @@ -55,7 +55,7 @@ public function process(File $phpcsFile, $stackPtr) { // Prepare the "Found" string to display. $contextLength = 4; - $endOfStatement = $phpcsFile->findEndOfStatement($stackPtr, T_COMMA); + $endOfStatement = $phpcsFile->findEndOfStatement($stackPtr, [T_COMMA, T_COLON]); if (($endOfStatement - $stackPtr) < $contextLength) { $contextLength = ($endOfStatement - $stackPtr); } diff --git a/src/Standards/Generic/Sniffs/PHP/SAPIUsageSniff.php b/src/Standards/Generic/Sniffs/PHP/SAPIUsageSniff.php index b768c4b147..19d58572c6 100644 --- a/src/Standards/Generic/Sniffs/PHP/SAPIUsageSniff.php +++ b/src/Standards/Generic/Sniffs/PHP/SAPIUsageSniff.php @@ -42,10 +42,11 @@ public function process(File $phpcsFile, $stackPtr) $tokens = $phpcsFile->getTokens(); $ignore = [ - T_DOUBLE_COLON => true, - T_OBJECT_OPERATOR => true, - T_FUNCTION => true, - T_CONST => true, + T_DOUBLE_COLON => true, + T_OBJECT_OPERATOR => true, + T_NULLSAFE_OBJECT_OPERATOR => true, + T_FUNCTION => true, + T_CONST => true, ]; $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); diff --git a/src/Standards/Generic/Sniffs/PHP/SyntaxSniff.php b/src/Standards/Generic/Sniffs/PHP/SyntaxSniff.php index 9ef8c2ea66..85e717bc99 100644 --- a/src/Standards/Generic/Sniffs/PHP/SyntaxSniff.php +++ b/src/Standards/Generic/Sniffs/PHP/SyntaxSniff.php @@ -13,6 +13,7 @@ use PHP_CodeSniffer\Config; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Common; class SyntaxSniff implements Sniff { @@ -32,7 +33,10 @@ class SyntaxSniff implements Sniff */ public function register() { - return [T_OPEN_TAG]; + return [ + T_OPEN_TAG, + T_OPEN_TAG_WITH_ECHO, + ]; }//end register() @@ -53,7 +57,7 @@ public function process(File $phpcsFile, $stackPtr) } $fileName = escapeshellarg($phpcsFile->getFilename()); - $cmd = escapeshellcmd($this->phpPath)." -l -d display_errors=1 -d error_prepend_string='' $fileName 2>&1"; + $cmd = Common::escapeshellcmd($this->phpPath)." -l -d display_errors=1 -d error_prepend_string='' $fileName 2>&1"; $output = shell_exec($cmd); $matches = []; if (preg_match('/^.*error:(.*) in .* on line ([0-9]+)/m', trim($output), $matches) === 1) { diff --git a/src/Standards/Generic/Sniffs/PHP/UpperCaseConstantSniff.php b/src/Standards/Generic/Sniffs/PHP/UpperCaseConstantSniff.php index 54aa07f22a..2740884bb4 100644 --- a/src/Standards/Generic/Sniffs/PHP/UpperCaseConstantSniff.php +++ b/src/Standards/Generic/Sniffs/PHP/UpperCaseConstantSniff.php @@ -10,30 +10,13 @@ namespace PHP_CodeSniffer\Standards\Generic\Sniffs\PHP; use PHP_CodeSniffer\Files\File; -use PHP_CodeSniffer\Sniffs\Sniff; -class UpperCaseConstantSniff implements Sniff +class UpperCaseConstantSniff extends LowerCaseConstantSniff { /** - * Returns an array of tokens this test wants to listen for. - * - * @return array - */ - public function register() - { - return [ - T_TRUE, - T_FALSE, - T_NULL, - ]; - - }//end register() - - - /** - * Processes this sniff, when one of its tokens is encountered. + * Processes a non-type declaration constant. * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token in the @@ -41,11 +24,12 @@ public function register() * * @return void */ - public function process(File $phpcsFile, $stackPtr) + protected function processConstant(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); $keyword = $tokens[$stackPtr]['content']; $expected = strtoupper($keyword); + if ($keyword !== $expected) { if ($keyword === strtolower($keyword)) { $phpcsFile->recordMetric($stackPtr, 'PHP constant case', 'lower'); @@ -67,7 +51,7 @@ public function process(File $phpcsFile, $stackPtr) $phpcsFile->recordMetric($stackPtr, 'PHP constant case', 'upper'); } - }//end process() + }//end processConstant() }//end class diff --git a/src/Standards/Generic/Sniffs/VersionControl/GitMergeConflictSniff.php b/src/Standards/Generic/Sniffs/VersionControl/GitMergeConflictSniff.php index e25d556b8b..83265b2e47 100644 --- a/src/Standards/Generic/Sniffs/VersionControl/GitMergeConflictSniff.php +++ b/src/Standards/Generic/Sniffs/VersionControl/GitMergeConflictSniff.php @@ -34,7 +34,10 @@ class GitMergeConflictSniff implements Sniff */ public function register() { - return [T_OPEN_TAG]; + return [ + T_OPEN_TAG, + T_OPEN_TAG_WITH_ECHO, + ]; }//end register() diff --git a/src/Standards/Generic/Sniffs/WhiteSpace/ArbitraryParenthesesSpacingSniff.php b/src/Standards/Generic/Sniffs/WhiteSpace/ArbitraryParenthesesSpacingSniff.php index 7a7c5d8eee..09aea5436e 100644 --- a/src/Standards/Generic/Sniffs/WhiteSpace/ArbitraryParenthesesSpacingSniff.php +++ b/src/Standards/Generic/Sniffs/WhiteSpace/ArbitraryParenthesesSpacingSniff.php @@ -58,7 +58,6 @@ public function register() $this->ignoreTokens[T_CLOSE_SHORT_ARRAY] = T_CLOSE_SHORT_ARRAY; $this->ignoreTokens[T_USE] = T_USE; - $this->ignoreTokens[T_DECLARE] = T_DECLARE; $this->ignoreTokens[T_THROW] = T_THROW; $this->ignoreTokens[T_YIELD] = T_YIELD; $this->ignoreTokens[T_YIELD_FROM] = T_YIELD_FROM; diff --git a/src/Standards/Generic/Sniffs/WhiteSpace/DisallowSpaceIndentSniff.php b/src/Standards/Generic/Sniffs/WhiteSpace/DisallowSpaceIndentSniff.php index 4fe89e767a..bbdbfe0a9a 100644 --- a/src/Standards/Generic/Sniffs/WhiteSpace/DisallowSpaceIndentSniff.php +++ b/src/Standards/Generic/Sniffs/WhiteSpace/DisallowSpaceIndentSniff.php @@ -41,7 +41,10 @@ class DisallowSpaceIndentSniff implements Sniff */ public function register() { - return [T_OPEN_TAG]; + return [ + T_OPEN_TAG, + T_OPEN_TAG_WITH_ECHO, + ]; }//end register() diff --git a/src/Standards/Generic/Sniffs/WhiteSpace/DisallowTabIndentSniff.php b/src/Standards/Generic/Sniffs/WhiteSpace/DisallowTabIndentSniff.php index a50e9f92bb..2140e55ef5 100644 --- a/src/Standards/Generic/Sniffs/WhiteSpace/DisallowTabIndentSniff.php +++ b/src/Standards/Generic/Sniffs/WhiteSpace/DisallowTabIndentSniff.php @@ -41,7 +41,10 @@ class DisallowTabIndentSniff implements Sniff */ public function register() { - return [T_OPEN_TAG]; + return [ + T_OPEN_TAG, + T_OPEN_TAG_WITH_ECHO, + ]; }//end register() @@ -73,6 +76,8 @@ public function process(File $phpcsFile, $stackPtr) T_DOC_COMMENT_WHITESPACE => true, T_DOC_COMMENT_STRING => true, T_COMMENT => true, + T_END_HEREDOC => true, + T_END_NOWDOC => true, ]; for ($i = 0; $i < $phpcsFile->numTokens; $i++) { diff --git a/src/Standards/Generic/Sniffs/WhiteSpace/ScopeIndentSniff.php b/src/Standards/Generic/Sniffs/WhiteSpace/ScopeIndentSniff.php index f231090827..7fd604849d 100644 --- a/src/Standards/Generic/Sniffs/WhiteSpace/ScopeIndentSniff.php +++ b/src/Standards/Generic/Sniffs/WhiteSpace/ScopeIndentSniff.php @@ -142,12 +142,13 @@ public function process(File $phpcsFile, $stackPtr) } } - $lastOpenTag = $stackPtr; - $lastCloseTag = null; - $openScopes = []; - $adjustments = []; - $setIndents = []; - $disableExactEnd = 0; + $lastOpenTag = $stackPtr; + $lastCloseTag = null; + $openScopes = []; + $adjustments = []; + $setIndents = []; + $disableExactStack = []; + $disableExactEnd = 0; $tokens = $phpcsFile->getTokens(); $first = $phpcsFile->findFirstOnLine(T_INLINE_HTML, $stackPtr); @@ -232,6 +233,7 @@ public function process(File $phpcsFile, $stackPtr) if ($tokens[$i]['code'] === T_OPEN_PARENTHESIS && isset($tokens[$i]['parenthesis_closer']) === true ) { + $disableExactStack[$tokens[$i]['parenthesis_closer']] = $tokens[$i]['parenthesis_closer']; $disableExactEnd = max($disableExactEnd, $tokens[$i]['parenthesis_closer']); if ($this->debug === true) { $line = $tokens[$i]['line']; @@ -337,7 +339,14 @@ public function process(File $phpcsFile, $stackPtr) echo "\t* open tag is inside condition; using open tag *".PHP_EOL; } - $checkIndent = ($tokens[$lastOpenTag]['column'] - 1); + $first = $phpcsFile->findFirstOnLine([T_WHITESPACE, T_INLINE_HTML], $lastOpenTag, true); + if ($this->debug === true) { + $line = $tokens[$first]['line']; + $type = $tokens[$first]['type']; + echo "\t* first token on line $line is $first ($type) *".PHP_EOL; + } + + $checkIndent = ($tokens[$first]['column'] - 1); if (isset($adjustments[$condition]) === true) { $checkIndent += $adjustments[$condition]; } @@ -609,11 +618,11 @@ public function process(File $phpcsFile, $stackPtr) // Scope closers reset the required indent to the same level as the opening condition. if (($checkToken !== null - && isset($openScopes[$checkToken]) === true + && (isset($openScopes[$checkToken]) === true || (isset($tokens[$checkToken]['scope_condition']) === true && isset($tokens[$checkToken]['scope_closer']) === true && $tokens[$checkToken]['scope_closer'] === $checkToken - && $tokens[$checkToken]['line'] !== $tokens[$tokens[$checkToken]['scope_opener']]['line'])) + && $tokens[$checkToken]['line'] !== $tokens[$tokens[$checkToken]['scope_opener']]['line']))) || ($checkToken === null && isset($openScopes[$i]) === true) ) { @@ -796,6 +805,19 @@ public function process(File $phpcsFile, $stackPtr) ) { $exact = true; + if ($disableExactEnd > $checkToken) { + foreach ($disableExactStack as $disableExactStackEnd) { + if ($disableExactStackEnd < $checkToken) { + continue; + } + + if ($tokens[$checkToken]['conditions'] === $tokens[$disableExactStackEnd]['conditions']) { + $exact = false; + break; + } + } + } + $lastOpener = null; if (empty($openScopes) === false) { end($openScopes); @@ -849,15 +871,29 @@ public function process(File $phpcsFile, $stackPtr) && $tokens[$next]['code'] !== T_VARIABLE && $tokens[$next]['code'] !== T_FN) ) { - if ($this->debug === true) { - $line = $tokens[$checkToken]['line']; - $type = $tokens[$checkToken]['type']; - echo "\t* method prefix ($type) found on line $line; indent set to exact *".PHP_EOL; + $isMethodPrefix = true; + if (isset($tokens[$checkToken]['nested_parenthesis']) === true) { + $parenthesis = array_keys($tokens[$checkToken]['nested_parenthesis']); + $deepestOpen = array_pop($parenthesis); + if (isset($tokens[$deepestOpen]['parenthesis_owner']) === true + && $tokens[$tokens[$deepestOpen]['parenthesis_owner']]['code'] === T_FUNCTION + ) { + // This is constructor property promotion and not a method prefix. + $isMethodPrefix = false; + } } - $exact = true; - } - } + if ($isMethodPrefix === true) { + if ($this->debug === true) { + $line = $tokens[$checkToken]['line']; + $type = $tokens[$checkToken]['type']; + echo "\t* method prefix ($type) found on line $line; indent set to exact *".PHP_EOL; + } + + $exact = true; + } + }//end if + }//end if // JS property indentation has to be exact or else if will break // things like function and object indentation. @@ -913,7 +949,8 @@ public function process(File $phpcsFile, $stackPtr) // Don't perform strict checking on chained method calls since they // are often covered by custom rules. if ($checkToken !== null - && $tokens[$checkToken]['code'] === T_OBJECT_OPERATOR + && ($tokens[$checkToken]['code'] === T_OBJECT_OPERATOR + || $tokens[$checkToken]['code'] === T_NULLSAFE_OBJECT_OPERATOR) && $exact === true ) { $exact = false; @@ -946,18 +983,38 @@ public function process(File $phpcsFile, $stackPtr) } if ($this->tabIndent === true) { - $error .= '%s tabs, found %s'; - $data = [ - floor($checkIndent / $this->tabWidth), - floor($tokenIndent / $this->tabWidth), - ]; + $expectedTabs = floor($checkIndent / $this->tabWidth); + $foundTabs = floor($tokenIndent / $this->tabWidth); + $foundSpaces = ($tokenIndent - ($foundTabs * $this->tabWidth)); + if ($foundSpaces > 0) { + if ($foundTabs > 0) { + $error .= '%s tabs, found %s tabs and %s spaces'; + $data = [ + $expectedTabs, + $foundTabs, + $foundSpaces, + ]; + } else { + $error .= '%s tabs, found %s spaces'; + $data = [ + $expectedTabs, + $foundSpaces, + ]; + } + } else { + $error .= '%s tabs, found %s'; + $data = [ + $expectedTabs, + $foundTabs, + ]; + }//end if } else { $error .= '%s spaces, found %s'; $data = [ $checkIndent, $tokenIndent, ]; - } + }//end if if ($this->debug === true) { $line = $tokens[$checkToken]['line']; @@ -988,15 +1045,17 @@ public function process(File $phpcsFile, $stackPtr) // Don't check indents exactly between arrays as they tend to have custom rules. if ($tokens[$i]['code'] === T_OPEN_SHORT_ARRAY) { + $disableExactStack[$tokens[$i]['bracket_closer']] = $tokens[$i]['bracket_closer']; $disableExactEnd = max($disableExactEnd, $tokens[$i]['bracket_closer']); if ($this->debug === true) { - $line = $tokens[$i]['line']; - $type = $tokens[$disableExactEnd]['type']; + $line = $tokens[$i]['line']; + $type = $tokens[$disableExactEnd]['type']; + $endLine = $tokens[$disableExactEnd]['line']; echo "Opening short array bracket found on line $line".PHP_EOL; if ($disableExactEnd === $tokens[$i]['bracket_closer']) { - echo "\t=> disabling exact indent checking until $disableExactEnd ($type)".PHP_EOL; + echo "\t=> disabling exact indent checking until $disableExactEnd ($type) on line $endLine".PHP_EOL; } else { - echo "\t=> continuing to disable exact indent checking until $disableExactEnd ($type)".PHP_EOL; + echo "\t=> continuing to disable exact indent checking until $disableExactEnd ($type) on line $endLine".PHP_EOL; } } } @@ -1008,7 +1067,6 @@ public function process(File $phpcsFile, $stackPtr) ) { if ($this->debug === true) { $line = $tokens[$i]['line']; - $type = $tokens[$disableExactEnd]['type']; echo "Here/nowdoc found on line $line".PHP_EOL; } @@ -1277,11 +1335,14 @@ public function process(File $phpcsFile, $stackPtr) continue; }//end if - // Closing an anon class or function. + // Closing an anon class, closure, or match. + // Each may be returned, which can confuse control structures that + // use return as a closer, like CASE statements. if (isset($tokens[$i]['scope_condition']) === true && $tokens[$i]['scope_closer'] === $i && ($tokens[$tokens[$i]['scope_condition']]['code'] === T_CLOSURE - || $tokens[$tokens[$i]['scope_condition']]['code'] === T_ANON_CLASS) + || $tokens[$tokens[$i]['scope_condition']]['code'] === T_ANON_CLASS + || $tokens[$tokens[$i]['scope_condition']]['code'] === T_MATCH) ) { if ($this->debug === true) { $type = str_replace('_', ' ', strtolower(substr($tokens[$tokens[$i]['scope_condition']]['type'], 2))); diff --git a/src/Standards/Generic/Sniffs/WhiteSpace/SpreadOperatorSpacingAfterSniff.php b/src/Standards/Generic/Sniffs/WhiteSpace/SpreadOperatorSpacingAfterSniff.php index 3146717e84..070bbcd1fb 100644 --- a/src/Standards/Generic/Sniffs/WhiteSpace/SpreadOperatorSpacingAfterSniff.php +++ b/src/Standards/Generic/Sniffs/WhiteSpace/SpreadOperatorSpacingAfterSniff.php @@ -62,6 +62,11 @@ public function process(File $phpcsFile, $stackPtr) return; } + if ($tokens[$nextNonEmpty]['code'] === T_CLOSE_PARENTHESIS) { + // Ignore PHP 8.1 first class callable syntax. + return; + } + if ($this->ignoreNewlines === true && $tokens[$stackPtr]['line'] !== $tokens[$nextNonEmpty]['line'] ) { diff --git a/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc b/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc index 044e4a1727..075fc34c5b 100644 --- a/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc +++ b/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc @@ -63,6 +63,22 @@ $array = [ $c ? $d : $e, ]; +$foo = +[ + 'bar' => + [ + ], +]; + +$foo = [ + 'foo' + . 'bar', + [ + 'baz', + 'qux', + ], +]; + // phpcs:set Generic.Arrays.ArrayIndent indent 2 $var = [ @@ -70,3 +86,28 @@ $var = [ 2 => 'two', /* three */ 3 => 'three', ]; + +// phpcs:set Generic.Arrays.ArrayIndent indent 4 + +$array = array( + match ($test) { 1 => 'a', 2 => 'b' } + => 'dynamic keys, woho!', +); + +$array = [ + match ($test) { 1 => 'a', 2 => 'b' } + => 'dynamic keys, woho!', +]; + +// Ensure that PHP 8.0 named parameters don't affect the sniff. +$array = [ + functionCall( + name: $value + ), +]; + +$array = [ + functionCall( + name: $value + ), +]; diff --git a/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc.fixed b/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc.fixed index 404313a4d1..505de5f780 100644 --- a/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.inc.fixed @@ -64,6 +64,22 @@ $array = [ $c ? $d : $e, ]; +$foo = +[ + 'bar' => + [ + ], +]; + +$foo = [ + 'foo' + . 'bar', + [ + 'baz', + 'qux', + ], +]; + // phpcs:set Generic.Arrays.ArrayIndent indent 2 $var = [ @@ -71,3 +87,28 @@ $var = [ 2 => 'two', /* three */ 3 => 'three', ]; + +// phpcs:set Generic.Arrays.ArrayIndent indent 4 + +$array = array( + match ($test) { 1 => 'a', 2 => 'b' } + => 'dynamic keys, woho!', +); + +$array = [ + match ($test) { 1 => 'a', 2 => 'b' } + => 'dynamic keys, woho!', +]; + +// Ensure that PHP 8.0 named parameters don't affect the sniff. +$array = [ + functionCall( + name: $value + ), +]; + +$array = [ + functionCall( + name: $value + ), +]; diff --git a/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.php b/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.php index 6c94bd1a8f..4861041f62 100644 --- a/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.php +++ b/src/Standards/Generic/Tests/Arrays/ArrayIndentUnitTest.php @@ -26,19 +26,25 @@ class ArrayIndentUnitTest extends AbstractSniffUnitTest public function getErrorList() { return [ - 14 => 1, - 15 => 1, - 17 => 1, - 30 => 1, - 31 => 1, - 33 => 1, - 41 => 1, - 62 => 1, - 63 => 1, - 69 => 1, - 70 => 1, - 71 => 1, - 72 => 1, + 14 => 1, + 15 => 1, + 17 => 1, + 30 => 1, + 31 => 1, + 33 => 1, + 41 => 1, + 62 => 1, + 63 => 1, + 69 => 1, + 77 => 1, + 78 => 1, + 79 => 1, + 85 => 1, + 86 => 1, + 87 => 1, + 88 => 1, + 98 => 1, + 110 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Generic/Tests/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/Classes/DuplicateClassNameUnitTest.1.inc b/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.1.inc index d0c136cbfa..91ab9d3975 100644 --- a/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.1.inc +++ b/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.1.inc @@ -5,7 +5,10 @@ interface MyInterface {} interface YourInterface {} trait MyTrait {} trait YourTrait {} +enum MyEnum {} +enum YourEnum {} class MyClass {} interface MyInterface {} trait MyTrait {} -?> \ No newline at end of file +enum MyEnum {} +?> diff --git a/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.2.inc b/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.2.inc index e6f92eb130..6829748a5f 100644 --- a/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.2.inc +++ b/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.2.inc @@ -2,4 +2,5 @@ class MyClass {} interface MyInterface {} trait MyTrait {} +enum MyEnum {} ?> \ No newline at end of file diff --git a/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.php b/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.php index b3b3edb502..68982f8686 100644 --- a/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.php +++ b/src/Standards/Generic/Tests/Classes/DuplicateClassNameUnitTest.php @@ -45,9 +45,10 @@ public function getWarningList($testFile='') switch ($testFile) { case 'DuplicateClassNameUnitTest.1.inc': return [ - 8 => 1, - 9 => 1, 10 => 1, + 11 => 1, + 12 => 1, + 13 => 1, ]; break; case 'DuplicateClassNameUnitTest.2.inc': @@ -55,6 +56,7 @@ public function getWarningList($testFile='') 2 => 1, 3 => 1, 4 => 1, + 5 => 1, ]; break; case 'DuplicateClassNameUnitTest.5.inc': diff --git a/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc b/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc index 8147b4c73e..fd3abc030e 100644 --- a/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc +++ b/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc @@ -89,3 +89,7 @@ class Test_Class_Bad_G /*some comment*/ { } + +enum Test_Enum +{ +} diff --git a/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc.fixed b/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc.fixed index 406eb230fd..a755ae4725 100644 --- a/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.inc.fixed @@ -89,3 +89,7 @@ class Test_Class_Bad_G /*some comment*/ { } + +enum Test_Enum { + +} diff --git a/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.php b/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.php index 61cd4c9e8f..c357afc927 100644 --- a/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.php +++ b/src/Standards/Generic/Tests/Classes/OpeningBraceSameLineUnitTest.php @@ -38,6 +38,7 @@ public function getErrorList() 70 => 1, 79 => 1, 90 => 1, + 94 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Generic/Tests/CodeAnalysis/AssignmentInConditionUnitTest.inc b/src/Standards/Generic/Tests/CodeAnalysis/AssignmentInConditionUnitTest.inc index 49c6cfa77e..9560abbbb1 100644 --- a/src/Standards/Generic/Tests/CodeAnalysis/AssignmentInConditionUnitTest.inc +++ b/src/Standards/Generic/Tests/CodeAnalysis/AssignmentInConditionUnitTest.inc @@ -91,3 +91,5 @@ while ( $sample = false ) {} if ($a = 123) : endif; + +match ($a[0] = 123) {}; diff --git a/src/Standards/Generic/Tests/CodeAnalysis/AssignmentInConditionUnitTest.php b/src/Standards/Generic/Tests/CodeAnalysis/AssignmentInConditionUnitTest.php index e489f1c35e..3ed57d2207 100644 --- a/src/Standards/Generic/Tests/CodeAnalysis/AssignmentInConditionUnitTest.php +++ b/src/Standards/Generic/Tests/CodeAnalysis/AssignmentInConditionUnitTest.php @@ -74,6 +74,7 @@ public function getWarningList() 88 => 1, 90 => 1, 92 => 1, + 95 => 1, ]; }//end getWarningList() diff --git a/src/Standards/Generic/Tests/CodeAnalysis/EmptyPHPStatementUnitTest.inc b/src/Standards/Generic/Tests/CodeAnalysis/EmptyPHPStatementUnitTest.inc index e627fd3edf..464b0e3d8c 100644 --- a/src/Standards/Generic/Tests/CodeAnalysis/EmptyPHPStatementUnitTest.inc +++ b/src/Standards/Generic/Tests/CodeAnalysis/EmptyPHPStatementUnitTest.inc @@ -79,3 +79,8 @@ echo $a{0}; if ($foo) { ; } + +// Do not remove semicolon after match +$c = match ($a) { + 1 => true, +}; diff --git a/src/Standards/Generic/Tests/CodeAnalysis/EmptyPHPStatementUnitTest.inc.fixed b/src/Standards/Generic/Tests/CodeAnalysis/EmptyPHPStatementUnitTest.inc.fixed index 2773f97b85..cc0f85e067 100644 --- a/src/Standards/Generic/Tests/CodeAnalysis/EmptyPHPStatementUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/CodeAnalysis/EmptyPHPStatementUnitTest.inc.fixed @@ -73,3 +73,8 @@ echo $a{0}; if ($foo) { } + +// Do not remove semicolon after match +$c = match ($a) { + 1 => true, +}; diff --git a/src/Standards/Generic/Tests/CodeAnalysis/EmptyStatementUnitTest.inc b/src/Standards/Generic/Tests/CodeAnalysis/EmptyStatementUnitTest.inc index 83780bce24..210788a1de 100644 --- a/src/Standards/Generic/Tests/CodeAnalysis/EmptyStatementUnitTest.inc +++ b/src/Standards/Generic/Tests/CodeAnalysis/EmptyStatementUnitTest.inc @@ -69,4 +69,6 @@ try { // TODO: Handle this exception later :-) } -if (true) {} elseif (false) {} \ No newline at end of file +if (true) {} elseif (false) {} + +match($foo) {}; diff --git a/src/Standards/Generic/Tests/CodeAnalysis/EmptyStatementUnitTest.php b/src/Standards/Generic/Tests/CodeAnalysis/EmptyStatementUnitTest.php index 9d89ce2456..4e00ada1ca 100644 --- a/src/Standards/Generic/Tests/CodeAnalysis/EmptyStatementUnitTest.php +++ b/src/Standards/Generic/Tests/CodeAnalysis/EmptyStatementUnitTest.php @@ -39,6 +39,7 @@ public function getErrorList() 64 => 1, 68 => 1, 72 => 2, + 74 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Generic/Tests/CodeAnalysis/UnnecessaryFinalModifierUnitTest.inc b/src/Standards/Generic/Tests/CodeAnalysis/UnnecessaryFinalModifierUnitTest.inc index 0ba5df778f..026cf48424 100644 --- a/src/Standards/Generic/Tests/CodeAnalysis/UnnecessaryFinalModifierUnitTest.inc +++ b/src/Standards/Generic/Tests/CodeAnalysis/UnnecessaryFinalModifierUnitTest.inc @@ -27,3 +27,8 @@ final class Bar_Foo { protected function foo() {} private function Bar() {} } + +final readonly class Foo_Bar { + public final function fooBar() {} + final protected function fool() {} +} diff --git a/src/Standards/Generic/Tests/CodeAnalysis/UnnecessaryFinalModifierUnitTest.php b/src/Standards/Generic/Tests/CodeAnalysis/UnnecessaryFinalModifierUnitTest.php index 732f30318c..33bb160a04 100644 --- a/src/Standards/Generic/Tests/CodeAnalysis/UnnecessaryFinalModifierUnitTest.php +++ b/src/Standards/Generic/Tests/CodeAnalysis/UnnecessaryFinalModifierUnitTest.php @@ -45,6 +45,8 @@ public function getWarningList() 12 => 1, 15 => 1, 18 => 1, + 32 => 1, + 33 => 1, ]; }//end getWarningList() diff --git a/src/Standards/Generic/Tests/CodeAnalysis/UnusedFunctionParameterUnitTest.inc b/src/Standards/Generic/Tests/CodeAnalysis/UnusedFunctionParameterUnitTest.inc index 9997e787c5..d800d690fd 100644 --- a/src/Standards/Generic/Tests/CodeAnalysis/UnusedFunctionParameterUnitTest.inc +++ b/src/Standards/Generic/Tests/CodeAnalysis/UnusedFunctionParameterUnitTest.inc @@ -123,3 +123,32 @@ function myCallback($a, $b, $c, $d) { } fn ($a, $b, $c) => $b; + +// phpcs:set Generic.CodeAnalysis.UnusedFunctionParameter ignoreTypeHints[] Exception + +function oneParam(Exception $foo) { + return 'foobar'; +} + +function moreParamFirst(Exception $foo, LogicException $bar) { + return 'foobar' . $bar; +} + +function moreParamSecond(LogicException $bar, Exception $foo) { + return 'foobar' . $bar; +} +// phpcs:set Generic.CodeAnalysis.UnusedFunctionParameter ignoreTypeHints[] + +class ConstructorPropertyPromotionNoContentInMethod { + public function __construct(protected int $id) {} +} + +class ConstructorPropertyPromotionWithContentInMethod { + public function __construct(protected int $id, $toggle = true) { + if ($toggle === true) { + doSomething(); + } + } +} + +$found = in_array_cb($needle, $haystack, fn($array, $needle) => $array[2] === $needle); diff --git a/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.inc b/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.inc index 725e15613f..ce458d8429 100644 --- a/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.inc +++ b/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.inc @@ -164,3 +164,12 @@ if ([function() { echo 'hi'; }] === [$foo] ) { } +echo match (5 == $num) { + true => "true\n", + false => "false\n" +}; + +echo match ($text) { + 'foo' => 10 === $y, + 10 === $y => 'bar', +}; diff --git a/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.php b/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.php index e69f3b88b1..c558ae8f6f 100644 --- a/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.php +++ b/src/Standards/Generic/Tests/ControlStructures/DisallowYodaConditionsUnitTest.php @@ -59,6 +59,9 @@ public function getErrorList() 142 => 1, 156 => 1, 160 => 1, + 167 => 1, + 173 => 1, + 174 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.1.inc b/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.1.inc index 1a6ab6900a..fb4a7380fb 100644 --- a/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.1.inc +++ b/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.1.inc @@ -253,3 +253,22 @@ for ($i = 1, $j = 0; $i <= 10; $j += $i, print $i, $i++); if ($this->valid(fn(): bool => 2 > 1)) { } + +// Issue 3345. +function testMultiCatch() +{ + if (true) + try { + } catch (\LogicException $e) { + } catch (\Exception $e) { + } +} + +function testFinally() +{ + if (true) + try { + } catch (\LogicException $e) { + } finally { + } +} diff --git a/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.1.inc.fixed b/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.1.inc.fixed index 4cd8ee4d6c..d80a32652a 100644 --- a/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.1.inc.fixed +++ b/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.1.inc.fixed @@ -286,3 +286,24 @@ for ($i = 1, $j = 0; $i <= 10; $j += $i, print $i, $i++); if ($this->valid(fn(): bool => 2 > 1)) { } + +// Issue 3345. +function testMultiCatch() +{ + if (true) { + try { + } catch (\LogicException $e) { + } catch (\Exception $e) { + } + } +} + +function testFinally() +{ + if (true) { + try { + } catch (\LogicException $e) { + } finally { + } + } +} diff --git a/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.php b/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.php index 92888b8581..a9c4f4f8ed 100644 --- a/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.php +++ b/src/Standards/Generic/Tests/ControlStructures/InlineControlStructureUnitTest.php @@ -71,6 +71,8 @@ public function getErrorList($testFile='InlineControlStructureUnitTest.1.inc') 236 => 1, 238 => 1, 242 => 1, + 260 => 1, + 269 => 1, ]; case 'InlineControlStructureUnitTest.js': diff --git a/src/Standards/Generic/Tests/Debug/JSHintUnitTest.php b/src/Standards/Generic/Tests/Debug/JSHintUnitTest.php index 618465a916..6a10e2ad8b 100644 --- a/src/Standards/Generic/Tests/Debug/JSHintUnitTest.php +++ b/src/Standards/Generic/Tests/Debug/JSHintUnitTest.php @@ -25,7 +25,7 @@ protected function shouldSkipTest() { $rhinoPath = Config::getExecutablePath('rhino'); $jshintPath = Config::getExecutablePath('jshint'); - if ($rhinoPath === null && $jshintPath === null) { + if ($jshintPath === null) { return true; } diff --git a/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.6.inc b/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.6.inc new file mode 100644 index 0000000000..4f2e47af9c --- /dev/null +++ b/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.6.inc @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.7.inc.fixed b/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.7.inc.fixed new file mode 100644 index 0000000000..d3c19feeb2 --- /dev/null +++ b/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.7.inc.fixed @@ -0,0 +1 @@ + diff --git a/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.8.inc b/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.8.inc new file mode 100644 index 0000000000..d3c19feeb2 --- /dev/null +++ b/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.8.inc @@ -0,0 +1 @@ + diff --git a/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.php b/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.php index b97e8ab295..f5ab9ffa6f 100644 --- a/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.php +++ b/src/Standards/Generic/Tests/Files/EndFileNewlineUnitTest.php @@ -33,6 +33,9 @@ public function getErrorList($testFile='') case 'EndFileNewlineUnitTest.3.css': case 'EndFileNewlineUnitTest.4.inc': return [2 => 1]; + case 'EndFileNewlineUnitTest.6.inc': + case 'EndFileNewlineUnitTest.7.inc': + return [1 => 1]; default: return []; }//end switch diff --git a/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.10.inc b/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.10.inc new file mode 100644 index 0000000000..884aad1b30 --- /dev/null +++ b/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.10.inc @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.8.inc b/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.8.inc new file mode 100644 index 0000000000..1f87609292 --- /dev/null +++ b/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.8.inc @@ -0,0 +1 @@ + diff --git a/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.9.inc.fixed b/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.9.inc.fixed new file mode 100644 index 0000000000..884aad1b30 --- /dev/null +++ b/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.9.inc.fixed @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.php b/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.php index 23c9980b44..88dc2f88a1 100644 --- a/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.php +++ b/src/Standards/Generic/Tests/Files/EndFileNoNewlineUnitTest.php @@ -37,6 +37,9 @@ public function getErrorList($testFile='') case 'EndFileNoNewlineUnitTest.2.js': case 'EndFileNoNewlineUnitTest.6.inc': return [2 => 1]; + case 'EndFileNoNewlineUnitTest.8.inc': + case 'EndFileNoNewlineUnitTest.9.inc': + return [1 => 1]; default: return []; }//end switch diff --git a/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.3.inc b/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.3.inc new file mode 100644 index 0000000000..ee44f5118f --- /dev/null +++ b/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.3.inc @@ -0,0 +1 @@ + diff --git a/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.4.inc b/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.4.inc new file mode 100755 index 0000000000..ee44f5118f --- /dev/null +++ b/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.4.inc @@ -0,0 +1 @@ + diff --git a/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.php b/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.php index f47fef6d36..269102dd3e 100644 --- a/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.php +++ b/src/Standards/Generic/Tests/Files/ExecutableFileUnitTest.php @@ -25,7 +25,7 @@ protected function shouldSkipTest() // PEAR doesn't preserve the executable flag, so skip // tests when running in a PEAR install. // Also skip on Windows which doesn't have the concept of executable files. - return ($GLOBALS['PHP_CODESNIFFER_PEAR'] || (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN')); + return ($GLOBALS['PHP_CODESNIFFER_PEAR'] || stripos(PHP_OS, 'WIN') === 0); }//end shouldSkipTest() @@ -44,6 +44,7 @@ public function getErrorList($testFile='') { switch ($testFile) { case 'ExecutableFileUnitTest.2.inc': + case 'ExecutableFileUnitTest.4.inc': return [1 => 1]; default: return []; diff --git a/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.inc.fixed b/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.1.inc similarity index 100% rename from src/Standards/Generic/Tests/Files/LineEndingsUnitTest.inc.fixed rename to src/Standards/Generic/Tests/Files/LineEndingsUnitTest.1.inc diff --git a/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.inc b/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.1.inc.fixed similarity index 88% rename from src/Standards/Generic/Tests/Files/LineEndingsUnitTest.inc rename to src/Standards/Generic/Tests/Files/LineEndingsUnitTest.1.inc.fixed index a2d3ed2093..b143fb4cf0 100644 --- a/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.inc +++ b/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.1.inc.fixed @@ -1,18 +1,18 @@ - - - - - -group('a.id, - uc.name, - ag.title, - ua.name' - ); -} + + + + + +group('a.id, + uc.name, + ag.title, + ua.name' + ); +} diff --git a/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.2.inc b/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.2.inc new file mode 100644 index 0000000000..260d7fd2fe --- /dev/null +++ b/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.2.inc @@ -0,0 +1,5 @@ + + + +
...more HTML...
+ diff --git a/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.2.inc.fixed b/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.2.inc.fixed new file mode 100644 index 0000000000..260d7fd2fe --- /dev/null +++ b/src/Standards/Generic/Tests/Files/LineEndingsUnitTest.2.inc.fixed @@ -0,0 +1,5 @@ + + + +
...more HTML...
+ diff --git a/src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.inc b/src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.1.inc similarity index 100% rename from src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.inc rename to src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.1.inc diff --git a/src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.2.inc b/src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.2.inc new file mode 100644 index 0000000000..708d19c6d6 --- /dev/null +++ b/src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.2.inc @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.php b/src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.php index 4b4bc97427..f67a40a3be 100644 --- a/src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.php +++ b/src/Standards/Generic/Tests/Files/LowercasedFilenameUnitTest.php @@ -21,11 +21,19 @@ class LowercasedFilenameUnitTest extends AbstractSniffUnitTest * 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. * + * @param string $testFile The name of the file being tested. + * * @return array */ - public function getErrorList() + public function getErrorList($testFile='') { - return [1 => 1]; + switch ($testFile) { + case 'LowercasedFilenameUnitTest.1.inc': + case 'LowercasedFilenameUnitTest.2.inc': + return [1 => 1]; + default: + return []; + } }//end getErrorList() diff --git a/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.inc b/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.inc index 94cebb4ee2..9f76d5d8d0 100644 --- a/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.inc +++ b/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.inc @@ -12,10 +12,15 @@ class baz { } trait barTrait { - + } interface barInterface { } -?> \ No newline at end of file + +enum barEnum { + +} + +?> diff --git a/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.php b/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.php index f429f98095..36a4bb9385 100644 --- a/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.php +++ b/src/Standards/Generic/Tests/Files/OneObjectStructurePerFileUnitTest.php @@ -30,6 +30,7 @@ public function getErrorList() 10 => 1, 14 => 1, 18 => 1, + 22 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Generic/Tests/Formatting/DisallowMultipleStatementsUnitTest.inc b/src/Standards/Generic/Tests/Formatting/DisallowMultipleStatementsUnitTest.inc index 5dd11cfdb7..ed316cc219 100644 --- a/src/Standards/Generic/Tests/Formatting/DisallowMultipleStatementsUnitTest.inc +++ b/src/Standards/Generic/Tests/Formatting/DisallowMultipleStatementsUnitTest.inc @@ -14,3 +14,7 @@ $this->wizardid = 10; $this->paint(); echo 'x'; 1, 399 => 1, 401 => 1, + 420 => 1, + 422 => 1, + 436 => 1, + 438 => 1, + 442 => 1, + 443 => 1, + 454 => 1, + 487 => 1, + 499 => 1, + 500 => 1, ]; break; case 'MultipleStatementAlignmentUnitTest.js': diff --git a/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc b/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc index 2747553cdb..8bd067b3f9 100644 --- a/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc +++ b/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc @@ -149,3 +149,26 @@ $foobar = functionCallAnonClassParam( }, $args=array(), ); + +$result = myFunction(param1: $arg1, param2: $arg2); +$result = myFunction(param1: $arg1 , param2:$arg2); +$result = myFunction(param1: $arg1, param2:$arg2, param3: $arg3,param4:$arg4, param5:$arg5); + +class Testing extends Bar +{ + public static function baz($foo, $bar) + { + $a = new parent($foo, $bar); + $a = new parent($foo ,$bar); + } +} + +// Ignore spacing after PHP 7.3+ trailing comma in single-line function calls to prevent fixer conflicts. +// This is something which should be decided by a sniff dealing with the function call parentheses. +$foo = new MyClass($obj, 'getMethod',); +$foo = new MyClass($obj, 'getMethod', ); +$foo = new MyClass($obj, 'getMethod', ); +$foo = new MyClass( + $obj, + 'getMethod', +); diff --git a/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc.fixed b/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc.fixed index 0ca70ca05f..69676524dc 100644 --- a/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.inc.fixed @@ -149,3 +149,26 @@ $foobar = functionCallAnonClassParam( }, $args=array(), ); + +$result = myFunction(param1: $arg1, param2: $arg2); +$result = myFunction(param1: $arg1, param2:$arg2); +$result = myFunction(param1: $arg1, param2:$arg2, param3: $arg3, param4:$arg4, param5:$arg5); + +class Testing extends Bar +{ + public static function baz($foo, $bar) + { + $a = new parent($foo, $bar); + $a = new parent($foo, $bar); + } +} + +// Ignore spacing after PHP 7.3+ trailing comma in single-line function calls to prevent fixer conflicts. +// This is something which should be decided by a sniff dealing with the function call parentheses. +$foo = new MyClass($obj, 'getMethod',); +$foo = new MyClass($obj, 'getMethod', ); +$foo = new MyClass($obj, 'getMethod', ); +$foo = new MyClass( + $obj, + 'getMethod', +); diff --git a/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.php b/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.php index cb1c2e0867..5f87962e62 100644 --- a/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.php +++ b/src/Standards/Generic/Tests/Functions/FunctionCallArgumentSpacingUnitTest.php @@ -52,6 +52,10 @@ public function getErrorList() 132 => 2, 133 => 2, 134 => 1, + 154 => 2, + 155 => 1, + 162 => 2, + 170 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.inc b/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.inc index 854c38886e..3ae3b1ed0b 100644 --- a/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.inc +++ b/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.inc @@ -236,3 +236,28 @@ function myFunction($a, $lot, $of, $params) : array /* comment */ { return null; } + +class Issue3357 +{ + public function extraLine(string: $a): void + + { + // code here. + } +} + +function issue3357WithoutIndent(string: $a): void + + +{ + // code here. +} + +class Issue3357WithComment +{ + public function extraLine(string: $a): void + // Comment. + { + // code here. + } +} diff --git a/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.inc.fixed b/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.inc.fixed index c1f904b218..f164c4934e 100644 --- a/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.inc.fixed @@ -256,3 +256,25 @@ function myFunction($a, $lot, $of, $params) { return null; } + +class Issue3357 +{ + public function extraLine(string: $a): void + { + // code here. + } +} + +function issue3357WithoutIndent(string: $a): void +{ + // code here. +} + +class Issue3357WithComment +{ + public function extraLine(string: $a): void + // Comment. + { + // code here. + } +} diff --git a/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.php b/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.php index 27eaa4fd77..10af4fd55a 100644 --- a/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.php +++ b/src/Standards/Generic/Tests/Functions/OpeningFunctionBraceBsdAllmanUnitTest.php @@ -58,6 +58,9 @@ public function getErrorList() 220 => 1, 231 => 1, 236 => 1, + 244 => 1, + 252 => 1, + 260 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.inc b/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.inc index 151ffed6a1..494dcc7694 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,294 @@ 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"), + }; +} + + +function complexitySevenWithNullSafeOperator() +{ + $foo = $object1->getX()?->getY()?->getZ(); + $bar = $object2->getX()?->getY()?->getZ(); + $baz = $object3->getX()?->getY()?->getZ(); +} + + +function complexityElevenWithNullSafeOperator() +{ + $foo = $object1->getX()?->getY()?->getZ(); + $bar = $object2->getX()?->getY()?->getZ(); + $baz = $object3->getX()?->getY()?->getZ(); + $bacon = $object4->getX()?->getY()?->getZ(); + $bits = $object5->getX()?->getY()?->getZ(); +} + ?> diff --git a/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.php b/src/Standards/Generic/Tests/Metrics/CyclomaticComplexityUnitTest.php index e8099317cd..d3860dff9e 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,15 @@ 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, + 445 => 1, ]; }//end getWarningList() diff --git a/src/Standards/Generic/Tests/NamingConventions/AbstractClassNamePrefixUnitTest.inc b/src/Standards/Generic/Tests/NamingConventions/AbstractClassNamePrefixUnitTest.inc new file mode 100644 index 0000000000..27b9d818da --- /dev/null +++ b/src/Standards/Generic/Tests/NamingConventions/AbstractClassNamePrefixUnitTest.inc @@ -0,0 +1,59 @@ + + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Standards\Generic\Tests\NamingConventions; + +use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; + +class AbstractClassNamePrefixUnitTest 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 [ + 3 => 1, + 13 => 1, + 18 => 1, + 23 => 1, + 42 => 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/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.inc b/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.inc index 9bda11472e..8441060d27 100644 --- a/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.inc +++ b/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.inc @@ -165,3 +165,21 @@ abstract class My_Class { public function my_class() {} public function _MY_CLASS() {} } + +enum Suit: string implements Colorful, CardGame { + // Magic methods. + function __call($name, $args) {} + static function __callStatic($name, $args) {} + function __invoke() {} + + // Valid Method Name. + public function getSomeValue() {} + + // Double underscore non-magic methods not allowed. + function __myFunction() {} + function __my_function() {} + + // Non-camelcase. + public function parseMyDSN() {} + public function get_some_value() {} +} diff --git a/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.php b/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.php index 502498d8bc..3d0320c3ae 100644 --- a/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.php +++ b/src/Standards/Generic/Tests/NamingConventions/CamelCapsFunctionNameUnitTest.php @@ -63,6 +63,10 @@ public function getErrorList() 147 => 2, 158 => 1, 159 => 1, + 179 => 1, + 180 => 2, + 183 => 1, + 184 => 1, ]; return $errors; diff --git a/src/Standards/Generic/Tests/NamingConventions/InterfaceNameSuffixUnitTest.inc b/src/Standards/Generic/Tests/NamingConventions/InterfaceNameSuffixUnitTest.inc new file mode 100644 index 0000000000..d562d3eaeb --- /dev/null +++ b/src/Standards/Generic/Tests/NamingConventions/InterfaceNameSuffixUnitTest.inc @@ -0,0 +1,27 @@ + + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Standards\Generic\Tests\NamingConventions; + +use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; + +class InterfaceNameSuffixUnitTest 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 [8 => 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/Generic/Tests/NamingConventions/TraitNameSuffixUnitTest.inc b/src/Standards/Generic/Tests/NamingConventions/TraitNameSuffixUnitTest.inc new file mode 100644 index 0000000000..e807697185 --- /dev/null +++ b/src/Standards/Generic/Tests/NamingConventions/TraitNameSuffixUnitTest.inc @@ -0,0 +1,13 @@ + + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Standards\Generic\Tests\NamingConventions; + +use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; + +class TraitNameSuffixUnitTest 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 [ + 3 => 1, + 9 => 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/Generic/Tests/NamingConventions/UpperCaseConstantNameUnitTest.inc b/src/Standards/Generic/Tests/NamingConventions/UpperCaseConstantNameUnitTest.inc index 7c33bbf119..6bf96f251c 100644 --- a/src/Standards/Generic/Tests/NamingConventions/UpperCaseConstantNameUnitTest.inc +++ b/src/Standards/Generic/Tests/NamingConventions/UpperCaseConstantNameUnitTest.inc @@ -29,3 +29,5 @@ class ClassConstBowOutTest { const // phpcs:ignore Standard.Category.Sniff some_constant = 2; } + +$foo->getBar()?->define('foo'); diff --git a/src/Standards/Generic/Tests/PHP/ClosingPHPTagUnitTest.inc b/src/Standards/Generic/Tests/PHP/ClosingPHPTagUnitTest.1.inc similarity index 100% rename from src/Standards/Generic/Tests/PHP/ClosingPHPTagUnitTest.inc rename to src/Standards/Generic/Tests/PHP/ClosingPHPTagUnitTest.1.inc diff --git a/src/Standards/Generic/Tests/PHP/ClosingPHPTagUnitTest.2.inc b/src/Standards/Generic/Tests/PHP/ClosingPHPTagUnitTest.2.inc new file mode 100644 index 0000000000..fc896ed197 --- /dev/null +++ b/src/Standards/Generic/Tests/PHP/ClosingPHPTagUnitTest.2.inc @@ -0,0 +1,5 @@ + +Bold text + +Italic text + */ - public function getErrorList() + public function getErrorList($testFile='') { - return [9 => 1]; + switch ($testFile) { + case 'ClosingPHPTagUnitTest.1.inc': + return [9 => 1]; + case 'ClosingPHPTagUnitTest.2.inc': + return [5 => 1]; + break; + default: + return []; + break; + } }//end getErrorList() diff --git a/src/Standards/Generic/Tests/PHP/ForbiddenFunctionsUnitTest.inc b/src/Standards/Generic/Tests/PHP/ForbiddenFunctionsUnitTest.inc index 4659b73289..060da6128c 100644 --- a/src/Standards/Generic/Tests/PHP/ForbiddenFunctionsUnitTest.inc +++ b/src/Standards/Generic/Tests/PHP/ForbiddenFunctionsUnitTest.inc @@ -54,4 +54,7 @@ class SizeOf implements Something {} function mymodule_form_callback(SizeOf $sizeof) { } -?> +$size = $class?->sizeof($array); + +#[SizeOf(10)] +function doSomething() {} diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc index 63aea51872..0307a0559d 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc +++ b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc @@ -81,3 +81,20 @@ var_dump(MyClass::TRUE); function tRUE() {} $input->getFilterChain()->attachByName('Null', ['type' => Null::TYPE_STRING]); + +// Issue #3332 - ignore type declarations, but not default values. +class TypedThings { + const MYCONST = FALSE; + + public int|FALSE $int = FALSE; + public Type|NULL $int = new MyObj(NULL); + + private function typed(int|FALSE $param = NULL, Type|NULL $obj = new MyObj(FALSE)) : string|FALSE|NULL + { + if (TRUE === FALSE) { + return NULL; + } + } +} + +$cl = function (int|FALSE $param = NULL, Type|NULL $obj = new MyObj(FALSE)) : string|FALSE|NULL {}; diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc.fixed b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc.fixed index b3c3d8a136..3a6b094c86 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.inc.fixed @@ -81,3 +81,20 @@ var_dump(MyClass::TRUE); function tRUE() {} $input->getFilterChain()->attachByName('Null', ['type' => Null::TYPE_STRING]); + +// Issue #3332 - ignore type declarations, but not default values. +class TypedThings { + const MYCONST = false; + + public int|FALSE $int = false; + public Type|NULL $int = new MyObj(null); + + private function typed(int|FALSE $param = null, Type|NULL $obj = new MyObj(false)) : string|FALSE|NULL + { + if (true === false) { + return null; + } + } +} + +$cl = function (int|FALSE $param = null, Type|NULL $obj = new MyObj(false)) : string|FALSE|NULL {}; diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php index 85e8f7011d..2fb2d6f6dc 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php +++ b/src/Standards/Generic/Tests/PHP/LowerCaseConstantUnitTest.php @@ -30,20 +30,27 @@ public function getErrorList($testFile='LowerCaseConstantUnitTest.inc') switch ($testFile) { case 'LowerCaseConstantUnitTest.inc': return [ - 7 => 1, - 10 => 1, - 15 => 1, - 16 => 1, - 23 => 1, - 26 => 1, - 31 => 1, - 32 => 1, - 39 => 1, - 42 => 1, - 47 => 1, - 48 => 1, - 70 => 1, - 71 => 1, + 7 => 1, + 10 => 1, + 15 => 1, + 16 => 1, + 23 => 1, + 26 => 1, + 31 => 1, + 32 => 1, + 39 => 1, + 42 => 1, + 47 => 1, + 48 => 1, + 70 => 1, + 71 => 1, + 87 => 1, + 89 => 1, + 90 => 1, + 92 => 2, + 94 => 2, + 95 => 1, + 100 => 2, ]; break; case 'LowerCaseConstantUnitTest.js': diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc index 96c2062256..37579d3217 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc +++ b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc @@ -29,5 +29,20 @@ class X extends Y { } } FN ($x) => $x; +$r = Match ($x) { + 1 => 1, + 2 => 2, + DEFAULT, => 3, +}; + +class Reading { + Public READOnly int $var; +} + +EnuM ENUM: string +{ + Case HEARTS; +} + __HALT_COMPILER(); // An exception due to phar support. function diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc.fixed b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc.fixed index f7d3b397fe..7063327ae8 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.inc.fixed @@ -29,5 +29,20 @@ class X extends Y { } } fn ($x) => $x; +$r = match ($x) { + 1 => 1, + 2 => 2, + default, => 3, +}; + +class Reading { + public readonly int $var; +} + +enum ENUM: string +{ + case HEARTS; +} + __HALT_COMPILER(); // An exception due to phar support. function diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.php b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.php index 2b9239062e..6d08e12751 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.php +++ b/src/Standards/Generic/Tests/PHP/LowerCaseKeywordUnitTest.php @@ -38,6 +38,11 @@ public function getErrorList() 25 => 1, 28 => 1, 31 => 1, + 32 => 1, + 35 => 1, + 39 => 2, + 42 => 1, + 44 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc index f4e0dd4628..011adcd530 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc +++ b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc @@ -15,7 +15,7 @@ $foo = (Int) $bar; $foo = (INTEGER) $bar; $foo = (BOOL) $bar; $foo = (String) $bar; -$foo = (Array) $bar; +$foo = ( Array ) $bar; function foo(int $a, string $b, bool $c, array $d, Foo\Bar $e) : int {} function foo(Int $a, String $b, BOOL $c, Array $d, Foo\Bar $e) : Foo\Bar {} @@ -45,3 +45,50 @@ $foo = function (?Int $a, ? Callable $b) $var = (BInARY) $string; $var = (binary)$string; + +function unionParamTypesA (bool|array| /* nullability operator not allowed in union */ NULL $var) {} + +function unionParamTypesB (\Package\ClassName | Int | \Package\Other_Class | FALSE $var) {} + +function unionReturnTypesA ($var): bool|array| /* nullability operator not allowed in union */ NULL {} + +function unionReturnTypesB ($var): \Package\ClassName | Int | \Package\Other_Class | FALSE {} + +class TypedProperties +{ + protected ClassName $class; + public Int $int; + private ?BOOL $bool; + public Self $self; + protected PaRenT $parent; + private ARRAY $array; + public Float $float; + protected ?STRING $string; + private IterablE $iterable; + public Object $object; + protected Mixed $mixed; + + public Iterable|FALSE|NULL $unionTypeA; + protected SELF|Parent /* comment */ |\Fully\Qualified\ClassName|UnQualifiedClass $unionTypeB; + private ClassName|/*comment*/Float|STRING|False $unionTypeC; + public sTRing | aRRaY | FaLSe $unionTypeD; +} + +class ConstructorPropertyPromotionWithTypes { + public function __construct(protected Float|Int $x, public ?STRING &$y = 'test', private mixed $z) {} +} + +class ConstructorPropertyPromotionAndNormalParams { + public function __construct(public Int $promotedProp, ?Int $normalArg) {} +} + +function (): NeVeR { + exit; +}; + +function intersectionParamTypes (\Package\ClassName&\Package\Other_Class $var) {} + +function intersectionReturnTypes ($var): \Package\ClassName&\Package\Other_Class {} + +$arrow = fn (int $a, string $b, bool $c, array $d, Foo\Bar $e) : int => $a * $b; +$arrow = fn (Int $a, String $b, BOOL $c, Array $d, Foo\Bar $e) : Float => $a * $b; diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc.fixed b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc.fixed index a64a4400d7..d866101b6f 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.inc.fixed @@ -15,7 +15,7 @@ $foo = (int) $bar; $foo = (integer) $bar; $foo = (bool) $bar; $foo = (string) $bar; -$foo = (array) $bar; +$foo = ( array ) $bar; function foo(int $a, string $b, bool $c, array $d, Foo\Bar $e) : int {} function foo(int $a, string $b, bool $c, array $d, Foo\Bar $e) : Foo\Bar {} @@ -45,3 +45,50 @@ $foo = function (?int $a, ? callable $b) $var = (binary) $string; $var = (binary)$string; + +function unionParamTypesA (bool|array| /* nullability operator not allowed in union */ null $var) {} + +function unionParamTypesB (\Package\ClassName | int | \Package\Other_Class | false $var) {} + +function unionReturnTypesA ($var): bool|array| /* nullability operator not allowed in union */ null {} + +function unionReturnTypesB ($var): \Package\ClassName | int | \Package\Other_Class | false {} + +class TypedProperties +{ + protected ClassName $class; + public int $int; + private ?bool $bool; + public self $self; + protected parent $parent; + private array $array; + public float $float; + protected ?string $string; + private iterable $iterable; + public object $object; + protected mixed $mixed; + + public iterable|false|null $unionTypeA; + protected self|parent /* comment */ |\Fully\Qualified\ClassName|UnQualifiedClass $unionTypeB; + private ClassName|/*comment*/float|string|false $unionTypeC; + public string | array | false $unionTypeD; +} + +class ConstructorPropertyPromotionWithTypes { + public function __construct(protected float|int $x, public ?string &$y = 'test', private mixed $z) {} +} + +class ConstructorPropertyPromotionAndNormalParams { + public function __construct(public int $promotedProp, ?int $normalArg) {} +} + +function (): never { + exit; +}; + +function intersectionParamTypes (\Package\ClassName&\Package\Other_Class $var) {} + +function intersectionReturnTypes ($var): \Package\ClassName&\Package\Other_Class {} + +$arrow = fn (int $a, string $b, bool $c, array $d, Foo\Bar $e) : int => $a * $b; +$arrow = fn (int $a, string $b, bool $c, array $d, Foo\Bar $e) : float => $a * $b; diff --git a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.php b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.php index 0a5f5e03ea..fa05aba6a3 100644 --- a/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.php +++ b/src/Standards/Generic/Tests/PHP/LowerCaseTypeUnitTest.php @@ -45,6 +45,28 @@ public function getErrorList() 43 => 2, 44 => 1, 46 => 1, + 49 => 1, + 51 => 2, + 53 => 1, + 55 => 2, + 60 => 1, + 61 => 1, + 62 => 1, + 63 => 1, + 64 => 1, + 65 => 1, + 66 => 1, + 67 => 1, + 68 => 1, + 69 => 1, + 71 => 3, + 72 => 2, + 73 => 3, + 74 => 3, + 78 => 3, + 82 => 2, + 85 => 1, + 94 => 5, ]; }//end getErrorList() diff --git a/src/Standards/Generic/Tests/PHP/NoSilencedErrorsUnitTest.inc b/src/Standards/Generic/Tests/PHP/NoSilencedErrorsUnitTest.inc index 7ff31ba704..72bffe2c4c 100644 --- a/src/Standards/Generic/Tests/PHP/NoSilencedErrorsUnitTest.inc +++ b/src/Standards/Generic/Tests/PHP/NoSilencedErrorsUnitTest.inc @@ -6,4 +6,5 @@ if (@in_array($array, $needle)) { echo '@'; } -?> + +$hasValue = @in_array(haystack: $array, needle: $needle); diff --git a/src/Standards/Generic/Tests/PHP/NoSilencedErrorsUnitTest.php b/src/Standards/Generic/Tests/PHP/NoSilencedErrorsUnitTest.php index 40b509bced..0e7a4eaa40 100644 --- a/src/Standards/Generic/Tests/PHP/NoSilencedErrorsUnitTest.php +++ b/src/Standards/Generic/Tests/PHP/NoSilencedErrorsUnitTest.php @@ -40,7 +40,10 @@ public function getErrorList() */ public function getWarningList() { - return [5 => 1]; + return [ + 5 => 1, + 10 => 1, + ]; }//end getWarningList() diff --git a/src/Standards/Generic/Tests/PHP/SAPIUsageUnitTest.inc b/src/Standards/Generic/Tests/PHP/SAPIUsageUnitTest.inc index 8f378c8db1..f0f350f374 100644 --- a/src/Standards/Generic/Tests/PHP/SAPIUsageUnitTest.inc +++ b/src/Standards/Generic/Tests/PHP/SAPIUsageUnitTest.inc @@ -2,3 +2,4 @@ if (php_sapi_name() !== 'cli') {} if (PHP_SAPI !== 'cli') {} if ($object->php_sapi_name() === true) {} +if ($object?->php_sapi_name() === true) {} diff --git a/src/Standards/Generic/Tests/PHP/SyntaxUnitTest.inc b/src/Standards/Generic/Tests/PHP/SyntaxUnitTest.1.inc similarity index 100% rename from src/Standards/Generic/Tests/PHP/SyntaxUnitTest.inc rename to src/Standards/Generic/Tests/PHP/SyntaxUnitTest.1.inc diff --git a/src/Standards/Generic/Tests/PHP/SyntaxUnitTest.2.inc b/src/Standards/Generic/Tests/PHP/SyntaxUnitTest.2.inc new file mode 100644 index 0000000000..11f8a58988 --- /dev/null +++ b/src/Standards/Generic/Tests/PHP/SyntaxUnitTest.2.inc @@ -0,0 +1,3 @@ + +
text
+ diff --git a/src/Standards/Generic/Tests/PHP/SyntaxUnitTest.php b/src/Standards/Generic/Tests/PHP/SyntaxUnitTest.php index d8cd7efaab..98d205ce43 100644 --- a/src/Standards/Generic/Tests/PHP/SyntaxUnitTest.php +++ b/src/Standards/Generic/Tests/PHP/SyntaxUnitTest.php @@ -22,11 +22,21 @@ class SyntaxUnitTest extends AbstractSniffUnitTest * 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. * + * @param string $testFile The name of the file being tested. + * * @return array */ - public function getErrorList() + public function getErrorList($testFile='') { - return [3 => 1]; + switch ($testFile) { + case 'SyntaxUnitTest.1.inc': + case 'SyntaxUnitTest.2.inc': + return [3 => 1]; + break; + default: + return []; + break; + } }//end getErrorList() diff --git a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc index 965bf3b511..30c6d2980d 100644 --- a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc +++ b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc @@ -78,4 +78,21 @@ class MyClass var_dump(MyClass::true); -function true() {} \ No newline at end of file +function true() {} + +// Issue #3332 - ignore type declarations, but not default values. +class TypedThings { + const MYCONST = false; + + public int|false $int = false; + public Type|null $int = new MyObj(null); + + private function typed(int|false $param = null, Type|null $obj = new MyObj(false)) : string|false|null + { + if (true === false) { + return null; + } + } +} + +$cl = function (int|false $param = null, Type|null $obj = new MyObj(false)) : string|false|null {}; diff --git a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc.fixed b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc.fixed index ae83dc91de..7705198c81 100644 --- a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.inc.fixed @@ -78,4 +78,21 @@ class MyClass var_dump(MyClass::true); -function true() {} \ No newline at end of file +function true() {} + +// Issue #3332 - ignore type declarations, but not default values. +class TypedThings { + const MYCONST = FALSE; + + public int|false $int = FALSE; + public Type|null $int = new MyObj(NULL); + + private function typed(int|false $param = NULL, Type|null $obj = new MyObj(FALSE)) : string|false|null + { + if (TRUE === FALSE) { + return NULL; + } + } +} + +$cl = function (int|false $param = NULL, Type|null $obj = new MyObj(FALSE)) : string|false|null {}; diff --git a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.php b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.php index 0bdafe6a43..30e5776337 100644 --- a/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.php +++ b/src/Standards/Generic/Tests/PHP/UpperCaseConstantUnitTest.php @@ -40,6 +40,13 @@ public function getErrorList() 48 => 1, 70 => 1, 71 => 1, + 85 => 1, + 87 => 1, + 88 => 1, + 90 => 2, + 92 => 2, + 93 => 1, + 98 => 2, ]; }//end getErrorList() diff --git a/src/Standards/Generic/Tests/VersionControl/GitMergeConflictUnitTest.7.inc b/src/Standards/Generic/Tests/VersionControl/GitMergeConflictUnitTest.7.inc new file mode 100644 index 0000000000..85cae1fdc8 --- /dev/null +++ b/src/Standards/Generic/Tests/VersionControl/GitMergeConflictUnitTest.7.inc @@ -0,0 +1,19 @@ + +
+<<<<<<< HEAD +

Testing a merge conflict.

+======= +

Another text string.

+>>>>>>> ref/heads/feature-branch +
+ + +
+<<<<<<< HEAD +

+======= +

+>>>>>>> ref/heads/feature-branch +
+ + diff --git a/src/Standards/Generic/Tests/VersionControl/GitMergeConflictUnitTest.php b/src/Standards/Generic/Tests/VersionControl/GitMergeConflictUnitTest.php index a36caafbd2..50986f48bb 100644 --- a/src/Standards/Generic/Tests/VersionControl/GitMergeConflictUnitTest.php +++ b/src/Standards/Generic/Tests/VersionControl/GitMergeConflictUnitTest.php @@ -99,6 +99,16 @@ public function getErrorList($testFile='GitMergeConflictUnitTest.1.inc') 32 => 1, ]; + case 'GitMergeConflictUnitTest.7.inc': + return [ + 3 => 1, + 5 => 1, + 7 => 1, + 12 => 1, + 14 => 1, + 16 => 1, + ]; + case 'GitMergeConflictUnitTest.1.css': return [ 3 => 1, diff --git a/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc b/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc index bad3998f97..2399a387e1 100644 --- a/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc +++ b/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc @@ -163,3 +163,15 @@ $a = ( if (true) {} ( 1+2) === 3 ? $a = 1 : $a = 2; class A {} ( 1+2) === 3 ? $a = 1 : $a = 2; function foo() {} ( 1+2) === 3 ? $a = 1 : $a = 2; + +// Issue #3618. +class NonArbitraryParenthesesWithKeywords { + public static function baz( $foo, $bar ) { + $a = new self(); + $b = new parent(); + $c = new static(); + + // self/static are already tested above, round line 45. + $d = new parent( $foo,$bar ); + } +} diff --git a/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc.fixed b/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc.fixed index 08fcd6244a..9162728e17 100644 --- a/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc.fixed @@ -151,3 +151,15 @@ $a = ( if (true) {} (1+2) === 3 ? $a = 1 : $a = 2; class A {} (1+2) === 3 ? $a = 1 : $a = 2; function foo() {} (1+2) === 3 ? $a = 1 : $a = 2; + +// Issue #3618. +class NonArbitraryParenthesesWithKeywords { + public static function baz( $foo, $bar ) { + $a = new self(); + $b = new parent(); + $c = new static(); + + // self/static are already tested above, round line 45. + $d = new parent( $foo,$bar ); + } +} diff --git a/src/Standards/Generic/Tests/WhiteSpace/DisallowSpaceIndentUnitTest.3.inc b/src/Standards/Generic/Tests/WhiteSpace/DisallowSpaceIndentUnitTest.3.inc new file mode 100644 index 0000000000..4df731a3c0 --- /dev/null +++ b/src/Standards/Generic/Tests/WhiteSpace/DisallowSpaceIndentUnitTest.3.inc @@ -0,0 +1,19 @@ + + + + Foo + + +
+
+
+
+
+
+ + + + + + + Foo + + +
+
+
+
+
+
+ + + + 1, ]; break; + case 'DisallowSpaceIndentUnitTest.3.inc': + return [ + 2 => 1, + 5 => 1, + 10 => 1, + 12 => 1, + 13 => 1, + 14 => 1, + 15 => 1, + ]; + break; case 'DisallowSpaceIndentUnitTest.js': return [3 => 1]; break; diff --git a/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.inc b/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.1.inc similarity index 100% rename from src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.inc rename to src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.1.inc diff --git a/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.inc.fixed b/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.1.inc.fixed similarity index 100% rename from src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.inc.fixed rename to src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.1.inc.fixed diff --git a/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.2.inc b/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.2.inc new file mode 100644 index 0000000000..4df731a3c0 --- /dev/null +++ b/src/Standards/Generic/Tests/WhiteSpace/DisallowTabIndentUnitTest.2.inc @@ -0,0 +1,19 @@ + + + + Foo + + +
+
+
+
+
+
+ + + + + + + Foo + + +
+
+
+
+
+
+ + + + */ - public function getErrorList($testFile='DisallowTabIndentUnitTest.inc') + public function getErrorList($testFile='') { switch ($testFile) { - case 'DisallowTabIndentUnitTest.inc': + case 'DisallowTabIndentUnitTest.1.inc': return [ 5 => 2, 9 => 1, @@ -83,23 +83,46 @@ public function getErrorList($testFile='DisallowTabIndentUnitTest.inc') 92 => 1, 93 => 1, ]; - break; + + case 'DisallowTabIndentUnitTest.2.inc': + return [ + 6 => 1, + 7 => 1, + 8 => 1, + 9 => 1, + 10 => 1, + 11 => 1, + 12 => 1, + 13 => 1, + 19 => 1, + ]; + + case 'DisallowTabIndentUnitTest.3.inc': + if (\PHP_VERSION_ID >= 70300) { + return [ + 7 => 1, + 13 => 1, + ]; + } + + // PHP 7.2 or lower: PHP version which doesn't support flexible heredocs/nowdocs yet. + return []; + case 'DisallowTabIndentUnitTest.js': return [ 3 => 1, 5 => 1, 6 => 1, ]; - break; + case 'DisallowTabIndentUnitTest.css': return [ 1 => 1, 2 => 1, ]; - break; + default: return []; - break; }//end switch }//end getErrorList() diff --git a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.1.inc b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.1.inc index bcce855e31..4061aff567 100644 --- a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.1.inc +++ b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.1.inc @@ -1454,6 +1454,132 @@ return [ ]), ]; +echo $string?->append('foo') + ?->outputUsing(); + +// phpcs:set Generic.WhiteSpace.ScopeIndent exact true +echo $string?->append('foo') + ?->outputUsing(); +// phpcs:set Generic.WhiteSpace.ScopeIndent exact false + +if (true) { + ?> null, + false => false, + 1, 2, 3 => true, + default => $value, +}; + +$value = match ($value) { + '' => null, +false => false, + 1, 2, 3 => true, + default => $value, +}; + +$value = match ( + $value + ) { + '' => null, + false + => false, + 1, + 2, + 3 => true, + default => +$value, +}; + +function toString(): string +{ + return sprintf( + '%s', + match ($type) { + 'foo' => 'bar', + }, + ); +} + +$list = [ + 'fn' => function ($a) { + if ($a === true) { + echo 'hi'; + } + }, + 'fn' => function ($a) { + if ($a === true) { + echo 'hi'; + $list2 = [ + 'fn' => function ($a) { + if ($a === true) { + echo 'hi'; + } + } + ]; + } + } +]; + +$foo = match ($type) { + 'a' => [ + 'aa' => 'DESC', + 'ab' => 'DESC', + ], + 'b' => [ + 'ba' => 'DESC', + 'bb' => 'DESC', + ], + default => [ + 'da' => 'DESC', + ], +}; + +$a = [ + 'a' => [ + 'a' => fn () => foo() + ], + 'a' => [ + 'a' => 'a', + ] +]; + +switch ($foo) { + case 'a': + $foo = match ($foo) { + 'bar' => 'custom_1', + default => 'a' + }; + return $foo; + case 'b': + return match ($foo) { + 'bar' => 'custom_1', + default => 'b' + }; + default: + return 'default'; +} + +foo(function ($foo) { + return [ + match ($foo) { + } + ]; +}); + +/* ADD NEW TESTS ABOVE THIS LINE AND MAKE SURE THAT THE 1 (space-based) AND 2 (tab-based) FILES ARE IN SYNC! */ ?> @@ -1461,7 +1587,7 @@ return [ + <<<'INTRO' + lorem ipsum + INTRO, + 'em' => [ + [ + '', + ], + ], + 'abc' => [ + 'a' => 'wop wop', + 'b' => 'ola ola.', + ], +]; + echo "" diff --git a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.1.inc.fixed b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.1.inc.fixed index 3ad7b793b8..7b5efea36a 100644 --- a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.1.inc.fixed +++ b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.1.inc.fixed @@ -1454,6 +1454,132 @@ return [ ]), ]; +echo $string?->append('foo') + ?->outputUsing(); + +// phpcs:set Generic.WhiteSpace.ScopeIndent exact true +echo $string?->append('foo') + ?->outputUsing(); +// phpcs:set Generic.WhiteSpace.ScopeIndent exact false + +if (true) { + ?> null, + false => false, + 1, 2, 3 => true, + default => $value, +}; + +$value = match ($value) { + '' => null, + false => false, + 1, 2, 3 => true, + default => $value, +}; + +$value = match ( + $value + ) { + '' => null, + false + => false, + 1, + 2, + 3 => true, + default => + $value, +}; + +function toString(): string +{ + return sprintf( + '%s', + match ($type) { + 'foo' => 'bar', + }, + ); +} + +$list = [ + 'fn' => function ($a) { + if ($a === true) { + echo 'hi'; + } + }, + 'fn' => function ($a) { + if ($a === true) { + echo 'hi'; + $list2 = [ + 'fn' => function ($a) { + if ($a === true) { + echo 'hi'; + } + } + ]; + } + } +]; + +$foo = match ($type) { + 'a' => [ + 'aa' => 'DESC', + 'ab' => 'DESC', + ], + 'b' => [ + 'ba' => 'DESC', + 'bb' => 'DESC', + ], + default => [ + 'da' => 'DESC', + ], +}; + +$a = [ + 'a' => [ + 'a' => fn () => foo() + ], + 'a' => [ + 'a' => 'a', + ] +]; + +switch ($foo) { + case 'a': + $foo = match ($foo) { + 'bar' => 'custom_1', + default => 'a' + }; + return $foo; + case 'b': + return match ($foo) { + 'bar' => 'custom_1', + default => 'b' + }; + default: + return 'default'; +} + +foo(function ($foo) { + return [ + match ($foo) { + } + ]; +}); + +/* ADD NEW TESTS ABOVE THIS LINE AND MAKE SURE THAT THE 1 (space-based) AND 2 (tab-based) FILES ARE IN SYNC! */ ?> @@ -1461,7 +1587,7 @@ return [ + <<<'INTRO' + lorem ipsum + INTRO, + 'em' => [ + [ + '', + ], + ], + 'abc' => [ + 'a' => 'wop wop', + 'b' => 'ola ola.', + ], +]; + echo "" diff --git a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.2.inc b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.2.inc index d128111de0..e7253141d4 100644 --- a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.2.inc +++ b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.2.inc @@ -1454,6 +1454,132 @@ return [ ]), ]; +echo $string?->append('foo') + ?->outputUsing(); + +// phpcs:set Generic.WhiteSpace.ScopeIndent exact true +echo $string?->append('foo') + ?->outputUsing(); +// phpcs:set Generic.WhiteSpace.ScopeIndent exact false + +if (true) { + ?> null, + false => false, + 1, 2, 3 => true, + default => $value, +}; + +$value = match ($value) { + '' => null, +false => false, + 1, 2, 3 => true, + default => $value, +}; + +$value = match ( + $value + ) { + '' => null, + false + => false, + 1, + 2, + 3 => true, + default => +$value, +}; + +function toString(): string +{ + return sprintf( + '%s', + match ($type) { + 'foo' => 'bar', + }, + ); +} + +$list = [ + 'fn' => function ($a) { + if ($a === true) { + echo 'hi'; + } + }, + 'fn' => function ($a) { + if ($a === true) { + echo 'hi'; + $list2 = [ + 'fn' => function ($a) { + if ($a === true) { + echo 'hi'; + } + } + ]; + } + } +]; + +$foo = match ($type) { + 'a' => [ + 'aa' => 'DESC', + 'ab' => 'DESC', + ], + 'b' => [ + 'ba' => 'DESC', + 'bb' => 'DESC', + ], + default => [ + 'da' => 'DESC', + ], +}; + +$a = [ + 'a' => [ + 'a' => fn () => foo() + ], + 'a' => [ + 'a' => 'a', + ] +]; + +switch ($foo) { + case 'a': + $foo = match ($foo) { + 'bar' => 'custom_1', + default => 'a' + }; + return $foo; + case 'b': + return match ($foo) { + 'bar' => 'custom_1', + default => 'b' + }; + default: + return 'default'; +} + +foo(function ($foo) { + return [ + match ($foo) { + } + ]; +}); + +/* ADD NEW TESTS ABOVE THIS LINE AND MAKE SURE THAT THE 1 (space-based) AND 2 (tab-based) FILES ARE IN SYNC! */ ?> @@ -1461,7 +1587,7 @@ return [ + <<<'INTRO' + lorem ipsum + INTRO, + 'em' => [ + [ + '', + ], + ], + 'abc' => [ + 'a' => 'wop wop', + 'b' => 'ola ola.', + ], +]; + echo "" diff --git a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.2.inc.fixed b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.2.inc.fixed index d6505e0ced..57caa29175 100644 --- a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.2.inc.fixed +++ b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.2.inc.fixed @@ -1454,6 +1454,132 @@ return [ ]), ]; +echo $string?->append('foo') + ?->outputUsing(); + +// phpcs:set Generic.WhiteSpace.ScopeIndent exact true +echo $string?->append('foo') + ?->outputUsing(); +// phpcs:set Generic.WhiteSpace.ScopeIndent exact false + +if (true) { + ?> null, + false => false, + 1, 2, 3 => true, + default => $value, +}; + +$value = match ($value) { + '' => null, + false => false, + 1, 2, 3 => true, + default => $value, +}; + +$value = match ( + $value + ) { + '' => null, + false + => false, + 1, + 2, + 3 => true, + default => + $value, +}; + +function toString(): string +{ + return sprintf( + '%s', + match ($type) { + 'foo' => 'bar', + }, + ); +} + +$list = [ + 'fn' => function ($a) { + if ($a === true) { + echo 'hi'; + } + }, + 'fn' => function ($a) { + if ($a === true) { + echo 'hi'; + $list2 = [ + 'fn' => function ($a) { + if ($a === true) { + echo 'hi'; + } + } + ]; + } + } +]; + +$foo = match ($type) { + 'a' => [ + 'aa' => 'DESC', + 'ab' => 'DESC', + ], + 'b' => [ + 'ba' => 'DESC', + 'bb' => 'DESC', + ], + default => [ + 'da' => 'DESC', + ], +}; + +$a = [ + 'a' => [ + 'a' => fn () => foo() + ], + 'a' => [ + 'a' => 'a', + ] +]; + +switch ($foo) { + case 'a': + $foo = match ($foo) { + 'bar' => 'custom_1', + default => 'a' + }; + return $foo; + case 'b': + return match ($foo) { + 'bar' => 'custom_1', + default => 'b' + }; + default: + return 'default'; +} + +foo(function ($foo) { + return [ + match ($foo) { + } + ]; +}); + +/* ADD NEW TESTS ABOVE THIS LINE AND MAKE SURE THAT THE 1 (space-based) AND 2 (tab-based) FILES ARE IN SYNC! */ ?> @@ -1461,7 +1587,7 @@ return [ + <<<'INTRO' + lorem ipsum + INTRO, + 'em' => [ + [ + '', + ], + ], + 'abc' => [ + 'a' => 'wop wop', + 'b' => 'ola ola.', + ], +]; + echo "" diff --git a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.php b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.php index f347678cac..6b0a71028e 100644 --- a/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.php +++ b/src/Standards/Generic/Tests/WhiteSpace/ScopeIndentUnitTest.php @@ -178,10 +178,19 @@ public function getErrorList($testFile='ScopeIndentUnitTest.inc') 1340 => 1, 1342 => 1, 1345 => 1, - 1464 => 1, - 1465 => 1, - 1466 => 1, - 1467 => 1, + 1488 => 1, + 1489 => 1, + 1500 => 1, + 1503 => 1, + 1518 => 1, + 1520 => 1, + 1527 => 1, + 1529 => 1, + 1530 => 1, + 1590 => 1, + 1591 => 1, + 1592 => 1, + 1593 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc b/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc index edce4dcc69..fb5c181429 100644 --- a/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc +++ b/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc @@ -67,7 +67,13 @@ function bar( & ... $spread ) { ); } +// Ignore PHP 8.1 first class callable declarations. +$map = array_map(strtolower(...), $map); + // phpcs:set Generic.WhiteSpace.SpreadOperatorSpacingAfter spacing 0 +// Ignore PHP 8.1 first class callable declarations. +$map = array_map(strtolower( ... ), $map); + // Intentional parse error. This has to be the last test in the file. function bar( ... diff --git a/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc.fixed b/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc.fixed index efec7ac15c..9388acfc63 100644 --- a/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc.fixed +++ b/src/Standards/Generic/Tests/WhiteSpace/SpreadOperatorSpacingAfterUnitTest.inc.fixed @@ -62,7 +62,13 @@ function bar( & ... $spread ) { ); } +// Ignore PHP 8.1 first class callable declarations. +$map = array_map(strtolower(...), $map); + // phpcs:set Generic.WhiteSpace.SpreadOperatorSpacingAfter spacing 0 +// Ignore PHP 8.1 first class callable declarations. +$map = array_map(strtolower( ... ), $map); + // Intentional parse error. This has to be the last test in the file. function bar( ... diff --git a/src/Standards/PEAR/Sniffs/Classes/ClassDeclarationSniff.php b/src/Standards/PEAR/Sniffs/Classes/ClassDeclarationSniff.php index a5b07a9e47..dbd61f3d23 100644 --- a/src/Standards/PEAR/Sniffs/Classes/ClassDeclarationSniff.php +++ b/src/Standards/PEAR/Sniffs/Classes/ClassDeclarationSniff.php @@ -27,6 +27,7 @@ public function register() T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, ]; }//end register() diff --git a/src/Standards/PEAR/Sniffs/Commenting/ClassCommentSniff.php b/src/Standards/PEAR/Sniffs/Commenting/ClassCommentSniff.php index 90e9eb8cab..2e9b59b7bf 100644 --- a/src/Standards/PEAR/Sniffs/Commenting/ClassCommentSniff.php +++ b/src/Standards/PEAR/Sniffs/Commenting/ClassCommentSniff.php @@ -10,7 +10,6 @@ namespace PHP_CodeSniffer\Standards\PEAR\Sniffs\Commenting; use PHP_CodeSniffer\Files\File; -use PHP_CodeSniffer\Util\Tokens; class ClassCommentSniff extends FileCommentSniff { @@ -27,6 +26,7 @@ public function register() T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, ]; }//end register() @@ -47,10 +47,28 @@ public function process(File $phpcsFile, $stackPtr) $type = strtolower($tokens[$stackPtr]['content']); $errorData = [$type]; - $find = Tokens::$methodPrefixes; - $find[] = T_WHITESPACE; + $find = [ + T_ABSTRACT => T_ABSTRACT, + T_FINAL => T_FINAL, + T_READONLY => T_READONLY, + T_WHITESPACE => T_WHITESPACE, + ]; + + for ($commentEnd = ($stackPtr - 1); $commentEnd >= 0; $commentEnd--) { + if (isset($find[$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($find, ($stackPtr - 1), null, true); if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG && $tokens[$commentEnd]['code'] !== T_COMMENT ) { diff --git a/src/Standards/PEAR/Sniffs/Commenting/FileCommentSniff.php b/src/Standards/PEAR/Sniffs/Commenting/FileCommentSniff.php index 86f5b0044b..0009f804a6 100644 --- a/src/Standards/PEAR/Sniffs/Commenting/FileCommentSniff.php +++ b/src/Standards/PEAR/Sniffs/Commenting/FileCommentSniff.php @@ -138,17 +138,30 @@ public function process(File $phpcsFile, $stackPtr) $commentEnd = $tokens[$commentStart]['comment_closer']; - $nextToken = $phpcsFile->findNext( - T_WHITESPACE, - ($commentEnd + 1), - null, - true - ); + for ($nextToken = ($commentEnd + 1); $nextToken < $phpcsFile->numTokens; $nextToken++) { + if ($tokens[$nextToken]['code'] === T_WHITESPACE) { + continue; + } + + if ($tokens[$nextToken]['code'] === T_ATTRIBUTE + && isset($tokens[$nextToken]['attribute_closer']) === true + ) { + $nextToken = $tokens[$nextToken]['attribute_closer']; + continue; + } + + break; + } + + if ($nextToken === $phpcsFile->numTokens) { + $nextToken--; + } $ignore = [ T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, T_FUNCTION, T_CLOSURE, T_PUBLIC, @@ -157,6 +170,7 @@ public function process(File $phpcsFile, $stackPtr) T_FINAL, T_STATIC, T_ABSTRACT, + T_READONLY, T_CONST, T_PROPERTY, ]; diff --git a/src/Standards/PEAR/Sniffs/Commenting/FunctionCommentSniff.php b/src/Standards/PEAR/Sniffs/Commenting/FunctionCommentSniff.php index a6a51f6b08..1bf9444784 100644 --- a/src/Standards/PEAR/Sniffs/Commenting/FunctionCommentSniff.php +++ b/src/Standards/PEAR/Sniffs/Commenting/FunctionCommentSniff.php @@ -16,6 +16,25 @@ class FunctionCommentSniff implements Sniff { + /** + * Disable the check for functions with a lower visibility than the value given. + * + * Allowed values are public, protected, and private. + * + * @var string + */ + public $minimumVisibility = 'private'; + + /** + * Array of methods which do not require a return type. + * + * @var array + */ + public $specialMethods = [ + '__construct', + '__destruct', + ]; + /** * Returns an array of tokens this test wants to listen for. @@ -40,17 +59,40 @@ public function register() */ public function process(File $phpcsFile, $stackPtr) { + $scopeModifier = $phpcsFile->getMethodProperties($stackPtr)['scope']; + if ($scopeModifier === 'protected' + && $this->minimumVisibility === 'public' + || $scopeModifier === 'private' + && ($this->minimumVisibility === 'public' || $this->minimumVisibility === 'protected') + ) { + return; + } + $tokens = $phpcsFile->getTokens(); - $find = Tokens::$methodPrefixes; - $find[] = T_WHITESPACE; + $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($find, ($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 // using the wrong comment type. If there is other code on the line, // assume they relate to that code. - $prev = $phpcsFile->findPrevious($find, ($commentEnd - 1), null, true); + $prev = $phpcsFile->findPrevious($ignore, ($commentEnd - 1), null, true); if ($prev !== false && $tokens[$prev]['line'] === $tokens[$commentEnd]['line']) { $commentEnd = $prev; } @@ -78,9 +120,34 @@ 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'; + $fix = $phpcsFile->addFixableError($error, $commentEnd, 'SpacingAfter'); + + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + + while ($i < $stackPtr + && $tokens[$i]['code'] === T_WHITESPACE + && $tokens[$i]['line'] !== $tokens[($i + 1)]['line'] + ) { + $phpcsFile->fixer->replaceToken($i++, ''); + } + + $phpcsFile->fixer->endChangeset(); + } + + break; + } + }//end for + }//end if $commentStart = $tokens[$commentEnd]['comment_opener']; foreach ($tokens[$commentStart]['comment_tags'] as $tag) { @@ -117,7 +184,7 @@ protected function processReturn(File $phpcsFile, $stackPtr, $commentStart) // Skip constructor and destructor. $methodName = $phpcsFile->getDeclarationName($stackPtr); - $isSpecialMethod = ($methodName === '__construct' || $methodName === '__destruct'); + $isSpecialMethod = in_array($methodName, $this->specialMethods, true); $return = null; foreach ($tokens[$commentStart]['comment_tags'] as $tag) { @@ -132,10 +199,6 @@ protected function processReturn(File $phpcsFile, $stackPtr, $commentStart) } } - if ($isSpecialMethod === true) { - return; - } - if ($return !== null) { $content = $tokens[($return + 2)]['content']; if (empty($content) === true || $tokens[($return + 2)]['code'] !== T_DOC_COMMENT_STRING) { @@ -143,6 +206,10 @@ protected function processReturn(File $phpcsFile, $stackPtr, $commentStart) $phpcsFile->addError($error, $return, 'MissingReturnType'); } } else { + if ($isSpecialMethod === true) { + return; + } + $error = 'Missing @return tag in function comment'; $phpcsFile->addError($error, $tokens[$commentStart]['comment_closer'], 'MissingReturn'); }//end if diff --git a/src/Standards/PEAR/Sniffs/ControlStructures/ControlSignatureSniff.php b/src/Standards/PEAR/Sniffs/ControlStructures/ControlSignatureSniff.php index 5724592c69..edd2d6beb9 100644 --- a/src/Standards/PEAR/Sniffs/ControlStructures/ControlSignatureSniff.php +++ b/src/Standards/PEAR/Sniffs/ControlStructures/ControlSignatureSniff.php @@ -39,6 +39,7 @@ protected function getPatterns() '} elseif (...) {EOL', '} else {EOL', 'do {EOL', + 'match (...) {EOL', ]; }//end getPatterns() diff --git a/src/Standards/PEAR/Sniffs/Functions/FunctionCallSignatureSniff.php b/src/Standards/PEAR/Sniffs/Functions/FunctionCallSignatureSniff.php index a4131fe65c..b5e8695c15 100644 --- a/src/Standards/PEAR/Sniffs/Functions/FunctionCallSignatureSniff.php +++ b/src/Standards/PEAR/Sniffs/Functions/FunctionCallSignatureSniff.php @@ -272,7 +272,7 @@ public function processSingleLineCall(File $phpcsFile, $stackPtr, $openBracket, $requiredSpacesBeforeClose, $spaceBeforeClose, ]; - $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceBeforeCloseBracket', $data); + $fix = $phpcsFile->addFixableError($error, $closer, 'SpaceBeforeCloseBracket', $data); if ($fix === true) { $padding = str_repeat(' ', $requiredSpacesBeforeClose); @@ -390,11 +390,14 @@ public function processMultiLineCall(File $phpcsFile, $stackPtr, $openBracket, $ $padding = str_repeat(' ', $functionIndent); if ($foundFunctionIndent === 0) { $phpcsFile->fixer->addContentBefore($first, $padding); + } else if ($tokens[$first]['code'] === T_INLINE_HTML) { + $newContent = $padding.ltrim($tokens[$first]['content']); + $phpcsFile->fixer->replaceToken($first, $newContent); } else { $phpcsFile->fixer->replaceToken(($first - 1), $padding); } } - } + }//end if $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($openBracket + 1), null, true); if ($tokens[$next]['line'] === $tokens[$openBracket]['line']) { @@ -581,7 +584,7 @@ public function processMultiLineCall(File $phpcsFile, $stackPtr, $openBracket, $ if ($inArg === false) { $argStart = $nextCode; - $argEnd = $phpcsFile->findEndOfStatement($nextCode); + $argEnd = $phpcsFile->findEndOfStatement($nextCode, [T_COLON]); } }//end if @@ -618,7 +621,7 @@ public function processMultiLineCall(File $phpcsFile, $stackPtr, $openBracket, $ }//end if $argStart = $next; - $argEnd = $phpcsFile->findEndOfStatement($next); + $argEnd = $phpcsFile->findEndOfStatement($next, [T_COLON]); }//end if }//end for diff --git a/src/Standards/PEAR/Sniffs/Functions/FunctionDeclarationSniff.php b/src/Standards/PEAR/Sniffs/Functions/FunctionDeclarationSniff.php index 9843629454..14479e2c0a 100644 --- a/src/Standards/PEAR/Sniffs/Functions/FunctionDeclarationSniff.php +++ b/src/Standards/PEAR/Sniffs/Functions/FunctionDeclarationSniff.php @@ -456,19 +456,23 @@ public function processArgumentList($phpcsFile, $stackPtr, $indent, $type='funct } // We changed lines, so this should be a whitespace indent token. - if ($tokens[$i]['code'] !== T_WHITESPACE) { - $foundIndent = 0; - } else if ($tokens[$i]['line'] !== $tokens[($i + 1)]['line']) { - // This is an empty line, so don't check the indent. - $foundIndent = $expectedIndent; - + $foundIndent = 0; + if ($tokens[$i]['code'] === T_WHITESPACE + && $tokens[$i]['line'] !== $tokens[($i + 1)]['line'] + ) { $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, ''); } - } else { + + // 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) { $foundIndent = $tokens[$i]['length']; + ++$expectedIndent; } if ($expectedIndent !== $foundIndent) { @@ -492,6 +496,19 @@ public function processArgumentList($phpcsFile, $stackPtr, $indent, $type='funct $lastLine = $tokens[$i]['line']; }//end if + if ($tokens[$i]['code'] === T_OPEN_PARENTHESIS + && isset($tokens[$i]['parenthesis_closer']) === true + ) { + $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($i - 1), null, true); + if ($tokens[$prevNonEmpty]['code'] !== T_USE) { + // Since PHP 8.1, a default value can contain a class instantiation. + // Skip over these "function calls" as they have their own indentation rules. + $i = $tokens[$i]['parenthesis_closer']; + $lastLine = $tokens[$i]['line']; + continue; + } + } + if ($tokens[$i]['code'] === T_ARRAY || $tokens[$i]['code'] === T_OPEN_SHORT_ARRAY) { // Skip arrays as they have their own indentation rules. if ($tokens[$i]['code'] === T_OPEN_SHORT_ARRAY) { @@ -503,6 +520,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/Sniffs/NamingConventions/ValidClassNameSniff.php b/src/Standards/PEAR/Sniffs/NamingConventions/ValidClassNameSniff.php index 34ca2830b5..00e68bfec5 100644 --- a/src/Standards/PEAR/Sniffs/NamingConventions/ValidClassNameSniff.php +++ b/src/Standards/PEAR/Sniffs/NamingConventions/ValidClassNameSniff.php @@ -27,6 +27,7 @@ public function register() T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, ]; }//end register() diff --git a/src/Standards/PEAR/Sniffs/NamingConventions/ValidFunctionNameSniff.php b/src/Standards/PEAR/Sniffs/NamingConventions/ValidFunctionNameSniff.php index 49486a54a3..e7f87d44dd 100644 --- a/src/Standards/PEAR/Sniffs/NamingConventions/ValidFunctionNameSniff.php +++ b/src/Standards/PEAR/Sniffs/NamingConventions/ValidFunctionNameSniff.php @@ -23,21 +23,23 @@ class ValidFunctionNameSniff extends AbstractScopeSniff * @var array */ protected $magicMethods = [ - 'construct' => true, - 'destruct' => true, - 'call' => true, - 'callstatic' => true, - 'get' => true, - 'set' => true, - 'isset' => true, - 'unset' => true, - 'sleep' => true, - 'wakeup' => true, - 'tostring' => true, - 'set_state' => true, - 'clone' => true, - 'invoke' => true, - 'debuginfo' => true, + 'construct' => true, + 'destruct' => true, + 'call' => true, + 'callstatic' => true, + 'get' => true, + 'set' => true, + 'isset' => true, + 'unset' => true, + 'sleep' => true, + 'wakeup' => true, + 'serialize' => true, + 'unserialize' => true, + 'tostring' => true, + 'invoke' => true, + 'set_state' => true, + 'clone' => true, + 'debuginfo' => true, ]; /** diff --git a/src/Standards/PEAR/Sniffs/WhiteSpace/ObjectOperatorIndentSniff.php b/src/Standards/PEAR/Sniffs/WhiteSpace/ObjectOperatorIndentSniff.php index 1929b3bbe8..fb1b79a329 100644 --- a/src/Standards/PEAR/Sniffs/WhiteSpace/ObjectOperatorIndentSniff.php +++ b/src/Standards/PEAR/Sniffs/WhiteSpace/ObjectOperatorIndentSniff.php @@ -29,6 +29,16 @@ class ObjectOperatorIndentSniff implements Sniff */ public $multilevel = false; + /** + * Tokens to listen for. + * + * @var array + */ + private $targets = [ + T_OBJECT_OPERATOR, + T_NULLSAFE_OBJECT_OPERATOR, + ]; + /** * Returns an array of tokens this test wants to listen for. @@ -37,7 +47,7 @@ class ObjectOperatorIndentSniff implements Sniff */ public function register() { - return [T_OBJECT_OPERATOR]; + return $this->targets; }//end register() @@ -57,14 +67,14 @@ public function process(File $phpcsFile, $stackPtr) // Make sure this is the first object operator in a chain of them. $start = $phpcsFile->findStartOfStatement($stackPtr); - $prev = $phpcsFile->findPrevious(T_OBJECT_OPERATOR, ($stackPtr - 1), $start); + $prev = $phpcsFile->findPrevious($this->targets, ($stackPtr - 1), $start); if ($prev !== false) { return; } // Make sure this is a chained call. $end = $phpcsFile->findEndOfStatement($stackPtr); - $next = $phpcsFile->findNext(T_OBJECT_OPERATOR, ($stackPtr + 1), $end); + $next = $phpcsFile->findNext($this->targets, ($stackPtr + 1), $end); if ($next === false) { // Not a chained call. return; @@ -179,7 +189,7 @@ public function process(File $phpcsFile, $stackPtr) }//end if $next = $phpcsFile->findNext( - T_OBJECT_OPERATOR, + $this->targets, ($next + 1), null, false, diff --git a/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc b/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc index d97ef4d29d..6942944b5c 100644 --- a/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc +++ b/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc @@ -110,3 +110,5 @@ if (!class_exists('ClassOpeningBraceTooMuchIndentation')) { { } } + +enum IncorrectBracePlacement {} diff --git a/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc.fixed b/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc.fixed index 5b0a2f93fb..26688b1527 100644 --- a/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc.fixed +++ b/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.1.inc.fixed @@ -119,3 +119,7 @@ if (!class_exists('ClassOpeningBraceTooMuchIndentation')) { { } } + +enum IncorrectBracePlacement +{ +} diff --git a/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.php b/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.php index 1514a70378..4c1d28e733 100644 --- a/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.php +++ b/src/Standards/PEAR/Tests/Classes/ClassDeclarationUnitTest.php @@ -61,6 +61,7 @@ public function getErrorList($testFile='') 99 => 1, 104 => 1, 110 => 1, + 114 => 1, ]; default: diff --git a/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.inc b/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.inc index 0ca87512f8..e340ff0041 100644 --- a/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.inc +++ b/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.inc @@ -118,3 +118,46 @@ trait Empty_Trait_Doc { }//end trait + + +/** + * + * + */ +enum Empty_Enum_Doc +{ + +}//end enum + + +/** + * Sample class comment + * + * @category PHP + * @package PHP_CodeSniffer + * @author Greg Sherwood + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + * @link http://pear.php.net/package/PHP_CodeSniffer + */ +#[Authenticate('admin_logged_in')] +class TodoController extends AbstractController implements MustBeLoggedInInterface +{ +} + +/** + * Docblock + * + * @category PHP + * @package PHP_CodeSniffer + * @author Greg Sherwood + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + * @link http://pear.php.net/package/PHP_CodeSniffer + */ +abstract readonly class AbstractReadonlyWithDocblock {} + +/* + * Docblock + */ +readonly class ReadonlyWrongStyle {} + +readonly final class ReadonlyFinalWithoutDocblock {} diff --git a/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.php b/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.php index 9a4bcf7dd6..73465fadc9 100644 --- a/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.php +++ b/src/Standards/PEAR/Tests/Commenting/ClassCommentUnitTest.php @@ -44,6 +44,9 @@ public function getErrorList() 96 => 5, 106 => 5, 116 => 5, + 126 => 5, + 161 => 1, + 163 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PEAR/Tests/Commenting/FileCommentUnitTest.inc b/src/Standards/PEAR/Tests/Commenting/FileCommentUnitTest.1.inc similarity index 100% rename from src/Standards/PEAR/Tests/Commenting/FileCommentUnitTest.inc rename to src/Standards/PEAR/Tests/Commenting/FileCommentUnitTest.1.inc diff --git a/src/Standards/PEAR/Tests/Commenting/FileCommentUnitTest.2.inc b/src/Standards/PEAR/Tests/Commenting/FileCommentUnitTest.2.inc new file mode 100644 index 0000000000..8845eb1906 --- /dev/null +++ b/src/Standards/PEAR/Tests/Commenting/FileCommentUnitTest.2.inc @@ -0,0 +1,9 @@ + */ - public function getErrorList() + public function getErrorList($testFile='FileCommentUnitTest.inc') { - return [ - 21 => 1, - 23 => 2, - 24 => 1, - 26 => 1, - 28 => 1, - 29 => 1, - 30 => 1, - 31 => 1, - 32 => 2, - 33 => 1, - 34 => 1, - 35 => 1, - 40 => 2, - 41 => 2, - 43 => 1, - ]; + switch ($testFile) { + case 'FileCommentUnitTest.1.inc': + return [ + 21 => 1, + 23 => 2, + 24 => 1, + 26 => 1, + 28 => 1, + 29 => 1, + 30 => 1, + 31 => 1, + 32 => 2, + 33 => 1, + 34 => 1, + 35 => 1, + 40 => 2, + 41 => 2, + 43 => 1, + ]; + + case 'FileCommentUnitTest.2.inc': + case 'FileCommentUnitTest.3.inc': + case 'FileCommentUnitTest.4.inc': + return [1 => 1]; + + default: + return []; + }//end switch }//end getErrorList() @@ -52,16 +65,24 @@ public function getErrorList() * 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. * + * @param string $testFile The name of the file being tested. + * * @return array */ - public function getWarningList() + public function getWarningList($testFile='FileCommentUnitTest.inc') { - return [ - 29 => 1, - 30 => 1, - 34 => 1, - 43 => 1, - ]; + switch ($testFile) { + case 'FileCommentUnitTest.1.inc': + return [ + 29 => 1, + 30 => 1, + 34 => 1, + 43 => 1, + ]; + + default: + return []; + }//end switch }//end getWarningList() diff --git a/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc b/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc index 461b490775..a20ba3a720 100644 --- a/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc +++ b/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc @@ -381,3 +381,132 @@ public function setTranslator($a, &$b): void { $this->translator = $translator; } + +// phpcs:set PEAR.Commenting.FunctionComment minimumVisibility protected +private function setTranslator2($a, &$b): void +{ + $this->translator = $translator; +} + +// phpcs:set PEAR.Commenting.FunctionComment minimumVisibility public +protected function setTranslator3($a, &$b): void +{ + $this->translator = $translator; +} + +private function setTranslator4($a, &$b): void +{ + $this->translator = $translator; +} + +class Bar { + /** + * The PHP5 constructor + * + * @return + */ + public function __construct() { + + } +} + +// phpcs:set PEAR.Commenting.FunctionComment specialMethods[] +class Bar { + /** + * The PHP5 constructor + */ + public function __construct() { + + } +} + +// phpcs:set PEAR.Commenting.FunctionComment specialMethods[] ignored +/** + * Should be ok + */ +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() {} +} + +class SpacingAfter { + /** + * There are multiple blank lines between this comment and the next function. + * + * @return void + */ + + + + + + + + + public function multipleBlankLines() {} + + /** + * There are multiple blank lines, and some "empty" lines with only + * spaces/tabs between this comment and the next function. + * + * @return void + */ + + + + + + + + + + public function multipleLinesSomeEmpty() {} +} diff --git a/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc.fixed b/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc.fixed index 80bf63f7be..fc6d4f7e71 100644 --- a/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc.fixed +++ b/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.inc.fixed @@ -381,3 +381,111 @@ public function setTranslator($a, &$b): void { $this->translator = $translator; } + +// phpcs:set PEAR.Commenting.FunctionComment minimumVisibility protected +private function setTranslator2($a, &$b): void +{ + $this->translator = $translator; +} + +// phpcs:set PEAR.Commenting.FunctionComment minimumVisibility public +protected function setTranslator3($a, &$b): void +{ + $this->translator = $translator; +} + +private function setTranslator4($a, &$b): void +{ + $this->translator = $translator; +} + +class Bar { + /** + * The PHP5 constructor + * + * @return + */ + public function __construct() { + + } +} + +// phpcs:set PEAR.Commenting.FunctionComment specialMethods[] +class Bar { + /** + * The PHP5 constructor + */ + public function __construct() { + + } +} + +// phpcs:set PEAR.Commenting.FunctionComment specialMethods[] ignored +/** + * Should be ok + */ +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() {} +} + +class SpacingAfter { + /** + * There are multiple blank lines between this comment and the next function. + * + * @return void + */ + public function multipleBlankLines() {} + + /** + * There are multiple blank lines, and some "empty" lines with only + * spaces/tabs between this comment and the next function. + * + * @return void + */ + public function multipleLinesSomeEmpty() {} +} diff --git a/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.php b/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.php index 2f27c18710..e7ec800d02 100644 --- a/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.php +++ b/src/Standards/PEAR/Tests/Commenting/FunctionCommentUnitTest.php @@ -68,6 +68,13 @@ public function getErrorList() 361 => 1, 363 => 1, 364 => 1, + 406 => 1, + 417 => 1, + 455 => 1, + 464 => 1, + 473 => 1, + 485 => 1, + 501 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PEAR/Tests/ControlStructures/ControlSignatureUnitTest.inc b/src/Standards/PEAR/Tests/ControlStructures/ControlSignatureUnitTest.inc index 66ab925535..cc9903aa13 100644 --- a/src/Standards/PEAR/Tests/ControlStructures/ControlSignatureUnitTest.inc +++ b/src/Standards/PEAR/Tests/ControlStructures/ControlSignatureUnitTest.inc @@ -156,4 +156,10 @@ if ($i == 0) { } else { } -?> \ No newline at end of file + +// match +$r = match ($x) { + 1 => 1, +}; + +$r = match( $x ){ 1 => 1 }; diff --git a/src/Standards/PEAR/Tests/ControlStructures/ControlSignatureUnitTest.php b/src/Standards/PEAR/Tests/ControlStructures/ControlSignatureUnitTest.php index 5cb267672b..98c3463b7b 100644 --- a/src/Standards/PEAR/Tests/ControlStructures/ControlSignatureUnitTest.php +++ b/src/Standards/PEAR/Tests/ControlStructures/ControlSignatureUnitTest.php @@ -48,6 +48,7 @@ public function getErrorList() 133 => 2, 147 => 1, 157 => 1, + 165 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc index 6b2c898c42..612748fedf 100644 --- a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc +++ b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc @@ -525,3 +525,45 @@ return trim(preg_replace_callback( $a = ['a' => function ($b) { return $b; }]; $a['a']( 1 ); + +// PHP 8.0 named parameters. +array_fill_keys( + keys: range( + 1, + 12, + ), + value: true, +); + +array_fill_keys( + keys: range( 1, + 12, + ), value: true, +); + +// phpcs:set PEAR.Functions.FunctionCallSignature allowMultipleArguments false +array_fill_keys( + keys: range( 1, + 12, + ), value: true, +); +// phpcs:set PEAR.Functions.FunctionCallSignature allowMultipleArguments true + +?> +
+

+
+
+

+
+ +content +

diff --git a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc.fixed b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc.fixed index 7396441878..00226de562 100644 --- a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc.fixed +++ b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.inc.fixed @@ -537,3 +537,48 @@ return trim( $a = ['a' => function ($b) { return $b; }]; $a['a'](1); + +// PHP 8.0 named parameters. +array_fill_keys( + keys: range( + 1, + 12, + ), + value: true, +); + +array_fill_keys( + keys: range( + 1, + 12, + ), value: true, +); + +// phpcs:set PEAR.Functions.FunctionCallSignature allowMultipleArguments false +array_fill_keys( + keys: range( + 1, + 12, + ), + value: true, +); +// phpcs:set PEAR.Functions.FunctionCallSignature allowMultipleArguments true + +?> +
+

+
+
+

+
+ +content +

diff --git a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.php b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.php index c6db5b08e5..4984de2bf6 100644 --- a/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.php +++ b/src/Standards/PEAR/Tests/Functions/FunctionCallSignatureUnitTest.php @@ -126,6 +126,14 @@ public function getErrorList($testFile='FunctionCallSignatureUnitTest.inc') 523 => 1, 524 => 3, 527 => 2, + 539 => 1, + 540 => 1, + 546 => 1, + 547 => 1, + 548 => 1, + 559 => 1, + 567 => 1, + 568 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc b/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc index 7af7200017..fb7cb52f4d 100644 --- a/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc +++ b/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc @@ -314,3 +314,154 @@ if(true) { ; } } + +class ConstructorPropertyPromotionSingleLineDocblockIndentOK +{ + public function __construct( + /** @var string */ + public string $public, + /** @var string */ + private string $private, + ) { + } +} + +class ConstructorPropertyPromotionMultiLineDocblockAndAttributeIndentOK +{ + public function __construct( + /** + * @var string + * @Assert\NotBlank() + */ + public string $public, + /** + * @var string + * @Assert\NotBlank() + */ + #[NotBlank] + private string $private, + ) { + } +} + +class ConstructorPropertyPromotionSingleLineDocblockIncorrectIndent +{ + public function __construct( + /** @var string */ + public string $public, + /** @var string */ + private string $private, + ) { + } +} + +class ConstructorPropertyPromotionMultiLineDocblockAndAttributeIncorrectIndent +{ + public function __construct( + /** + * @var string + * @Assert\NotBlank() + */ + public string $public, +/** + * @var string + * @Assert\NotBlank() + */ +#[NotBlank] +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. + } +} + +// PHP 8.1: new in initializers means that class instantiations with parameters can occur in a function declaration. +function usingNewInInitializersCallParamsIndented( + int $paramA, + string $paramB, + object $paramC = new SomeClass( + new InjectedDependencyA(), + new InjectedDependencyB + ) +) {} + +function usingNewInInitializersCallParamsNotIndented( + int $paramA, + string $paramB, + object $paramC = new SomeClass( + new InjectedDependencyA, + new InjectedDependencyB() + ) +) {} + +function usingNewInInitializersCallParamsIncorrectlyIndentedShouldNotBeFlaggedNorFixed( + int $paramA, + string $paramB, + object $paramC = new SomeClass( +new InjectedDependencyA(), new InjectedDependencyB() +) +) {} + +class UsingNewInInitializers { + public function doSomething( + object $paramA, + stdClass $paramB = new stdClass(), + Exception $paramC = new Exception( + new ExceptionMessage(), + new ExceptionCode(), + ), + ) { + } + + public function callParamsIncorrectlyIndentedShouldNotBeFlaggedNorFixed( + Exception $param = new Exception( +new ExceptionMessage(), + new ExceptionCode(), + ), + ) { + } +} diff --git a/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc.fixed b/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc.fixed index b993e3f079..f82b5665a5 100644 --- a/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc.fixed +++ b/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.inc.fixed @@ -312,3 +312,154 @@ if(true) { abstract function baz(); } } + +class ConstructorPropertyPromotionSingleLineDocblockIndentOK +{ + public function __construct( + /** @var string */ + public string $public, + /** @var string */ + private string $private, + ) { + } +} + +class ConstructorPropertyPromotionMultiLineDocblockAndAttributeIndentOK +{ + public function __construct( + /** + * @var string + * @Assert\NotBlank() + */ + public string $public, + /** + * @var string + * @Assert\NotBlank() + */ + #[NotBlank] + private string $private, + ) { + } +} + +class ConstructorPropertyPromotionSingleLineDocblockIncorrectIndent +{ + public function __construct( + /** @var string */ + public string $public, + /** @var string */ + private string $private, + ) { + } +} + +class ConstructorPropertyPromotionMultiLineDocblockAndAttributeIncorrectIndent +{ + public function __construct( + /** + * @var string + * @Assert\NotBlank() + */ + public string $public, + /** + * @var string + * @Assert\NotBlank() + */ + #[NotBlank] + 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. + } +} + +// PHP 8.1: new in initializers means that class instantiations with parameters can occur in a function declaration. +function usingNewInInitializersCallParamsIndented( + int $paramA, + string $paramB, + object $paramC = new SomeClass( + new InjectedDependencyA(), + new InjectedDependencyB + ) +) {} + +function usingNewInInitializersCallParamsNotIndented( + int $paramA, + string $paramB, + object $paramC = new SomeClass( + new InjectedDependencyA, + new InjectedDependencyB() + ) +) {} + +function usingNewInInitializersCallParamsIncorrectlyIndentedShouldNotBeFlaggedNorFixed( + int $paramA, + string $paramB, + object $paramC = new SomeClass( +new InjectedDependencyA(), new InjectedDependencyB() +) +) {} + +class UsingNewInInitializers { + public function doSomething( + object $paramA, + stdClass $paramB = new stdClass(), + Exception $paramC = new Exception( + new ExceptionMessage(), + new ExceptionCode(), + ), + ) { + } + + public function callParamsIncorrectlyIndentedShouldNotBeFlaggedNorFixed( + Exception $param = new Exception( +new ExceptionMessage(), + new ExceptionCode(), + ), + ) { + } +} diff --git a/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.php b/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.php index 7a8a53a1c3..01ab3e8482 100644 --- a/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.php +++ b/src/Standards/PEAR/Tests/Functions/FunctionDeclarationUnitTest.php @@ -82,6 +82,23 @@ public function getErrorList($testFile='FunctionDeclarationUnitTest.inc') 309 => 1, 313 => 1, 314 => 1, + 350 => 1, + 351 => 1, + 352 => 1, + 353 => 1, + 361 => 1, + 362 => 1, + 363 => 1, + 364 => 1, + 365 => 1, + 366 => 1, + 367 => 1, + 368 => 1, + 369 => 1, + 370 => 1, + 371 => 1, + 402 => 1, + 406 => 1, ]; } else { $errors = [ diff --git a/src/Standards/PEAR/Tests/Functions/ValidDefaultValueUnitTest.inc b/src/Standards/PEAR/Tests/Functions/ValidDefaultValueUnitTest.inc index c9233734fc..8f8d64ab73 100644 --- a/src/Standards/PEAR/Tests/Functions/ValidDefaultValueUnitTest.inc +++ b/src/Standards/PEAR/Tests/Functions/ValidDefaultValueUnitTest.inc @@ -100,5 +100,20 @@ $closure = function(array $arg2=array(), array $arg1) {} $fn = fn($a = [], $b) => $a[] = $b; +class OnlyConstructorPropertyPromotion { + public function __construct( + public string $name = '', + protected $bar + ) {} +} + +class ConstructorPropertyPromotionMixedWithNormalParams { + public function __construct( + public string $name = '', + ?int $optionalParam = 0, + mixed $requiredParam, + ) {} +} + // Intentional syntax error. Must be last thing in the file. function diff --git a/src/Standards/PEAR/Tests/Functions/ValidDefaultValueUnitTest.php b/src/Standards/PEAR/Tests/Functions/ValidDefaultValueUnitTest.php index 73489a35c0..60d261cbbb 100644 --- a/src/Standards/PEAR/Tests/Functions/ValidDefaultValueUnitTest.php +++ b/src/Standards/PEAR/Tests/Functions/ValidDefaultValueUnitTest.php @@ -35,6 +35,8 @@ public function getErrorList() 91 => 1, 99 => 1, 101 => 1, + 106 => 1, + 114 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.inc b/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.inc index c6d15df7f0..053a4fee2f 100644 --- a/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.inc +++ b/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.inc @@ -66,3 +66,25 @@ trait _Invalid_Name {} trait ___ {} trait Invalid__Name {} + +enum Valid_Name: string {} + +enum invalid_Name : String {} + +enum invalid_name {} + +enum Invalid_name: Int {} + +enum VALID_Name {} + +enum VALID_NAME {} + +enum VALID_Name : int {} + +enum ValidName {} + +enum _Invalid_Name {} + +enum ___ {} + +enum Invalid__Name {} diff --git a/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.php b/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.php index 8ff9c674b2..54ee74aac1 100644 --- a/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.php +++ b/src/Standards/PEAR/Tests/NamingConventions/ValidClassNameUnitTest.php @@ -44,6 +44,12 @@ public function getErrorList() 64 => 1, 66 => 2, 68 => 1, + 72 => 1, + 74 => 2, + 76 => 1, + 86 => 1, + 88 => 2, + 90 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.inc b/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.inc index 78280cdd90..18b1a48176 100644 --- a/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.inc +++ b/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.inc @@ -220,3 +220,24 @@ abstract class My_Class { public function my_class() {} public function _MY_CLASS() {} } + +enum Suit: string implements Colorful, CardGame { + // Magic methods. + function __call($name, $args) {} + static function __callStatic($name, $args) {} + function __invoke() {} + + // Valid Method Name. + public function parseMyDSN() {} + private function _getAnotherValue() {} + + // Double underscore non-magic methods not allowed. + function __myFunction() {} + function __my_function() {} + + // Non-camelcase. + public function get_some_value() {} + + // Private without underscore prefix. + private function getMe() {} +} diff --git a/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.php b/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.php index 9bb6de0d84..4639a1e2ab 100644 --- a/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.php +++ b/src/Standards/PEAR/Tests/NamingConventions/ValidFunctionNameUnitTest.php @@ -122,6 +122,10 @@ public function getErrorList() 212 => 1, 213 => 1, 214 => 1, + 235 => 1, + 236 => 2, + 239 => 1, + 242 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.inc b/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.inc index 70fd3d8ccb..b1b09d9323 100644 --- a/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.inc +++ b/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.inc @@ -110,3 +110,33 @@ $rootNode ->five(); // phpcs:set PEAR.WhiteSpace.ObjectOperatorIndent multilevel false + +$object + ?->setBar($foo) + ?->setFoo($bar); + +$someObject?->someFunction("some", "parameter") +->someOtherFunc(23, 42)?-> + someOtherFunc2($one, $two) + +->someOtherFunc3(23, 42) + ?->andAThirdFunction(); + +// phpcs:set PEAR.WhiteSpace.ObjectOperatorIndent multilevel true +$object + ?->setBar($foo) + ?->setFoo($bar); + +$someObject?->someFunction("some", "parameter") +->someOtherFunc(23, 42) + ?->someOtherFunc2($one, $two) + +->someOtherFunc3(23, 42) + ?->andAThirdFunction(); +// phpcs:set PEAR.WhiteSpace.ObjectOperatorIndent multilevel false + +$someObject + ->startSomething(paramName: $value) + ->someOtherFunc(nameA: 23, nameB: 42) +->endSomething($value, name: $value) +->endEverything(); diff --git a/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.inc.fixed b/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.inc.fixed index dfa642f46d..5d5b77bef6 100644 --- a/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.inc.fixed +++ b/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.inc.fixed @@ -110,3 +110,33 @@ $rootNode ->five(); // phpcs:set PEAR.WhiteSpace.ObjectOperatorIndent multilevel false + +$object + ?->setBar($foo) + ?->setFoo($bar); + +$someObject?->someFunction("some", "parameter") + ->someOtherFunc(23, 42) + ?->someOtherFunc2($one, $two) + + ->someOtherFunc3(23, 42) + ?->andAThirdFunction(); + +// phpcs:set PEAR.WhiteSpace.ObjectOperatorIndent multilevel true +$object + ?->setBar($foo) + ?->setFoo($bar); + +$someObject?->someFunction("some", "parameter") + ->someOtherFunc(23, 42) + ?->someOtherFunc2($one, $two) + + ->someOtherFunc3(23, 42) + ?->andAThirdFunction(); +// phpcs:set PEAR.WhiteSpace.ObjectOperatorIndent multilevel false + +$someObject + ->startSomething(paramName: $value) + ->someOtherFunc(nameA: 23, nameB: 42) + ->endSomething($value, name: $value) + ->endEverything(); diff --git a/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.php b/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.php index 0ae7b72ef4..0cad3efc15 100644 --- a/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.php +++ b/src/Standards/PEAR/Tests/WhiteSpace/ObjectOperatorIndentUnitTest.php @@ -44,6 +44,13 @@ public function getErrorList() 82 => 1, 95 => 1, 103 => 1, + 119 => 2, + 122 => 1, + 131 => 1, + 134 => 1, + 140 => 1, + 141 => 1, + 142 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc b/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc index ce211da453..3f90067920 100644 --- a/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc +++ b/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc @@ -144,3 +144,21 @@ switch ( $a ) { ?> getSummaryCount(); ?>
class="empty"> + + 'a', 2 => 'b' }; + +$match = match ($test) { + 1 => 'a', + 2 => 'b' + }; + +enum Enum +{ +} + +enum Suits {} + +enum Cards +{ + } diff --git a/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc.fixed b/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc.fixed index 05c8e8a258..f369c2b2f6 100644 --- a/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc.fixed +++ b/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc.fixed @@ -148,3 +148,23 @@ switch ( $a ) { getSummaryCount(); ?>
class="empty"> + + 'a', 2 => 'b' +}; + +$match = match ($test) { + 1 => 'a', + 2 => 'b' +}; + +enum Enum +{ +} + +enum Suits { +} + +enum Cards +{ +} diff --git a/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.php b/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.php index 9de3a69ea7..265932549a 100644 --- a/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.php +++ b/src/Standards/PEAR/Tests/WhiteSpace/ScopeClosingBraceUnitTest.php @@ -40,6 +40,10 @@ public function getErrorList() 135 => 1, 141 => 1, 146 => 1, + 149 => 1, + 154 => 1, + 160 => 1, + 164 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PSR1/Sniffs/Classes/ClassDeclarationSniff.php b/src/Standards/PSR1/Sniffs/Classes/ClassDeclarationSniff.php index ac6407d6f1..3db26f6fd1 100644 --- a/src/Standards/PSR1/Sniffs/Classes/ClassDeclarationSniff.php +++ b/src/Standards/PSR1/Sniffs/Classes/ClassDeclarationSniff.php @@ -27,6 +27,7 @@ public function register() T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, ]; }//end register() @@ -50,7 +51,7 @@ public function process(File $phpcsFile, $stackPtr) $errorData = [strtolower($tokens[$stackPtr]['content'])]; - $nextClass = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT], ($tokens[$stackPtr]['scope_closer'] + 1)); + $nextClass = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], ($tokens[$stackPtr]['scope_closer'] + 1)); if ($nextClass !== false) { $error = 'Each %s must be in a file by itself'; $phpcsFile->addError($error, $nextClass, 'MultipleClasses', $errorData); @@ -59,7 +60,7 @@ public function process(File $phpcsFile, $stackPtr) $phpcsFile->recordMetric($stackPtr, 'One class per file', 'yes'); } - $namespace = $phpcsFile->findNext([T_NAMESPACE, T_CLASS, T_INTERFACE, T_TRAIT], 0); + $namespace = $phpcsFile->findNext([T_NAMESPACE, T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], 0); if ($tokens[$namespace]['code'] !== T_NAMESPACE) { $error = 'Each %s must be in a namespace of at least one level (a top-level vendor name)'; $phpcsFile->addError($error, $stackPtr, 'MissingNamespace', $errorData); diff --git a/src/Standards/PSR1/Sniffs/Files/SideEffectsSniff.php b/src/Standards/PSR1/Sniffs/Files/SideEffectsSniff.php index 12508fad4c..7ad52fa708 100644 --- a/src/Standards/PSR1/Sniffs/Files/SideEffectsSniff.php +++ b/src/Standards/PSR1/Sniffs/Files/SideEffectsSniff.php @@ -82,6 +82,7 @@ private function searchForConflict($phpcsFile, $start, $end, $tokens) T_CLASS => T_CLASS, T_INTERFACE => T_INTERFACE, T_TRAIT => T_TRAIT, + T_ENUM => T_ENUM, T_FUNCTION => T_FUNCTION, ]; @@ -168,7 +169,9 @@ private function searchForConflict($phpcsFile, $start, $end, $tokens) } // Ignore function/class prefixes. - if (isset(Tokens::$methodPrefixes[$tokens[$i]['code']]) === true) { + if (isset(Tokens::$methodPrefixes[$tokens[$i]['code']]) === true + || $tokens[$i]['code'] === T_READONLY + ) { continue; } @@ -178,6 +181,14 @@ private function searchForConflict($phpcsFile, $start, $end, $tokens) continue; } + // Ignore attributes. + if ($tokens[$i]['code'] === T_ATTRIBUTE + && isset($tokens[$i]['attribute_closer']) === true + ) { + $i = $tokens[$i]['attribute_closer']; + continue; + } + // Detect and skip over symbols. if (isset($symbols[$tokens[$i]['code']]) === true && isset($tokens[$i]['scope_closer']) === true @@ -193,6 +204,7 @@ private function searchForConflict($phpcsFile, $start, $end, $tokens) ) { $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($i - 1), null, true); if ($tokens[$prev]['code'] !== T_OBJECT_OPERATOR + && $tokens[$prev]['code'] !== T_NULLSAFE_OBJECT_OPERATOR && $tokens[$prev]['code'] !== T_DOUBLE_COLON && $tokens[$prev]['code'] !== T_FUNCTION ) { @@ -216,11 +228,13 @@ private function searchForConflict($phpcsFile, $start, $end, $tokens) && strtolower($tokens[$i]['content']) === 'defined' ) { $openBracket = $phpcsFile->findNext(Tokens::$emptyTokens, ($i + 1), null, true); - if ($tokens[$openBracket]['code'] === T_OPEN_PARENTHESIS + if ($openBracket !== false + && $tokens[$openBracket]['code'] === T_OPEN_PARENTHESIS && isset($tokens[$openBracket]['parenthesis_closer']) === true ) { $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($i - 1), null, true); if ($tokens[$prev]['code'] !== T_OBJECT_OPERATOR + && $tokens[$prev]['code'] !== T_NULLSAFE_OBJECT_OPERATOR && $tokens[$prev]['code'] !== T_DOUBLE_COLON && $tokens[$prev]['code'] !== T_FUNCTION ) { diff --git a/src/Standards/PSR1/Tests/Classes/ClassDeclarationUnitTest.3.inc b/src/Standards/PSR1/Tests/Classes/ClassDeclarationUnitTest.3.inc new file mode 100644 index 0000000000..302908eb12 --- /dev/null +++ b/src/Standards/PSR1/Tests/Classes/ClassDeclarationUnitTest.3.inc @@ -0,0 +1,3 @@ + diff --git a/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.13.inc b/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.13.inc new file mode 100644 index 0000000000..9c1de6267b --- /dev/null +++ b/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.13.inc @@ -0,0 +1,2 @@ +define("MAXSIZE", 100); diff --git a/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.14.inc b/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.14.inc new file mode 100644 index 0000000000..9499885b69 --- /dev/null +++ b/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.14.inc @@ -0,0 +1,2 @@ +define("MAXSIZE", 100); diff --git a/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.15.inc b/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.15.inc new file mode 100644 index 0000000000..0500d10e6b --- /dev/null +++ b/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.15.inc @@ -0,0 +1,2 @@ +defined('MINSIZE') or define("MAXSIZE", 100); diff --git a/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.16.inc b/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.16.inc new file mode 100644 index 0000000000..588ece5840 --- /dev/null +++ b/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.16.inc @@ -0,0 +1,2 @@ +defined('MINSIZE') or define("MAXSIZE", 100); diff --git a/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.php b/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.php index b7728d1074..013bf7ce5a 100644 --- a/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.php +++ b/src/Standards/PSR1/Tests/Files/SideEffectsUnitTest.php @@ -67,6 +67,8 @@ public function getWarningList($testFile='') case 'SideEffectsUnitTest.5.inc': case 'SideEffectsUnitTest.10.inc': case 'SideEffectsUnitTest.12.inc': + case 'SideEffectsUnitTest.15.inc': + case 'SideEffectsUnitTest.16.inc': return [1 => 1]; default: return []; diff --git a/src/Standards/PSR12/Sniffs/Classes/AnonClassDeclarationSniff.php b/src/Standards/PSR12/Sniffs/Classes/AnonClassDeclarationSniff.php index 7a33bd9471..db67fb9428 100644 --- a/src/Standards/PSR12/Sniffs/Classes/AnonClassDeclarationSniff.php +++ b/src/Standards/PSR12/Sniffs/Classes/AnonClassDeclarationSniff.php @@ -21,14 +21,14 @@ class AnonClassDeclarationSniff extends ClassDeclarationSniff /** * The PSR2 MultiLineFunctionDeclarations sniff. * - * @var MultiLineFunctionDeclarationSniff + * @var \PHP_CodeSniffer\Standards\Squiz\Sniffs\Functions\MultiLineFunctionDeclarationSniff */ private $multiLineSniff = null; /** * The Generic FunctionCallArgumentSpacing sniff. * - * @var FunctionCallArgumentSpacingSniff + * @var \PHP_CodeSniffer\Standards\Generic\Sniffs\Functions\FunctionCallArgumentSpacingSniff */ private $functionCallSniff = null; diff --git a/src/Standards/PSR12/Sniffs/Classes/ClassInstantiationSniff.php b/src/Standards/PSR12/Sniffs/Classes/ClassInstantiationSniff.php index f474a26b8d..2298da39f4 100644 --- a/src/Standards/PSR12/Sniffs/Classes/ClassInstantiationSniff.php +++ b/src/Standards/PSR12/Sniffs/Classes/ClassInstantiationSniff.php @@ -44,14 +44,16 @@ public function process(File $phpcsFile, $stackPtr) // Find the class name. $allowed = [ - T_STRING => T_STRING, - T_NS_SEPARATOR => T_NS_SEPARATOR, - T_SELF => T_SELF, - T_STATIC => T_STATIC, - T_VARIABLE => T_VARIABLE, - T_DOLLAR => T_DOLLAR, - T_OBJECT_OPERATOR => T_OBJECT_OPERATOR, - T_DOUBLE_COLON => T_DOUBLE_COLON, + T_STRING => T_STRING, + T_NS_SEPARATOR => T_NS_SEPARATOR, + T_SELF => T_SELF, + T_STATIC => T_STATIC, + T_PARENT => T_PARENT, + T_VARIABLE => T_VARIABLE, + T_DOLLAR => T_DOLLAR, + T_OBJECT_OPERATOR => T_OBJECT_OPERATOR, + T_NULLSAFE_OBJECT_OPERATOR => T_NULLSAFE_OBJECT_OPERATOR, + T_DOUBLE_COLON => T_DOUBLE_COLON, ]; $allowed += Tokens::$emptyTokens; @@ -62,6 +64,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 ) { @@ -71,7 +81,7 @@ public function process(File $phpcsFile, $stackPtr) $classNameEnd = $i; break; - } + }//end for if ($classNameEnd === null) { return; @@ -87,6 +97,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/ClosingBraceSniff.php b/src/Standards/PSR12/Sniffs/Classes/ClosingBraceSniff.php index 0f9752b1f4..fa1f8d60ed 100644 --- a/src/Standards/PSR12/Sniffs/Classes/ClosingBraceSniff.php +++ b/src/Standards/PSR12/Sniffs/Classes/ClosingBraceSniff.php @@ -27,6 +27,7 @@ public function register() T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, T_FUNCTION, ]; diff --git a/src/Standards/PSR12/Sniffs/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/Sniffs/ControlStructures/BooleanOperatorPlacementSniff.php b/src/Standards/PSR12/Sniffs/ControlStructures/BooleanOperatorPlacementSniff.php index ad663db986..b87c33918d 100644 --- a/src/Standards/PSR12/Sniffs/ControlStructures/BooleanOperatorPlacementSniff.php +++ b/src/Standards/PSR12/Sniffs/ControlStructures/BooleanOperatorPlacementSniff.php @@ -37,6 +37,7 @@ public function register() T_WHILE, T_SWITCH, T_ELSEIF, + T_MATCH, ]; }//end register() @@ -90,35 +91,56 @@ public function process(File $phpcsFile, $stackPtr) break; } - $operators[] = $operator; - $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($operator - 1), $parenOpener, true); if ($prev === false) { // Parse error. return; } + $next = $phpcsFile->findNext(T_WHITESPACE, ($operator + 1), $parenCloser, true); + if ($next === false) { + // Parse error. + return; + } + + $firstOnLine = false; + $lastOnLine = false; + if ($tokens[$prev]['line'] < $tokens[$operator]['line']) { // The boolean operator is the first content on the line. - if ($position === null) { - $position = 'first'; - } + $firstOnLine = true; + } - if ($position !== 'first') { - $error = true; - } + if ($tokens[$next]['line'] > $tokens[$operator]['line']) { + // The boolean operator is the last content on the line. + $lastOnLine = true; + } + if ($firstOnLine === true && $lastOnLine === true) { + // The operator is the only content on the line. + // Don't record it because we can't determine + // placement information from looking at it. continue; } - $next = $phpcsFile->findNext(T_WHITESPACE, ($operator + 1), $parenCloser, true); - if ($next === false) { - // Parse error. - return; + $operators[] = $operator; + + if ($firstOnLine === false && $lastOnLine === false) { + // It's in the middle of content, so we can't determine + // placement information from looking at it, but we may + // still need to process it. + continue; } - if ($tokens[$next]['line'] > $tokens[$operator]['line']) { - // The boolean operator is the last content on the line. + if ($firstOnLine === true) { + if ($position === null) { + $position = 'first'; + } + + if ($position !== 'first') { + $error = true; + } + } else { if ($position === null) { $position = 'last'; } @@ -126,8 +148,6 @@ public function process(File $phpcsFile, $stackPtr) if ($position !== 'last') { $error = true; } - - continue; } } while ($operator !== false); @@ -135,8 +155,18 @@ public function process(File $phpcsFile, $stackPtr) return; } - $error = 'Boolean operators between conditions must be at the beginning or end of the line, but not both'; - $fix = $phpcsFile->addFixableError($error, $stackPtr, 'FoundMixed'); + switch ($this->allowOnly) { + case 'first': + $error = 'Boolean operators between conditions must be at the beginning of the line'; + break; + case 'last': + $error = 'Boolean operators between conditions must be at the end of the line'; + break; + default: + $error = 'Boolean operators between conditions must be at the beginning or end of the line, but not both'; + } + + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'FoundMixed'); if ($fix === false) { return; } diff --git a/src/Standards/PSR12/Sniffs/ControlStructures/ControlStructureSpacingSniff.php b/src/Standards/PSR12/Sniffs/ControlStructures/ControlStructureSpacingSniff.php index fb19f5694b..3d29c4ace1 100644 --- a/src/Standards/PSR12/Sniffs/ControlStructures/ControlStructureSpacingSniff.php +++ b/src/Standards/PSR12/Sniffs/ControlStructures/ControlStructureSpacingSniff.php @@ -41,6 +41,7 @@ public function register() T_ELSE, T_ELSEIF, T_CATCH, + T_MATCH, ]; }//end register() diff --git a/src/Standards/PSR12/Sniffs/Files/FileHeaderSniff.php b/src/Standards/PSR12/Sniffs/Files/FileHeaderSniff.php index c9a5955006..c3d0d0cadc 100644 --- a/src/Standards/PSR12/Sniffs/Files/FileHeaderSniff.php +++ b/src/Standards/PSR12/Sniffs/Files/FileHeaderSniff.php @@ -166,11 +166,29 @@ public function getHeaderLines(File $phpcsFile, $stackPtr) } // Make sure this is not a code-level docblock. - $end = $tokens[$next]['comment_closer']; - $docToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($end + 1), null, true); + $end = $tokens[$next]['comment_closer']; + for ($docToken = ($end + 1); $docToken < $phpcsFile->numTokens; $docToken++) { + if (isset(Tokens::$emptyTokens[$tokens[$docToken]['code']]) === true) { + continue; + } + + if ($tokens[$docToken]['code'] === T_ATTRIBUTE + && isset($tokens[$docToken]['attribute_closer']) === true + ) { + $docToken = $tokens[$docToken]['attribute_closer']; + continue; + } + + break; + } + + if ($docToken === $phpcsFile->numTokens) { + $docToken--; + } if (isset($commentOpeners[$tokens[$docToken]['code']]) === false && isset(Tokens::$methodPrefixes[$tokens[$docToken]['code']]) === false + && $tokens[$docToken]['code'] !== T_READONLY ) { // Check for an @var annotation. $annotation = false; diff --git a/src/Standards/PSR12/Sniffs/Functions/NullableTypeDeclarationSniff.php b/src/Standards/PSR12/Sniffs/Functions/NullableTypeDeclarationSniff.php index f4d63d4a36..8d90734311 100644 --- a/src/Standards/PSR12/Sniffs/Functions/NullableTypeDeclarationSniff.php +++ b/src/Standards/PSR12/Sniffs/Functions/NullableTypeDeclarationSniff.php @@ -26,6 +26,7 @@ class NullableTypeDeclarationSniff implements Sniff T_CALLABLE => true, T_SELF => true, T_PARENT => true, + T_STATIC => true, ]; diff --git a/src/Standards/PSR12/Sniffs/Properties/ConstantVisibilitySniff.php b/src/Standards/PSR12/Sniffs/Properties/ConstantVisibilitySniff.php index 8a1000f72f..7f63d1e87a 100644 --- a/src/Standards/PSR12/Sniffs/Properties/ConstantVisibilitySniff.php +++ b/src/Standards/PSR12/Sniffs/Properties/ConstantVisibilitySniff.php @@ -47,7 +47,10 @@ public function process(File $phpcsFile, $stackPtr) return; } - $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + $ignore = Tokens::$emptyTokens; + $ignore[] = T_FINAL; + + $prev = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true); if (isset(Tokens::$scopeModifiers[$tokens[$prev]['code']]) === true) { return; } diff --git a/src/Standards/PSR12/Tests/Classes/AnonClassDeclarationUnitTest.php b/src/Standards/PSR12/Tests/Classes/AnonClassDeclarationUnitTest.php index 3f65602e6e..cc162b290b 100644 --- a/src/Standards/PSR12/Tests/Classes/AnonClassDeclarationUnitTest.php +++ b/src/Standards/PSR12/Tests/Classes/AnonClassDeclarationUnitTest.php @@ -32,7 +32,7 @@ public function getErrorList() 32 => 1, 33 => 1, 34 => 1, - 35 => 2, + 35 => 1, 36 => 1, 37 => 3, 39 => 1, diff --git a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc index dcd849e6bd..9fd1548072 100644 --- a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc +++ b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc @@ -32,3 +32,16 @@ $foo = new $bar['a'] [$baz['a']/* comment */ ['b']]['b']; $a = new self::$transport[$cap_string]; $renderer = new $this->inline_diff_renderer; $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'; +}; + +$foo = new parent(); +$foo = new parent; diff --git a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc.fixed b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc.fixed index d8438dac13..aa9d0c7209 100644 --- a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc.fixed +++ b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.inc.fixed @@ -32,3 +32,16 @@ $foo = new $bar['a'] [$baz['a']/* comment */ ['b']]['b'](); $a = new self::$transport[$cap_string](); $renderer = new $this->inline_diff_renderer(); $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'; +}; + +$foo = new parent(); +$foo = new parent(); diff --git a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.php b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.php index 40c90c8306..0a16af8fc0 100644 --- a/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.php +++ b/src/Standards/PSR12/Tests/Classes/ClassInstantiationUnitTest.php @@ -41,6 +41,9 @@ public function getErrorList() 32 => 1, 33 => 1, 34 => 1, + 37 => 1, + 38 => 1, + 47 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.inc b/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.inc index 1d2e92c97e..2562d26c06 100644 --- a/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.inc +++ b/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.inc @@ -45,3 +45,8 @@ $instance = new class extends \Foo implements \HandleableInterface { $app->get('/hello/{name}', function ($name) use ($app) { return 'Hello ' . $app->escape($name); }); + +enum Foo4 +{ + +}//end diff --git a/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.php b/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.php index 1deac1cdf5..d402f1bb94 100644 --- a/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.php +++ b/src/Standards/PSR12/Tests/Classes/ClosingBraceUnitTest.php @@ -31,6 +31,7 @@ public function getErrorList() 19 => 1, 24 => 1, 31 => 1, + 52 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.inc b/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.inc new file mode 100644 index 0000000000..2c41bde994 --- /dev/null +++ b/src/Standards/PSR12/Tests/Classes/OpeningBraceSpaceUnitTest.inc @@ -0,0 +1,57 @@ + + * @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, + 55 => 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/ControlStructures/BooleanOperatorPlacementUnitTest.inc b/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.inc index 3289f7eee7..4bc2eceba8 100644 --- a/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.inc +++ b/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.inc @@ -108,3 +108,24 @@ if ( ) { // elseif body } + +if ( + ($value == 1 || + $value == 2) + && + ($value == 3 || + $value == 4) +) { + return 5; +} + +// Reset to default. +// phpcs:set PSR12.ControlStructures.BooleanOperatorPlacement allowOnly + +match ( + $expr1 + && $expr2 && + $expr3 +) { + // structure body +}; diff --git a/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.inc.fixed b/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.inc.fixed index 5e8f0c3f9b..5f4d223e84 100644 --- a/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.inc.fixed +++ b/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.inc.fixed @@ -118,3 +118,24 @@ if ( ) { // elseif body } + +if ( + ($value == 1 || + $value == 2) + && + ($value == 3 || + $value == 4) +) { + return 5; +} + +// Reset to default. +// phpcs:set PSR12.ControlStructures.BooleanOperatorPlacement allowOnly + +match ( + $expr1 + && $expr2 + && $expr3 +) { + // structure body +}; diff --git a/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.php b/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.php index ffd4d68574..3eeaeebc42 100644 --- a/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.php +++ b/src/Standards/PSR12/Tests/ControlStructures/BooleanOperatorPlacementUnitTest.php @@ -35,6 +35,7 @@ public function getErrorList() 90 => 1, 98 => 1, 104 => 1, + 125 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc b/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc index 2cdb535650..9c037d6cd3 100644 --- a/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc +++ b/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc @@ -84,3 +84,17 @@ EOD ) { break; } + +match ( + $expr1 && + $expr2 && + $expr3 + ) { + // structure body +}; + +match ($expr1 && +$expr2 && + $expr3) { + // structure body +}; diff --git a/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc.fixed b/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc.fixed index 7f6fbf9b06..7ea61b1e2a 100644 --- a/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc.fixed +++ b/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc.fixed @@ -85,3 +85,19 @@ EOD ) { break; } + +match ( + $expr1 && + $expr2 && + $expr3 +) { + // structure body +}; + +match ( + $expr1 && + $expr2 && + $expr3 +) { + // structure body +}; diff --git a/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.php b/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.php index c2c0ea9251..6ee076f2ea 100644 --- a/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.php +++ b/src/Standards/PSR12/Tests/ControlStructures/ControlStructureSpacingUnitTest.php @@ -41,6 +41,10 @@ public function getErrorList() 48 => 2, 58 => 1, 59 => 1, + 92 => 1, + 96 => 1, + 97 => 1, + 98 => 2, ]; }//end getErrorList() diff --git a/src/Standards/PSR12/Tests/Files/FileHeaderUnitTest.17.inc b/src/Standards/PSR12/Tests/Files/FileHeaderUnitTest.17.inc new file mode 100644 index 0000000000..0b1ebe2315 --- /dev/null +++ b/src/Standards/PSR12/Tests/Files/FileHeaderUnitTest.17.inc @@ -0,0 +1,13 @@ + 2, 58 => 2, 59 => 2, + 87 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PSR12/Tests/Functions/ReturnTypeDeclarationUnitTest.inc b/src/Standards/PSR12/Tests/Functions/ReturnTypeDeclarationUnitTest.inc index 2a68862c32..59ab1aa785 100644 --- a/src/Standards/PSR12/Tests/Functions/ReturnTypeDeclarationUnitTest.inc +++ b/src/Standards/PSR12/Tests/Functions/ReturnTypeDeclarationUnitTest.inc @@ -62,3 +62,5 @@ function functionName(?string $arg1, ?int &$arg2): ?string {} fn (?\DateTime $arg) : ?\DateTime => $arg; + +return (!$a ? [ new class { public function b(): c {} } ] : []); diff --git a/src/Standards/PSR12/Tests/Functions/ReturnTypeDeclarationUnitTest.inc.fixed b/src/Standards/PSR12/Tests/Functions/ReturnTypeDeclarationUnitTest.inc.fixed index 827e03c7b4..cd79f78163 100644 --- a/src/Standards/PSR12/Tests/Functions/ReturnTypeDeclarationUnitTest.inc.fixed +++ b/src/Standards/PSR12/Tests/Functions/ReturnTypeDeclarationUnitTest.inc.fixed @@ -58,3 +58,5 @@ function functionName(?string $arg1, ?int &$arg2): ?string {} function functionName(?string $arg1, ?int &$arg2): ?string {} fn (?\DateTime $arg): ?\DateTime => $arg; + +return (!$a ? [ new class { public function b(): c {} } ] : []); diff --git a/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc b/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc index 82c68f2585..c067e6a2a8 100644 --- a/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc +++ b/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc @@ -56,3 +56,22 @@ function name($a = -1) {} $a =& $ref; $a = [ 'a' => &$something ]; + +$fn = fn(array &$one) => 1; +$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 abab60279d..76764291fa 100644 --- a/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc.fixed +++ b/src/Standards/PSR12/Tests/Operators/OperatorSpacingUnitTest.inc.fixed @@ -56,3 +56,22 @@ function name($a = -1) {} $a =& $ref; $a = [ 'a' => &$something ]; + +$fn = fn(array &$one) => 1; +$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/Properties/ConstantVisibilityUnitTest.inc b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.inc index c07b1b91eb..84ea24b2e8 100644 --- a/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.inc +++ b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.inc @@ -5,3 +5,18 @@ class Foo { } const APPLICATION_ENV = 'development'; + +// Issue 3526, PHP 8.1 final constants. +class SampleEnum +{ + final const FOO = 'SAMPLE'; + + public final const BAR = 'SAMPLE'; + + final private const BAZ = 'SAMPLE'; +} + +enum SomeEnum { + public const BAR = 'bar'; + const BAZ = 'baz'; +} diff --git a/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.php b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.php index 3917eb6821..b738706da2 100644 --- a/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.php +++ b/src/Standards/PSR12/Tests/Properties/ConstantVisibilityUnitTest.php @@ -40,7 +40,11 @@ public function getErrorList() */ public function getWarningList() { - return [4 => 1]; + return [ + 4 => 1, + 12 => 1, + 21 => 1, + ]; }//end getWarningList() diff --git a/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc b/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc index e62489fe03..c8ad746a73 100644 --- a/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc +++ b/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc @@ -207,3 +207,15 @@ class Foo implements Bar */ use Baz; } + +enum SomeEnum1 +{ + use FirstTrait; +} + +enum SomeEnum2 +{ + + use FirstTrait; + +} diff --git a/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc.fixed b/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc.fixed index 0090b4e931..1c5d8185c0 100644 --- a/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc.fixed +++ b/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.inc.fixed @@ -201,3 +201,13 @@ class Foo implements Bar */ use Baz; } + +enum SomeEnum1 +{ + use FirstTrait; +} + +enum SomeEnum2 +{ + use FirstTrait; +} diff --git a/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.php b/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.php index c406805a9b..797a2912e7 100644 --- a/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.php +++ b/src/Standards/PSR12/Tests/Traits/UseDeclarationUnitTest.php @@ -47,6 +47,7 @@ public function getErrorList() 165 => 1, 170 => 1, 208 => 1, + 219 => 3, ]; }//end getErrorList() diff --git a/src/Standards/PSR12/ruleset.xml b/src/Standards/PSR12/ruleset.xml index 312f27fc70..ce8b71a756 100644 --- a/src/Standards/PSR12/ruleset.xml +++ b/src/Standards/PSR12/ruleset.xml @@ -41,11 +41,7 @@ - - - - - + 0 @@ -127,6 +123,7 @@ + diff --git a/src/Standards/PSR2/Docs/Files/ClosingTagStandard.xml b/src/Standards/PSR2/Docs/Files/ClosingTagStandard.xml new file mode 100644 index 0000000000..60d5e7fbaa --- /dev/null +++ b/src/Standards/PSR2/Docs/Files/ClosingTagStandard.xml @@ -0,0 +1,23 @@ + + + + + + + + ]]> + + + ?> + ]]> + + + diff --git a/src/Standards/PSR2/Docs/Methods/FunctionCallSignatureStandard.xml b/src/Standards/PSR2/Docs/Methods/FunctionCallSignatureStandard.xml new file mode 100644 index 0000000000..257bcab0b2 --- /dev/null +++ b/src/Standards/PSR2/Docs/Methods/FunctionCallSignatureStandard.xml @@ -0,0 +1,107 @@ + + + + + + + ($bar, $baz); + ]]> + + + ( $bar, $baz ); + ]]> + + + + + $bar, + $baz +); + ]]> + + + $bar, + $baz +); + ]]> + + + + + ); + ]]> + + + ); + ]]> + + + + + $bar, + $baz +); + ]]> + + + $bar, + $baz +); + ]]> + + + + + $baz +); + ]]> + + + $baz +); + ]]> + + + + + + + + + $baz +); + ]]> + + + diff --git a/src/Standards/PSR2/Docs/Methods/FunctionClosingBraceStandard.xml b/src/Standards/PSR2/Docs/Methods/FunctionClosingBraceStandard.xml new file mode 100644 index 0000000000..3b1b6555e5 --- /dev/null +++ b/src/Standards/PSR2/Docs/Methods/FunctionClosingBraceStandard.xml @@ -0,0 +1,26 @@ + + + + + + + } + ]]> + + + +} + ]]> + + + diff --git a/src/Standards/PSR2/Sniffs/Classes/ClassDeclarationSniff.php b/src/Standards/PSR2/Sniffs/Classes/ClassDeclarationSniff.php index f96b004906..c620582d41 100644 --- a/src/Standards/PSR2/Sniffs/Classes/ClassDeclarationSniff.php +++ b/src/Standards/PSR2/Sniffs/Classes/ClassDeclarationSniff.php @@ -71,7 +71,7 @@ public function processOpen(File $phpcsFile, $stackPtr) $blankSpace = substr($prevContent, strpos($prevContent, $phpcsFile->eolChar)); $spaces = strlen($blankSpace); - if (in_array($tokens[($stackPtr - 2)]['code'], [T_ABSTRACT, T_FINAL], true) === true + if (in_array($tokens[($stackPtr - 2)]['code'], [T_ABSTRACT, T_FINAL, T_READONLY], true) === true && $spaces !== 1 ) { $prevContent = strtolower($tokens[($stackPtr - 2)]['content']); @@ -89,6 +89,7 @@ public function processOpen(File $phpcsFile, $stackPtr) } } else if ($tokens[($stackPtr - 2)]['code'] === T_ABSTRACT || $tokens[($stackPtr - 2)]['code'] === T_FINAL + || $tokens[($stackPtr - 2)]['code'] === T_READONLY ) { $prevContent = strtolower($tokens[($stackPtr - 2)]['content']); $error = 'Expected 1 space between %s and %s keywords; newline found'; diff --git a/src/Standards/PSR2/Sniffs/Classes/PropertyDeclarationSniff.php b/src/Standards/PSR2/Sniffs/Classes/PropertyDeclarationSniff.php index 8a158d966d..aca0be2d0d 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; @@ -117,30 +118,70 @@ protected function processMemberVar(File $phpcsFile, $stackPtr) $phpcsFile->addError($error, $stackPtr, 'ScopeMissing', $data); } + /* + * Note: per PSR-PER section 4.6, the order should be: + * - Inheritance modifier: `abstract` or `final`. + * - Visibility modifier: `public`, `protected`, or `private`. + * - Scope modifier: `static`. + * - Mutation modifier: `readonly`. + * - Type declaration. + * - Name. + * + * Ref: https://www.php-fig.org/per/coding-style/#46-modifier-keywords + * + * At this time (PHP 8.2), inheritance modifiers cannot be applied to properties and + * the `static` and `readonly` modifiers are mutually exclusive and cannot be used together. + * + * Based on that, the below modifier keyword order checks are sufficient (for now). + */ + if ($propertyInfo['scope_specified'] === true && $propertyInfo['is_static'] === true) { $scopePtr = $phpcsFile->findPrevious(Tokens::$scopeModifiers, ($stackPtr - 1)); $staticPtr = $phpcsFile->findPrevious(T_STATIC, ($stackPtr - 1)); - if ($scopePtr < $staticPtr) { - return; - } + if ($scopePtr > $staticPtr) { + $error = 'The static declaration must come after the visibility declaration'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'StaticBeforeVisibility'); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); - $error = 'The static declaration must come after the visibility declaration'; - $fix = $phpcsFile->addFixableError($error, $stackPtr, 'StaticBeforeVisibility'); - if ($fix === true) { - $phpcsFile->fixer->beginChangeset(); + for ($i = ($scopePtr + 1); $scopePtr < $stackPtr; $i++) { + if ($tokens[$i]['code'] !== T_WHITESPACE) { + break; + } - for ($i = ($scopePtr + 1); $scopePtr < $stackPtr; $i++) { - if ($tokens[$i]['code'] !== T_WHITESPACE) { - break; + $phpcsFile->fixer->replaceToken($i, ''); } - $phpcsFile->fixer->replaceToken($i, ''); + $phpcsFile->fixer->replaceToken($scopePtr, ''); + $phpcsFile->fixer->addContentBefore($staticPtr, $propertyInfo['scope'].' '); + + $phpcsFile->fixer->endChangeset(); } + } + }//end if + + if ($propertyInfo['scope_specified'] === true && $propertyInfo['is_readonly'] === true) { + $scopePtr = $phpcsFile->findPrevious(Tokens::$scopeModifiers, ($stackPtr - 1)); + $readonlyPtr = $phpcsFile->findPrevious(T_READONLY, ($stackPtr - 1)); + if ($scopePtr > $readonlyPtr) { + $error = 'The readonly declaration must come after the visibility declaration'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'ReadonlyBeforeVisibility'); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + + for ($i = ($scopePtr + 1); $scopePtr < $stackPtr; $i++) { + if ($tokens[$i]['code'] !== T_WHITESPACE) { + break; + } + + $phpcsFile->fixer->replaceToken($i, ''); + } - $phpcsFile->fixer->replaceToken($scopePtr, ''); - $phpcsFile->fixer->addContentBefore($staticPtr, $propertyInfo['scope'].' '); + $phpcsFile->fixer->replaceToken($scopePtr, ''); + $phpcsFile->fixer->addContentBefore($readonlyPtr, $propertyInfo['scope'].' '); - $phpcsFile->fixer->endChangeset(); + $phpcsFile->fixer->endChangeset(); + } } }//end if diff --git a/src/Standards/PSR2/Sniffs/ControlStructures/ControlStructureSpacingSniff.php b/src/Standards/PSR2/Sniffs/ControlStructures/ControlStructureSpacingSniff.php index 7fba165acc..09d2c14a07 100644 --- a/src/Standards/PSR2/Sniffs/ControlStructures/ControlStructureSpacingSniff.php +++ b/src/Standards/PSR2/Sniffs/ControlStructures/ControlStructureSpacingSniff.php @@ -47,6 +47,7 @@ public function register() T_ELSE, T_ELSEIF, T_CATCH, + T_MATCH, ]; }//end register() diff --git a/src/Standards/PSR2/Sniffs/ControlStructures/SwitchDeclarationSniff.php b/src/Standards/PSR2/Sniffs/ControlStructures/SwitchDeclarationSniff.php index c38a10d2d3..de81c530ee 100644 --- a/src/Standards/PSR2/Sniffs/ControlStructures/SwitchDeclarationSniff.php +++ b/src/Standards/PSR2/Sniffs/ControlStructures/SwitchDeclarationSniff.php @@ -107,13 +107,13 @@ public function process(File $phpcsFile, $stackPtr) } } - $next = $phpcsFile->findNext(T_WHITESPACE, ($opener + 1), null, true); - if ($tokens[$next]['line'] === $tokens[$opener]['line'] - && ($tokens[$next]['code'] === T_COMMENT - || isset(Tokens::$phpcsCommentTokens[$tokens[$next]['code']]) === true) - ) { - // Skip comments on the same line. - $next = $phpcsFile->findNext(T_WHITESPACE, ($next + 1), null, true); + for ($next = ($opener + 1); $next < $nextCloser; $next++) { + if (isset(Tokens::$emptyTokens[$tokens[$next]['code']]) === false + || (isset(Tokens::$commentTokens[$tokens[$next]['code']]) === true + && $tokens[$next]['line'] !== $tokens[$opener]['line']) + ) { + break; + } } if ($tokens[$next]['line'] !== ($tokens[$opener]['line'] + 1)) { @@ -126,6 +126,11 @@ public function process(File $phpcsFile, $stackPtr) } else { $phpcsFile->fixer->beginChangeset(); for ($i = ($opener + 1); $i < $next; $i++) { + if ($tokens[$i]['line'] === $tokens[$opener]['line']) { + // Ignore trailing comments. + continue; + } + if ($tokens[$i]['line'] === $tokens[$next]['line']) { break; } @@ -133,10 +138,9 @@ public function process(File $phpcsFile, $stackPtr) $phpcsFile->fixer->replaceToken($i, ''); } - $phpcsFile->fixer->addNewLineBefore($i); $phpcsFile->fixer->endChangeset(); } - } + }//end if }//end if if ($tokens[$nextCloser]['scope_condition'] === $nextCase) { @@ -152,7 +156,7 @@ public function process(File $phpcsFile, $stackPtr) $phpcsFile->fixer->replaceToken($nextCloser, trim($tokens[$nextCloser]['content'])); } } else { - $diff = ($caseAlignment + $this->indent - $tokens[$nextCloser]['column']); + $diff = ($tokens[$nextCase]['column'] + $this->indent - $tokens[$nextCloser]['column']); if ($diff !== 0) { $error = 'Terminating statement must be indented to the same level as the CASE body'; $fix = $phpcsFile->addFixableError($error, $nextCloser, 'BreakIndent'); @@ -186,7 +190,7 @@ public function process(File $phpcsFile, $stackPtr) $nextCode = $this->findNextCase($phpcsFile, ($opener + 1), $nextCloser); if ($nextCode !== false) { $prevCode = $phpcsFile->findPrevious(T_WHITESPACE, ($nextCode - 1), $nextCase, true); - if ($tokens[$prevCode]['code'] !== T_COMMENT + if (isset(Tokens::$commentTokens[$tokens[$prevCode]['code']]) === false && $this->findNestedTerminator($phpcsFile, ($opener + 1), $nextCode) === false ) { $error = 'There must be a comment when fall-through is intentional in a non-empty case body'; @@ -229,85 +233,159 @@ private function findNextCase($phpcsFile, $stackPtr, $end) /** - * Returns true if a nested terminating statement is found. + * Returns the position of the nested terminating statement. + * + * Returns false if no terminating statement was found. * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $stackPtr The position to start looking at. * @param int $end The position to stop looking at. * - * @return bool + * @return int|false */ private function findNestedTerminator($phpcsFile, $stackPtr, $end) { - $tokens = $phpcsFile->getTokens(); - $terminators = [ - T_RETURN, - T_BREAK, - T_CONTINUE, - T_THROW, - T_EXIT, - ]; - - $lastToken = $phpcsFile->findPrevious(T_WHITESPACE, ($end - 1), $stackPtr, true); - if ($lastToken !== false) { - if ($tokens[$lastToken]['code'] === T_CLOSE_CURLY_BRACKET) { - // We found a closing curly bracket and want to check if its - // block belongs to an IF, ELSEIF or ELSE clause. If yes, we - // continue searching for a terminating statement within that - // block. Note that we have to make sure that every block of - // the entire if/else statement has a terminating statement. - $currentCloser = $lastToken; - $hasElseBlock = false; - do { - $scopeOpener = $tokens[$currentCloser]['scope_opener']; - $scopeCloser = $tokens[$currentCloser]['scope_closer']; - - $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($scopeOpener - 1), $stackPtr, true); - if ($prevToken === false) { + $tokens = $phpcsFile->getTokens(); + + $lastToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($end - 1), $stackPtr, true); + if ($lastToken === false) { + return false; + } + + if ($tokens[$lastToken]['code'] === T_CLOSE_CURLY_BRACKET) { + // We found a closing curly bracket and want to check if its block + // belongs to a SWITCH, IF, ELSEIF or ELSE, TRY, CATCH OR FINALLY clause. + // If yes, we continue searching for a terminating statement within that + // block. Note that we have to make sure that every block of + // the entire if/else/switch statement has a terminating statement. + // For a try/catch/finally statement, either the finally block has + // to have a terminating statement or every try/catch block has to have one. + $currentCloser = $lastToken; + $hasElseBlock = false; + $hasCatchWithoutTerminator = false; + do { + $scopeOpener = $tokens[$currentCloser]['scope_opener']; + $scopeCloser = $tokens[$currentCloser]['scope_closer']; + + $prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($scopeOpener - 1), $stackPtr, true); + if ($prevToken === false) { + return false; + } + + // SWITCH, IF, ELSEIF, CATCH clauses possess a condition we have to account for. + if ($tokens[$prevToken]['code'] === T_CLOSE_PARENTHESIS) { + $prevToken = $tokens[$prevToken]['parenthesis_owner']; + } + + if ($tokens[$prevToken]['code'] === T_IF) { + // If we have not encountered an ELSE clause by now, we cannot + // be sure that the whole statement terminates in every case. + if ($hasElseBlock === false) { return false; } - // IF and ELSEIF clauses possess a condition we have to account for. - if ($tokens[$prevToken]['code'] === T_CLOSE_PARENTHESIS) { - $prevToken = $tokens[$prevToken]['parenthesis_owner']; + return $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser); + } else if ($tokens[$prevToken]['code'] === T_ELSEIF + || $tokens[$prevToken]['code'] === T_ELSE + ) { + // If we find a terminating statement within this block, + // we continue with the previous ELSEIF or IF clause. + $hasTerminator = $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser); + if ($hasTerminator === false) { + return false; } - if ($tokens[$prevToken]['code'] === T_IF) { - // If we have not encountered an ELSE clause by now, we cannot - // be sure that the whole statement terminates in every case. - if ($hasElseBlock === false) { - return false; + $currentCloser = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevToken - 1), $stackPtr, true); + if ($tokens[$prevToken]['code'] === T_ELSE) { + $hasElseBlock = true; + } + } else if ($tokens[$prevToken]['code'] === T_FINALLY) { + // If we find a terminating statement within this block, + // the whole try/catch/finally statement is covered. + $hasTerminator = $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser); + if ($hasTerminator !== false) { + return $hasTerminator; + } + + // Otherwise, we continue with the previous TRY or CATCH clause. + $currentCloser = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevToken - 1), $stackPtr, true); + } else if ($tokens[$prevToken]['code'] === T_TRY) { + // If we've seen CATCH blocks without terminator statement and + // have not seen a FINALLY *with* a terminator statement, we + // don't even need to bother checking the TRY. + if ($hasCatchWithoutTerminator === true) { + return false; + } + + return $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser); + } else if ($tokens[$prevToken]['code'] === T_CATCH) { + // Keep track of seen catch statements without terminating statement, + // but don't bow out yet as there may still be a FINALLY clause + // with a terminating statement before the CATCH. + $hasTerminator = $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser); + if ($hasTerminator === false) { + $hasCatchWithoutTerminator = true; + } + + $currentCloser = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevToken - 1), $stackPtr, true); + } else if ($tokens[$prevToken]['code'] === T_SWITCH) { + $hasDefaultBlock = false; + $endOfSwitch = $tokens[$prevToken]['scope_closer']; + $nextCase = $prevToken; + + // We look for a terminating statement within every blocks. + while (($nextCase = $this->findNextCase($phpcsFile, ($nextCase + 1), $endOfSwitch)) !== false) { + if ($tokens[$nextCase]['code'] === T_DEFAULT) { + $hasDefaultBlock = true; } - return $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser); - } else if ($tokens[$prevToken]['code'] === T_ELSEIF - || $tokens[$prevToken]['code'] === T_ELSE - ) { - // If we find a terminating statement within this block, - // we continue with the previous ELSEIF or IF clause. - $hasTerminator = $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser); + $opener = $tokens[$nextCase]['scope_opener']; + + $nextCode = $phpcsFile->findNext(Tokens::$emptyTokens, ($opener + 1), $endOfSwitch, true); + if ($tokens[$nextCode]['code'] === T_CASE || $tokens[$nextCode]['code'] === T_DEFAULT) { + // This case statement has no content, so skip it. + continue; + } + + $endOfCase = $this->findNextCase($phpcsFile, ($opener + 1), $endOfSwitch); + if ($endOfCase === false) { + $endOfCase = $endOfSwitch; + } + + $hasTerminator = $this->findNestedTerminator($phpcsFile, ($opener + 1), $endOfCase); if ($hasTerminator === false) { return false; } + }//end while - $currentCloser = $phpcsFile->findPrevious(T_WHITESPACE, ($prevToken - 1), $stackPtr, true); - if ($tokens[$prevToken]['code'] === T_ELSE) { - $hasElseBlock = true; - } - } else { + // If we have not encountered a DEFAULT block by now, we cannot + // be sure that the whole statement terminates in every case. + if ($hasDefaultBlock === false) { return false; - }//end if - } while ($currentCloser !== false && $tokens[$currentCloser]['code'] === T_CLOSE_CURLY_BRACKET); - - return true; - } else if ($tokens[$lastToken]['code'] === T_SEMICOLON) { - // We found the last statement of the CASE. Now we want to - // check whether it is a terminating one. - $terminator = $phpcsFile->findStartOfStatement(($lastToken - 1)); - if (in_array($tokens[$terminator]['code'], $terminators, true) === true) { - return $terminator; - } - }//end if + } + + return $hasTerminator; + } else { + return false; + }//end if + } while ($currentCloser !== false && $tokens[$currentCloser]['code'] === T_CLOSE_CURLY_BRACKET); + + return true; + } else if ($tokens[$lastToken]['code'] === T_SEMICOLON) { + // We found the last statement of the CASE. Now we want to + // check whether it is a terminating one. + $terminators = [ + T_RETURN => T_RETURN, + T_BREAK => T_BREAK, + T_CONTINUE => T_CONTINUE, + T_THROW => T_THROW, + T_EXIT => T_EXIT, + ]; + + $terminator = $phpcsFile->findStartOfStatement(($lastToken - 1)); + if (isset($terminators[$tokens[$terminator]['code']]) === true) { + return $terminator; + } }//end if return false; diff --git a/src/Standards/PSR2/Sniffs/Files/EndFileNewlineSniff.php b/src/Standards/PSR2/Sniffs/Files/EndFileNewlineSniff.php index 241fa5a8f4..aed461f83f 100644 --- a/src/Standards/PSR2/Sniffs/Files/EndFileNewlineSniff.php +++ b/src/Standards/PSR2/Sniffs/Files/EndFileNewlineSniff.php @@ -23,7 +23,10 @@ class EndFileNewlineSniff implements Sniff */ public function register() { - return [T_OPEN_TAG]; + return [ + T_OPEN_TAG, + T_OPEN_TAG_WITH_ECHO, + ]; }//end register() diff --git a/src/Standards/PSR2/Sniffs/Methods/FunctionCallSignatureSniff.php b/src/Standards/PSR2/Sniffs/Methods/FunctionCallSignatureSniff.php index d7a6522781..406f27948a 100644 --- a/src/Standards/PSR2/Sniffs/Methods/FunctionCallSignatureSniff.php +++ b/src/Standards/PSR2/Sniffs/Methods/FunctionCallSignatureSniff.php @@ -48,7 +48,7 @@ public function isMultiLineCall(File $phpcsFile, $stackPtr, $openBracket, $token $closeBracket = $tokens[$openBracket]['parenthesis_closer']; - $end = $phpcsFile->findEndOfStatement($openBracket + 1); + $end = $phpcsFile->findEndOfStatement(($openBracket + 1), [T_COLON]); while ($tokens[$end]['code'] === T_COMMA) { // If the next bit of code is not on the same line, this is a // multi-line function call. @@ -61,7 +61,7 @@ public function isMultiLineCall(File $phpcsFile, $stackPtr, $openBracket, $token return true; } - $end = $phpcsFile->findEndOfStatement($next); + $end = $phpcsFile->findEndOfStatement($next, [T_COLON]); } // We've reached the last argument, so see if the next content diff --git a/src/Standards/PSR2/Sniffs/Namespaces/NamespaceDeclarationSniff.php b/src/Standards/PSR2/Sniffs/Namespaces/NamespaceDeclarationSniff.php index 9607dccee2..bf37af09bb 100644 --- a/src/Standards/PSR2/Sniffs/Namespaces/NamespaceDeclarationSniff.php +++ b/src/Standards/PSR2/Sniffs/Namespaces/NamespaceDeclarationSniff.php @@ -11,6 +11,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Tokens; class NamespaceDeclarationSniff implements Sniff { @@ -41,6 +42,12 @@ public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if ($tokens[$nextNonEmpty]['code'] === T_NS_SEPARATOR) { + // Namespace keyword as operator. Not a declaration. + return; + } + $end = $phpcsFile->findEndOfStatement($stackPtr); for ($i = ($end + 1); $i < ($phpcsFile->numTokens - 1); $i++) { if ($tokens[$i]['line'] === $tokens[$end]['line']) { diff --git a/src/Standards/PSR2/Sniffs/Namespaces/UseDeclarationSniff.php b/src/Standards/PSR2/Sniffs/Namespaces/UseDeclarationSniff.php index ea26657b03..aba9caa717 100644 --- a/src/Standards/PSR2/Sniffs/Namespaces/UseDeclarationSniff.php +++ b/src/Standards/PSR2/Sniffs/Namespaces/UseDeclarationSniff.php @@ -77,6 +77,10 @@ public function process(File $phpcsFile, $stackPtr) $baseUse = 'use'; } + if ($tokens[($next + 1)]['code'] !== T_WHITESPACE) { + $baseUse .= ' '; + } + $phpcsFile->fixer->replaceToken($next, ';'.$phpcsFile->eolChar.$baseUse); } } else { @@ -281,7 +285,7 @@ private function shouldIgnoreUse($phpcsFile, $stackPtr) } // Ignore USE keywords for traits. - if ($phpcsFile->hasCondition($stackPtr, [T_CLASS, T_TRAIT]) === true) { + if ($phpcsFile->hasCondition($stackPtr, [T_CLASS, T_TRAIT, T_ENUM]) === true) { return true; } diff --git a/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.inc b/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.inc index 13e596e17a..dd78809366 100644 --- a/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.inc +++ b/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.inc @@ -239,3 +239,12 @@ C8 foo(new class { }); + +readonly +class Test +{ +} + +readonly class Test +{ +} diff --git a/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.inc.fixed b/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.inc.fixed index 3ee394e789..3be473639e 100644 --- a/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.inc.fixed +++ b/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.inc.fixed @@ -232,3 +232,11 @@ class C8 foo(new class { }); + +readonly class Test +{ +} + +readonly class Test +{ +} diff --git a/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.php b/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.php index 635ac710ba..95c225edb4 100644 --- a/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.php +++ b/src/Standards/PSR2/Tests/Classes/ClassDeclarationUnitTest.php @@ -63,6 +63,8 @@ public function getErrorList() 216 => 1, 231 => 2, 235 => 1, + 244 => 1, + 248 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc index 031d2a8378..3e086c6f22 100644 --- a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc +++ b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc @@ -71,3 +71,17 @@ 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; + + readonly public int $wrongOrder1; + + readonly protected ?string $wrongOrder2; +} diff --git a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed index aca7c2fcc3..c4e22fc18b 100644 --- a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed +++ b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.inc.fixed @@ -68,3 +68,17 @@ 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; + + public readonly int $wrongOrder1; + + protected readonly ?string $wrongOrder2; +} diff --git a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php index 20da24d976..6a1778b904 100644 --- a/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php +++ b/src/Standards/PSR2/Tests/Classes/PropertyDeclarationUnitTest.php @@ -46,6 +46,11 @@ public function getErrorList() 69 => 1, 71 => 1, 72 => 1, + 76 => 1, + 80 => 1, + 82 => 1, + 84 => 1, + 86 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc b/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc index 24de0dc1ea..542ab3cf2c 100644 --- a/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc +++ b/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc @@ -68,3 +68,14 @@ if ($expr1 && $expr2 /* comment */ ) { } + +$r = match ($x) {}; +$r = match ( $x ) {}; + +// phpcs:set PSR2.ControlStructures.ControlStructureSpacing requiredSpacesAfterOpen 1 +// phpcs:set PSR2.ControlStructures.ControlStructureSpacing requiredSpacesBeforeClose 1 +$r = match ($x) {}; +$r = match ( $x ) {}; +$r = match ( $x ) {}; +// phpcs:set PSR2.ControlStructures.ControlStructureSpacing requiredSpacesAfterOpen 0 +// phpcs:set PSR2.ControlStructures.ControlStructureSpacing requiredSpacesBeforeClose 0 diff --git a/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc.fixed b/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc.fixed index f57e792d1d..a29534beed 100644 --- a/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc.fixed +++ b/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.inc.fixed @@ -67,3 +67,14 @@ if ($expr1 && $expr2 /* comment */) { } + +$r = match ($x) {}; +$r = match ($x) {}; + +// phpcs:set PSR2.ControlStructures.ControlStructureSpacing requiredSpacesAfterOpen 1 +// phpcs:set PSR2.ControlStructures.ControlStructureSpacing requiredSpacesBeforeClose 1 +$r = match ( $x ) {}; +$r = match ( $x ) {}; +$r = match ( $x ) {}; +// phpcs:set PSR2.ControlStructures.ControlStructureSpacing requiredSpacesAfterOpen 0 +// phpcs:set PSR2.ControlStructures.ControlStructureSpacing requiredSpacesBeforeClose 0 diff --git a/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.php b/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.php index b7b251992d..35d5e51508 100644 --- a/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.php +++ b/src/Standards/PSR2/Tests/ControlStructures/ControlStructureSpacingUnitTest.php @@ -36,6 +36,9 @@ public function getErrorList() 60 => 1, 64 => 1, 69 => 1, + 73 => 2, + 77 => 2, + 79 => 2, ]; }//end getErrorList() diff --git a/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc b/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc index af605cd71d..2ca60a93e8 100644 --- a/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc +++ b/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc @@ -271,3 +271,328 @@ $foo = $foo ? } } : null; + +switch ($foo) { +case Foo::INTERFACE: + echo '1'; + return self::INTERFACE; +case Foo::TRAIT: +case Foo::ARRAY: + echo '1'; + return self::VALUE; +} + +// OK: Every clause terminates +switch ($foo) { + case 1: + switch ($bar) { + case 1: + return 1; + default: + return 3; + } + case 2: + return 2; +} + +// KO: Not every clause terminates +switch ($foo) { + case 1: + switch ($bar) { + case 1: + return; + } + case 2: + return 2; +} + +// KO: Not every clause terminates +switch ($foo) { + case 1: + switch ($bar) { + case 1: + return; + default: + $a = 1; + } + case 2: + return 2; +} + +// OK: Every clause terminates +switch ($foo) { + case 1: + switch ($bar) { + case 1: + return 1; + default: + throw new \Exception(); + } + case 2: + return 2; +} + +switch ($foo) { + case 1: + // phpcs:ignore + case 2: + return 1; + case 3: + return 2; +} + +// Issue 3352. +switch ( $test ) { + case 2: // comment followed by empty line + + break; + + case 3: /* phpcs:ignore Stnd.Cat.SniffName -- Verify correct handling of ignore comments. */ + + + + break; + + case 4: /** inline docblock */ + + + + break; + + case 5: /* checking how it handles */ /* two trailing comments */ + + break; + + case 6: + // Comment as first content of the body. + + break; + + case 7: + /* phpcs:ignore Stnd.Cat.SniffName -- Verify correct handling of ignore comments at start of body. */ + + break; + + case 8: + /** inline docblock */ + + break; +} + +// Handle comments correctly. +switch ($foo) { + case 1: + if ($bar > 0) { + doSomething(); + } + // Comment + else { + return 1; + } + case 2: + return 2; +} + +switch ($foo) { + case 1: + if ($bar > 0) /*comment*/ { + return doSomething(); + } + else { + return 1; + } + case 2: + return 2; +} + +// Issue #3297. +// Okay - finally will always be executed, so all branches are covered by the `return` in finally. +switch ( $a ) { + case 1: + try { + doSomething(); + } catch (Exception $e) { + doSomething(); + } catch (AnotherException $e) { + doSomething(); + } finally { + return true; + } + default: + $other = $code; + break; +} + +// Okay - all - non-finally - branches have a terminating statement. +switch ( $a ) { + case 1: + try { + return false; + } catch (Exception $e) /*comment*/ { + return true; + } + // Comment + catch (AnotherException $e) { + return true; + } finally { + doSomething(); + } + default: + $other = $code; + break; +} + +// Okay - finally will always be executed, so all branches are covered by the `return` in finally. +// Non-standard structure order. +switch ( $a ) { + case 1: + try { + doSomething(); + } catch (Exception $e) { + doSomething(); + } finally { + return true; + } catch (AnotherException $e) { + doSomething(); + } + default: + $other = $code; + break; +} + +// Okay - all - non-finally - branches have a terminating statement. +// Non-standard structure order. +switch ( $a ) { + case 1: + try { + return false; + } finally { + doSomething(); + } catch (MyException $e) { + return true; + } catch (AnotherException $e) { + return true; + } + default: + $other = $code; + break; +} + +// All okay, no finally. Any exception still uncaught will terminate the case anyhow, so we're good. +switch ( $a ) { + case 1: + try { + return false; + } catch (MyException $e) { + return true; + } catch (AnotherException $e) { + return true; + } + default: + $other = $code; + break; +} + +// All okay, no catch +switch ( $a ) { + case 1: + try { + return true; + } finally { + doSomething(); + } + case 2: + $other = $code; + break; +} + +// All okay, try-catch nested in if. +switch ( $a ) { + case 1: + if ($a) { + try { + return true; // Comment. + } catch (MyException $e) { + throw new Exception($e->getMessage()); + } + } else { + return true; + } + case 2: + $other = $code; + break; +} + +// Missing fall-through comment. +switch ( $a ) { + case 1: + try { + doSomething(); + } finally { + doSomething(); + } + case 2: + $other = $code; + break; +} + +// Missing fall-through comment. One of the catches does not have a terminating statement. +switch ( $a ) { + case 1: + try { + return false; + } catch (Exception $e) { + doSomething(); + } catch (AnotherException $e) { + return true; + } finally { + doSomething(); + } + default: + $other = $code; + break; +} + +// Missing fall-through comment. Try does not have a terminating statement. +switch ( $a ) { + case 1: + try { + doSomething(); + } finally { + doSomething(); + } catch (Exception $e) { + return true; + } catch (AnotherException $e) { + return true; + } + default: + $other = $code; + break; +} + +// Missing fall-through comment. One of the catches does not have a terminating statement. +switch ( $a ) { + case 1: + try { + return false; + } catch (Exception $e) { + doSomething(); + } catch (AnotherException $e) { + return true; + } + default: + $other = $code; + break; +} + +// Issue 3550 - comment after terminating statement. +switch (rand()) { + case 1: + if (rand() === 1) { + break; + } else { + break; // comment + } + default: + break; +} diff --git a/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc.fixed b/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc.fixed index d5671feea3..bbc8b7c48e 100644 --- a/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc.fixed +++ b/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.inc.fixed @@ -274,3 +274,320 @@ $foo = $foo ? } } : null; + +switch ($foo) { +case Foo::INTERFACE: + echo '1'; + return self::INTERFACE; +case Foo::TRAIT: +case Foo::ARRAY: + echo '1'; + return self::VALUE; +} + +// OK: Every clause terminates +switch ($foo) { + case 1: + switch ($bar) { + case 1: + return 1; + default: + return 3; + } + case 2: + return 2; +} + +// KO: Not every clause terminates +switch ($foo) { + case 1: + switch ($bar) { + case 1: + return; + } + case 2: + return 2; +} + +// KO: Not every clause terminates +switch ($foo) { + case 1: + switch ($bar) { + case 1: + return; + default: + $a = 1; + } + case 2: + return 2; +} + +// OK: Every clause terminates +switch ($foo) { + case 1: + switch ($bar) { + case 1: + return 1; + default: + throw new \Exception(); + } + case 2: + return 2; +} + +switch ($foo) { + case 1: + // phpcs:ignore + case 2: + return 1; + case 3: + return 2; +} + +// Issue 3352. +switch ( $test ) { + case 2: // comment followed by empty line + break; + + case 3: /* phpcs:ignore Stnd.Cat.SniffName -- Verify correct handling of ignore comments. */ + break; + + case 4: /** inline docblock */ + break; + + case 5: /* checking how it handles */ /* two trailing comments */ + break; + + case 6: + // Comment as first content of the body. + + break; + + case 7: + /* phpcs:ignore Stnd.Cat.SniffName -- Verify correct handling of ignore comments at start of body. */ + + break; + + case 8: + /** inline docblock */ + + break; +} + +// Handle comments correctly. +switch ($foo) { + case 1: + if ($bar > 0) { + doSomething(); + } + // Comment + else { + return 1; + } + case 2: + return 2; +} + +switch ($foo) { + case 1: + if ($bar > 0) /*comment*/ { + return doSomething(); + } + else { + return 1; + } + case 2: + return 2; +} + +// Issue #3297. +// Okay - finally will always be executed, so all branches are covered by the `return` in finally. +switch ( $a ) { + case 1: + try { + doSomething(); + } catch (Exception $e) { + doSomething(); + } catch (AnotherException $e) { + doSomething(); + } finally { + return true; + } + default: + $other = $code; + break; +} + +// Okay - all - non-finally - branches have a terminating statement. +switch ( $a ) { + case 1: + try { + return false; + } catch (Exception $e) /*comment*/ { + return true; + } + // Comment + catch (AnotherException $e) { + return true; + } finally { + doSomething(); + } + default: + $other = $code; + break; +} + +// Okay - finally will always be executed, so all branches are covered by the `return` in finally. +// Non-standard structure order. +switch ( $a ) { + case 1: + try { + doSomething(); + } catch (Exception $e) { + doSomething(); + } finally { + return true; + } catch (AnotherException $e) { + doSomething(); + } + default: + $other = $code; + break; +} + +// Okay - all - non-finally - branches have a terminating statement. +// Non-standard structure order. +switch ( $a ) { + case 1: + try { + return false; + } finally { + doSomething(); + } catch (MyException $e) { + return true; + } catch (AnotherException $e) { + return true; + } + default: + $other = $code; + break; +} + +// All okay, no finally. Any exception still uncaught will terminate the case anyhow, so we're good. +switch ( $a ) { + case 1: + try { + return false; + } catch (MyException $e) { + return true; + } catch (AnotherException $e) { + return true; + } + default: + $other = $code; + break; +} + +// All okay, no catch +switch ( $a ) { + case 1: + try { + return true; + } finally { + doSomething(); + } + case 2: + $other = $code; + break; +} + +// All okay, try-catch nested in if. +switch ( $a ) { + case 1: + if ($a) { + try { + return true; // Comment. + } catch (MyException $e) { + throw new Exception($e->getMessage()); + } + } else { + return true; + } + case 2: + $other = $code; + break; +} + +// Missing fall-through comment. +switch ( $a ) { + case 1: + try { + doSomething(); + } finally { + doSomething(); + } + case 2: + $other = $code; + break; +} + +// Missing fall-through comment. One of the catches does not have a terminating statement. +switch ( $a ) { + case 1: + try { + return false; + } catch (Exception $e) { + doSomething(); + } catch (AnotherException $e) { + return true; + } finally { + doSomething(); + } + default: + $other = $code; + break; +} + +// Missing fall-through comment. Try does not have a terminating statement. +switch ( $a ) { + case 1: + try { + doSomething(); + } finally { + doSomething(); + } catch (Exception $e) { + return true; + } catch (AnotherException $e) { + return true; + } + default: + $other = $code; + break; +} + +// Missing fall-through comment. One of the catches does not have a terminating statement. +switch ( $a ) { + case 1: + try { + return false; + } catch (Exception $e) { + doSomething(); + } catch (AnotherException $e) { + return true; + } + default: + $other = $code; + break; +} + +// Issue 3550 - comment after terminating statement. +switch (rand()) { + case 1: + if (rand() === 1) { + break; + } else { + break; // comment + } + default: + break; +} diff --git a/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.php b/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.php index 9c1735563f..0cd946d888 100644 --- a/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.php +++ b/src/Standards/PSR2/Tests/ControlStructures/SwitchDeclarationUnitTest.php @@ -47,6 +47,17 @@ public function getErrorList() 224 => 1, 236 => 1, 260 => 1, + 300 => 1, + 311 => 1, + 346 => 1, + 350 => 1, + 356 => 1, + 362 => 1, + 384 => 1, + 528 => 1, + 541 => 1, + 558 => 1, + 575 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.11.inc b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.11.inc new file mode 100644 index 0000000000..4f2e47af9c --- /dev/null +++ b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.11.inc @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.12.inc.fixed b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.12.inc.fixed new file mode 100644 index 0000000000..d3c19feeb2 --- /dev/null +++ b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.12.inc.fixed @@ -0,0 +1 @@ + diff --git a/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.13.inc b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.13.inc new file mode 100644 index 0000000000..fa2f476a92 --- /dev/null +++ b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.13.inc @@ -0,0 +1,5 @@ + diff --git a/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.php b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.php index a80c16dbed..456106fe5a 100644 --- a/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.php +++ b/src/Standards/PSR2/Tests/Files/EndFileNewlineUnitTest.php @@ -35,6 +35,10 @@ public function getErrorList($testFile='') case 'EndFileNewlineUnitTest.9.inc': case 'EndFileNewlineUnitTest.10.inc': return [2 => 1]; + case 'EndFileNewlineUnitTest.11.inc': + case 'EndFileNewlineUnitTest.12.inc': + case 'EndFileNewlineUnitTest.13.inc': + return [1 => 1]; default: return []; }//end switch diff --git a/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.inc b/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.inc index dcaf00bd6a..1ca477d054 100644 --- a/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.inc +++ b/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.inc @@ -242,3 +242,26 @@ function (array $term) use ($mode): string { }, $search )); + +// PHP 8.0 named parameters. +array_fill_keys( + keys: range( + 1, + 12, + ), + value: true, +); + +array_fill_keys( + keys: range( 1, + 12, + ), value: true, +); + +// phpcs:set PSR2.Methods.FunctionCallSignature allowMultipleArguments true +array_fill_keys( + keys: range( 1, + 12, + ), value: true, +); +// phpcs:set PSR2.Methods.FunctionCallSignature allowMultipleArguments false diff --git a/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.inc.fixed b/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.inc.fixed index f61b479c50..dc383ed2a7 100644 --- a/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.inc.fixed +++ b/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.inc.fixed @@ -255,3 +255,29 @@ return trim(preg_replace_callback( }, $search )); + +// PHP 8.0 named parameters. +array_fill_keys( + keys: range( + 1, + 12, + ), + value: true, +); + +array_fill_keys( + keys: range( + 1, + 12, + ), + value: true, +); + +// phpcs:set PSR2.Methods.FunctionCallSignature allowMultipleArguments true +array_fill_keys( + keys: range( + 1, + 12, + ), value: true, +); +// phpcs:set PSR2.Methods.FunctionCallSignature allowMultipleArguments false diff --git a/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.php b/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.php index c6862ca17f..1d87825891 100644 --- a/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.php +++ b/src/Standards/PSR2/Tests/Methods/FunctionCallSignatureUnitTest.php @@ -35,20 +35,19 @@ public function getErrorList() 103 => 1, 111 => 1, 117 => 4, - 121 => 1, - 125 => 1, - 129 => 1, - 133 => 1, - 138 => 1, - 146 => 1, - 150 => 1, - 154 => 1, - 158 => 1, - 162 => 1, - 167 => 1, - 172 => 1, + 123 => 1, + 127 => 1, + 131 => 1, + 136 => 1, + 143 => 1, + 148 => 1, + 152 => 1, + 156 => 1, + 160 => 1, + 165 => 1, + 170 => 1, 175 => 1, - 178 => 1, + 178 => 2, 186 => 1, 187 => 1, 194 => 3, @@ -67,6 +66,11 @@ public function getErrorList() 234 => 1, 242 => 1, 243 => 1, + 256 => 1, + 257 => 1, + 258 => 1, + 263 => 1, + 264 => 1, ]; }//end getErrorList() diff --git a/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc b/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc index 21c03119dd..096b44bc8a 100644 --- a/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc +++ b/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc @@ -64,3 +64,12 @@ class Nested_Function { }; } } + +enum MyEnum +{ + function _myFunction() {} + function __myFunction() {} + public static function myFunction() {} + static public function myFunction() {} + public function _() {} +} diff --git a/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc.fixed b/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc.fixed index 5fb88613ed..eae8d28f5c 100644 --- a/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc.fixed +++ b/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.inc.fixed @@ -64,3 +64,12 @@ class Nested_Function { }; } } + +enum MyEnum +{ + function _myFunction() {} + function __myFunction() {} + public static function myFunction() {} + public static function myFunction() {} + public function _() {} +} diff --git a/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.php b/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.php index 2d508d4c62..a8dcdfea58 100644 --- a/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.php +++ b/src/Standards/PSR2/Tests/Methods/MethodDeclarationUnitTest.php @@ -40,6 +40,7 @@ public function getErrorList() 54 => 1, 56 => 3, 63 => 2, + 73 => 1, ]; }//end getErrorList() @@ -61,6 +62,7 @@ public function getWarningList() 30 => 1, 46 => 1, 63 => 1, + 70 => 1, ]; }//end getWarningList() diff --git a/src/Standards/PSR2/Tests/Namespaces/NamespaceDeclarationUnitTest.inc b/src/Standards/PSR2/Tests/Namespaces/NamespaceDeclarationUnitTest.inc index 2500b6448e..703393396f 100644 --- a/src/Standards/PSR2/Tests/Namespaces/NamespaceDeclarationUnitTest.inc +++ b/src/Standards/PSR2/Tests/Namespaces/NamespaceDeclarationUnitTest.inc @@ -20,3 +20,7 @@ namespace Vendor\ Package; namespace Vendor\Package; + +$call = namespace\function_name(); +echo namespace\CONSTANT_NAME; +// Something which is not a blank line. diff --git a/src/Standards/PSR2/Tests/Namespaces/NamespaceDeclarationUnitTest.inc.fixed b/src/Standards/PSR2/Tests/Namespaces/NamespaceDeclarationUnitTest.inc.fixed index a98985d117..0b39bfa086 100644 --- a/src/Standards/PSR2/Tests/Namespaces/NamespaceDeclarationUnitTest.inc.fixed +++ b/src/Standards/PSR2/Tests/Namespaces/NamespaceDeclarationUnitTest.inc.fixed @@ -22,3 +22,7 @@ namespace Vendor\ Package; namespace Vendor\Package; + +$call = namespace\function_name(); +echo namespace\CONSTANT_NAME; +// Something which is not a blank line. diff --git a/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.1.inc b/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.1.inc index c4e83da440..61befc9e73 100644 --- a/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.1.inc +++ b/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.1.inc @@ -30,9 +30,14 @@ trait HelloWorld use Hello, World; } +enum SomeEnum +{ + use Hello, World; +} + $x = $foo ? function ($foo) use /* comment */ ($bar): int { return 1; } : $bar; // Testcase must be on last line in the file. -use \ No newline at end of file +use diff --git a/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.2.inc b/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.2.inc index 068b5233f1..bc0d7a961e 100644 --- a/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.2.inc +++ b/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.2.inc @@ -6,6 +6,10 @@ use My\Full\Classname as Another, My\Full\NSname; use function My\Full\functionname as somefunction, My\Full\otherfunction; use const My\Full\constantname as someconstant, My\Full\otherconstant; +use BarClass as Bar,FooClass,BazClass as Baz; +use function My\Full\functionname as somefunction,My\Full\otherfunction; +use const My\Full\constantname as someconstant,My\Full\otherconstant; + namespace AnotherProject; diff --git a/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.2.inc.fixed b/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.2.inc.fixed index 21e574ddb2..6579613b12 100644 --- a/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.2.inc.fixed +++ b/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.2.inc.fixed @@ -9,6 +9,14 @@ use function My\Full\otherfunction; use const My\Full\constantname as someconstant; use const My\Full\otherconstant; +use BarClass as Bar; +use FooClass; +use BazClass as Baz; +use function My\Full\functionname as somefunction; +use function My\Full\otherfunction; +use const My\Full\constantname as someconstant; +use const My\Full\otherconstant; + namespace AnotherProject; diff --git a/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.php b/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.php index b0b47e3530..b424a2c113 100644 --- a/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.php +++ b/src/Standards/PSR2/Tests/Namespaces/UseDeclarationUnitTest.php @@ -34,7 +34,10 @@ public function getErrorList($testFile='') 5 => 1, 6 => 1, 7 => 1, - 12 => 1, + 9 => 1, + 10 => 1, + 11 => 1, + 16 => 1, ]; case 'UseDeclarationUnitTest.3.inc': return [ diff --git a/src/Standards/Squiz/Sniffs/Arrays/ArrayDeclarationSniff.php b/src/Standards/Squiz/Sniffs/Arrays/ArrayDeclarationSniff.php index de5ee8535d..64d5d64da2 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) { @@ -429,9 +430,13 @@ public function processMultiLineArray($phpcsFile, $stackPtr, $arrayStart, $array } if ($keyUsed === true && $tokens[$lastToken]['code'] === T_COMMA) { - $error = 'No key specified for array entry; first entry specifies key'; - $phpcsFile->addError($error, $nextToken, 'NoKeySpecified'); - return; + $nextToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($lastToken + 1), null, true); + // Allow for PHP 7.4+ array unpacking within an array declaration. + if ($tokens[$nextToken]['code'] !== T_ELLIPSIS) { + $error = 'No key specified for array entry; first entry specifies key'; + $phpcsFile->addError($error, $nextToken, 'NoKeySpecified'); + return; + } } if ($keyUsed === false) { @@ -450,9 +455,14 @@ public function processMultiLineArray($phpcsFile, $stackPtr, $arrayStart, $array $error = 'Expected 0 spaces before comma; %s found'; $data = [$spaceLength]; - $fix = $phpcsFile->addFixableError($error, $nextToken, 'SpaceBeforeComma', $data); - if ($fix === true) { - $phpcsFile->fixer->replaceToken(($nextToken - 1), ''); + // The error is only fixable if there is only whitespace between the tokens. + if ($prev === $phpcsFile->findPrevious(T_WHITESPACE, ($nextToken - 1), null, true)) { + $fix = $phpcsFile->addFixableError($error, $nextToken, 'SpaceBeforeComma', $data); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($nextToken - 1), ''); + } + } else { + $phpcsFile->addError($error, $nextToken, 'SpaceBeforeComma', $data); } } }//end if @@ -464,8 +474,17 @@ public function processMultiLineArray($phpcsFile, $stackPtr, $arrayStart, $array true ); - $indices[] = ['value' => $valueContent]; - $singleUsed = true; + $indices[] = ['value' => $valueContent]; + $usesArrayUnpacking = $phpcsFile->findPrevious( + Tokens::$emptyTokens, + ($nextToken - 2), + null, + true + ); + if ($tokens[$usesArrayUnpacking]['code'] !== T_ELLIPSIS) { + // Don't decide if an array is key => value indexed or not when PHP 7.4+ array unpacking is used. + $singleUsed = true; + } }//end if $lastToken = $nextToken; @@ -620,7 +639,19 @@ public function processMultiLineArray($phpcsFile, $stackPtr, $arrayStart, $array $valuePointer = $value['value']; - $previous = $phpcsFile->findPrevious([T_WHITESPACE, T_COMMA], ($valuePointer - 1), ($arrayStart + 1), true); + $ignoreTokens = [ + T_WHITESPACE => T_WHITESPACE, + T_COMMA => T_COMMA, + ]; + $ignoreTokens += Tokens::$castTokens; + + if ($tokens[$valuePointer]['code'] === T_CLOSURE + || $tokens[$valuePointer]['code'] === T_FN + ) { + $ignoreTokens += [T_STATIC => T_STATIC]; + } + + $previous = $phpcsFile->findPrevious($ignoreTokens, ($valuePointer - 1), ($arrayStart + 1), true); if ($previous === false) { $previous = $stackPtr; } @@ -652,12 +683,12 @@ public function processMultiLineArray($phpcsFile, $stackPtr, $arrayStart, $array $found, ]; - $fix = $phpcsFile->addFixableError($error, $valuePointer, 'ValueNotAligned', $data); + $fix = $phpcsFile->addFixableError($error, $first, 'ValueNotAligned', $data); if ($fix === true) { if ($found === 0) { - $phpcsFile->fixer->addContent(($valuePointer - 1), str_repeat(' ', $expected)); + $phpcsFile->fixer->addContent(($first - 1), str_repeat(' ', $expected)); } else { - $phpcsFile->fixer->replaceToken(($valuePointer - 1), str_repeat(' ', $expected)); + $phpcsFile->fixer->replaceToken(($first - 1), str_repeat(' ', $expected)); } } } diff --git a/src/Standards/Squiz/Sniffs/Classes/ClassDeclarationSniff.php b/src/Standards/Squiz/Sniffs/Classes/ClassDeclarationSniff.php index 3d2c4db1aa..69188326d9 100644 --- a/src/Standards/Squiz/Sniffs/Classes/ClassDeclarationSniff.php +++ b/src/Standards/Squiz/Sniffs/Classes/ClassDeclarationSniff.php @@ -65,6 +65,7 @@ public function processOpen(File $phpcsFile, $stackPtr) if ($tokens[($stackPtr - 2)]['code'] !== T_ABSTRACT && $tokens[($stackPtr - 2)]['code'] !== T_FINAL + && $tokens[($stackPtr - 2)]['code'] !== T_READONLY ) { if ($spaces !== 0) { $type = strtolower($tokens[$stackPtr]['content']); diff --git a/src/Standards/Squiz/Sniffs/Classes/ClassFileNameSniff.php b/src/Standards/Squiz/Sniffs/Classes/ClassFileNameSniff.php index afbec4fa7e..88a7e0daae 100644 --- a/src/Standards/Squiz/Sniffs/Classes/ClassFileNameSniff.php +++ b/src/Standards/Squiz/Sniffs/Classes/ClassFileNameSniff.php @@ -27,6 +27,7 @@ public function register() T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, ]; }//end register() diff --git a/src/Standards/Squiz/Sniffs/Classes/LowercaseClassKeywordsSniff.php b/src/Standards/Squiz/Sniffs/Classes/LowercaseClassKeywordsSniff.php index 5f4f78c0a2..2ea5ad8bc2 100644 --- a/src/Standards/Squiz/Sniffs/Classes/LowercaseClassKeywordsSniff.php +++ b/src/Standards/Squiz/Sniffs/Classes/LowercaseClassKeywordsSniff.php @@ -29,6 +29,7 @@ public function register() $targets[] = T_IMPLEMENTS; $targets[] = T_ABSTRACT; $targets[] = T_FINAL; + $targets[] = T_READONLY; $targets[] = T_VAR; $targets[] = T_CONST; diff --git a/src/Standards/Squiz/Sniffs/Classes/ValidClassNameSniff.php b/src/Standards/Squiz/Sniffs/Classes/ValidClassNameSniff.php index 10de719dca..ffddd2cde7 100644 --- a/src/Standards/Squiz/Sniffs/Classes/ValidClassNameSniff.php +++ b/src/Standards/Squiz/Sniffs/Classes/ValidClassNameSniff.php @@ -28,6 +28,7 @@ public function register() T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, ]; }//end register() @@ -58,7 +59,7 @@ public function process(File $phpcsFile, $stackPtr) // starting with the number will be multiple tokens. $opener = $tokens[$stackPtr]['scope_opener']; $nameStart = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), $opener, true); - $nameEnd = $phpcsFile->findNext(T_WHITESPACE, $nameStart, $opener); + $nameEnd = $phpcsFile->findNext([T_WHITESPACE, T_COLON], $nameStart, $opener); if ($nameEnd === false) { $name = $tokens[$nameStart]['content']; } else { diff --git a/src/Standards/Squiz/Sniffs/Commenting/BlockCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/BlockCommentSniff.php index ae9db7944c..eb647f5fe7 100644 --- a/src/Standards/Squiz/Sniffs/Commenting/BlockCommentSniff.php +++ b/src/Standards/Squiz/Sniffs/Commenting/BlockCommentSniff.php @@ -69,11 +69,22 @@ 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, + T_ENUM => true, T_FUNCTION => true, T_PUBLIC => true, T_PRIVATE => true, @@ -83,6 +94,7 @@ public function process(File $phpcsFile, $stackPtr) T_ABSTRACT => true, T_CONST => true, T_VAR => true, + T_READONLY => true, ]; if (isset($ignore[$tokens[$nextToken]['code']]) === true) { return; @@ -363,6 +375,7 @@ public function process(File $phpcsFile, $stackPtr) if ((isset($tokens[$contentBefore]['scope_closer']) === true && $tokens[$contentBefore]['scope_opener'] === $contentBefore) || $tokens[$contentBefore]['code'] === T_OPEN_TAG + || $tokens[$contentBefore]['code'] === T_OPEN_TAG_WITH_ECHO ) { if (($tokens[$stackPtr]['line'] - $tokens[$contentBefore]['line']) !== 1) { $error = 'Empty line not required before block comment'; diff --git a/src/Standards/Squiz/Sniffs/Commenting/ClassCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/ClassCommentSniff.php index 3aa969dc78..3f30036a8d 100644 --- a/src/Standards/Squiz/Sniffs/Commenting/ClassCommentSniff.php +++ b/src/Standards/Squiz/Sniffs/Commenting/ClassCommentSniff.php @@ -19,7 +19,6 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; -use PHP_CodeSniffer\Util\Tokens; class ClassCommentSniff implements Sniff { @@ -49,10 +48,33 @@ public function register() public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); - $find = Tokens::$methodPrefixes; - $find[] = T_WHITESPACE; + $find = [ + T_ABSTRACT => T_ABSTRACT, + T_FINAL => T_FINAL, + T_READONLY => T_READONLY, + T_WHITESPACE => T_WHITESPACE, + ]; + + $previousContent = null; + for ($commentEnd = ($stackPtr - 1); $commentEnd >= 0; $commentEnd--) { + if (isset($find[$tokens[$commentEnd]['code']]) === true) { + continue; + } + + if ($previousContent === null) { + $previousContent = $commentEnd; + } + + if ($tokens[$commentEnd]['code'] === T_ATTRIBUTE_END + && isset($tokens[$commentEnd]['attribute_opener']) === true + ) { + $commentEnd = $tokens[$commentEnd]['attribute_opener']; + continue; + } + + break; + } - $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1), null, true); if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG && $tokens[$commentEnd]['code'] !== T_COMMENT ) { @@ -69,7 +91,7 @@ public function process(File $phpcsFile, $stackPtr) return; } - if ($tokens[$commentEnd]['line'] !== ($tokens[$stackPtr]['line'] - 1)) { + if ($tokens[$previousContent]['line'] !== ($tokens[$stackPtr]['line'] - 1)) { $error = 'There must be no blank lines after the class comment'; $phpcsFile->addError($error, $commentEnd, 'SpacingAfter'); } diff --git a/src/Standards/Squiz/Sniffs/Commenting/ClosingDeclarationCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/ClosingDeclarationCommentSniff.php index 6ab6280f60..cd509d0c89 100644 --- a/src/Standards/Squiz/Sniffs/Commenting/ClosingDeclarationCommentSniff.php +++ b/src/Standards/Squiz/Sniffs/Commenting/ClosingDeclarationCommentSniff.php @@ -27,6 +27,7 @@ public function register() T_FUNCTION, T_CLASS, T_INTERFACE, + T_ENUM, ]; }//end register() @@ -69,8 +70,10 @@ public function process(File $phpcsFile, $stackPtr) $comment = '//end '.$decName.'()'; } else if ($tokens[$stackPtr]['code'] === T_CLASS) { $comment = '//end class'; - } else { + } else if ($tokens[$stackPtr]['code'] === T_INTERFACE) { $comment = '//end interface'; + } else { + $comment = '//end enum'; }//end if if (isset($tokens[$stackPtr]['scope_closer']) === false) { diff --git a/src/Standards/Squiz/Sniffs/Commenting/DocCommentAlignmentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/DocCommentAlignmentSniff.php index 2624bc2246..1f49d2c0c9 100644 --- a/src/Standards/Squiz/Sniffs/Commenting/DocCommentAlignmentSniff.php +++ b/src/Standards/Squiz/Sniffs/Commenting/DocCommentAlignmentSniff.php @@ -64,6 +64,8 @@ public function process(File $phpcsFile, $stackPtr) $ignore = [ T_CLASS => true, T_INTERFACE => true, + T_ENUM => true, + T_ENUM_CASE => true, T_FUNCTION => true, T_PUBLIC => true, T_PRIVATE => true, @@ -74,6 +76,7 @@ public function process(File $phpcsFile, $stackPtr) T_OBJECT => true, T_PROTOTYPE => true, T_VAR => true, + T_READONLY => true, ]; if ($nextToken === false || isset($ignore[$tokens[$nextToken]['code']]) === false) { diff --git a/src/Standards/Squiz/Sniffs/Commenting/FileCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/FileCommentSniff.php index 66fc5e4db1..a1c79c6a25 100644 --- a/src/Standards/Squiz/Sniffs/Commenting/FileCommentSniff.php +++ b/src/Standards/Squiz/Sniffs/Commenting/FileCommentSniff.php @@ -72,17 +72,30 @@ public function process(File $phpcsFile, $stackPtr) $commentEnd = $tokens[$commentStart]['comment_closer']; - $nextToken = $phpcsFile->findNext( - T_WHITESPACE, - ($commentEnd + 1), - null, - true - ); + for ($nextToken = ($commentEnd + 1); $nextToken < $phpcsFile->numTokens; $nextToken++) { + if ($tokens[$nextToken]['code'] === T_WHITESPACE) { + continue; + } + + if ($tokens[$nextToken]['code'] === T_ATTRIBUTE + && isset($tokens[$nextToken]['attribute_closer']) === true + ) { + $nextToken = $tokens[$nextToken]['attribute_closer']; + continue; + } + + break; + } + + if ($nextToken === $phpcsFile->numTokens) { + $nextToken--; + } $ignore = [ T_CLASS, T_INTERFACE, T_TRAIT, + T_ENUM, T_FUNCTION, T_CLOSURE, T_PUBLIC, @@ -91,6 +104,7 @@ public function process(File $phpcsFile, $stackPtr) T_FINAL, T_STATIC, T_ABSTRACT, + T_READONLY, T_CONST, T_PROPERTY, T_INCLUDE, @@ -115,7 +129,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 38fc4d0f94..3c79a7a8c4 100644 --- a/src/Standards/Squiz/Sniffs/Commenting/FunctionCommentSniff.php +++ b/src/Standards/Squiz/Sniffs/Commenting/FunctionCommentSniff.php @@ -17,6 +17,13 @@ class FunctionCommentSniff extends PEARFunctionCommentSniff { + /** + * Whether to skip inheritdoc comments. + * + * @var boolean + */ + public $skipIfInheritdoc = false; + /** * The current PHP version. * @@ -40,6 +47,12 @@ protected function processReturn(File $phpcsFile, $stackPtr, $commentStart) $tokens = $phpcsFile->getTokens(); $return = null; + if ($this->skipIfInheritdoc === true) { + if ($this->checkInheritdoc($phpcsFile, $stackPtr, $commentStart) === true) { + return; + } + } + foreach ($tokens[$commentStart]['comment_tags'] as $tag) { if ($tokens[$tag]['content'] === '@return') { if ($return !== null) { @@ -54,10 +67,7 @@ protected function processReturn(File $phpcsFile, $stackPtr, $commentStart) // Skip constructor and destructor. $methodName = $phpcsFile->getDeclarationName($stackPtr); - $isSpecialMethod = ($methodName === '__construct' || $methodName === '__destruct'); - if ($isSpecialMethod === true) { - return; - } + $isSpecialMethod = in_array($methodName, $this->specialMethods, true); if ($return !== null) { $content = $tokens[($return + 2)]['content']; @@ -133,9 +143,12 @@ protected function processReturn(File $phpcsFile, $stackPtr, $commentStart) } } }//end if - } else if ($returnType !== 'mixed' && in_array('void', $typeNames, true) === false) { - // If return type is not void, there needs to be a return statement - // somewhere in the function that returns something. + } else if ($returnType !== 'mixed' + && $returnType !== 'never' + && in_array('void', $typeNames, true) === false + ) { + // If return type is not void, never, or mixed, there needs to be a + // return statement somewhere in the function that returns something. if (isset($tokens[$stackPtr]['scope_closer']) === true) { $endToken = $tokens[$stackPtr]['scope_closer']; for ($returnToken = $stackPtr; $returnToken < $endToken; $returnToken++) { @@ -168,6 +181,10 @@ protected function processReturn(File $phpcsFile, $stackPtr, $commentStart) }//end if }//end if } else { + if ($isSpecialMethod === true) { + return; + } + $error = 'Missing @return tag in function comment'; $phpcsFile->addError($error, $tokens[$commentStart]['comment_closer'], 'MissingReturn'); }//end if @@ -189,6 +206,12 @@ protected function processThrows(File $phpcsFile, $stackPtr, $commentStart) { $tokens = $phpcsFile->getTokens(); + if ($this->skipIfInheritdoc === true) { + if ($this->checkInheritdoc($phpcsFile, $stackPtr, $commentStart) === true) { + return; + } + } + foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { if ($tokens[$tag]['content'] !== '@throws') { continue; @@ -225,6 +248,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) { @@ -264,6 +289,12 @@ protected function processParams(File $phpcsFile, $stackPtr, $commentStart) $tokens = $phpcsFile->getTokens(); + if ($this->skipIfInheritdoc === true) { + if ($this->checkInheritdoc($phpcsFile, $stackPtr, $commentStart) === true) { + return; + } + } + $params = []; $maxType = 0; $maxVar = 0; @@ -377,6 +408,10 @@ protected function processParams(File $phpcsFile, $stackPtr, $commentStart) $suggestedTypeNames = []; foreach ($typeNames as $typeName) { + if ($typeName === '') { + continue; + } + // Strip nullable operator. if ($typeName[0] === '?') { $typeName = substr($typeName, 1); @@ -419,6 +454,12 @@ protected function processParams(File $phpcsFile, $stackPtr, $commentStart) } } + if ($this->phpVersion >= 80000) { + if ($suggestedName === 'mixed') { + $suggestedTypeHint = 'mixed'; + } + } + if ($suggestedTypeHint !== '' && isset($realParams[$pos]) === true) { $typeHint = $realParams[$pos]['type_hint']; @@ -517,16 +558,38 @@ protected function processParams(File $phpcsFile, $stackPtr, $commentStart) // Make sure the param name is correct. if (isset($realParams[$pos]) === true) { - $realName = $realParams[$pos]['name']; - if ($realName !== $param['var']) { + $realName = $realParams[$pos]['name']; + $paramVarName = $param['var']; + + if ($param['var'][0] === '&') { + // Even when passed by reference, the variable name in $realParams does not have + // a leading '&'. This sniff will accept both '&$var' and '$var' in these cases. + $paramVarName = substr($param['var'], 1); + + // This makes sure that the 'MissingParamTag' check won't throw a false positive. + $foundParams[(count($foundParams) - 1)] = $paramVarName; + + if ($realParams[$pos]['pass_by_reference'] !== true && $realName === $paramVarName) { + // Don't complain about this unless the param name is otherwise correct. + $error = 'Doc comment for parameter %s is prefixed with "&" but parameter is not passed by reference'; + $code = 'ParamNameUnexpectedAmpersandPrefix'; + $data = [$paramVarName]; + + // We're not offering an auto-fix here because we can't tell if the docblock + // is wrong, or the parameter should be passed by reference. + $phpcsFile->addError($error, $param['tag'], $code, $data); + } + } + + if ($realName !== $paramVarName) { $code = 'ParamNameNoMatch'; $data = [ - $param['var'], + $paramVarName, $realName, ]; $error = 'Doc comment for parameter %s does not match '; - if (strtolower($param['var']) === strtolower($realName)) { + if (strtolower($paramVarName) === strtolower($realName)) { $error .= 'case of '; $code = 'ParamNameNoCaseMatch'; } @@ -534,7 +597,7 @@ protected function processParams(File $phpcsFile, $stackPtr, $commentStart) $error .= 'actual variable name %s'; $phpcsFile->addError($error, $param['tag'], $code, $data); - } + }//end if } else if (substr($param['var'], -4) !== ',...') { // We must have an extra parameter comment. $error = 'Superfluous parameter comment'; @@ -695,4 +758,40 @@ protected function checkSpacingAfterParamName(File $phpcsFile, $param, $maxVar, }//end checkSpacingAfterParamName() + /** + * Determines whether the whole comment is an inheritdoc comment. + * + * @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. + * @param int $commentStart The position in the stack where the comment started. + * + * @return boolean TRUE if the docblock contains only {@inheritdoc} (case-insensitive). + */ + protected function checkInheritdoc(File $phpcsFile, $stackPtr, $commentStart) + { + $tokens = $phpcsFile->getTokens(); + + $allowedTokens = [ + T_DOC_COMMENT_OPEN_TAG, + T_DOC_COMMENT_WHITESPACE, + T_DOC_COMMENT_STAR, + ]; + for ($i = $commentStart; $i <= $tokens[$commentStart]['comment_closer']; $i++) { + if (in_array($tokens[$i]['code'], $allowedTokens) === false) { + $trimmedContent = strtolower(trim($tokens[$i]['content'])); + + if ($trimmedContent === '{@inheritdoc}') { + return true; + } else { + return false; + } + } + } + + return false; + + }//end checkInheritdoc() + + }//end class diff --git a/src/Standards/Squiz/Sniffs/Commenting/InlineCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/InlineCommentSniff.php index 09b4ee2528..e3ef5c0dc1 100644 --- a/src/Standards/Squiz/Sniffs/Commenting/InlineCommentSniff.php +++ b/src/Standards/Squiz/Sniffs/Commenting/InlineCommentSniff.php @@ -59,17 +59,22 @@ 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, T_INTERFACE, T_TRAIT, + T_ENUM, T_FUNCTION, T_CLOSURE, T_PUBLIC, @@ -78,6 +83,7 @@ public function process(File $phpcsFile, $stackPtr) T_FINAL, T_STATIC, T_ABSTRACT, + T_READONLY, T_CONST, T_PROPERTY, T_INCLUDE, diff --git a/src/Standards/Squiz/Sniffs/Commenting/LongConditionClosingCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/LongConditionClosingCommentSniff.php index 8d8c4f898a..103d90ffba 100644 --- a/src/Standards/Squiz/Sniffs/Commenting/LongConditionClosingCommentSniff.php +++ b/src/Standards/Squiz/Sniffs/Commenting/LongConditionClosingCommentSniff.php @@ -38,6 +38,7 @@ class LongConditionClosingCommentSniff implements Sniff T_WHILE, T_TRY, T_CASE, + T_MATCH, ]; /** @@ -154,6 +155,17 @@ public function process(File $phpcsFile, $stackPtr) } while (isset($tokens[$nextToken]['scope_closer']) === true); } + if ($startCondition['code'] === T_MATCH) { + // Move the stackPtr to after the semi-colon/comma if there is one. + $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); + if ($nextToken !== false + && ($tokens[$nextToken]['code'] === T_SEMICOLON + || $tokens[$nextToken]['code'] === T_COMMA) + ) { + $stackPtr = $nextToken; + } + } + $lineDifference = ($endBrace['line'] - $startBrace['line']); $expected = sprintf($this->commentFormat, $startCondition['content']); diff --git a/src/Standards/Squiz/Sniffs/Commenting/PostStatementCommentSniff.php b/src/Standards/Squiz/Sniffs/Commenting/PostStatementCommentSniff.php index 1164c6f234..ce19d5c1d9 100644 --- a/src/Standards/Squiz/Sniffs/Commenting/PostStatementCommentSniff.php +++ b/src/Standards/Squiz/Sniffs/Commenting/PostStatementCommentSniff.php @@ -40,6 +40,7 @@ class PostStatementCommentSniff implements Sniff T_WHILE => true, T_FOR => true, T_FOREACH => true, + T_MATCH => true, ]; 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/ControlStructures/ControlSignatureSniff.php b/src/Standards/Squiz/Sniffs/ControlStructures/ControlSignatureSniff.php index 032c59ace8..8a56b23fcb 100644 --- a/src/Standards/Squiz/Sniffs/ControlStructures/ControlSignatureSniff.php +++ b/src/Standards/Squiz/Sniffs/ControlStructures/ControlSignatureSniff.php @@ -53,6 +53,7 @@ public function register() T_ELSE, T_ELSEIF, T_SWITCH, + T_MATCH, ]; }//end register() diff --git a/src/Standards/Squiz/Sniffs/ControlStructures/ForLoopDeclarationSniff.php b/src/Standards/Squiz/Sniffs/ControlStructures/ForLoopDeclarationSniff.php index 6803f9abb4..370eab1890 100644 --- a/src/Standards/Squiz/Sniffs/ControlStructures/ForLoopDeclarationSniff.php +++ b/src/Standards/Squiz/Sniffs/ControlStructures/ForLoopDeclarationSniff.php @@ -222,7 +222,7 @@ public function process(File $phpcsFile, $stackPtr) $semicolon = $openingBracket; $targetNestinglevel = 0; if (isset($tokens[$openingBracket]['conditions']) === true) { - $targetNestinglevel += count($tokens[$openingBracket]['conditions']); + $targetNestinglevel = count($tokens[$openingBracket]['conditions']); } do { diff --git a/src/Standards/Squiz/Sniffs/ControlStructures/LowercaseDeclarationSniff.php b/src/Standards/Squiz/Sniffs/ControlStructures/LowercaseDeclarationSniff.php index ae4a51e7a3..cba076840a 100644 --- a/src/Standards/Squiz/Sniffs/ControlStructures/LowercaseDeclarationSniff.php +++ b/src/Standards/Squiz/Sniffs/ControlStructures/LowercaseDeclarationSniff.php @@ -34,6 +34,7 @@ public function register() T_WHILE, T_TRY, T_CATCH, + T_MATCH, ]; }//end register() diff --git a/src/Standards/Squiz/Sniffs/Debug/JSLintSniff.php b/src/Standards/Squiz/Sniffs/Debug/JSLintSniff.php index c50e764e48..49dfc2c42f 100644 --- a/src/Standards/Squiz/Sniffs/Debug/JSLintSniff.php +++ b/src/Standards/Squiz/Sniffs/Debug/JSLintSniff.php @@ -12,6 +12,7 @@ use PHP_CodeSniffer\Config; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Common; class JSLintSniff implements Sniff { @@ -48,7 +49,7 @@ public function register() */ public function process(File $phpcsFile, $stackPtr) { - $rhinoPath = Config::getExecutablePath('jslint'); + $rhinoPath = Config::getExecutablePath('rhino'); $jslintPath = Config::getExecutablePath('jslint'); if ($rhinoPath === null || $jslintPath === null) { return; @@ -56,8 +57,8 @@ public function process(File $phpcsFile, $stackPtr) $fileName = $phpcsFile->getFilename(); - $rhinoPath = escapeshellcmd($rhinoPath); - $jslintPath = escapeshellcmd($jslintPath); + $rhinoPath = Common::escapeshellcmd($rhinoPath); + $jslintPath = Common::escapeshellcmd($jslintPath); $cmd = "$rhinoPath \"$jslintPath\" ".escapeshellarg($fileName); exec($cmd, $output, $retval); diff --git a/src/Standards/Squiz/Sniffs/Debug/JavaScriptLintSniff.php b/src/Standards/Squiz/Sniffs/Debug/JavaScriptLintSniff.php index 0c0f35566e..30dca6722d 100644 --- a/src/Standards/Squiz/Sniffs/Debug/JavaScriptLintSniff.php +++ b/src/Standards/Squiz/Sniffs/Debug/JavaScriptLintSniff.php @@ -13,6 +13,7 @@ use PHP_CodeSniffer\Exceptions\RuntimeException; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Common; class JavaScriptLintSniff implements Sniff { @@ -56,7 +57,7 @@ public function process(File $phpcsFile, $stackPtr) $fileName = $phpcsFile->getFilename(); - $cmd = '"'.escapeshellcmd($jslPath).'" -nologo -nofilelisting -nocontext -nosummary -output-format __LINE__:__ERROR__ -process '.escapeshellarg($fileName); + $cmd = '"'.Common::escapeshellcmd($jslPath).'" -nologo -nofilelisting -nocontext -nosummary -output-format __LINE__:__ERROR__ -process '.escapeshellarg($fileName); $msg = exec($cmd, $output, $retval); // Variable $exitCode is the last line of $output if no error occurs, on diff --git a/src/Standards/Squiz/Sniffs/Files/FileExtensionSniff.php b/src/Standards/Squiz/Sniffs/Files/FileExtensionSniff.php index bae882146d..6277b809e6 100644 --- a/src/Standards/Squiz/Sniffs/Files/FileExtensionSniff.php +++ b/src/Standards/Squiz/Sniffs/Files/FileExtensionSniff.php @@ -42,7 +42,7 @@ public function process(File $phpcsFile, $stackPtr) $tokens = $phpcsFile->getTokens(); $fileName = $phpcsFile->getFilename(); $extension = substr($fileName, strrpos($fileName, '.')); - $nextClass = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT], $stackPtr); + $nextClass = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM], $stackPtr); if ($nextClass !== false) { $phpcsFile->recordMetric($stackPtr, 'File extension for class files', $extension); diff --git a/src/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php b/src/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php index 3d67f9b78f..8becb74a4e 100644 --- a/src/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php +++ b/src/Standards/Squiz/Sniffs/Formatting/OperatorBracketSniff.php @@ -80,7 +80,8 @@ public function process(File $phpcsFile, $stackPtr) $isAssignment = isset(Tokens::$assignmentTokens[$tokens[$previous]['code']]); $isEquality = isset(Tokens::$equalityTokens[$tokens[$previous]['code']]); $isComparison = isset(Tokens::$comparisonTokens[$tokens[$previous]['code']]); - if ($isAssignment === true || $isEquality === true || $isComparison === true) { + $isUnary = isset(Tokens::$operators[$tokens[$previous]['code']]); + if ($isAssignment === true || $isEquality === true || $isComparison === true || $isUnary === true) { // This is a negative assignment or comparison. // We need to check that the minus and the number are // adjacent. @@ -107,6 +108,8 @@ public function process(File $phpcsFile, $stackPtr) T_OPEN_CURLY_BRACKET => true, T_OPEN_SHORT_ARRAY => true, T_CASE => true, + T_EXIT => true, + T_MATCH_ARROW => true, ]; if (isset($invalidTokens[$tokens[$previousToken]['code']]) === true) { @@ -139,7 +142,9 @@ public function process(File $phpcsFile, $stackPtr) T_THIS, T_SELF, T_STATIC, + T_PARENT, T_OBJECT_OPERATOR, + T_NULLSAFE_OBJECT_OPERATOR, T_DOUBLE_COLON, T_OPEN_SQUARE_BRACKET, T_CLOSE_SQUARE_BRACKET, @@ -163,7 +168,7 @@ public function process(File $phpcsFile, $stackPtr) break; } - if ($prevCode === T_STRING || $prevCode === T_SWITCH) { + if ($prevCode === T_STRING || $prevCode === T_SWITCH || $prevCode === T_MATCH) { // We allow simple operations to not be bracketed. // For example, ceil($one / $two). for ($prev = ($stackPtr - 1); $prev > $bracket; $prev--) { @@ -202,8 +207,8 @@ public function process(File $phpcsFile, $stackPtr) if (in_array($prevCode, Tokens::$scopeOpeners, true) === true) { // This operation is inside a control structure like FOREACH // or IF, but has no bracket of it's own. - // The only control structure allowed to do this is SWITCH. - if ($prevCode !== T_SWITCH) { + // The only control structures allowed to do this are SWITCH and MATCH. + if ($prevCode !== T_SWITCH && $prevCode !== T_MATCH) { break; } } @@ -283,6 +288,7 @@ public function addMissingBracketsError($phpcsFile, $stackPtr) T_SELF => true, T_STATIC => true, T_OBJECT_OPERATOR => true, + T_NULLSAFE_OBJECT_OPERATOR => true, T_DOUBLE_COLON => true, T_MODULUS => true, T_ISSET => true, @@ -327,6 +333,10 @@ public function addMissingBracketsError($phpcsFile, $stackPtr) $before = $phpcsFile->findNext(Tokens::$emptyTokens, ($before + 1), null, true); + // A few extra tokens are allowed to be on the right side of the expression. + $allowed[T_EQUAL] = true; + $allowed[T_NEW] = true; + // Find the last token in the expression. for ($after = ($stackPtr + 1); $after < $phpcsFile->numTokens; $after++) { // Special case for plus operators because we can't tell if they are used diff --git a/src/Standards/Squiz/Sniffs/Functions/FunctionDeclarationArgumentSpacingSniff.php b/src/Standards/Squiz/Sniffs/Functions/FunctionDeclarationArgumentSpacingSniff.php index 2f0d3ab5b7..e696d8001d 100644 --- a/src/Standards/Squiz/Sniffs/Functions/FunctionDeclarationArgumentSpacingSniff.php +++ b/src/Standards/Squiz/Sniffs/Functions/FunctionDeclarationArgumentSpacingSniff.php @@ -120,8 +120,14 @@ public function processBracket($phpcsFile, $openBracket) $next = $phpcsFile->findNext(T_WHITESPACE, ($openBracket + 1), $closeBracket, true); if ($next === false) { if (($closeBracket - $openBracket) !== 1) { + if ($tokens[$openBracket]['line'] !== $tokens[$closeBracket]['line']) { + $found = 'newline'; + } else { + $found = $tokens[($openBracket + 1)]['length']; + } + $error = 'Expected 0 spaces between parenthesis of function declaration; %s found'; - $data = [$tokens[($openBracket + 1)]['length']]; + $data = [$found]; $fix = $phpcsFile->addFixableError($error, $openBracket, 'SpacingBetween', $data); if ($fix === true) { $phpcsFile->fixer->replaceToken(($openBracket + 1), ''); @@ -131,7 +137,7 @@ public function processBracket($phpcsFile, $openBracket) // No params, so we don't check normal spacing rules. return; } - } + }//end if foreach ($params as $paramNumber => $param) { if ($param['pass_by_reference'] === true) { diff --git a/src/Standards/Squiz/Sniffs/Functions/MultiLineFunctionDeclarationSniff.php b/src/Standards/Squiz/Sniffs/Functions/MultiLineFunctionDeclarationSniff.php index a4ea79cfbd..c12bd0c47c 100644 --- a/src/Standards/Squiz/Sniffs/Functions/MultiLineFunctionDeclarationSniff.php +++ b/src/Standards/Squiz/Sniffs/Functions/MultiLineFunctionDeclarationSniff.php @@ -200,7 +200,7 @@ public function processBracket($phpcsFile, $openBracket, $tokens, $type='functio // The open bracket should be the last thing on the line. if ($tokens[$openBracket]['line'] !== $tokens[$closeBracket]['line']) { $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($openBracket + 1), null, true); - if ($tokens[$next]['line'] !== ($tokens[$openBracket]['line'] + 1)) { + if ($tokens[$next]['line'] === $tokens[$openBracket]['line']) { $error = 'The first parameter of a multi-line '.$type.' declaration must be on the line after the opening bracket'; $fix = $phpcsFile->addFixableError($error, $next, $errorPrefix.'FirstParamSpacing'); if ($fix === true) { diff --git a/src/Standards/Squiz/Sniffs/NamingConventions/ValidVariableNameSniff.php b/src/Standards/Squiz/Sniffs/NamingConventions/ValidVariableNameSniff.php index ed2505817b..5c3a74930e 100644 --- a/src/Standards/Squiz/Sniffs/NamingConventions/ValidVariableNameSniff.php +++ b/src/Standards/Squiz/Sniffs/NamingConventions/ValidVariableNameSniff.php @@ -38,7 +38,9 @@ protected function processVariable(File $phpcsFile, $stackPtr) } $objOperator = $phpcsFile->findNext([T_WHITESPACE], ($stackPtr + 1), null, true); - if ($tokens[$objOperator]['code'] === T_OBJECT_OPERATOR) { + if ($tokens[$objOperator]['code'] === T_OBJECT_OPERATOR + || $tokens[$objOperator]['code'] === T_NULLSAFE_OBJECT_OPERATOR + ) { // Check to see if we are using a variable from an object. $var = $phpcsFile->findNext([T_WHITESPACE], ($objOperator + 1), null, true); if ($tokens[$var]['code'] === T_STRING) { @@ -55,28 +57,38 @@ protected function processVariable(File $phpcsFile, $stackPtr) } if (Common::isCamelCaps($objVarName, false, true, false) === false) { - $error = 'Variable "%s" is not in valid camel caps format'; + $error = 'Member variable "%s" is not in valid camel caps format'; $data = [$originalVarName]; - $phpcsFile->addError($error, $var, 'NotCamelCaps', $data); + $phpcsFile->addError($error, $var, 'MemberNotCamelCaps', $data); } }//end if }//end if }//end if + $objOperator = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); + if ($tokens[$objOperator]['code'] === T_DOUBLE_COLON) { + // The variable lives within a class, and is referenced like + // this: MyClass::$_variable, so we don't know its scope. + $objVarName = $varName; + if (substr($objVarName, 0, 1) === '_') { + $objVarName = substr($objVarName, 1); + } + + if (Common::isCamelCaps($objVarName, false, true, false) === false) { + $error = 'Member variable "%s" is not in valid camel caps format'; + $data = [$tokens[$stackPtr]['content']]; + $phpcsFile->addError($error, $stackPtr, 'MemberNotCamelCaps', $data); + } + + return; + } + // There is no way for us to know if the var is public or private, // so we have to ignore a leading underscore if there is one and just // check the main part of the variable name. $originalVarName = $varName; if (substr($varName, 0, 1) === '_') { - $objOperator = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); - if ($tokens[$objOperator]['code'] === T_DOUBLE_COLON) { - // The variable lives within a class, and is referenced like - // this: MyClass::$_variable, so we don't know its scope. - $inClass = true; - } else { - $inClass = $phpcsFile->hasCondition($stackPtr, Tokens::$ooScopeTokens); - } - + $inClass = $phpcsFile->hasCondition($stackPtr, Tokens::$ooScopeTokens); if ($inClass === true) { $varName = substr($varName, 1); } diff --git a/src/Standards/Squiz/Sniffs/Objects/ObjectInstantiationSniff.php b/src/Standards/Squiz/Sniffs/Objects/ObjectInstantiationSniff.php index fb714e16cf..84facf0540 100644 --- a/src/Standards/Squiz/Sniffs/Objects/ObjectInstantiationSniff.php +++ b/src/Standards/Squiz/Sniffs/Objects/ObjectInstantiationSniff.php @@ -48,19 +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_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/Operators/IncrementDecrementUsageSniff.php b/src/Standards/Squiz/Sniffs/Operators/IncrementDecrementUsageSniff.php index c3a8b3915e..cb3778453a 100644 --- a/src/Standards/Squiz/Sniffs/Operators/IncrementDecrementUsageSniff.php +++ b/src/Standards/Squiz/Sniffs/Operators/IncrementDecrementUsageSniff.php @@ -74,7 +74,8 @@ protected function processIncDec($phpcsFile, $stackPtr) // start looking for other operators. if ($tokens[($stackPtr - 1)]['code'] === T_VARIABLE || ($tokens[($stackPtr - 1)]['code'] === T_STRING - && $tokens[($stackPtr - 2)]['code'] === T_OBJECT_OPERATOR) + && ($tokens[($stackPtr - 2)]['code'] === T_OBJECT_OPERATOR + || $tokens[($stackPtr - 2)]['code'] === T_NULLSAFE_OBJECT_OPERATOR)) ) { $start = ($stackPtr + 1); } else { diff --git a/src/Standards/Squiz/Sniffs/PHP/DisallowComparisonAssignmentSniff.php b/src/Standards/Squiz/Sniffs/PHP/DisallowComparisonAssignmentSniff.php index aaed48d22a..9eb2124233 100644 --- a/src/Standards/Squiz/Sniffs/PHP/DisallowComparisonAssignmentSniff.php +++ b/src/Standards/Squiz/Sniffs/PHP/DisallowComparisonAssignmentSniff.php @@ -52,30 +52,34 @@ public function process(File $phpcsFile, $stackPtr) } } - // Ignore values in array definitions. - $array = $phpcsFile->findNext( - T_ARRAY, + // Ignore values in array definitions or match structures. + $nextNonEmpty = $phpcsFile->findNext( + Tokens::$emptyTokens, ($stackPtr + 1), null, - false, - null, true ); - if ($array !== false) { + if ($nextNonEmpty !== false + && ($tokens[$nextNonEmpty]['code'] === T_ARRAY + || $tokens[$nextNonEmpty]['code'] === T_MATCH) + ) { return; } // Ignore function calls. $ignore = [ + T_NULLSAFE_OBJECT_OPERATOR, + T_OBJECT_OPERATOR, T_STRING, + T_VARIABLE, T_WHITESPACE, - T_OBJECT_OPERATOR, ]; $next = $phpcsFile->findNext($ignore, ($stackPtr + 1), null, true); - if ($tokens[$next]['code'] === T_OPEN_PARENTHESIS - && $tokens[($next - 1)]['code'] === T_STRING + if ($tokens[$next]['code'] === T_CLOSURE + || ($tokens[$next]['code'] === T_OPEN_PARENTHESIS + && $tokens[($next - 1)]['code'] === T_STRING) ) { // Code will look like: $var = myFunction( // and will be ignored. diff --git a/src/Standards/Squiz/Sniffs/PHP/DisallowMultipleAssignmentsSniff.php b/src/Standards/Squiz/Sniffs/PHP/DisallowMultipleAssignmentsSniff.php index 84032fec01..4448d2445b 100644 --- a/src/Standards/Squiz/Sniffs/PHP/DisallowMultipleAssignmentsSniff.php +++ b/src/Standards/Squiz/Sniffs/PHP/DisallowMultipleAssignmentsSniff.php @@ -43,7 +43,7 @@ public function process(File $phpcsFile, $stackPtr) $tokens = $phpcsFile->getTokens(); // Ignore default value assignments in function definitions. - $function = $phpcsFile->findPrevious([T_FUNCTION, T_CLOSURE], ($stackPtr - 1), null, false, null, true); + $function = $phpcsFile->findPrevious([T_FUNCTION, T_CLOSURE, T_FN], ($stackPtr - 1), null, false, null, true); if ($function !== false) { $opener = $tokens[$function]['parenthesis_opener']; $closer = $tokens[$function]['parenthesis_closer']; @@ -83,6 +83,12 @@ public function process(File $phpcsFile, $stackPtr) */ for ($varToken = ($stackPtr - 1); $varToken >= 0; $varToken--) { + if (in_array($tokens[$varToken]['code'], [T_SEMICOLON, T_OPEN_CURLY_BRACKET], true) === true) { + // We've reached the next statement, so we + // didn't find a variable. + return; + } + // Skip brackets. if (isset($tokens[$varToken]['parenthesis_opener']) === true && $tokens[$varToken]['parenthesis_opener'] < $varToken) { $varToken = $tokens[$varToken]['parenthesis_opener']; @@ -94,12 +100,6 @@ public function process(File $phpcsFile, $stackPtr) continue; } - if ($tokens[$varToken]['code'] === T_SEMICOLON) { - // We've reached the next statement, so we - // didn't find a variable. - return; - } - if ($tokens[$varToken]['code'] === T_VARIABLE) { // We found our variable. break; @@ -146,6 +146,7 @@ public function process(File $phpcsFile, $stackPtr) if ($tokens[$varToken]['code'] === T_VARIABLE || $tokens[$varToken]['code'] === T_OPEN_TAG + || $tokens[$varToken]['code'] === T_GOTO_LABEL || $tokens[$varToken]['code'] === T_INLINE_THEN || $tokens[$varToken]['code'] === T_INLINE_ELSE || $tokens[$varToken]['code'] === T_SEMICOLON @@ -165,6 +166,7 @@ public function process(File $phpcsFile, $stackPtr) T_SWITCH => T_SWITCH, T_CASE => T_CASE, T_FOR => T_FOR, + T_MATCH => T_MATCH, ]; foreach ($nested as $opener => $closer) { if (isset($tokens[$opener]['parenthesis_owner']) === true diff --git a/src/Standards/Squiz/Sniffs/PHP/DisallowSizeFunctionsInLoopsSniff.php b/src/Standards/Squiz/Sniffs/PHP/DisallowSizeFunctionsInLoopsSniff.php index cb1436631a..a0f1161c6a 100644 --- a/src/Standards/Squiz/Sniffs/PHP/DisallowSizeFunctionsInLoopsSniff.php +++ b/src/Standards/Squiz/Sniffs/PHP/DisallowSizeFunctionsInLoopsSniff.php @@ -95,7 +95,9 @@ public function process(File $phpcsFile, $stackPtr) $functionName = 'object.'.$functionName; } else { // Make sure it isn't a member var. - if ($tokens[($i - 1)]['code'] === T_OBJECT_OPERATOR) { + if ($tokens[($i - 1)]['code'] === T_OBJECT_OPERATOR + || $tokens[($i - 1)]['code'] === T_NULLSAFE_OBJECT_OPERATOR + ) { continue; } diff --git a/src/Standards/Squiz/Sniffs/PHP/InnerFunctionsSniff.php b/src/Standards/Squiz/Sniffs/PHP/InnerFunctionsSniff.php index 4e3ee2152c..e42ef5a16a 100644 --- a/src/Standards/Squiz/Sniffs/PHP/InnerFunctionsSniff.php +++ b/src/Standards/Squiz/Sniffs/PHP/InnerFunctionsSniff.php @@ -11,6 +11,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Tokens; class InnerFunctionsSniff implements Sniff { @@ -41,21 +42,28 @@ public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); - $function = $phpcsFile->getCondition($stackPtr, T_FUNCTION); - if ($function === false) { - // Not a nested function. + if (isset($tokens[$stackPtr]['conditions']) === false) { return; } - $class = $phpcsFile->getCondition($stackPtr, T_ANON_CLASS, false); - if ($class !== false && $class > $function) { - // Ignore methods in anon classes. - return; + $conditions = $tokens[$stackPtr]['conditions']; + $reversedConditions = array_reverse($conditions, true); + + $outerFuncToken = null; + foreach ($reversedConditions as $condToken => $condition) { + if ($condition === T_FUNCTION || $condition === T_CLOSURE) { + $outerFuncToken = $condToken; + break; + } + + if (\array_key_exists($condition, Tokens::$ooScopeTokens) === true) { + // Ignore methods in OOP structures defined within functions. + return; + } } - $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); - if ($tokens[$prev]['code'] === T_EQUAL) { - // Ignore closures. + if ($outerFuncToken === null) { + // Not a nested function. return; } diff --git a/src/Standards/Squiz/Sniffs/PHP/LowercasePHPFunctionsSniff.php b/src/Standards/Squiz/Sniffs/PHP/LowercasePHPFunctionsSniff.php index 486edcc01f..37f1e40e7b 100644 --- a/src/Standards/Squiz/Sniffs/PHP/LowercasePHPFunctionsSniff.php +++ b/src/Standards/Squiz/Sniffs/PHP/LowercasePHPFunctionsSniff.php @@ -75,6 +75,11 @@ public function process(File $phpcsFile, $stackPtr) } // Make sure this is a function call or a use statement. + if (empty($tokens[$stackPtr]['nested_attributes']) === false) { + // Class instantiation in attribute, not function call. + return; + } + $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); if ($next === false) { // Not a function call. @@ -133,7 +138,9 @@ public function process(File $phpcsFile, $stackPtr) return; } - if ($tokens[$prev]['code'] === T_OBJECT_OPERATOR) { + if ($tokens[$prev]['code'] === T_OBJECT_OPERATOR + || $tokens[$prev]['code'] === T_NULLSAFE_OBJECT_OPERATOR + ) { // Not an inbuilt function. return; } diff --git a/src/Standards/Squiz/Sniffs/PHP/NonExecutableCodeSniff.php b/src/Standards/Squiz/Sniffs/PHP/NonExecutableCodeSniff.php index fa3da745bc..31d9c5a351 100644 --- a/src/Standards/Squiz/Sniffs/PHP/NonExecutableCodeSniff.php +++ b/src/Standards/Squiz/Sniffs/PHP/NonExecutableCodeSniff.php @@ -16,6 +16,22 @@ class NonExecutableCodeSniff implements Sniff { + /** + * Tokens for terminating expressions, which can be used inline. + * + * This is in contrast to terminating statements, which cannot be used inline + * and would result in a parse error (which is not the concern of this sniff). + * + * `throw` can be used as an expression since PHP 8.0. + * {@link https://wiki.php.net/rfc/throw_expression} + * + * @var array + */ + private $expressionTokens = [ + T_EXIT => T_EXIT, + T_THROW => T_THROW, + ]; + /** * Returns an array of tokens this test wants to listen for. @@ -49,12 +65,34 @@ public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); - // If this token is preceded with an "or", it only relates to one line - // and should be ignored. For example: fopen() or die(). $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); - if ($tokens[$prev]['code'] === T_LOGICAL_OR || $tokens[$prev]['code'] === T_BOOLEAN_OR) { - return; - } + + // Tokens which can be used in inline expressions need special handling. + if (isset($this->expressionTokens[$tokens[$stackPtr]['code']]) === true) { + // If this token is preceded by a logical operator, it only relates to one line + // and should be ignored. For example: fopen() or die(). + // Note: There is one exception: throw expressions can not be used with xor. + if (isset(Tokens::$booleanOperators[$tokens[$prev]['code']]) === true + && ($tokens[$stackPtr]['code'] === T_THROW && $tokens[$prev]['code'] === T_LOGICAL_XOR) === false + ) { + return; + } + + // Expressions are allowed in the `else` clause of ternaries. + if ($tokens[$prev]['code'] === T_INLINE_THEN || $tokens[$prev]['code'] === T_INLINE_ELSE) { + return; + } + + // Expressions are allowed with PHP 7.0+ null coalesce and PHP 7.4+ null coalesce equals. + if ($tokens[$prev]['code'] === T_COALESCE || $tokens[$prev]['code'] === T_COALESCE_EQUAL) { + return; + } + + // Expressions are allowed in arrow functions. + if ($tokens[$prev]['code'] === T_FN_ARROW) { + return; + } + }//end if // Check if this token is actually part of a one-line IF or ELSE statement. for ($i = ($stackPtr - 1); $i > 0; $i--) { @@ -139,7 +177,6 @@ public function process(File $phpcsFile, $stackPtr) // This token may be part of an inline condition. // If we find a closing parenthesis that belongs to a condition // we should ignore this token. - $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); if (isset($tokens[$prev]['parenthesis_owner']) === true) { $owner = $tokens[$prev]['parenthesis_owner']; $ignore = [ @@ -204,24 +241,28 @@ public function process(File $phpcsFile, $stackPtr) $end = ($phpcsFile->numTokens - 1); }//end if - // Find the semicolon that ends this statement, skipping - // nested statements like FOR loops and closures. + // Find the semicolon or closing PHP tag that ends this statement, + // skipping nested statements like FOR loops and closures. for ($start = ($stackPtr + 1); $start < $phpcsFile->numTokens; $start++) { if ($start === $end) { break; } - if ($tokens[$start]['code'] === T_OPEN_PARENTHESIS) { + if (isset($tokens[$start]['parenthesis_closer']) === true + && $tokens[$start]['code'] === T_OPEN_PARENTHESIS + ) { $start = $tokens[$start]['parenthesis_closer']; continue; } - if ($tokens[$start]['code'] === T_OPEN_CURLY_BRACKET) { + if (isset($tokens[$start]['bracket_closer']) === true + && $tokens[$start]['code'] === T_OPEN_CURLY_BRACKET + ) { $start = $tokens[$start]['bracket_closer']; continue; } - if ($tokens[$start]['code'] === T_SEMICOLON) { + if ($tokens[$start]['code'] === T_SEMICOLON || $tokens[$start]['code'] === T_CLOSE_TAG) { break; } }//end for @@ -254,6 +295,16 @@ public function process(File $phpcsFile, $stackPtr) continue; } + // Skip HTML whitespace. + if ($tokens[$i]['code'] === T_INLINE_HTML && \trim($tokens[$i]['content']) === '') { + continue; + } + + // Skip PHP re-open tag (eg, after inline HTML). + if ($tokens[$i]['code'] === T_OPEN_TAG) { + continue; + } + $line = $tokens[$i]['line']; if ($line > $lastLine) { $type = substr($tokens[$stackPtr]['type'], 2); diff --git a/src/Standards/Squiz/Sniffs/Scope/MethodScopeSniff.php b/src/Standards/Squiz/Sniffs/Scope/MethodScopeSniff.php index 8c60208c6c..8a34a4f29f 100644 --- a/src/Standards/Squiz/Sniffs/Scope/MethodScopeSniff.php +++ b/src/Standards/Squiz/Sniffs/Scope/MethodScopeSniff.php @@ -54,17 +54,8 @@ protected function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScop return; } - $modifier = null; - for ($i = ($stackPtr - 1); $i > 0; $i--) { - if ($tokens[$i]['line'] < $tokens[$stackPtr]['line']) { - break; - } else if (isset(Tokens::$scopeModifiers[$tokens[$i]['code']]) === true) { - $modifier = $i; - break; - } - } - - if ($modifier === null) { + $properties = $phpcsFile->getMethodProperties($stackPtr); + if ($properties['scope_specified'] === false) { $error = 'Visibility must be declared on method "%s"'; $data = [$methodName]; $phpcsFile->addError($error, $stackPtr, 'Missing', $data); diff --git a/src/Standards/Squiz/Sniffs/Scope/StaticThisUsageSniff.php b/src/Standards/Squiz/Sniffs/Scope/StaticThisUsageSniff.php index 0bafbf4db4..f3b5495d90 100644 --- a/src/Standards/Squiz/Sniffs/Scope/StaticThisUsageSniff.php +++ b/src/Standards/Squiz/Sniffs/Scope/StaticThisUsageSniff.php @@ -22,7 +22,7 @@ class StaticThisUsageSniff extends AbstractScopeSniff */ public function __construct() { - parent::__construct([T_CLASS, T_TRAIT, T_ANON_CLASS], [T_FUNCTION]); + parent::__construct([T_CLASS, T_TRAIT, T_ENUM, T_ANON_CLASS], [T_FUNCTION]); }//end __construct() @@ -76,9 +76,9 @@ public function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScope) /** * Check for $this variable usage between $next and $end tokens. * - * @param File $phpcsFile The current file being scanned. - * @param int $next The position of the next token to check. - * @param int $end The position of the last token to check. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The current file being scanned. + * @param int $next The position of the next token to check. + * @param int $end The position of the last token to check. * * @return void */ diff --git a/src/Standards/Squiz/Sniffs/Strings/ConcatenationSpacingSniff.php b/src/Standards/Squiz/Sniffs/Strings/ConcatenationSpacingSniff.php index 4125b63ad0..9d32e54a0a 100644 --- a/src/Standards/Squiz/Sniffs/Strings/ConcatenationSpacingSniff.php +++ b/src/Standards/Squiz/Sniffs/Strings/ConcatenationSpacingSniff.php @@ -55,6 +55,10 @@ public function register() public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); + if (isset($tokens[($stackPtr + 2)]) === false) { + // Syntax error or live coding, bow out. + return; + } $ignoreBefore = false; $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); diff --git a/src/Standards/Squiz/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php b/src/Standards/Squiz/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php index cfeb657e39..808888f404 100644 --- a/src/Standards/Squiz/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php +++ b/src/Standards/Squiz/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php @@ -46,6 +46,7 @@ public function register() T_TRY, T_CATCH, T_FINALLY, + T_MATCH, ]; }//end register() @@ -144,6 +145,7 @@ public function process(File $phpcsFile, $stackPtr) T_CLASS => true, T_INTERFACE => true, T_TRAIT => true, + T_ENUM => true, T_DOC_COMMENT_OPEN_TAG => true, ]; @@ -232,6 +234,16 @@ public function process(File $phpcsFile, $stackPtr) }//end if }//end if + if ($tokens[$stackPtr]['code'] === T_MATCH) { + // Move the scope closer to the semicolon/comma. + $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($scopeCloser + 1), null, true); + if ($next !== false + && ($tokens[$next]['code'] === T_SEMICOLON || $tokens[$next]['code'] === T_COMMA) + ) { + $scopeCloser = $next; + } + } + $trailingContent = $phpcsFile->findNext( T_WHITESPACE, ($scopeCloser + 1), diff --git a/src/Standards/Squiz/Sniffs/WhiteSpace/FunctionSpacingSniff.php b/src/Standards/Squiz/Sniffs/WhiteSpace/FunctionSpacingSniff.php index 83a62d3542..1f4704b91f 100644 --- a/src/Standards/Squiz/Sniffs/WhiteSpace/FunctionSpacingSniff.php +++ b/src/Standards/Squiz/Sniffs/WhiteSpace/FunctionSpacingSniff.php @@ -115,6 +115,12 @@ public function process(File $phpcsFile, $stackPtr) $ignore = ([T_WHITESPACE => T_WHITESPACE] + Tokens::$methodPrefixes); $prev = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true); + + while ($tokens[$prev]['code'] === T_ATTRIBUTE_END) { + // Skip past function attributes. + $prev = $phpcsFile->findPrevious($ignore, ($tokens[$prev]['attribute_opener'] - 1), null, true); + } + if ($tokens[$prev]['code'] === T_DOC_COMMENT_CLOSE_TAG) { // Skip past function docblocks. $prev = $phpcsFile->findPrevious($ignore, ($tokens[$prev]['comment_opener'] - 1), null, true); @@ -241,6 +247,14 @@ public function process(File $phpcsFile, $stackPtr) return; } + while ($tokens[$prevContent]['code'] === T_ATTRIBUTE_END + && $tokens[$prevContent]['line'] === ($currentLine - 1) + ) { + // Account for function attributes. + $currentLine = $tokens[$tokens[$prevContent]['attribute_opener']]['line']; + $prevContent = $phpcsFile->findPrevious(T_WHITESPACE, ($tokens[$prevContent]['attribute_opener'] - 1), null, true); + } + if ($tokens[$prevContent]['code'] === T_DOC_COMMENT_CLOSE_TAG && $tokens[$prevContent]['line'] === ($currentLine - 1) ) { 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/ObjectOperatorSpacingSniff.php b/src/Standards/Squiz/Sniffs/WhiteSpace/ObjectOperatorSpacingSniff.php index a1e83aadd1..cc5db16446 100644 --- a/src/Standards/Squiz/Sniffs/WhiteSpace/ObjectOperatorSpacingSniff.php +++ b/src/Standards/Squiz/Sniffs/WhiteSpace/ObjectOperatorSpacingSniff.php @@ -33,6 +33,7 @@ public function register() return [ T_OBJECT_OPERATOR, T_DOUBLE_COLON, + T_NULLSAFE_OBJECT_OPERATOR, ]; }//end register() diff --git a/src/Standards/Squiz/Sniffs/WhiteSpace/OperatorSpacingSniff.php b/src/Standards/Squiz/Sniffs/WhiteSpace/OperatorSpacingSniff.php index c69b0b6433..f1e2fce692 100644 --- a/src/Standards/Squiz/Sniffs/WhiteSpace/OperatorSpacingSniff.php +++ b/src/Standards/Squiz/Sniffs/WhiteSpace/OperatorSpacingSniff.php @@ -77,36 +77,31 @@ public function register() // Returning/printing a negative value; eg. (return -1). $this->nonOperandTokens += [ - T_RETURN => T_RETURN, - T_ECHO => T_ECHO, - T_PRINT => T_PRINT, - T_YIELD => T_YIELD, + T_RETURN => T_RETURN, + T_ECHO => T_ECHO, + T_EXIT => T_EXIT, + T_PRINT => T_PRINT, + T_YIELD => T_YIELD, + T_FN_ARROW => T_FN_ARROW, + T_MATCH_ARROW => T_MATCH_ARROW, ]; // Trying to use a negative value; eg. myFunction($var, -2). $this->nonOperandTokens += [ - T_COMMA => T_COMMA, - T_OPEN_PARENTHESIS => T_OPEN_PARENTHESIS, - T_OPEN_SQUARE_BRACKET => T_OPEN_SQUARE_BRACKET, - T_OPEN_SHORT_ARRAY => T_OPEN_SHORT_ARRAY, - T_DOUBLE_ARROW => T_DOUBLE_ARROW, + T_CASE => T_CASE, T_COLON => T_COLON, - T_INLINE_THEN => T_INLINE_THEN, + T_COMMA => T_COMMA, T_INLINE_ELSE => T_INLINE_ELSE, - T_CASE => T_CASE, + T_INLINE_THEN => T_INLINE_THEN, T_OPEN_CURLY_BRACKET => T_OPEN_CURLY_BRACKET, + T_OPEN_PARENTHESIS => T_OPEN_PARENTHESIS, + T_OPEN_SHORT_ARRAY => T_OPEN_SHORT_ARRAY, + T_OPEN_SQUARE_BRACKET => T_OPEN_SQUARE_BRACKET, + T_STRING_CONCAT => T_STRING_CONCAT, ]; // Casting a negative value; eg. (array) -$a. - $this->nonOperandTokens += [ - T_ARRAY_CAST => T_ARRAY_CAST, - T_BOOL_CAST => T_BOOL_CAST, - T_DOUBLE_CAST => T_DOUBLE_CAST, - T_INT_CAST => T_INT_CAST, - T_OBJECT_CAST => T_OBJECT_CAST, - T_STRING_CAST => T_STRING_CAST, - T_UNSET_CAST => T_UNSET_CAST, - ]; + $this->nonOperandTokens += Tokens::$castTokens; /* These are the tokens the sniff is looking for. @@ -344,6 +339,7 @@ protected function isOperator(File $phpcsFile, $stackPtr) $function = $tokens[$bracket]['parenthesis_owner']; if ($tokens[$function]['code'] === T_FUNCTION || $tokens[$function]['code'] === T_CLOSURE + || $tokens[$function]['code'] === T_FN || $tokens[$function]['code'] === T_DECLARE ) { return false; 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/Sniffs/WhiteSpace/ScopeKeywordSpacingSniff.php b/src/Standards/Squiz/Sniffs/WhiteSpace/ScopeKeywordSpacingSniff.php index e0fab85a8d..2d800f0520 100644 --- a/src/Standards/Squiz/Sniffs/WhiteSpace/ScopeKeywordSpacingSniff.php +++ b/src/Standards/Squiz/Sniffs/WhiteSpace/ScopeKeywordSpacingSniff.php @@ -26,6 +26,7 @@ public function register() { $register = Tokens::$scopeModifiers; $register[] = T_STATIC; + $register[] = T_READONLY; return $register; }//end register() @@ -51,20 +52,63 @@ public function process(File $phpcsFile, $stackPtr) $prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); $nextToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); - if ($tokens[$stackPtr]['code'] === T_STATIC - && (($nextToken === false || $tokens[$nextToken]['code'] === T_DOUBLE_COLON) - || $tokens[$prevToken]['code'] === T_NEW) - ) { - // Late static binding, e.g., static:: OR new static() usage or live coding. - return; - } + if ($tokens[$stackPtr]['code'] === T_STATIC) { + if (($nextToken === false || $tokens[$nextToken]['code'] === T_DOUBLE_COLON) + || $tokens[$prevToken]['code'] === T_NEW + ) { + // Late static binding, e.g., static:: OR new static() usage or live coding. + return; + } + + if ($prevToken !== false + && $tokens[$prevToken]['code'] === T_TYPE_UNION + ) { + // Not a scope keyword, but a union return type. + return; + } + + if ($prevToken !== false + && $tokens[$prevToken]['code'] === T_NULLABLE + ) { + // Not a scope keyword, but a return type. + return; + } + + if ($prevToken !== false + && $tokens[$prevToken]['code'] === T_COLON + ) { + $prevPrevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevToken - 1), null, true); + if ($prevPrevToken !== false + && $tokens[$prevPrevToken]['code'] === T_CLOSE_PARENTHESIS + ) { + // Not a scope keyword, but a return type. + return; + } + } + }//end if if ($tokens[$prevToken]['code'] === T_AS) { // Trait visibility change, e.g., "use HelloWorld { sayHello as private; }". return; } - if ($nextToken !== false && $tokens[$nextToken]['code'] === T_VARIABLE) { + $isInFunctionDeclaration = false; + if (empty($tokens[$stackPtr]['nested_parenthesis']) === false) { + // Check if this is PHP 8.0 constructor property promotion. + // In that case, we can't have multi-property definitions. + $nestedParens = $tokens[$stackPtr]['nested_parenthesis']; + $lastCloseParens = end($nestedParens); + if (isset($tokens[$lastCloseParens]['parenthesis_owner']) === true + && $tokens[$tokens[$lastCloseParens]['parenthesis_owner']]['code'] === T_FUNCTION + ) { + $isInFunctionDeclaration = true; + } + } + + if ($nextToken !== false + && $tokens[$nextToken]['code'] === T_VARIABLE + && $isInFunctionDeclaration === false + ) { $endOfStatement = $phpcsFile->findNext(T_SEMICOLON, ($nextToken + 1)); if ($endOfStatement === false) { // Live coding. diff --git a/src/Standards/Squiz/Sniffs/WhiteSpace/SuperfluousWhitespaceSniff.php b/src/Standards/Squiz/Sniffs/WhiteSpace/SuperfluousWhitespaceSniff.php index 831e832766..e99d829bca 100644 --- a/src/Standards/Squiz/Sniffs/WhiteSpace/SuperfluousWhitespaceSniff.php +++ b/src/Standards/Squiz/Sniffs/WhiteSpace/SuperfluousWhitespaceSniff.php @@ -49,6 +49,7 @@ public function register() { return [ T_OPEN_TAG, + T_OPEN_TAG_WITH_ECHO, T_CLOSE_TAG, T_WHITESPACE, T_COMMENT, diff --git a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc index e6fdbdc1f8..b96aec7bde 100644 --- a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc +++ b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc @@ -447,7 +447,85 @@ $c); array('a' => $a, 'b' => $b, 'c' => $c); +array( + static function() { + return null; + }, + (array) array(), + (bool) array(), + (double) array(), + (int) array(), + (object) array(), + (string) array(), + (unset) array(), +); + +array( + 'foo', + 'bar' + // This is a non-fixable error. + , +); + +yield array( + static fn () : string => '', +); + +yield array( + static fn () : string => '', + ); + +$foo = array( + 'foo' => match ($anything) { + 'foo' => 'bar', + default => null, + }, + ); + // Intentional syntax error. $a = array( 'a' => ); + +// Safeguard correct errors for key/no key when PHP 7.4+ array unpacking is encountered. +$x = array( + ...$a, + 'foo' => 'bar', + ); + +$x = array( + 'foo' => 'bar', + ...$a, + ); + +$x = array( + 'foo' => 'bar', + ...$a, + 'baz' => 'bar', + ); + +$x = array( + ...$a, + 'foo' => 'bar', // OK. + 'bar', // NoKeySpecified Error (based on second entry). + ); + +$x = array( + ...$a, + 'bar', // OK. + 'foo' => 'bar', // KeySpecified Error (based on second entry). + ); + +$x = array( + 'foo' => 'bar', + ...$a, + 'baz' => 'bar', + 'bar', // NoKeySpecified Error (based on first entry). + ); + +$x = array( + 'bar', + ...$a, + 'bar', + 'baz' => 'bar', // KeySpecified (based on first entry). + ); diff --git a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc.fixed b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc.fixed index 336ab0af9f..9da6a4279b 100644 --- a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc.fixed +++ b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.1.inc.fixed @@ -483,7 +483,85 @@ array( 'c' => $c, ); +array( + static function() { + return null; + }, + (array) array(), + (bool) array(), + (double) array(), + (int) array(), + (object) array(), + (string) array(), + (unset) array(), +); + +array( + 'foo', + 'bar' + // This is a non-fixable error. + , +); + +yield array( + static fn () : string => '', + ); + +yield array( + static fn () : string => '', + ); + +$foo = array( + 'foo' => match ($anything) { + 'foo' => 'bar', + default => null, + }, + ); + // Intentional syntax error. $a = array( 'a' => ); + +// Safeguard correct errors for key/no key when PHP 7.4+ array unpacking is encountered. +$x = array( + ...$a, + 'foo' => 'bar', + ); + +$x = array( + 'foo' => 'bar', + ...$a, + ); + +$x = array( + 'foo' => 'bar', + ...$a, + 'baz' => 'bar', + ); + +$x = array( + ...$a, + 'foo' => 'bar', // OK. + 'bar', // NoKeySpecified Error (based on second entry). + ); + +$x = array( + ...$a, + 'bar', // OK. + 'foo' => 'bar', // KeySpecified Error (based on second entry). + ); + +$x = array( + 'foo' => 'bar', + ...$a, + 'baz' => 'bar', + 'bar', // NoKeySpecified Error (based on first entry). + ); + +$x = array( + 'bar', + ...$a, + 'bar', + 'baz' => 'bar', // KeySpecified (based on first entry). + ); diff --git a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.2.inc b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.2.inc index 60ff35d64c..0c8b48fc89 100644 --- a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.2.inc +++ b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.2.inc @@ -436,7 +436,85 @@ $c]; ['a' => $a, 'b' => $b, 'c' => $c]; +[ + static function() { + return null; + }, + (array) [], + (bool) [], + (double) [], + (int) [], + (object) [], + (string) [], + (unset) [], +]; + +[ + 'foo', + 'bar' + // This is a non-fixable error. + , +]; + +yield [ + static fn () : string => '', +]; + +yield [ + static fn () : string => '', + ]; + +$foo = [ + 'foo' => match ($anything) { + 'foo' => 'bar', + default => null, + }, + ]; + // Intentional syntax error. $a = [ 'a' => ]; + +// Safeguard correct errors for key/no key when PHP 7.4+ array unpacking is encountered. +$x = [ + ...$a, + 'foo' => 'bar', + ]; + +$x = [ + 'foo' => 'bar', + ...$a, + ]; + +$x = [ + 'foo' => 'bar', + ...$a, + 'baz' => 'bar', + ]; + +$x = [ + ...$a, + 'foo' => 'bar', // OK. + 'bar', // NoKeySpecified Error (based on second entry). + ]; + +$x = [ + ...$a, + 'bar', // OK. + 'foo' => 'bar', // KeySpecified Error (based on second entry). + ]; + +$x = [ + 'foo' => 'bar', + ...$a, + 'baz' => 'bar', + 'bar', // NoKeySpecified Error (based on first entry). + ]; + +$x = [ + 'bar', + ...$a, + 'bar', + 'baz' => 'bar', // KeySpecified (based on first entry). + ]; diff --git a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.2.inc.fixed b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.2.inc.fixed index 1d17fa0c4d..4b09e2f234 100644 --- a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.2.inc.fixed +++ b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.2.inc.fixed @@ -470,7 +470,85 @@ $foo = [ 'c' => $c, ]; +[ + static function() { + return null; + }, + (array) [], + (bool) [], + (double) [], + (int) [], + (object) [], + (string) [], + (unset) [], +]; + +[ + 'foo', + 'bar' + // This is a non-fixable error. + , +]; + +yield [ + static fn () : string => '', + ]; + +yield [ + static fn () : string => '', + ]; + +$foo = [ + 'foo' => match ($anything) { + 'foo' => 'bar', + default => null, + }, + ]; + // Intentional syntax error. $a = [ 'a' => ]; + +// Safeguard correct errors for key/no key when PHP 7.4+ array unpacking is encountered. +$x = [ + ...$a, + 'foo' => 'bar', + ]; + +$x = [ + 'foo' => 'bar', + ...$a, + ]; + +$x = [ + 'foo' => 'bar', + ...$a, + 'baz' => 'bar', + ]; + +$x = [ + ...$a, + 'foo' => 'bar', // OK. + 'bar', // NoKeySpecified Error (based on second entry). + ]; + +$x = [ + ...$a, + 'bar', // OK. + 'foo' => 'bar', // KeySpecified Error (based on second entry). + ]; + +$x = [ + 'foo' => 'bar', + ...$a, + 'baz' => 'bar', + 'bar', // NoKeySpecified Error (based on first entry). + ]; + +$x = [ + 'bar', + ...$a, + 'bar', + 'baz' => 'bar', // KeySpecified (based on first entry). + ]; diff --git a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.php b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.php index 818b1b5562..9d917b46b0 100644 --- a/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.php +++ b/src/Standards/Squiz/Tests/Arrays/ArrayDeclarationUnitTest.php @@ -121,6 +121,13 @@ public function getErrorList($testFile='') 445 => 2, 447 => 2, 448 => 3, + 467 => 1, + 471 => 1, + 472 => 1, + 510 => 1, + 516 => 1, + 523 => 1, + 530 => 1, ]; case 'ArrayDeclarationUnitTest.2.inc': return [ @@ -204,6 +211,13 @@ public function getErrorList($testFile='') 434 => 2, 436 => 2, 437 => 3, + 456 => 1, + 460 => 1, + 461 => 1, + 499 => 1, + 505 => 1, + 512 => 1, + 519 => 1, ]; default: return []; diff --git a/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.inc b/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.inc index 43ef6c9cdd..2af42d3721 100644 --- a/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.inc +++ b/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.inc @@ -119,3 +119,12 @@ class IncorrectCodeBeforeClosingBrace { echo phpinfo();} + +readonly +class Test +{ +} + +readonly class Test +{ +} diff --git a/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.inc.fixed index 9083633fa9..5d01b68e0a 100644 --- a/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.inc.fixed @@ -130,3 +130,11 @@ class IncorrectCodeBeforeClosingBrace echo phpinfo(); } + +readonly class Test +{ +} + +readonly class Test +{ +} diff --git a/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.php b/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.php index 914e6c87ef..23d89ec77a 100644 --- a/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.php +++ b/src/Standards/Squiz/Tests/Classes/ClassDeclarationUnitTest.php @@ -61,6 +61,8 @@ public function getErrorList() 116 => 1, 118 => 1, 121 => 1, + 124 => 2, + 128 => 2, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.inc b/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.inc index a346a00f21..8b5a5aa708 100644 --- a/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.inc +++ b/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.inc @@ -5,7 +5,8 @@ class ClassFileNameUnitTest {} interface ClassFileNameUnitTest {} trait ClassFileNameUnitTest {} - +enum ClassFileNameUnitTest {} +enum ClassFileNameUnitTest: int {} // Invalid filename matching class name (case sensitive). class classFileNameUnitTest {} @@ -17,6 +18,9 @@ interface CLASSFILENAMEUNITTEST {} trait classFileNameUnitTest {} trait classfilenameunittest {} trait CLASSFILENAMEUNITTEST {} +enum classFileNameUnitTest {} +enum classfilenameunittest {} +enum CLASSFILENAMEUNITTEST {} // Invalid non-filename matching class names. @@ -32,6 +36,10 @@ trait CompletelyWrongClassName {} trait ClassFileNameUnitTestExtra {} trait ClassFileNameUnitTestInc {} trait ExtraClassFileNameUnitTest {} +enum CompletelyWrongClassName {} +enum ClassFileNameUnitTestExtra {} +enum ClassFileNameUnitTestInc {} +enum ExtraClassFileNameUnitTest {} -?> \ No newline at end of file +?> diff --git a/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.php b/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.php index b229a2fc3f..5964d2b1ec 100644 --- a/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.php +++ b/src/Standards/Squiz/Tests/Classes/ClassFileNameUnitTest.php @@ -26,7 +26,6 @@ class ClassFileNameUnitTest extends AbstractSniffUnitTest public function getErrorList() { return [ - 11 => 1, 12 => 1, 13 => 1, 14 => 1, @@ -35,10 +34,10 @@ public function getErrorList() 17 => 1, 18 => 1, 19 => 1, + 20 => 1, + 21 => 1, + 22 => 1, 23 => 1, - 24 => 1, - 25 => 1, - 26 => 1, 27 => 1, 28 => 1, 29 => 1, @@ -47,6 +46,14 @@ public function getErrorList() 32 => 1, 33 => 1, 34 => 1, + 35 => 1, + 36 => 1, + 37 => 1, + 38 => 1, + 39 => 1, + 40 => 1, + 41 => 1, + 42 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc b/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc index 511bbe4710..eb724505eb 100644 --- a/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc +++ b/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc @@ -3,8 +3,9 @@ Abstract Class MyClass Extends MyClass {} Final Class MyClass Implements MyInterface {} Interface MyInterface {} Trait MyTrait {} +Enum MyEnum IMPLEMENTS Colorful {} -class MyClass +ReadOnly class MyClass { Var $myVar = null; Const myConst = true; diff --git a/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc.fixed index 859d0d2db2..672fdfb3c4 100644 --- a/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.inc.fixed @@ -3,8 +3,9 @@ abstract class MyClass extends MyClass {} final class MyClass implements MyInterface {} interface MyInterface {} trait MyTrait {} +enum MyEnum implements Colorful {} -class MyClass +readonly class MyClass { var $myVar = null; const myConst = true; diff --git a/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.php b/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.php index f2fc20b438..b4d5aae5a1 100644 --- a/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.php +++ b/src/Standards/Squiz/Tests/Classes/LowercaseClassKeywordsUnitTest.php @@ -30,9 +30,11 @@ public function getErrorList() 3 => 3, 4 => 1, 5 => 1, - 9 => 1, + 6 => 2, + 8 => 1, 10 => 1, - 13 => 1, + 11 => 1, + 14 => 1, ]; return $errors; diff --git a/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.inc b/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.inc index aadbab5b8e..3fe39435b4 100644 --- a/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.inc +++ b/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.inc @@ -137,6 +137,50 @@ trait Base } } +// Valid enum name. +enum ValidCamelCaseClass: string {} + + +// Incorrect usage of camel case. +enum invalidCamelCaseClass {} +enum Invalid_Camel_Case_Class_With_Underscores {} + + +// All lowercase. +enum invalidlowercaseclass: INT {} +enum invalid_lowercase_class_with_underscores {} + + +// All uppercase. +enum VALIDUPPERCASECLASS: int {} +enum INVALID_UPPERCASE_CLASS_WITH_UNDERSCORES {} + + +// Mix camel case with uppercase. +enum ValidCamelCaseClassWithUPPERCASE : string {} + + +// Usage of numeric characters. +enum ValidCamelCaseClassWith1Number {} +enum ValidCamelCaseClassWith12345Numbers : string {} +enum ValidCamelCaseClassEndingWithNumber5 {} + +enum Testing{} + +enum Base +{ + public function __construct() + { + $this->anonymous = new class extends ArrayObject + { + public function __construct() + { + parent::__construct(['a' => 1, 'b' => 2]); + } + }; + } +} + if ( class_exists( Test :: class ) ) {} if ( class_exists( Test2 ::class ) ) {} diff --git a/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.php b/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.php index 70777c541b..b7de260b65 100644 --- a/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.php +++ b/src/Standards/Squiz/Tests/Classes/ValidClassNameUnitTest.php @@ -47,6 +47,11 @@ public function getErrorList() 108 => 1, 118 => 1, 120 => 1, + 145 => 1, + 146 => 1, + 150 => 1, + 151 => 1, + 156 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc index 877bca648e..7cd04a2114 100644 --- a/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc +++ b/src/Standards/Squiz/Tests/Commenting/BlockCommentUnitTest.inc @@ -256,3 +256,54 @@ $y = 10 + /* test */ -2; /* * No blank line allowed above the comment if it's the first non-empty token after a PHP open tag. */ + +?> + + + + 1, 233 => 1, 256 => 1, + 271 => 1, + 273 => 1, ]; return $errors; diff --git a/src/Standards/Squiz/Tests/Commenting/ClassCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/ClassCommentUnitTest.inc index eefa44c9bb..8de3d0a702 100644 --- a/src/Standards/Squiz/Tests/Commenting/ClassCommentUnitTest.inc +++ b/src/Standards/Squiz/Tests/Commenting/ClassCommentUnitTest.inc @@ -123,3 +123,23 @@ class Space_At_end { }//end class + +/** + * Sample class comment + */ +#[Attribute] +class AllGood +{ +} + +/** + * Docblock + */ +abstract readonly class AbstractReadonlyWithDocblock {} + +/* + * Docblock + */ +readonly class ReadonlyWrongStyle {} + +readonly final class ReadonlyFinalWithoutDocblock {} diff --git a/src/Standards/Squiz/Tests/Commenting/ClassCommentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/ClassCommentUnitTest.php index 80a404be07..b370d547c0 100644 --- a/src/Standards/Squiz/Tests/Commenting/ClassCommentUnitTest.php +++ b/src/Standards/Squiz/Tests/Commenting/ClassCommentUnitTest.php @@ -26,10 +26,12 @@ class ClassCommentUnitTest extends AbstractSniffUnitTest public function getErrorList() { return [ - 2 => 1, - 15 => 1, - 31 => 1, - 54 => 1, + 2 => 1, + 15 => 1, + 31 => 1, + 54 => 1, + 143 => 1, + 145 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.inc index 9c3255cf9b..1a57149b26 100644 --- a/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.inc +++ b/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.inc @@ -79,4 +79,8 @@ class TestClass } //end class -?> \ No newline at end of file +enum MissingClosingComment { +} + +enum HasClosingComment { +}//end enum diff --git a/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.php index cdc89ba670..6f5166ec6b 100644 --- a/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.php +++ b/src/Standards/Squiz/Tests/Commenting/ClosingDeclarationCommentUnitTest.php @@ -34,6 +34,7 @@ public function getErrorList() 63 => 1, 67 => 1, 79 => 1, + 83 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc index e7d880d682..d95acd2ce1 100644 --- a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc +++ b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc @@ -31,7 +31,7 @@ class MyClass * Some info about the class here * */ -class MyClass +readonly class MyClass { /** * Some info about the function here. @@ -77,6 +77,26 @@ class MyClass2 var $x; } +abstract class MyClass +{ + /** +* Property comment + */ + readonly public string $prop; +} + +/** + * Some info about the enum here + * +*/ +enum Suits: string +{ + /** + * Some info about the case here. + */ + case HEARTS; +} + /** ************************************************************************ * Example with no errors. **************************************************************************/ diff --git a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc.fixed index 4d8cb39273..ea6488a02b 100644 --- a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.inc.fixed @@ -31,7 +31,7 @@ class MyClass * Some info about the class here * */ -class MyClass +readonly class MyClass { /** * Some info about the function here. @@ -77,6 +77,26 @@ class MyClass2 var $x; } +abstract class MyClass +{ + /** + * Property comment + */ + readonly public string $prop; +} + +/** + * Some info about the enum here + * + */ +enum Suits: string +{ + /** + * Some info about the case here. + */ + case HEARTS; +} + /** ************************************************************************ * Example with no errors. **************************************************************************/ diff --git a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.php index 974951ce42..acbf13e869 100644 --- a/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.php +++ b/src/Standards/Squiz/Tests/Commenting/DocCommentAlignmentUnitTest.php @@ -45,6 +45,12 @@ public function getErrorList($testFile='DocCommentAlignmentUnitTest.inc') if ($testFile === 'DocCommentAlignmentUnitTest.inc') { $errors[75] = 1; + $errors[83] = 1; + $errors[84] = 1; + $errors[90] = 1; + $errors[91] = 1; + $errors[95] = 1; + $errors[96] = 1; } return $errors; diff --git a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.inc.fixed b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.inc.fixed index bae8e7f810..3bcd945388 100644 --- a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.inc.fixed +++ b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.inc.fixed @@ -25,7 +25,7 @@ * @author * @copyright 1997 Squiz Pty Ltd (ABN 77 084 670 600) * @copyright 1994-1997 Squiz Pty Ltd (ABN 77 084 670 600) -* @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600) +* @copyright 2023 Squiz Pty Ltd (ABN 77 084 670 600) * @license http://www.php.net/license/3_0.txt * @summary An unknown summary tag * diff --git a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.js.fixed b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.js.fixed index 82d63d9036..56a392d985 100644 --- a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.js.fixed +++ b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.1.js.fixed @@ -25,7 +25,7 @@ * @author * @copyright 1997 Squiz Pty Ltd (ABN 77 084 670 600) * @copyright 1994-1997 Squiz Pty Ltd (ABN 77 084 670 600) -* @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600) +* @copyright 2023 Squiz Pty Ltd (ABN 77 084 670 600) * @license http://www.php.net/license/3_0.txt * @summary An unknown summary tag * diff --git a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.10.inc b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.10.inc new file mode 100644 index 0000000000..1f82abfeaf --- /dev/null +++ b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.10.inc @@ -0,0 +1,12 @@ + + * @copyright 2010-2014 Squiz Pty Ltd (ABN 77 084 670 600) + */ + +readonly class Foo { +} diff --git a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.7.inc b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.7.inc new file mode 100644 index 0000000000..7074dac234 --- /dev/null +++ b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.7.inc @@ -0,0 +1,12 @@ + + * @copyright 2010-2014 Squiz Pty Ltd (ABN 77 084 670 600) + */ +#[Attribute] +class Foo { +} 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/FileCommentUnitTest.9.inc b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.9.inc new file mode 100644 index 0000000000..f6c9d99682 --- /dev/null +++ b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.9.inc @@ -0,0 +1,12 @@ + + * @copyright 2010-2014 Squiz Pty Ltd (ABN 77 084 670 600) + */ + +enum Foo { +} diff --git a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.php index b3708e80cf..1402432aec 100644 --- a/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.php +++ b/src/Standards/Squiz/Tests/Commenting/FileCommentUnitTest.php @@ -44,6 +44,9 @@ public function getErrorList($testFile='FileCommentUnitTest.inc') case 'FileCommentUnitTest.4.inc': case 'FileCommentUnitTest.6.inc': + case 'FileCommentUnitTest.7.inc': + case 'FileCommentUnitTest.9.inc': + case 'FileCommentUnitTest.10.inc': return [1 => 1]; case 'FileCommentUnitTest.5.inc': diff --git a/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc index 1a7020003f..a44f5e0e2e 100644 --- a/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc +++ b/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc @@ -666,7 +666,7 @@ class Baz { * Test * * @return void - * @throws E + * @throws E */ function myFunction() {} @@ -1000,3 +1000,137 @@ public function foo(object $a, ?object $b) {} * @return void */ function foo($foo) {} + +/** + * {@inheritDoc} + */ +public function foo($a, $b) {} + +// phpcs:set Squiz.Commenting.FunctionComment skipIfInheritdoc true + +/** + * {@inheritDoc} + */ +public function foo($a, $b) {} + +/** + * Foo. + * + * @param mixed $a Comment. + * + * @return mixed + */ +public function foo(mixed $a): mixed {} + +// phpcs:set Squiz.Commenting.FunctionComment specialMethods[] +class Bar { + /** + * The PHP5 constructor + */ + public function __construct() { + + } +} + +// phpcs:set Squiz.Commenting.FunctionComment specialMethods[] ignored +/** + * Should be ok + */ +public function ignored() { + +} + +// phpcs:set Squiz.Commenting.FunctionComment specialMethods[] __construct,__destruct + +/** + * @return void + * @throws Exception If any other error occurs. */ +function throwCommentOneLine() {} + +/** + * When two adjacent pipe symbols are used (by mistake), the sniff should not throw a PHP Fatal error + * + * @param stdClass||null $object While invalid, this should not throw a PHP Fatal error. + * @return void + */ +function doublePipeFatalError(?stdClass $object) {} + +/** + * Test for passing variables by reference + * + * This sniff treats the '&' as optional for parameters passed by reference, but + * forbidden for parameters which are not passed by reference. + * + * Because mismatches may be in either direction, we cannot auto-fix these. + * + * @param string $foo A string passed in by reference. + * @param string &$bar A string passed in by reference. + * @param string $baz A string NOT passed in by reference. + * @param string &$qux A string NOT passed in by reference. + * @param string &$case1 A string passed in by reference with a case mismatch. + * @param string &$CASE2 A string NOT passed in by reference, also with a case mismatch. + * + * @return void + */ +public function variablesPassedByReference(&$foo, &$bar, $baz, $qux, &$CASE1, $case2) +{ + return; +} + +/** + * Test for param tag containing ref, but param in declaration not being by ref. + * + * @param string &$foo This should be flagged as (only) ParamNameUnexpectedAmpersandPrefix. + * @param string &$bar This should be flagged as (only) ParamNameNoMatch. + * @param string &$baz This should be flagged as (only) ParamNameNoCaseMatch. + * + * @return void + */ +function passedByRefMismatch($foo, $bra, $BAZ) { + return; +} + +/** + * Test variable case + * + * @param string $foo This parameter is lowercase. + * @param string $BAR This parameter is UPPERCASE. + * @param string $BazQux This parameter is TitleCase. + * @param string $corgeGrault This parameter is camelCase. + * @param string $GARPLY This parameter should be in lowercase. + * @param string $waldo This parameter should be in TitleCase. + * @param string $freD This parameter should be in UPPERCASE. + * @param string $PLUGH This parameter should be in TitleCase. + * + * @return void + */ +public function variableCaseTest( + $foo, + $BAR, + $BazQux, + $corgeGrault, + $garply, + $Waldo, + $FRED, + $PluGh +) { + return; +} + +/** + * Test variable order mismatch + * + * @param string $foo This is the third parameter. + * @param string $bar This is the first parameter. + * @param string $baz This is the second parameter. + * + * @return void + */ +public function variableOrderMismatch($bar, $baz, $foo) { + return; +} + +/** + * @return never + */ +function foo() {} diff --git a/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc.fixed index 4d91ecdcab..3d2e10fd6a 100644 --- a/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.inc.fixed @@ -666,7 +666,7 @@ class Baz { * Test * * @return void - * @throws E + * @throws E */ function myFunction() {} @@ -1000,3 +1000,137 @@ public function foo(object $a, ?object $b) {} * @return void */ function foo($foo) {} + +/** + * {@inheritDoc} + */ +public function foo($a, $b) {} + +// phpcs:set Squiz.Commenting.FunctionComment skipIfInheritdoc true + +/** + * {@inheritDoc} + */ +public function foo($a, $b) {} + +/** + * Foo. + * + * @param mixed $a Comment. + * + * @return mixed + */ +public function foo(mixed $a): mixed {} + +// phpcs:set Squiz.Commenting.FunctionComment specialMethods[] +class Bar { + /** + * The PHP5 constructor + */ + public function __construct() { + + } +} + +// phpcs:set Squiz.Commenting.FunctionComment specialMethods[] ignored +/** + * Should be ok + */ +public function ignored() { + +} + +// phpcs:set Squiz.Commenting.FunctionComment specialMethods[] __construct,__destruct + +/** + * @return void + * @throws Exception If any other error occurs. */ +function throwCommentOneLine() {} + +/** + * When two adjacent pipe symbols are used (by mistake), the sniff should not throw a PHP Fatal error + * + * @param stdClass|null $object While invalid, this should not throw a PHP Fatal error. + * @return void + */ +function doublePipeFatalError(?stdClass $object) {} + +/** + * Test for passing variables by reference + * + * This sniff treats the '&' as optional for parameters passed by reference, but + * forbidden for parameters which are not passed by reference. + * + * Because mismatches may be in either direction, we cannot auto-fix these. + * + * @param string $foo A string passed in by reference. + * @param string &$bar A string passed in by reference. + * @param string $baz A string NOT passed in by reference. + * @param string &$qux A string NOT passed in by reference. + * @param string &$case1 A string passed in by reference with a case mismatch. + * @param string &$CASE2 A string NOT passed in by reference, also with a case mismatch. + * + * @return void + */ +public function variablesPassedByReference(&$foo, &$bar, $baz, $qux, &$CASE1, $case2) +{ + return; +} + +/** + * Test for param tag containing ref, but param in declaration not being by ref. + * + * @param string &$foo This should be flagged as (only) ParamNameUnexpectedAmpersandPrefix. + * @param string &$bar This should be flagged as (only) ParamNameNoMatch. + * @param string &$baz This should be flagged as (only) ParamNameNoCaseMatch. + * + * @return void + */ +function passedByRefMismatch($foo, $bra, $BAZ) { + return; +} + +/** + * Test variable case + * + * @param string $foo This parameter is lowercase. + * @param string $BAR This parameter is UPPERCASE. + * @param string $BazQux This parameter is TitleCase. + * @param string $corgeGrault This parameter is camelCase. + * @param string $GARPLY This parameter should be in lowercase. + * @param string $waldo This parameter should be in TitleCase. + * @param string $freD This parameter should be in UPPERCASE. + * @param string $PLUGH This parameter should be in TitleCase. + * + * @return void + */ +public function variableCaseTest( + $foo, + $BAR, + $BazQux, + $corgeGrault, + $garply, + $Waldo, + $FRED, + $PluGh +) { + return; +} + +/** + * Test variable order mismatch + * + * @param string $foo This is the third parameter. + * @param string $bar This is the first parameter. + * @param string $baz This is the second parameter. + * + * @return void + */ +public function variableOrderMismatch($bar, $baz, $foo) { + return; +} + +/** + * @return never + */ +function foo() {} diff --git a/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.php index e8e02f8496..ff1d10c487 100644 --- a/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.php +++ b/src/Standards/Squiz/Tests/Commenting/FunctionCommentUnitTest.php @@ -26,93 +26,112 @@ class FunctionCommentUnitTest extends AbstractSniffUnitTest public function getErrorList() { $errors = [ - 5 => 1, - 10 => 3, - 12 => 2, - 13 => 2, - 14 => 1, - 15 => 1, - 28 => 1, - 43 => 1, - 76 => 1, - 87 => 1, - 103 => 1, - 109 => 1, - 112 => 1, - 122 => 1, - 123 => 3, - 124 => 2, - 125 => 1, - 126 => 1, - 137 => 4, - 138 => 4, - 139 => 4, - 143 => 2, - 152 => 1, - 155 => 2, - 159 => 1, - 166 => 1, - 173 => 1, - 183 => 1, - 190 => 2, - 193 => 2, - 196 => 1, - 199 => 2, - 210 => 1, - 211 => 1, - 222 => 1, - 223 => 1, - 224 => 1, - 225 => 1, - 226 => 1, - 227 => 1, - 230 => 2, - 232 => 2, - 246 => 1, - 248 => 4, - 261 => 1, - 263 => 1, - 276 => 1, - 277 => 1, - 278 => 1, - 279 => 1, - 280 => 1, - 281 => 1, - 284 => 1, - 286 => 7, - 294 => 1, - 302 => 1, - 312 => 1, - 358 => 1, - 359 => 2, - 372 => 1, - 373 => 1, - 387 => 1, - 407 => 1, - 441 => 1, - 500 => 1, - 526 => 1, - 548 => 1, - 641 => 1, - 669 => 1, - 688 => 1, - 744 => 1, - 748 => 1, - 767 => 1, - 789 => 1, - 792 => 1, - 794 => 1, - 797 => 1, - 801 => 1, - 828 => 1, - 840 => 1, - 852 => 1, - 864 => 1, - 886 => 1, - 888 => 1, - 890 => 1, - 978 => 1, - 997 => 1, + 5 => 1, + 10 => 3, + 12 => 2, + 13 => 2, + 14 => 1, + 15 => 1, + 28 => 1, + 43 => 1, + 76 => 1, + 87 => 1, + 103 => 1, + 109 => 1, + 112 => 1, + 122 => 1, + 123 => 3, + 124 => 2, + 125 => 1, + 126 => 1, + 137 => 4, + 138 => 4, + 139 => 4, + 143 => 2, + 155 => 1, + 159 => 1, + 166 => 1, + 173 => 1, + 183 => 1, + 190 => 2, + 193 => 2, + 196 => 1, + 199 => 2, + 210 => 1, + 211 => 1, + 222 => 1, + 223 => 1, + 224 => 1, + 225 => 1, + 226 => 1, + 227 => 1, + 230 => 2, + 232 => 2, + 246 => 1, + 248 => 4, + 261 => 1, + 263 => 1, + 276 => 1, + 277 => 1, + 278 => 1, + 279 => 1, + 280 => 1, + 281 => 1, + 284 => 1, + 286 => 7, + 294 => 1, + 302 => 1, + 312 => 1, + 358 => 1, + 359 => 2, + 372 => 1, + 373 => 1, + 387 => 1, + 407 => 1, + 441 => 1, + 500 => 1, + 526 => 1, + 548 => 1, + 641 => 1, + 669 => 1, + 688 => 1, + 744 => 1, + 748 => 1, + 767 => 1, + 789 => 1, + 792 => 1, + 794 => 1, + 797 => 1, + 801 => 1, + 828 => 1, + 840 => 1, + 852 => 1, + 864 => 1, + 886 => 1, + 888 => 1, + 890 => 1, + 978 => 1, + 997 => 1, + 1004 => 2, + 1006 => 1, + 1029 => 1, + 1053 => 1, + 1058 => 2, + 1069 => 1, + 1070 => 1, + 1071 => 1, + 1080 => 2, + 1083 => 1, + 1084 => 1, + 1085 => 1, + 1093 => 4, + 1100 => 1, + 1101 => 1, + 1102 => 1, + 1103 => 1, + 1123 => 1, + 1124 => 1, + 1125 => 1, ]; // Scalar type hints only work from PHP 7 onwards. @@ -128,12 +147,16 @@ public function getErrorList() $errors[575] = 2; $errors[627] = 1; $errors[1002] = 1; + $errors[1075] = 6; + $errors[1089] = 3; + $errors[1107] = 8; + $errors[1129] = 3; } else { $errors[729] = 4; $errors[740] = 2; $errors[752] = 2; $errors[982] = 1; - } + }//end if // Object type hints only work from PHP 7.2 onwards. if (PHP_VERSION_ID >= 70200) { @@ -142,6 +165,15 @@ public function getErrorList() $errors[992] = 2; } + // Mixed type hints only work from PHP 8.0 onwards. + if (PHP_VERSION_ID >= 80000) { + $errors[265] = 1; + $errors[459] = 1; + $errors[893] = 3; + } else { + $errors[1023] = 1; + } + return $errors; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc index 377db023d9..024876842e 100644 --- a/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc +++ b/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc @@ -149,6 +149,43 @@ 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() {} +} + +/** + * Comment should be ignored. + * + */ +enum MyEnum { + +} + +/** + * Comment should be ignored. + * + */ +readonly class MyClass +{ + /** + * Comment should be ignored. + * + */ + readonly $property = 10; +} + /* * 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..949a9ff949 100644 --- a/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/Commenting/InlineCommentUnitTest.inc.fixed @@ -142,6 +142,43 @@ 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() {} +} + +/** + * Comment should be ignored. + * + */ +enum MyEnum { + +} + +/** + * Comment should be ignored. + * + */ +readonly class MyClass +{ + /** + * Comment should be ignored. + * + */ + readonly $property = 10; +} + /* * 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/LongConditionClosingCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.inc index 732003b70a..d6c4cf6c87 100644 --- a/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.inc +++ b/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.inc @@ -960,3 +960,74 @@ try { } finally { // some code here. } + +$expr = match ($foo) { + // Line 1 + // Line 2 + // Line 3 + // Line 4 + // Line 5 + // Line 6 + // Line 7 + // Line 8 + // Line 9 + // Line 10 + // Line 11 + // Line 12 + // Line 13 + // Line 14 + // Line 15 + // Line 16 + // Line 17 + // Line 18 + // Line 19 + // Line 20 +}; //end switch + +$expr = match ($foo) { + // Line 1 + // Line 2 + // Line 3 + // Line 4 + // Line 5 + // Line 6 + // Line 7 + // Line 8 + // Line 9 + // Line 10 + // Line 11 + // Line 12 + // Line 13 + // Line 14 + // Line 15 + // Line 16 + // Line 17 + // Line 18 + // Line 19 + // Line 20 +}; + +$array = [ + 'match' => match ($foo) { + // Line 1 + // Line 2 + // Line 3 + // Line 4 + // Line 5 + // Line 6 + // Line 7 + // Line 8 + // Line 9 + // Line 10 + // Line 11 + // Line 12 + // Line 13 + // Line 14 + // Line 15 + // Line 16 + // Line 17 + // Line 18 + // Line 19 + // Line 20 + }, +]; diff --git a/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.inc.fixed index 6edc34dfc4..176cfe2486 100644 --- a/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.inc.fixed @@ -960,3 +960,74 @@ try { } finally { // some code here. }//end try + +$expr = match ($foo) { + // Line 1 + // Line 2 + // Line 3 + // Line 4 + // Line 5 + // Line 6 + // Line 7 + // Line 8 + // Line 9 + // Line 10 + // Line 11 + // Line 12 + // Line 13 + // Line 14 + // Line 15 + // Line 16 + // Line 17 + // Line 18 + // Line 19 + // Line 20 +}; //end match + +$expr = match ($foo) { + // Line 1 + // Line 2 + // Line 3 + // Line 4 + // Line 5 + // Line 6 + // Line 7 + // Line 8 + // Line 9 + // Line 10 + // Line 11 + // Line 12 + // Line 13 + // Line 14 + // Line 15 + // Line 16 + // Line 17 + // Line 18 + // Line 19 + // Line 20 +};//end match + +$array = [ + 'match' => match ($foo) { + // Line 1 + // Line 2 + // Line 3 + // Line 4 + // Line 5 + // Line 6 + // Line 7 + // Line 8 + // Line 9 + // Line 10 + // Line 11 + // Line 12 + // Line 13 + // Line 14 + // Line 15 + // Line 16 + // Line 17 + // Line 18 + // Line 19 + // Line 20 + },//end match +]; diff --git a/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.php index 60d0292d23..5b61612c50 100644 --- a/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.php +++ b/src/Standards/Squiz/Tests/Commenting/LongConditionClosingCommentUnitTest.php @@ -30,32 +30,35 @@ public function getErrorList($testFile='LongConditionClosingCommentUnitTest.inc' switch ($testFile) { case 'LongConditionClosingCommentUnitTest.inc': return [ - 49 => 1, - 99 => 1, - 146 => 1, - 192 => 1, - 215 => 1, - 238 => 1, - 261 => 1, - 286 => 1, - 309 => 1, - 332 => 1, - 355 => 1, - 378 => 1, - 493 => 1, - 531 => 1, - 536 => 1, - 540 => 1, - 562 => 1, - 601 => 1, - 629 => 1, - 663 => 1, - 765 => 1, - 798 => 1, - 811 => 1, - 897 => 1, - 931 => 1, - 962 => 1, + 49 => 1, + 99 => 1, + 146 => 1, + 192 => 1, + 215 => 1, + 238 => 1, + 261 => 1, + 286 => 1, + 309 => 1, + 332 => 1, + 355 => 1, + 378 => 1, + 493 => 1, + 531 => 1, + 536 => 1, + 540 => 1, + 562 => 1, + 601 => 1, + 629 => 1, + 663 => 1, + 765 => 1, + 798 => 1, + 811 => 1, + 897 => 1, + 931 => 1, + 962 => 1, + 985 => 2, + 1008 => 1, + 1032 => 1, ]; break; case 'LongConditionClosingCommentUnitTest.js': diff --git a/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.inc b/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.inc index 5650e19b38..b83469613f 100644 --- a/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.inc +++ b/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.inc @@ -46,3 +46,9 @@ for ( if ( $condition === true // comment && $anotherCondition === false ) {} + +$match = match($foo // comment + && $bar +) { + 1 => 1, // comment +}; diff --git a/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.inc.fixed index 99c9e92a29..21a4bbe032 100644 --- a/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.inc.fixed @@ -50,3 +50,10 @@ for ( if ( $condition === true // comment && $anotherCondition === false ) {} + +$match = match($foo // comment + && $bar +) { + 1 => 1, +// comment +}; diff --git a/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.php b/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.php index fbc08007c9..a04426f15a 100644 --- a/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.php +++ b/src/Standards/Squiz/Tests/Commenting/PostStatementCommentUnitTest.php @@ -34,6 +34,7 @@ public function getErrorList($testFile='PostStatementCommentUnitTest.inc') 10 => 1, 18 => 1, 35 => 1, + 53 => 1, ]; case 'PostStatementCommentUnitTest.1.js': 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/ControlStructures/ControlSignatureUnitTest.inc b/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.inc index 249ae15847..8eaf1b0373 100644 --- a/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.inc +++ b/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.inc @@ -299,6 +299,12 @@ while ( $level-- ): ob_end_clean(); endwhile; +$r = match ($x) { + 1 => 1, +}; + +$r = match($x){1 => 1}; + // Intentional parse error. This should be the last test in the file. foreach // Some unrelated comment. diff --git a/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.inc.fixed b/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.inc.fixed index f0cdde7d78..dc5233d917 100644 --- a/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.inc.fixed @@ -302,6 +302,13 @@ while ( $level-- ): ob_end_clean(); endwhile; +$r = match ($x) { + 1 => 1, +}; + +$r = match ($x) { +1 => 1}; + // Intentional parse error. This should be the last test in the file. foreach // Some unrelated comment. diff --git a/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.php b/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.php index 07e57b4661..92427f0da1 100644 --- a/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.php +++ b/src/Standards/Squiz/Tests/ControlStructures/ControlSignatureUnitTest.php @@ -76,6 +76,7 @@ public function getErrorList($testFile='ControlSignatureUnitTest.inc') $errors[276] = 1; $errors[279] = 1; $errors[283] = 1; + $errors[306] = 3; }//end if return $errors; diff --git a/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.inc b/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.inc index c82f35aee4..1acf3eac43 100644 --- a/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.inc +++ b/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.inc @@ -20,3 +20,5 @@ If ($condition) { TRY { } Catch (Exception $e) { } + +$r = MATCH ($x) {}; diff --git a/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.inc.fixed b/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.inc.fixed index 973781d77f..3c0d3c6378 100644 --- a/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.inc.fixed @@ -20,3 +20,5 @@ if ($condition) { try { } catch (Exception $e) { } + +$r = match ($x) {}; diff --git a/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.php b/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.php index 758119084f..dc8a3ac6c0 100644 --- a/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.php +++ b/src/Standards/Squiz/Tests/ControlStructures/LowercaseDeclarationUnitTest.php @@ -37,6 +37,7 @@ public function getErrorList() 16 => 1, 20 => 1, 21 => 1, + 24 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/ControlStructures/SwitchDeclarationUnitTest.inc.fixed b/src/Standards/Squiz/Tests/ControlStructures/SwitchDeclarationUnitTest.inc.fixed new file mode 100644 index 0000000000..fd8ad1e683 --- /dev/null +++ b/src/Standards/Squiz/Tests/ControlStructures/SwitchDeclarationUnitTest.inc.fixed @@ -0,0 +1,342 @@ + diff --git a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc index e9c8061bfd..8e62896387 100644 --- a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc +++ b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc @@ -170,3 +170,34 @@ $value = (binary) $blah + b"binary $foo"; $test = (1 * static::TEST); $test = myfunc(1 * static::TEST); + +$errorPos = $params[$x]?->getLine() + $commentStart; + +$foo = $this->gmail ?? $this->gmail = new Google_Service_Gmail($this->google); + +exit -1; + +$expr = match ($number - 10) { + -1 => 0, +}; + +$expr = match ($number % 10) { + 1 => 2 * $num, +}; + +$expr = match (true) { + $num * 100 > 500 => 'expression in key', +}; + +// PHP 8.0 named parameters. +if ($pos === count(value: $this->tokens) - 1) { + $file = '...'.substr(string: $file, offset: $padding * -1 + 3); +} + +match ($a) { + 'a' => -1, + 'b', 'c', 'd' => -2, + default => -3, +}; + +$cntPages = ceil(count($items) / parent::ON_PAGE); diff --git a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc.fixed index a0e0a895b3..9fa0216cb6 100644 --- a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.inc.fixed @@ -170,3 +170,34 @@ $value = ((binary) $blah + b"binary $foo"); $test = (1 * static::TEST); $test = myfunc(1 * static::TEST); + +$errorPos = ($params[$x]?->getLine() + $commentStart); + +$foo = ($this->gmail ?? $this->gmail = new Google_Service_Gmail($this->google)); + +exit -1; + +$expr = match ($number - 10) { + -1 => 0, +}; + +$expr = match ($number % 10) { + 1 => (2 * $num), +}; + +$expr = match (true) { + ($num * 100) > 500 => 'expression in key', +}; + +// PHP 8.0 named parameters. +if ($pos === (count(value: $this->tokens) - 1)) { + $file = '...'.substr(string: $file, offset: ($padding * -1 + 3)); +} + +match ($a) { + 'a' => -1, + 'b', 'c', 'd' => -2, + default => -3, +}; + +$cntPages = ceil(count($items) / parent::ON_PAGE); diff --git a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.php b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.php index 651319b877..4f04a6ebd0 100644 --- a/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.php +++ b/src/Standards/Squiz/Tests/Formatting/OperatorBracketUnitTest.php @@ -66,6 +66,12 @@ public function getErrorList($testFile='OperatorBracketUnitTest.inc') 163 => 2, 165 => 2, 169 => 1, + 174 => 1, + 176 => 1, + 185 => 1, + 189 => 1, + 193 => 1, + 194 => 2, ]; break; case 'OperatorBracketUnitTest.js': diff --git a/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.inc b/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.inc index 3076104a2b..811c56ec14 100644 --- a/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.inc +++ b/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.inc @@ -197,3 +197,108 @@ function foo( $bar ) { } + +class ConstructorPropertyPromotionSingleLineDocblockIndentOK +{ + public function __construct( + /** @var string */ + public string $public, + /** @var string */ + private string $private, + ) { + } +} + +class ConstructorPropertyPromotionMultiLineDocblockAndAttributeIndentOK +{ + public function __construct( + /** + * @var string + * @Assert\NotBlank() + */ + public string $public, + /** + * @var string + * @Assert\NotBlank() + */ + #[NotBlank] + private string $private, + ) { + } +} + +class ConstructorPropertyPromotionSingleLineDocblockIncorrectIndent +{ + public function __construct( + /** @var string */ + public string $public, + /** @var string */ + private string $private, + ) { + } +} + +class ConstructorPropertyPromotionMultiLineDocblockAndAttributeIncorrectIndent +{ + public function __construct( + /** + * @var string + * @Assert\NotBlank() + */ + public string $public, +/** + * @var string + * @Assert\NotBlank() + */ +#[NotBlank] +private string $private, + ) { + } +} + +// PHP 8.1: new in initializers means that class instantiations with parameters can occur in a function declaration. +function usingNewInInitializersCallParamsIndented( + int $paramA, + string $paramB, + object $paramC = new SomeClass( + new InjectedDependencyA(), + new InjectedDependencyB + ) +) {} + +function usingNewInInitializersCallParamsNotIndented( + int $paramA, + string $paramB, + object $paramC = new SomeClass( + new InjectedDependencyA, + new InjectedDependencyB() + ) +) {} + +function usingNewInInitializersCallParamsIncorrectlyIndentedShouldNotBeFlaggedNorFixed( + int $paramA, + string $paramB, + object $paramC = new SomeClass( +new InjectedDependencyA(), new InjectedDependencyB() +) +) {} + +class UsingNewInInitializers { + public function doSomething( + object $paramA, + stdClass $paramB = new stdClass(), + Exception $paramC = new Exception( + new ExceptionMessage(), + new ExceptionCode(), + ), + ) { + } + + public function callParamsIncorrectlyIndentedShouldNotBeFlaggedNorFixed( + Exception $param = new Exception( +new ExceptionMessage(), + new ExceptionCode(), + ), + ) { + } +} diff --git a/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.inc.fixed b/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.inc.fixed index 4965f237c0..c38e3ecc0a 100644 --- a/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.inc.fixed @@ -209,3 +209,108 @@ function foo( $bar ) { } + +class ConstructorPropertyPromotionSingleLineDocblockIndentOK +{ + public function __construct( + /** @var string */ + public string $public, + /** @var string */ + private string $private, + ) { + } +} + +class ConstructorPropertyPromotionMultiLineDocblockAndAttributeIndentOK +{ + public function __construct( + /** + * @var string + * @Assert\NotBlank() + */ + public string $public, + /** + * @var string + * @Assert\NotBlank() + */ + #[NotBlank] + private string $private, + ) { + } +} + +class ConstructorPropertyPromotionSingleLineDocblockIncorrectIndent +{ + public function __construct( + /** @var string */ + public string $public, + /** @var string */ + private string $private, + ) { + } +} + +class ConstructorPropertyPromotionMultiLineDocblockAndAttributeIncorrectIndent +{ + public function __construct( + /** + * @var string + * @Assert\NotBlank() + */ + public string $public, + /** + * @var string + * @Assert\NotBlank() + */ + #[NotBlank] + private string $private, + ) { + } +} + +// PHP 8.1: new in initializers means that class instantiations with parameters can occur in a function declaration. +function usingNewInInitializersCallParamsIndented( + int $paramA, + string $paramB, + object $paramC = new SomeClass( + new InjectedDependencyA(), + new InjectedDependencyB + ) +) {} + +function usingNewInInitializersCallParamsNotIndented( + int $paramA, + string $paramB, + object $paramC = new SomeClass( + new InjectedDependencyA, + new InjectedDependencyB() + ) +) {} + +function usingNewInInitializersCallParamsIncorrectlyIndentedShouldNotBeFlaggedNorFixed( + int $paramA, + string $paramB, + object $paramC = new SomeClass( +new InjectedDependencyA(), new InjectedDependencyB() +) +) {} + +class UsingNewInInitializers { + public function doSomething( + object $paramA, + stdClass $paramB = new stdClass(), + Exception $paramC = new Exception( + new ExceptionMessage(), + new ExceptionCode(), + ), + ) { + } + + public function callParamsIncorrectlyIndentedShouldNotBeFlaggedNorFixed( + Exception $param = new Exception( +new ExceptionMessage(), + new ExceptionCode(), + ), + ) { + } +} diff --git a/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.php b/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.php index caf14d1de1..5208ad0cdb 100644 --- a/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.php +++ b/src/Standards/Squiz/Tests/Functions/MultiLineFunctionDeclarationUnitTest.php @@ -55,7 +55,21 @@ public function getErrorList($testFile='MultiLineFunctionDeclarationUnitTest.inc 190 => 2, 194 => 1, 195 => 1, - 196 => 1, + 233 => 1, + 234 => 1, + 235 => 1, + 236 => 1, + 244 => 1, + 245 => 1, + 246 => 1, + 247 => 1, + 248 => 1, + 249 => 1, + 250 => 1, + 251 => 1, + 252 => 1, + 253 => 1, + 254 => 1, ]; } else { $errors = [ diff --git a/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.inc b/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.inc index 5692d6b33b..87c3bdf2e7 100644 --- a/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.inc +++ b/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.inc @@ -142,3 +142,16 @@ $anonClass = new class() { } }; +echo $obj?->varName; +echo $obj?->var_name; +echo $obj?->varname; +echo $obj?->_varName; + +enum SomeEnum +{ + public function foo($foo, $_foo, $foo_bar) { + $bar = 1; + $_bar = 2; + $bar_foo = 3; + } +} diff --git a/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.php b/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.php index 4ec65e5e1a..9acbe241a8 100644 --- a/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.php +++ b/src/Standards/Squiz/Tests/NamingConventions/ValidVariableNameUnitTest.php @@ -60,6 +60,9 @@ public function getErrorList() 123 => 1, 138 => 1, 141 => 1, + 146 => 1, + 152 => 1, + 155 => 1, ]; return $errors; diff --git a/src/Standards/Squiz/Tests/Objects/ObjectInstantiationUnitTest.inc b/src/Standards/Squiz/Tests/Objects/ObjectInstantiationUnitTest.inc index 8a5c232319..725c60ed69 100644 --- a/src/Standards/Squiz/Tests/Objects/ObjectInstantiationUnitTest.inc +++ b/src/Standards/Squiz/Tests/Objects/ObjectInstantiationUnitTest.inc @@ -13,5 +13,41 @@ function foo() { return new MyClass(); } $doodad = $x ? new Foo : new Bar; +function returnFn() { + $fn = fn($x) => new MyClass(); +} + +function returnMatch() { + $match = match($x) { + 0 => new MyClass() + } +} + +// 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. + } +} + +// Test for tokenizer issue #3789. +$a = $b !== null + ? match ($c) { + default => 5, + } + : new Foo; + +// 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/Operators/IncrementDecrementUsageUnitTest.inc b/src/Standards/Squiz/Tests/Operators/IncrementDecrementUsageUnitTest.inc index e8d3beb94c..a4f82d1a8c 100644 --- a/src/Standards/Squiz/Tests/Operators/IncrementDecrementUsageUnitTest.inc +++ b/src/Standards/Squiz/Tests/Operators/IncrementDecrementUsageUnitTest.inc @@ -37,4 +37,6 @@ $var = (1 + $var); $expected[$i]['sort_order'] = ($i + 1); $expected[($i + 1)]['sort_order'] = ($i + 1); -$id = $id.($this->i++).$id; +$id = $id.($obj->i++).$id; +$id = $obj?->i++.$id; +$id = $obj?->i++*10; diff --git a/src/Standards/Squiz/Tests/Operators/IncrementDecrementUsageUnitTest.php b/src/Standards/Squiz/Tests/Operators/IncrementDecrementUsageUnitTest.php index 768911b508..3846905da0 100644 --- a/src/Standards/Squiz/Tests/Operators/IncrementDecrementUsageUnitTest.php +++ b/src/Standards/Squiz/Tests/Operators/IncrementDecrementUsageUnitTest.php @@ -35,6 +35,8 @@ public function getErrorList() 27 => 1, 29 => 1, 31 => 1, + 41 => 1, + 42 => 1, ]; }//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/PHP/DisallowComparisonAssignmentUnitTest.inc b/src/Standards/Squiz/Tests/PHP/DisallowComparisonAssignmentUnitTest.inc index d4fce10f4d..a07047b196 100644 --- a/src/Standards/Squiz/Tests/PHP/DisallowComparisonAssignmentUnitTest.inc +++ b/src/Standards/Squiz/Tests/PHP/DisallowComparisonAssignmentUnitTest.inc @@ -62,3 +62,22 @@ $a = [ $a = [ 'a' => ($foo) ? fn() => return 1 : fn() => return 2, ]; + +$var = $foo->something(!$var); +$var = $foo?->something(!$var); + +$callback = function ($value) { + if ($value > 10) { + return false; + } +}; + +function issue3616() { + $food = 'cake'; + + $returnValue = match (true) { + $food === 'apple' => 'This food is an apple', + $food === 'bar' => 'This food is a bar', + $food === 'cake' => 'This food is a cake', + }; +} diff --git a/src/Standards/Squiz/Tests/PHP/DisallowMultipleAssignmentsUnitTest.inc b/src/Standards/Squiz/Tests/PHP/DisallowMultipleAssignmentsUnitTest.inc index 98c1bb5661..f657fb4a4c 100644 --- a/src/Standards/Squiz/Tests/PHP/DisallowMultipleAssignmentsUnitTest.inc +++ b/src/Standards/Squiz/Tests/PHP/DisallowMultipleAssignmentsUnitTest.inc @@ -75,3 +75,36 @@ class Bar public bool $c = false, $d = true; protected int $e = 123, $f = 987; } + +switch ($b < 10 && $a = 10) { + case true: + break; +} + +$array = [ + match ($b < 10 && $a = 10) { + true => 10, + false => 0 + }, +]; + +$arrow_function = fn ($a = null) => $a; + +function ($html) { + $regEx = '/regexp/'; + + return preg_replace_callback($regEx, function ($matches) { + [$all] = $matches; + return $all; + }, $html); +}; + + +function () { + $a = false; + + some_label: + + $b = getB(); +}; + diff --git a/src/Standards/Squiz/Tests/PHP/DisallowMultipleAssignmentsUnitTest.php b/src/Standards/Squiz/Tests/PHP/DisallowMultipleAssignmentsUnitTest.php index 919d4570d0..618d76efa4 100644 --- a/src/Standards/Squiz/Tests/PHP/DisallowMultipleAssignmentsUnitTest.php +++ b/src/Standards/Squiz/Tests/PHP/DisallowMultipleAssignmentsUnitTest.php @@ -33,6 +33,8 @@ public function getErrorList() 12 => 1, 14 => 1, 15 => 1, + 79 => 1, + 85 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/PHP/DisallowSizeFunctionsInLoopsUnitTest.inc b/src/Standards/Squiz/Tests/PHP/DisallowSizeFunctionsInLoopsUnitTest.inc index 76ec49962c..56802e37aa 100644 --- a/src/Standards/Squiz/Tests/PHP/DisallowSizeFunctionsInLoopsUnitTest.inc +++ b/src/Standards/Squiz/Tests/PHP/DisallowSizeFunctionsInLoopsUnitTest.inc @@ -55,4 +55,4 @@ do { } while($a->count); for ($i = 0; $i < $a->count; $i++) {} -?> +for ($i = 0; $i < $a?->count; $i++) {} diff --git a/src/Standards/Squiz/Tests/PHP/DiscouragedFunctionsUnitTest.inc b/src/Standards/Squiz/Tests/PHP/DiscouragedFunctionsUnitTest.inc index ca457a2e9a..3c875d0983 100644 --- a/src/Standards/Squiz/Tests/PHP/DiscouragedFunctionsUnitTest.inc +++ b/src/Standards/Squiz/Tests/PHP/DiscouragedFunctionsUnitTest.inc @@ -2,4 +2,6 @@ error_log('test'); print_r($array); var_dump($array); -?> + +#[Var_Dump(10)] +function debugMe() {} diff --git a/src/Standards/Squiz/Tests/PHP/InnerFunctionsUnitTest.inc b/src/Standards/Squiz/Tests/PHP/InnerFunctionsUnitTest.inc index dd851461b9..d16c7f2eb6 100644 --- a/src/Standards/Squiz/Tests/PHP/InnerFunctionsUnitTest.inc +++ b/src/Standards/Squiz/Tests/PHP/InnerFunctionsUnitTest.inc @@ -48,3 +48,40 @@ new class { } } }; + +$outerClosure = function () +{ + // Functions inside closures are not allowed. + function innerFunction() { + } +}; + +// Allow methods in classes/traits/interfaces defined inside functions +function foo() { + if (class_exists('MyClass') === false) { + class MyClass { + function foo() {} + } + } + + if (trait_exists('MyTrait') === false) { + trait MyTrait { + function foo() {} + } + } + + if (interface_exists('MyInterface') === false) { + interface MyInterface { + function foo(); + } + } + + // But disallow functions nested inside those methods + if (class_exists('NestedFunctionInMethod') === false) { + class NestedFunctionInMethod { + function foo() { + function innerFunction() {} + } + } + } +} diff --git a/src/Standards/Squiz/Tests/PHP/InnerFunctionsUnitTest.php b/src/Standards/Squiz/Tests/PHP/InnerFunctionsUnitTest.php index 3c9ad07bd3..b0b13b6147 100644 --- a/src/Standards/Squiz/Tests/PHP/InnerFunctionsUnitTest.php +++ b/src/Standards/Squiz/Tests/PHP/InnerFunctionsUnitTest.php @@ -28,6 +28,8 @@ public function getErrorList() return [ 5 => 1, 46 => 1, + 55 => 1, + 83 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/PHP/LowercasePHPFunctionsUnitTest.inc b/src/Standards/Squiz/Tests/PHP/LowercasePHPFunctionsUnitTest.inc index 330919d1c0..702b13de0a 100644 --- a/src/Standards/Squiz/Tests/PHP/LowercasePHPFunctionsUnitTest.inc +++ b/src/Standards/Squiz/Tests/PHP/LowercasePHPFunctionsUnitTest.inc @@ -39,3 +39,12 @@ $callToNamespacedFunction = MyNamespace /* phpcs:ignore Standard */ \STR_REPEAT( $callToNamespacedFunction = namespace\STR_REPEAT($a, 2); // Could potentially be false negative. $filePath = new \File($path); + +$count = $object?->Count(); + +class AttributesShouldBeIgnored +{ + #[Putenv('FOO', 'foo')] + public function foo(): void + {} +} diff --git a/src/Standards/Squiz/Tests/PHP/LowercasePHPFunctionsUnitTest.inc.fixed b/src/Standards/Squiz/Tests/PHP/LowercasePHPFunctionsUnitTest.inc.fixed index eae5b4ade4..281425c595 100644 --- a/src/Standards/Squiz/Tests/PHP/LowercasePHPFunctionsUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/PHP/LowercasePHPFunctionsUnitTest.inc.fixed @@ -39,3 +39,12 @@ $callToNamespacedFunction = MyNamespace /* phpcs:ignore Standard */ \STR_REPEAT( $callToNamespacedFunction = namespace\STR_REPEAT($a, 2); // Could potentially be false negative. $filePath = new \File($path); + +$count = $object?->Count(); + +class AttributesShouldBeIgnored +{ + #[Putenv('FOO', 'foo')] + public function foo(): void + {} +} diff --git a/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.1.inc b/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.1.inc index 163c312569..051b6c6b11 100644 --- a/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.1.inc +++ b/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.1.inc @@ -296,3 +296,105 @@ class TestAlternativeControlStructures { $var_after_class_in_global_space = 1; do_something_else(); + +// These are parse errors, but that's not the concern of the sniff. +function parseError1() { + defined('FOO') or return 'foo'; + echo 'unreachable'; +} + +function parseError2() { + defined('FOO') || continue; + echo 'unreachable'; +} + +// All logical operators are allowed with inline expressions (but this was not correctly handled by the sniff). +function exitExpressionsWithLogicalOperators() { + $condition = false; + $condition || exit(); + $condition or die(); + + $condition = true; + $condition && die(); + $condition and exit; + + $condition xor die(); + + echo 'still executable as exit, in all of the above cases, is used as part of an expression'; +} + +// Inline expressions are allowed in ternaries. +function exitExpressionsInTernary() { + $value = $myValue ? $myValue : exit(); + $value = $myValue ?: exit(); + $value = $var == 'foo' ? 'bar' : die( 'world' ); + + $value = (!$myValue ) ? exit() : $myValue; + $value = $var != 'foo' ? die( 'world' ) : 'bar'; + + echo 'still executable'; +} + +// Inline expressions are allowed with null coalesce and null coalesce equals. +function exitExpressionsWithNullCoalesce() { + $value = $nullableValue ?? exit(); + $value ??= die(); + echo 'still executable'; +} + +// Inline expressions are allowed in arrow functions. +function exitExpressionsInArrowFunction() { + $callable = fn() => die(); + echo 'still executable'; +} + +// PHP 8.0+: throw expressions which don't stop execution. +function nonStoppingThrowExpressions() { + $callable = fn() => throw new Exception(); + + $value = $myValue ? 'something' : throw new Exception(); + $value = $myValue ?: throw new Exception(); + $value = $myValue ? throw new Exception() : 'something'; + + $value = $nullableValue ?? throw new Exception(); + $value ??= throw new Exception(); + + $condition && throw new Exception(); + $condition || throw new Exception(); + $condition and throw new Exception(); + $condition or throw new Exception(); + + echo 'still executable as throw, in all of the above cases, is used as part of an expression'; + + throw new Exception(); + echo 'non-executable'; +} + +// PHP 8.0+: throw expressions which do stop execution. +function executionStoppingThrowExpressionsA() { + $condition xor throw new Exception(); + echo 'non-executable'; +} + +function executionStoppingThrowExpressionsB() { + throw $userIsAuthorized ? new ForbiddenException() : new UnauthorizedException(); + echo 'non-executable'; +} + +function executionStoppingThrowExpressionsC() { + throw $condition1 && $condition2 ? new Exception1() : new Exception2(); + echo 'non-executable'; +} + +function executionStoppingThrowExpressionsD() { + throw $exception ??= new Exception(); + echo 'non-executable'; +} + +function executionStoppingThrowExpressionsE() { + throw $maybeNullException ?? new Exception(); + echo 'non-executable'; +} + +// Intentional syntax error. +return array_map( diff --git a/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.2.inc b/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.2.inc index 407c4740b1..9b7a22bcc9 100644 --- a/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.2.inc +++ b/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.2.inc @@ -45,6 +45,12 @@ trait Something { } } +enum Something { + function getReturnType() { + echo 'no error'; + } +} + $a = new class { public function log($msg) { @@ -52,4 +58,16 @@ $a = new class { } }; +// Multiple statements are still one line of unreachable code, so should get +// only one complaint from this sniff. (Well, technically two here since there +// are two 'exit()' statements above, so one complaint from each of those. So, +// two here, but not six.) +echo 'one'; echo 'two'; echo 'three'; + +// A single statement split across multiple lines. Here we get complaints for +// each line, even though they're all part of one statement. +echo 'one' . 'two' + . 'three' . 'four' + . 'five' . 'six'; + interface MyInterface { diff --git a/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.3.inc b/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.3.inc new file mode 100644 index 0000000000..6fe5c16c66 --- /dev/null +++ b/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.3.inc @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
non-executable
+ + + + + + + + +
non-executable
+ + + + + + + + +
non-executable
+ + + + + + + + + + + + + + + + + + + diff --git a/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.php b/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.php index f66eb3f7fe..40e4068e42 100644 --- a/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.php +++ b/src/Standards/Squiz/Tests/PHP/NonExecutableCodeUnitTest.php @@ -74,6 +74,14 @@ public function getWarningList($testFile='') 252 => 1, 253 => 1, 254 => 2, + 303 => 1, + 308 => 1, + 370 => 1, + 376 => 1, + 381 => 1, + 386 => 1, + 391 => 1, + 396 => 1, ]; break; case 'NonExecutableCodeUnitTest.2.inc': @@ -83,9 +91,21 @@ public function getWarningList($testFile='') 9 => 1, 10 => 2, 14 => 1, - 48 => 2, + 54 => 2, + 65 => 2, + 69 => 2, + 70 => 2, + 71 => 2, ]; break; + case 'NonExecutableCodeUnitTest.3.inc': + return [ + 27 => 1, + 36 => 1, + 45 => 1, + 54 => 1, + 62 => 1, + ]; default: return []; break; diff --git a/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.inc b/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.inc index cec0355c43..3cc617d75b 100644 --- a/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.inc +++ b/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.inc @@ -40,3 +40,18 @@ class Nested { }; } } + +enum SomeEnum +{ + function func1() {} + public function func1() {} + private function func1() {} + protected function func1() {} +} + +class UnconventionalSpacing { + public + static + function + myFunction() {} +} diff --git a/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.php b/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.php index 7fdab23b77..4dc7177953 100644 --- a/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.php +++ b/src/Standards/Squiz/Tests/Scope/MethodScopeUnitTest.php @@ -29,6 +29,7 @@ public function getErrorList() 6 => 1, 30 => 1, 39 => 1, + 46 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.inc b/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.inc index 38b443f2fd..dd6530e802 100644 --- a/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.inc +++ b/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.inc @@ -115,3 +115,13 @@ $b = new class() return $This; } } + +enum MyEnum { + private function notStatic () { + $this->doSomething(); + } + + public static function myFunc() { + $this->doSomething(); + } +} diff --git a/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.php b/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.php index 2935241b44..b1a5dd6afd 100644 --- a/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.php +++ b/src/Standards/Squiz/Tests/Scope/StaticThisUsageUnitTest.php @@ -26,18 +26,19 @@ class StaticThisUsageUnitTest extends AbstractSniffUnitTest public function getErrorList() { return [ - 7 => 1, - 8 => 1, - 9 => 1, - 14 => 1, - 20 => 1, - 41 => 1, - 61 => 1, - 69 => 1, - 76 => 1, - 80 => 1, - 84 => 1, - 99 => 1, + 7 => 1, + 8 => 1, + 9 => 1, + 14 => 1, + 20 => 1, + 41 => 1, + 61 => 1, + 69 => 1, + 76 => 1, + 80 => 1, + 84 => 1, + 99 => 1, + 125 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc b/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc index 785a4a8cc3..70abae434d 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc +++ b/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc @@ -251,3 +251,19 @@ echo 'hi'; ?> + + 1, + 2 => 2, + +}; +echo $expr; + +if($true) { + + enum SomeEnum {} + +} diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc.fixed b/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc.fixed index 1674b3dbfa..c64de25e16 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.inc.fixed @@ -244,3 +244,18 @@ echo 'hi'; ?> + + 1, + 2 => 2, +}; + +echo $expr; + +if($true) { + + enum SomeEnum {} + +} diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.php b/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.php index eeef2b0635..ac3f5d6ff5 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.php +++ b/src/Standards/Squiz/Tests/WhiteSpace/ControlStructureSpacingUnitTest.php @@ -57,6 +57,9 @@ public function getErrorList($testFile='ControlStructureSpacingUnitTest.inc') 242 => 1, 246 => 1, 248 => 1, + 257 => 3, + 261 => 1, + 262 => 1, ]; break; case 'ControlStructureSpacingUnitTest.js': diff --git a/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.1.inc b/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.1.inc index 2e058050d8..36a287f077 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.1.inc +++ b/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.1.inc @@ -540,6 +540,37 @@ class MyClass { // function d() {} } +class ClassWithAttributes { + + #[Attribute1] + #[Attribute2] + function a(){} + + + #[Attribute3] + function b(){} + #[Attribute4] + function c(){} + + + /** + * Description. + */ + #[Attribute5] + function d(){} + /** + * Description. + */ + #[Attribute6] + #[Attribute7] + function e(){} + + + #[Attribute8] + #[Attribute9] + function f(){} +} + // phpcs:set Squiz.WhiteSpace.FunctionSpacing spacing 2 // phpcs:set Squiz.WhiteSpace.FunctionSpacing spacingBeforeFirst 2 // phpcs:set Squiz.WhiteSpace.FunctionSpacing spacingAfterLast 2 diff --git a/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.1.inc.fixed b/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.1.inc.fixed index eec26c6913..ac2df1cb7b 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.1.inc.fixed +++ b/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.1.inc.fixed @@ -624,6 +624,35 @@ class MyClass { // function d() {} } +class ClassWithAttributes { + #[Attribute1] + #[Attribute2] + function a(){} + + #[Attribute3] + function b(){} + + #[Attribute4] + function c(){} + + /** + * Description. + */ + #[Attribute5] + function d(){} + + /** + * Description. + */ + #[Attribute6] + #[Attribute7] + function e(){} + + #[Attribute8] + #[Attribute9] + function f(){} +} + // phpcs:set Squiz.WhiteSpace.FunctionSpacing spacing 2 // phpcs:set Squiz.WhiteSpace.FunctionSpacing spacingBeforeFirst 2 // phpcs:set Squiz.WhiteSpace.FunctionSpacing spacingAfterLast 2 diff --git a/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.php b/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.php index 9f202888a9..fabb6adb92 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.php +++ b/src/Standards/Squiz/Tests/WhiteSpace/FunctionSpacingUnitTest.php @@ -90,6 +90,11 @@ public function getErrorList($testFile='') 495 => 1, 529 => 1, 539 => 1, + 547 => 2, + 551 => 1, + 553 => 1, + 560 => 1, + 566 => 1, ]; case 'FunctionSpacingUnitTest.2.inc': diff --git a/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc b/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc index fd7c6e34fc..12b55176c4 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc +++ b/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc @@ -332,3 +332,43 @@ 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; +} + +enum SomeEnum +{ + // Enum cannot have properties + + case ONE = 'one'; +} diff --git a/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc.fixed b/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc.fixed index b6ebcc9ab1..d683eaadfb 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/WhiteSpace/MemberVarSpacingUnitTest.inc.fixed @@ -319,3 +319,41 @@ 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; +} + +enum SomeEnum +{ + // Enum cannot have properties + + case ONE = 'one'; +} 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/ObjectOperatorSpacingUnitTest.inc b/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc index bcea3dc7a8..67f8ee1447 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc +++ b/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc @@ -46,3 +46,7 @@ thisObject:: testThis(); // phpcs:set Squiz.WhiteSpace.ObjectOperatorSpacing ignoreNewlines false + +$this?->testThis(); +$this?-> testThis(); +$this ?-> testThis(); diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc.fixed b/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc.fixed index fdeb6f3140..730b8e4a72 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc.fixed @@ -42,3 +42,7 @@ thisObject:: testThis(); // phpcs:set Squiz.WhiteSpace.ObjectOperatorSpacing ignoreNewlines false + +$this?->testThis(); +$this?->testThis(); +$this?->testThis(); diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.php b/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.php index 48208cef32..82a4056f7e 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.php +++ b/src/Standards/Squiz/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.php @@ -43,6 +43,8 @@ public function getErrorList() 39 => 1, 40 => 2, 42 => 2, + 51 => 1, + 52 => 2, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc b/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc index 8d0efb3246..06462acc35 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc +++ b/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc @@ -463,5 +463,25 @@ $a = [$a, - $b]; $a = $a[- $b]; $a = $a ? - $b : - $b; +exit -1; + +$cl = function ($boo =-1) {}; +$cl = function ($boo =+1) {}; +$fn = fn ($boo =-1) => $boo; +$fn = fn ($boo =+1) => $boo; + +$fn = static fn(DateTime $a, DateTime $b): int => -($a->getTimestamp() <=> $b->getTimestamp()); + +$a = 'a '.-MY_CONSTANT; +$a = 'a '.-$b; +$a = 'a '.- MY_CONSTANT; +$a = 'a '.- $b; + +match ($a) { + 'a' => -1, + 'b', 'c', 'd' => -2, + default => -3, +}; + /* Intentional parse error. This has to be the last test in the file. */ $a = 10 + diff --git a/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc.fixed b/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc.fixed index a03b642003..8b92a4875a 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/WhiteSpace/OperatorSpacingUnitTest.inc.fixed @@ -457,5 +457,25 @@ $a = [$a, - $b]; $a = $a[- $b]; $a = $a ? - $b : - $b; +exit -1; + +$cl = function ($boo =-1) {}; +$cl = function ($boo =+1) {}; +$fn = fn ($boo =-1) => $boo; +$fn = fn ($boo =+1) => $boo; + +$fn = static fn(DateTime $a, DateTime $b): int => -($a->getTimestamp() <=> $b->getTimestamp()); + +$a = 'a '.-MY_CONSTANT; +$a = 'a '.-$b; +$a = 'a '.- MY_CONSTANT; +$a = 'a '.- $b; + +match ($a) { + 'a' => -1, + 'b', 'c', 'd' => -2, + default => -3, +}; + /* Intentional parse error. This has to be the last test in the file. */ $a = 10 + diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc b/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc index 95cd371706..ecae5c6d53 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc +++ b/src/Standards/Squiz/Tests/WhiteSpace/ScopeClosingBraceUnitTest.inc @@ -107,3 +107,28 @@ public function foo() } $fn1 = fn($x) => $x + $y; + +$match = match ($test) { 1 => 'a', 2 => 'b' }; + +$match = match ($test) { + 1 => 'a', + 2 => 'b' + }; + +?> + +
+ +
+ + $x + $y; + +$match = match ($test) { 1 => 'a', 2 => 'b' +}; + +$match = match ($test) { + 1 => 'a', + 2 => 'b' +}; + +?> + +
+ +
+ + + 1, 80 => 1, 102 => 1, + 111 => 1, + 116 => 1, + 122 => 1, + 130 => 1, + 134 => 1, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc b/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc index 791cc83f93..12685dc97d 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc +++ b/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc @@ -25,7 +25,7 @@ class MyClass public static$var = null; - public + public static $var = null; } @@ -82,3 +82,60 @@ class MyOtherClass $varS, $varT } + +// Issue #3188 - static as return type. +public static function fCreate($attributes = []): static +{ + return static::factory()->create($attributes); +} + +public static function fCreate($attributes = []): ?static +{ + return static::factory()->create($attributes); +} + +// Also account for static used within union types. +public function fCreate($attributes = []): object|static +{ +} + +// Ensure that static as a scope keyword when preceeded by a colon which is not for a type declaration is still handled. +$callback = $cond ? get_fn_name() : static function ($a) { return $a * 10; }; + +class TypedProperties { + public + int $var; + + protected string $stringA, $stringB; + + private bool + $boolA, + $boolB; +} + +// PHP 8.0 constructor property promotion. +class ConstructorPropertyPromotionTest { + public function __construct( + public $x = 0.0, + protected $y = '', + private $z = null, + $normalParam, + ) {} +} + +class ConstructorPropertyPromotionWithTypesTest { + public function __construct(protected float|int $x, public?string &$y = 'test', private mixed $z) {} +} + +// PHP 8.1 readonly keywords. +class ReadonlyTest { + public readonly int $publicReadonlyProperty; + + protected readonly int $protectedReadonlyProperty; + + readonly protected int $protectedReadonlyProperty; + + readonly private int $privateReadonlyProperty; + + public function __construct(readonly protected float|int $x, public readonly?string &$y = 'test') {} +} diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc.fixed b/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc.fixed index a4b792b3c5..d3b682ed75 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc.fixed +++ b/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.inc.fixed @@ -77,3 +77,59 @@ class MyOtherClass $varS, $varT } + +// Issue #3188 - static as return type. +public static function fCreate($attributes = []): static +{ + return static::factory()->create($attributes); +} + +public static function fCreate($attributes = []): ?static +{ + return static::factory()->create($attributes); +} + +// Also account for static used within union types. +public function fCreate($attributes = []): object|static +{ +} + +// Ensure that static as a scope keyword when preceeded by a colon which is not for a type declaration is still handled. +$callback = $cond ? get_fn_name() : static function ($a) { return $a * 10; }; + +class TypedProperties { + public int $var; + + protected string $stringA, $stringB; + + private bool + $boolA, + $boolB; +} + +// PHP 8.0 constructor property promotion. +class ConstructorPropertyPromotionTest { + public function __construct( + public $x = 0.0, + protected $y = '', + private $z = null, + $normalParam, + ) {} +} + +class ConstructorPropertyPromotionWithTypesTest { + public function __construct(protected float|int $x, public ?string &$y = 'test', private mixed $z) {} +} + +// PHP 8.1 readonly keywords. +class ReadonlyTest { + public readonly int $publicReadonlyProperty; + + protected readonly int $protectedReadonlyProperty; + + readonly protected int $protectedReadonlyProperty; + + readonly private int $privateReadonlyProperty; + + public function __construct(readonly protected float|int $x, public readonly ?string &$y = 'test') {} +} diff --git a/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.php b/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.php index c24dcc49d7..30b66215c9 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.php +++ b/src/Standards/Squiz/Tests/WhiteSpace/ScopeKeywordSpacingUnitTest.php @@ -26,18 +26,27 @@ class ScopeKeywordSpacingUnitTest extends AbstractSniffUnitTest public function getErrorList() { return [ - 7 => 2, - 8 => 1, - 13 => 1, - 14 => 1, - 15 => 1, - 17 => 2, - 26 => 1, - 28 => 1, - 29 => 1, - 64 => 1, - 67 => 1, - 71 => 1, + 7 => 2, + 8 => 1, + 13 => 1, + 14 => 1, + 15 => 1, + 17 => 2, + 26 => 1, + 28 => 1, + 29 => 1, + 64 => 1, + 67 => 1, + 71 => 1, + 103 => 1, + 106 => 1, + 111 => 1, + 119 => 1, + 121 => 1, + 127 => 2, + 134 => 2, + 138 => 2, + 140 => 3, ]; }//end getErrorList() diff --git a/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.1.css b/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.1.css index 1dd1b6e6ba..e3f3f02918 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.1.css +++ b/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.1.css @@ -23,3 +23,10 @@ } /* phpcs:set Squiz.WhiteSpace.SuperfluousWhitespace ignoreBlankLines false */ +// /** +// * This text is in two types of comment: each line is commented out +// * individually, and the whole block is in what looks like a +// * docblock-comment. This sniff should ignore all this text as there +// * is no superfluous white-space here. +// */ + diff --git a/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.1.css.fixed b/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.1.css.fixed index 59ddddb084..11be21d5c9 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.1.css.fixed +++ b/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.1.css.fixed @@ -21,3 +21,10 @@ float: left; } /* phpcs:set Squiz.WhiteSpace.SuperfluousWhitespace ignoreBlankLines false */ + +// /** +// * This text is in two types of comment: each line is commented out +// * individually, and the whole block is in what looks like a +// * docblock-comment. This sniff should ignore all this text as there +// * is no superfluous white-space here. +// */ diff --git a/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.php b/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.php index b9ff96fbc7..2f1ead2307 100644 --- a/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.php +++ b/src/Standards/Squiz/Tests/WhiteSpace/SuperfluousWhitespaceUnitTest.php @@ -84,7 +84,7 @@ public function getErrorList($testFile='SuperfluousWhitespaceUnitTest.inc') 8 => 1, 9 => 1, 11 => 1, - 25 => 1, + 32 => 1, ]; break; default: diff --git a/src/Standards/Zend/Sniffs/Debug/CodeAnalyzerSniff.php b/src/Standards/Zend/Sniffs/Debug/CodeAnalyzerSniff.php index d18c947294..5df4b0fc2c 100644 --- a/src/Standards/Zend/Sniffs/Debug/CodeAnalyzerSniff.php +++ b/src/Standards/Zend/Sniffs/Debug/CodeAnalyzerSniff.php @@ -14,6 +14,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Config; use PHP_CodeSniffer\Exceptions\RuntimeException; +use PHP_CodeSniffer\Util\Common; class CodeAnalyzerSniff implements Sniff { @@ -53,7 +54,7 @@ public function process(File $phpcsFile, $stackPtr) // In the command, 2>&1 is important because the code analyzer sends its // findings to stderr. $output normally contains only stdout, so using 2>&1 // will pipe even stderr to stdout. - $cmd = escapeshellcmd($analyzerPath).' '.escapeshellarg($fileName).' 2>&1'; + $cmd = Common::escapeshellcmd($analyzerPath).' '.escapeshellarg($fileName).' 2>&1'; // There is the possibility to pass "--ide" as an option to the analyzer. // This would result in an output format which would be easier to parse. diff --git a/src/Standards/Zend/Sniffs/NamingConventions/ValidVariableNameSniff.php b/src/Standards/Zend/Sniffs/NamingConventions/ValidVariableNameSniff.php index ea98fd2847..267cd0ad62 100644 --- a/src/Standards/Zend/Sniffs/NamingConventions/ValidVariableNameSniff.php +++ b/src/Standards/Zend/Sniffs/NamingConventions/ValidVariableNameSniff.php @@ -38,7 +38,9 @@ protected function processVariable(File $phpcsFile, $stackPtr) } $objOperator = $phpcsFile->findNext([T_WHITESPACE], ($stackPtr + 1), null, true); - if ($tokens[$objOperator]['code'] === T_OBJECT_OPERATOR) { + if ($tokens[$objOperator]['code'] === T_OBJECT_OPERATOR + || $tokens[$objOperator]['code'] === T_NULLSAFE_OBJECT_OPERATOR + ) { // Check to see if we are using a variable from an object. $var = $phpcsFile->findNext([T_WHITESPACE], ($objOperator + 1), null, true); if ($tokens[$var]['code'] === T_STRING) { diff --git a/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.inc b/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.inc index 1bf486cab6..3325e1152d 100644 --- a/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.inc +++ b/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.inc @@ -116,3 +116,16 @@ $anonClass = new class() { $bar_foo = 3; } }; + +echo $obj?->varName; +echo $obj?->var_name; +echo $obj?->varName; + +enum SomeEnum +{ + public function foo($foo, $_foo, $foo_bar) { + $bar = 1; + $_bar = 2; + $bar_foo = 3; + } +} diff --git a/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.php b/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.php index d22b24fd3f..e57c735646 100644 --- a/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.php +++ b/src/Standards/Zend/Tests/NamingConventions/ValidVariableNameUnitTest.php @@ -53,6 +53,9 @@ public function getErrorList() 99 => 1, 113 => 1, 116 => 1, + 121 => 1, + 126 => 1, + 129 => 1, ]; }//end getErrorList() diff --git a/src/Tokenizers/CSS.php b/src/Tokenizers/CSS.php index b7c2018bcc..2f486d6cfa 100644 --- a/src/Tokenizers/CSS.php +++ b/src/Tokenizers/CSS.php @@ -196,7 +196,11 @@ public function tokenize($string) // The first and last tokens are the open/close tags. array_shift($commentTokens); - array_pop($commentTokens); + $closeTag = array_pop($commentTokens); + + while ($closeTag['content'] !== '?'.'>') { + $closeTag = array_pop($commentTokens); + } if ($leadingZero === true) { $commentTokens[0]['content'] = substr($commentTokens[0]['content'], 1); diff --git a/src/Tokenizers/PHP.php b/src/Tokenizers/PHP.php index 2fc86a86d6..0fa6d42c17 100644 --- a/src/Tokenizers/PHP.php +++ b/src/Tokenizers/PHP.php @@ -152,6 +152,13 @@ class PHP extends Tokenizer 'shared' => false, 'with' => [], ], + T_ENUM => [ + 'start' => [T_OPEN_CURLY_BRACKET => T_OPEN_CURLY_BRACKET], + 'end' => [T_CLOSE_CURLY_BRACKET => T_CLOSE_CURLY_BRACKET], + 'strict' => true, + 'shared' => false, + 'with' => [], + ], T_USE => [ 'start' => [T_OPEN_CURLY_BRACKET => T_OPEN_CURLY_BRACKET], 'end' => [T_CLOSE_CURLY_BRACKET => T_CLOSE_CURLY_BRACKET], @@ -251,6 +258,13 @@ class PHP extends Tokenizer T_SWITCH => T_SWITCH, ], ], + T_MATCH => [ + 'start' => [T_OPEN_CURLY_BRACKET => T_OPEN_CURLY_BRACKET], + 'end' => [T_CLOSE_CURLY_BRACKET => T_CLOSE_CURLY_BRACKET], + 'strict' => true, + 'shared' => false, + 'with' => [], + ], T_START_HEREDOC => [ 'start' => [T_START_HEREDOC => T_START_HEREDOC], 'end' => [T_END_HEREDOC => T_END_HEREDOC], @@ -283,8 +297,10 @@ class PHP extends Tokenizer T_ENDFOREACH => T_ENDFOREACH, T_ENDWHILE => T_ENDWHILE, T_ENDSWITCH => T_ENDSWITCH, + T_ENDDECLARE => T_ENDDECLARE, T_BREAK => T_BREAK, T_END_HEREDOC => T_END_HEREDOC, + T_END_NOWDOC => T_END_NOWDOC, ]; /** @@ -320,6 +336,7 @@ class PHP extends Tokenizer T_DOUBLE_ARROW => 2, T_DOUBLE_COLON => 2, T_ECHO => 4, + T_ELLIPSIS => 3, T_ELSE => 4, T_ELSEIF => 6, T_EMPTY => 5, @@ -329,11 +346,14 @@ class PHP extends Tokenizer T_ENDIF => 5, T_ENDSWITCH => 9, T_ENDWHILE => 8, + T_ENUM => 4, + T_ENUM_CASE => 4, T_EVAL => 4, T_EXTENDS => 7, T_FILE => 8, T_FINAL => 5, T_FINALLY => 7, + T_FN => 2, T_FOR => 3, T_FOREACH => 7, T_FUNCTION => 8, @@ -361,6 +381,9 @@ class PHP extends Tokenizer T_LOGICAL_AND => 3, T_LOGICAL_OR => 2, T_LOGICAL_XOR => 3, + T_MATCH => 5, + T_MATCH_ARROW => 2, + T_MATCH_DEFAULT => 7, T_METHOD_C => 10, T_MINUS_EQUAL => 2, T_POW_EQUAL => 3, @@ -370,6 +393,7 @@ class PHP extends Tokenizer T_NS_C => 13, T_NS_SEPARATOR => 1, T_NEW => 3, + T_NULLSAFE_OBJECT_OPERATOR => 3, T_OBJECT_OPERATOR => 2, T_OPEN_TAG_WITH_ECHO => 3, T_OR_EQUAL => 2, @@ -378,6 +402,7 @@ class PHP extends Tokenizer T_PRIVATE => 7, T_PUBLIC => 6, T_PROTECTED => 9, + T_READONLY => 8, T_REQUIRE => 7, T_REQUIRE_ONCE => 12, T_RETURN => 6, @@ -436,6 +461,33 @@ class PHP extends Tokenizer T_BACKTICK => 1, T_OPEN_SHORT_ARRAY => 1, T_CLOSE_SHORT_ARRAY => 1, + T_TYPE_UNION => 1, + T_TYPE_INTERSECTION => 1, + ]; + + /** + * Contexts in which keywords should always be tokenized as T_STRING. + * + * @var array + */ + protected $tstringContexts = [ + T_OBJECT_OPERATOR => true, + T_NULLSAFE_OBJECT_OPERATOR => true, + T_FUNCTION => true, + T_CLASS => true, + T_INTERFACE => true, + T_TRAIT => true, + T_ENUM => true, + T_ENUM_CASE => true, + T_EXTENDS => true, + T_IMPLEMENTS => true, + T_ATTRIBUTE => true, + T_NEW => true, + T_CONST => true, + T_NS_SEPARATOR => true, + T_USE => true, + T_NAMESPACE => true, + T_PAAMAYIM_NEKUDOTAYIM => true, ]; /** @@ -462,7 +514,7 @@ protected function tokenize($string) if (PHP_CODESNIFFER_VERBOSITY > 1) { echo "\t*** START PHP TOKENIZING ***".PHP_EOL; $isWin = false; - if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + if (stripos(PHP_OS, 'WIN') === 0) { $isWin = true; } } @@ -549,6 +601,116 @@ protected function tokenize($string) echo PHP_EOL; } + /* + Tokenize context sensitive keyword as string when it should be string. + */ + + if ($tokenIsArray === true + && isset(Util\Tokens::$contextSensitiveKeywords[$token[0]]) === true + && (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true + || $finalTokens[$lastNotEmptyToken]['content'] === '&') + ) { + if (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true) { + $preserveKeyword = false; + + // `new class`, and `new static` should be preserved. + if ($finalTokens[$lastNotEmptyToken]['code'] === T_NEW + && ($token[0] === T_CLASS + || $token[0] === T_STATIC) + ) { + $preserveKeyword = true; + } + + // `new class extends` `new class implements` should be preserved + if (($token[0] === T_EXTENDS || $token[0] === T_IMPLEMENTS) + && $finalTokens[$lastNotEmptyToken]['code'] === T_CLASS + ) { + $preserveKeyword = true; + } + + // `namespace\` should be preserved + if ($token[0] === T_NAMESPACE) { + for ($i = ($stackPtr + 1); $i < $numTokens; $i++) { + if (is_array($tokens[$i]) === false) { + break; + } + + if (isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === true) { + continue; + } + + if ($tokens[$i][0] === T_NS_SEPARATOR) { + $preserveKeyword = true; + } + + break; + } + } + }//end if + + if ($finalTokens[$lastNotEmptyToken]['content'] === '&') { + $preserveKeyword = true; + + for ($i = ($lastNotEmptyToken - 1); $i >= 0; $i--) { + if (isset(Util\Tokens::$emptyTokens[$finalTokens[$i]['code']]) === true) { + continue; + } + + if ($finalTokens[$i]['code'] === T_FUNCTION) { + $preserveKeyword = false; + } + + break; + } + } + + if ($preserveKeyword === false) { + if (PHP_CODESNIFFER_VERBOSITY > 1) { + $type = Util\Tokens::tokenName($token[0]); + echo "\t\t* token $stackPtr changed from $type to T_STRING".PHP_EOL; + } + + $finalTokens[$newStackPtr] = [ + 'code' => T_STRING, + 'type' => 'T_STRING', + 'content' => $token[1], + ]; + + $newStackPtr++; + continue; + } + }//end if + + /* + Special case for `static` used as a function name, i.e. `static()`. + */ + + if ($tokenIsArray === true + && $token[0] === T_STATIC + && $finalTokens[$lastNotEmptyToken]['code'] !== T_NEW + ) { + for ($i = ($stackPtr + 1); $i < $numTokens; $i++) { + if (is_array($tokens[$i]) === true + && isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === true + ) { + continue; + } + + if ($tokens[$i][0] === '(') { + $finalTokens[$newStackPtr] = [ + 'code' => T_STRING, + 'type' => 'T_STRING', + 'content' => $token[1], + ]; + + $newStackPtr++; + continue 2; + } + + break; + } + }//end if + /* Parse doc blocks into something that can be easily iterated over. */ @@ -566,6 +728,107 @@ protected function tokenize($string) continue; } + /* + PHP 8 tokenizes a new line after a slash and hash comment to the next whitespace token. + */ + + if (PHP_VERSION_ID >= 80000 + && $tokenIsArray === true + && ($token[0] === T_COMMENT && (strpos($token[1], '//') === 0 || strpos($token[1], '#') === 0)) + && isset($tokens[($stackPtr + 1)]) === true + && is_array($tokens[($stackPtr + 1)]) === true + && $tokens[($stackPtr + 1)][0] === T_WHITESPACE + ) { + $nextToken = $tokens[($stackPtr + 1)]; + + // If the next token is a single new line, merge it into the comment token + // and set to it up to be skipped. + if ($nextToken[1] === "\n" || $nextToken[1] === "\r\n" || $nextToken[1] === "\n\r") { + $token[1] .= $nextToken[1]; + $tokens[($stackPtr + 1)] = null; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* merged newline after comment into comment token $stackPtr".PHP_EOL; + } + } else { + // This may be a whitespace token consisting of multiple new lines. + if (strpos($nextToken[1], "\r\n") === 0) { + $token[1] .= "\r\n"; + $tokens[($stackPtr + 1)][1] = substr($nextToken[1], 2); + } else if (strpos($nextToken[1], "\n\r") === 0) { + $token[1] .= "\n\r"; + $tokens[($stackPtr + 1)][1] = substr($nextToken[1], 2); + } else if (strpos($nextToken[1], "\n") === 0) { + $token[1] .= "\n"; + $tokens[($stackPtr + 1)][1] = substr($nextToken[1], 1); + } + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* stripped first newline after comment and added it to comment token $stackPtr".PHP_EOL; + } + }//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 + && isset($tokens[($stackPtr + 1)][1][0], $tokens[($stackPtr + 1)][1][1]) === true + && strtolower($tokens[($stackPtr + 1)][1][0]) === 'o' + && $tokens[($stackPtr + 1)][1][1] !== '_') + && preg_match('`^(o[0-7]+(?:_[0-7]+)?)([0-9_]*)$`i', $tokens[($stackPtr + 1)][1], $matches) === 1 + ) { + $finalTokens[$newStackPtr] = [ + 'code' => T_LNUMBER, + 'type' => 'T_LNUMBER', + 'content' => $token[1] .= $matches[1], + ]; + $newStackPtr++; + + if (isset($matches[2]) === true && $matches[2] !== '') { + $type = 'T_LNUMBER'; + if ($matches[2][0] === '_') { + $type = 'T_STRING'; + } + + $finalTokens[$newStackPtr] = [ + 'code' => constant($type), + 'type' => $type, + 'content' => $matches[2], + ]; + $newStackPtr++; + } + + $stackPtr++; + continue; + }//end if + + /* + PHP 8.1 introduced two dedicated tokens for the & character. + Retokenizing both of these to T_BITWISE_AND, which is the + 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 @@ -592,7 +855,8 @@ protected function tokenize($string) if ($subTokenIsArray === true) { $tokenContent .= $subToken[1]; - if ($subToken[1] === '{' + if (($subToken[1] === '{' + || $subToken[1] === '${') && $subToken[0] !== T_ENCAPSED_AND_WHITESPACE ) { $nestedVars[] = $i; @@ -771,6 +1035,312 @@ protected function tokenize($string) continue; }//end if + /* + Enum keyword for PHP < 8.1 + */ + + if ($tokenIsArray === true + && $token[0] === T_STRING + && strtolower($token[1]) === 'enum' + ) { + // Get the next non-empty token. + for ($i = ($stackPtr + 1); $i < $numTokens; $i++) { + if (is_array($tokens[$i]) === false + || isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === false + ) { + break; + } + } + + if (isset($tokens[$i]) === true + && is_array($tokens[$i]) === true + && $tokens[$i][0] === T_STRING + ) { + // Modify $tokens directly so we can use it later when converting enum "case". + $tokens[$stackPtr][0] = T_ENUM; + + $newToken = []; + $newToken['code'] = T_ENUM; + $newToken['type'] = 'T_ENUM'; + $newToken['content'] = $token[1]; + $finalTokens[$newStackPtr] = $newToken; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* token $stackPtr changed from T_STRING to T_ENUM".PHP_EOL; + } + + $newStackPtr++; + continue; + } + }//end if + + /* + Convert enum "case" to T_ENUM_CASE + */ + + if ($tokenIsArray === true + && $token[0] === T_CASE + && isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false + ) { + $isEnumCase = false; + $scope = 1; + + for ($i = ($stackPtr - 1); $i > 0; $i--) { + if ($tokens[$i] === '}') { + $scope++; + continue; + } + + if ($tokens[$i] === '{') { + $scope--; + continue; + } + + if (is_array($tokens[$i]) === false) { + continue; + } + + if ($scope !== 0) { + continue; + } + + if ($tokens[$i][0] === T_SWITCH) { + break; + } + + if ($tokens[$i][0] === T_ENUM || $tokens[$i][0] === T_ENUM_CASE) { + $isEnumCase = true; + break; + } + }//end for + + if ($isEnumCase === true) { + // Modify $tokens directly so we can use it as optimisation for other enum "case". + $tokens[$stackPtr][0] = T_ENUM_CASE; + + $newToken = []; + $newToken['code'] = T_ENUM_CASE; + $newToken['type'] = 'T_ENUM_CASE'; + $newToken['content'] = $token[1]; + $finalTokens[$newStackPtr] = $newToken; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* token $stackPtr changed from T_CASE to T_ENUM_CASE".PHP_EOL; + } + + $newStackPtr++; + continue; + } + }//end if + + /* + As of PHP 8.0 fully qualified, partially qualified and namespace relative + identifier names are tokenized differently. + This "undoes" the new tokenization so the tokenization will be the same in + in PHP 5, 7 and 8. + */ + + if (PHP_VERSION_ID >= 80000 + && $tokenIsArray === true + && ($token[0] === T_NAME_QUALIFIED + || $token[0] === T_NAME_FULLY_QUALIFIED + || $token[0] === T_NAME_RELATIVE) + ) { + $name = $token[1]; + + if ($token[0] === T_NAME_FULLY_QUALIFIED) { + $newToken = []; + $newToken['code'] = T_NS_SEPARATOR; + $newToken['type'] = 'T_NS_SEPARATOR'; + $newToken['content'] = '\\'; + $finalTokens[$newStackPtr] = $newToken; + ++$newStackPtr; + + $name = ltrim($name, '\\'); + } + + if ($token[0] === T_NAME_RELATIVE) { + $newToken = []; + $newToken['code'] = T_NAMESPACE; + $newToken['type'] = 'T_NAMESPACE'; + $newToken['content'] = substr($name, 0, 9); + $finalTokens[$newStackPtr] = $newToken; + ++$newStackPtr; + + $newToken = []; + $newToken['code'] = T_NS_SEPARATOR; + $newToken['type'] = 'T_NS_SEPARATOR'; + $newToken['content'] = '\\'; + $finalTokens[$newStackPtr] = $newToken; + ++$newStackPtr; + + $name = substr($name, 10); + } + + $parts = explode('\\', $name); + $partCount = count($parts); + $lastPart = ($partCount - 1); + + foreach ($parts as $i => $part) { + $newToken = []; + $newToken['code'] = T_STRING; + $newToken['type'] = 'T_STRING'; + $newToken['content'] = $part; + $finalTokens[$newStackPtr] = $newToken; + ++$newStackPtr; + + if ($i !== $lastPart) { + $newToken = []; + $newToken['code'] = T_NS_SEPARATOR; + $newToken['type'] = 'T_NS_SEPARATOR'; + $newToken['content'] = '\\'; + $finalTokens[$newStackPtr] = $newToken; + ++$newStackPtr; + } + } + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + $type = Util\Tokens::tokenName($token[0]); + $content = Util\Common::prepareForOutput($token[1]); + echo "\t\t* token $stackPtr split into individual tokens; was: $type => $content".PHP_EOL; + } + + continue; + }//end if + + /* + PHP 8.0 Attributes + */ + + if (PHP_VERSION_ID < 80000 + && $token[0] === T_COMMENT + && strpos($token[1], '#[') === 0 + ) { + $subTokens = $this->parsePhpAttribute($tokens, $stackPtr); + if ($subTokens !== null) { + array_splice($tokens, $stackPtr, 1, $subTokens); + $numTokens = count($tokens); + + $tokenIsArray = true; + $token = $tokens[$stackPtr]; + } else { + $token[0] = T_ATTRIBUTE; + } + } + + if ($tokenIsArray === true + && $token[0] === T_ATTRIBUTE + ) { + // Go looking for the close bracket. + $bracketCloser = $this->findCloser($tokens, ($stackPtr + 1), ['[', '#['], ']'); + + $newToken = []; + $newToken['code'] = T_ATTRIBUTE; + $newToken['type'] = 'T_ATTRIBUTE'; + $newToken['content'] = '#['; + $finalTokens[$newStackPtr] = $newToken; + + $tokens[$bracketCloser] = []; + $tokens[$bracketCloser][0] = T_ATTRIBUTE_END; + $tokens[$bracketCloser][1] = ']'; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* token $bracketCloser changed from T_CLOSE_SQUARE_BRACKET to T_ATTRIBUTE_END".PHP_EOL; + } + + $newStackPtr++; + continue; + }//end if + + /* + Tokenize the parameter labels for PHP 8.0 named parameters as a special T_PARAM_NAME + token and ensures that the colon after it is always T_COLON. + */ + + if ($tokenIsArray === true + && ($token[0] === T_STRING + || preg_match('`^[a-zA-Z_\x80-\xff]`', $token[1]) === 1) + ) { + // Get the next non-empty token. + for ($i = ($stackPtr + 1); $i < $numTokens; $i++) { + if (is_array($tokens[$i]) === false + || isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === false + ) { + break; + } + } + + if (isset($tokens[$i]) === true + && is_array($tokens[$i]) === false + && $tokens[$i] === ':' + ) { + // Get the previous non-empty token. + for ($j = ($stackPtr - 1); $j > 0; $j--) { + if (is_array($tokens[$j]) === false + || isset(Util\Tokens::$emptyTokens[$tokens[$j][0]]) === false + ) { + break; + } + } + + if (is_array($tokens[$j]) === false + && ($tokens[$j] === '(' + || $tokens[$j] === ',') + ) { + $newToken = []; + $newToken['code'] = T_PARAM_NAME; + $newToken['type'] = 'T_PARAM_NAME'; + $newToken['content'] = $token[1]; + $finalTokens[$newStackPtr] = $newToken; + + $newStackPtr++; + + // Modify the original token stack so that future checks, like + // determining T_COLON vs T_INLINE_ELSE can handle this correctly. + $tokens[$stackPtr][0] = T_PARAM_NAME; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + $type = Util\Tokens::tokenName($token[0]); + echo "\t\t* token $stackPtr changed from $type to T_PARAM_NAME".PHP_EOL; + } + + continue; + } + }//end if + }//end if + + /* + "readonly" keyword for PHP < 8.1 + */ + + if (PHP_VERSION_ID < 80100 + && $tokenIsArray === true + && strtolower($token[1]) === 'readonly' + && isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false + ) { + // Get the next non-whitespace token. + for ($i = ($stackPtr + 1); $i < $numTokens; $i++) { + if (is_array($tokens[$i]) === false + || $tokens[$i][0] !== T_WHITESPACE + ) { + break; + } + } + + if (isset($tokens[$i]) === false + || $tokens[$i] !== '(' + ) { + $finalTokens[$newStackPtr] = [ + 'code' => T_READONLY, + 'type' => 'T_READONLY', + 'content' => $token[1], + ]; + $newStackPtr++; + + continue; + } + }//end if + /* Before PHP 7.0, the "yield from" was tokenized as T_YIELD, T_WHITESPACE and T_STRING. So look for @@ -787,7 +1357,7 @@ protected function tokenize($string) && $tokens[($stackPtr + 2)][0] === T_STRING && strtolower($tokens[($stackPtr + 2)][1]) === 'from' ) { - // Could be multi-line, so just the token stack. + // Could be multi-line, so adjust the token stack. $token[0] = T_YIELD_FROM; $token[1] .= $tokens[($stackPtr + 1)][1].$tokens[($stackPtr + 2)][1]; @@ -814,6 +1384,7 @@ protected function tokenize($string) && $tokenIsArray === true && $token[0] === T_STRING && strtolower($token[1]) === 'yield' + && isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false ) { if (isset($tokens[($stackPtr + 1)]) === true && isset($tokens[($stackPtr + 2)]) === true @@ -919,7 +1490,7 @@ protected function tokenize($string) /* Before PHP 7, the ??= operator was tokenized as T_INLINE_THEN, T_INLINE_THEN, T_EQUAL. - Between PHP 7.0 and 7.2, the ??= operator was tokenized as + Between PHP 7.0 and 7.3, the ??= operator was tokenized as T_COALESCE, T_EQUAL. So look for and combine these tokens in earlier versions. */ @@ -974,6 +1545,29 @@ protected function tokenize($string) continue; } + /* + Before PHP 8, the ?-> operator was tokenized as + T_INLINE_THEN followed by T_OBJECT_OPERATOR. + So look for and combine these tokens in earlier versions. + */ + + if ($tokenIsArray === false + && $token[0] === '?' + && isset($tokens[($stackPtr + 1)]) === true + && is_array($tokens[($stackPtr + 1)]) === true + && $tokens[($stackPtr + 1)][0] === T_OBJECT_OPERATOR + ) { + $newToken = []; + $newToken['code'] = T_NULLSAFE_OBJECT_OPERATOR; + $newToken['type'] = 'T_NULLSAFE_OBJECT_OPERATOR'; + $newToken['content'] = '?->'; + $finalTokens[$newStackPtr] = $newToken; + + $newStackPtr++; + $stackPtr++; + continue; + } + /* Before PHP 7.4, underscores inside T_LNUMBER and T_DNUMBER tokens split the token with a T_STRING. So look for @@ -1032,6 +1626,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 @@ -1052,6 +1647,121 @@ protected function tokenize($string) continue; }//end if + /* + Backfill the T_MATCH token for PHP versions < 8.0 and + do initial correction for non-match expression T_MATCH tokens + to T_STRING for PHP >= 8.0. + A final check for non-match expression T_MATCH tokens is done + in PHP::processAdditional(). + */ + + if ($tokenIsArray === true + && (($token[0] === T_STRING + && strtolower($token[1]) === 'match') + || $token[0] === T_MATCH) + ) { + $isMatch = false; + for ($x = ($stackPtr + 1); $x < $numTokens; $x++) { + if (isset($tokens[$x][0], Util\Tokens::$emptyTokens[$tokens[$x][0]]) === true) { + continue; + } + + if ($tokens[$x] !== '(') { + // This is not a match expression. + break; + } + + if (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true) { + // Also not a match expression. + break; + } + + $isMatch = true; + break; + }//end for + + if ($isMatch === true && $token[0] === T_STRING) { + $newToken = []; + $newToken['code'] = T_MATCH; + $newToken['type'] = 'T_MATCH'; + $newToken['content'] = $token[1]; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* token $stackPtr changed from T_STRING to T_MATCH".PHP_EOL; + } + + $finalTokens[$newStackPtr] = $newToken; + $newStackPtr++; + continue; + } else if ($isMatch === false && $token[0] === T_MATCH) { + // PHP 8.0, match keyword, but not a match expression. + $newToken = []; + $newToken['code'] = T_STRING; + $newToken['type'] = 'T_STRING'; + $newToken['content'] = $token[1]; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* token $stackPtr changed from T_MATCH to T_STRING".PHP_EOL; + } + + $finalTokens[$newStackPtr] = $newToken; + $newStackPtr++; + continue; + }//end if + }//end if + + /* + Retokenize the T_DEFAULT in match control structures as T_MATCH_DEFAULT + to prevent scope being set and the scope for switch default statements + breaking. + */ + + if ($tokenIsArray === true + && $token[0] === T_DEFAULT + && isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === false + ) { + for ($x = ($stackPtr + 1); $x < $numTokens; $x++) { + if ($tokens[$x] === ',') { + // Skip over potential trailing comma (supported in PHP). + continue; + } + + if (is_array($tokens[$x]) === false + || isset(Util\Tokens::$emptyTokens[$tokens[$x][0]]) === false + ) { + // Non-empty, non-comma content. + break; + } + } + + if (isset($tokens[$x]) === true + && is_array($tokens[$x]) === true + && $tokens[$x][0] === T_DOUBLE_ARROW + ) { + // Modify the original token stack for the double arrow so that + // future checks can disregard the double arrow token more easily. + // For match expression "case" statements, this is handled + // in PHP::processAdditional(). + $tokens[$x][0] = T_MATCH_ARROW; + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* token $x changed from T_DOUBLE_ARROW to T_MATCH_ARROW".PHP_EOL; + } + + $newToken = []; + $newToken['code'] = T_MATCH_DEFAULT; + $newToken['type'] = 'T_MATCH_DEFAULT'; + $newToken['content'] = $token[1]; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* token $stackPtr changed from T_DEFAULT to T_MATCH_DEFAULT".PHP_EOL; + } + + $finalTokens[$newStackPtr] = $newToken; + $newStackPtr++; + continue; + }//end if + }//end if + /* Convert ? to T_NULLABLE OR T_INLINE_THEN */ @@ -1064,7 +1774,7 @@ protected function tokenize($string) * Check if the next non-empty token is one of the tokens which can be used * in type declarations. If not, it's definitely a ternary. * At this point, the only token types which need to be taken into consideration - * as potential type declarations are T_STRING, T_ARRAY, T_CALLABLE and T_NS_SEPARATOR. + * as potential type declarations are identifier names, T_ARRAY, T_CALLABLE and T_NS_SEPARATOR. */ $lastRelevantNonEmpty = null; @@ -1081,7 +1791,11 @@ protected function tokenize($string) } if ($tokenType === T_STRING + || $tokenType === T_NAME_FULLY_QUALIFIED + || $tokenType === T_NAME_RELATIVE + || $tokenType === T_NAME_QUALIFIED || $tokenType === T_ARRAY + || $tokenType === T_NAMESPACE || $tokenType === T_NS_SEPARATOR ) { $lastRelevantNonEmpty = $tokenType; @@ -1092,7 +1806,10 @@ protected function tokenize($string) && isset($lastRelevantNonEmpty) === false) || ($lastRelevantNonEmpty === T_ARRAY && $tokenType === '(') - || ($lastRelevantNonEmpty === T_STRING + || (($lastRelevantNonEmpty === T_STRING + || $lastRelevantNonEmpty === T_NAME_FULLY_QUALIFIED + || $lastRelevantNonEmpty === T_NAME_RELATIVE + || $lastRelevantNonEmpty === T_NAME_QUALIFIED) && ($tokenType === T_DOUBLE_COLON || $tokenType === '(' || $tokenType === ':')) @@ -1141,7 +1858,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'; @@ -1233,10 +1950,9 @@ protected function tokenize($string) } /* - The string-like token after a function keyword should always be - tokenized as T_STRING even if it appears to be a different token, - such as when writing code like: function default(): foo - so go forward and change the token type before it is processed. + This is a special condition for T_ARRAY tokens used for + function return types. We want to keep the parenthesis map clean, + so let's tag these tokens as T_STRING. */ if ($tokenIsArray === true @@ -1244,32 +1960,6 @@ protected function tokenize($string) || $token[0] === T_FN) && $finalTokens[$lastNotEmptyToken]['code'] !== T_USE ) { - if ($token[0] === T_FUNCTION) { - for ($x = ($stackPtr + 1); $x < $numTokens; $x++) { - if (is_array($tokens[$x]) === false - || isset(Util\Tokens::$emptyTokens[$tokens[$x][0]]) === false - ) { - // Non-empty content. - break; - } - } - - if ($x < $numTokens && is_array($tokens[$x]) === true) { - if (PHP_CODESNIFFER_VERBOSITY > 1) { - $oldType = Util\Tokens::tokenName($tokens[$x][0]); - echo "\t\t* token $x changed from $oldType to T_STRING".PHP_EOL; - } - - $tokens[$x][0] = T_STRING; - } - }//end if - - /* - This is a special condition for T_ARRAY tokens used for - function return types. We want to keep the parenthesis map clean, - so let's tag these tokens as T_STRING. - */ - // Go looking for the colon to start the return type hint. // Start by finding the closing parenthesis of the function. $parenthesisStack = []; @@ -1309,17 +1999,6 @@ function return types. We want to keep the parenthesis map clean, && is_array($tokens[$x]) === false && $tokens[$x] === ':' ) { - $allowed = [ - T_STRING => T_STRING, - T_ARRAY => T_ARRAY, - T_CALLABLE => T_CALLABLE, - T_SELF => T_SELF, - T_PARENT => T_PARENT, - T_NS_SEPARATOR => T_NS_SEPARATOR, - ]; - - $allowed += Util\Tokens::$emptyTokens; - // Find the start of the return type. for ($x += 1; $x < $numTokens; $x++) { if (is_array($tokens[$x]) === true @@ -1331,7 +2010,7 @@ function return types. We want to keep the parenthesis map clean, if (is_array($tokens[$x]) === false && $tokens[$x] === '?') { // Found a nullable operator, so skip it. - // But also covert the token to save the tokenizer + // But also convert the token to save the tokenizer // a bit of time later on. $tokens[$x] = [ T_NULLABLE, @@ -1347,21 +2026,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 || isset($allowed[$tokens[$x][0]]) === false) { - break; - } else if ($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 @@ -1400,13 +2064,16 @@ function return types. We want to keep the parenthesis map clean, && $token[0] === T_STRING && isset($tokens[($stackPtr + 1)]) === true && $tokens[($stackPtr + 1)] === ':' - && $tokens[($stackPtr - 1)][0] !== T_PAAMAYIM_NEKUDOTAYIM + && (is_array($tokens[($stackPtr - 1)]) === false + || $tokens[($stackPtr - 1)][0] !== T_PAAMAYIM_NEKUDOTAYIM) ) { $stopTokens = [ T_CASE => true, T_SEMICOLON => true, + T_OPEN_TAG => true, T_OPEN_CURLY_BRACKET => true, T_INLINE_THEN => true, + T_ENUM => true, ]; for ($x = ($newStackPtr - 1); $x > 0; $x--) { @@ -1417,6 +2084,7 @@ function return types. We want to keep the parenthesis map clean, if ($finalTokens[$x]['code'] !== T_CASE && $finalTokens[$x]['code'] !== T_INLINE_THEN + && $finalTokens[$x]['code'] !== T_ENUM ) { $finalTokens[$newStackPtr] = [ 'content' => $token[1].':', @@ -1465,45 +2133,61 @@ function return types. We want to keep the parenthesis map clean, $newStackPtr++; } } else { + // Some T_STRING tokens should remain that way due to their context. if ($tokenIsArray === true && $token[0] === T_STRING) { - // Some T_STRING tokens should remain that way - // due to their context. - $context = [ - T_OBJECT_OPERATOR => true, - T_FUNCTION => true, - T_CLASS => true, - T_EXTENDS => true, - T_IMPLEMENTS => true, - T_NEW => true, - T_CONST => true, - T_NS_SEPARATOR => true, - T_USE => true, - T_NAMESPACE => true, - T_PAAMAYIM_NEKUDOTAYIM => true, - ]; + $preserveTstring = false; + + if (isset($this->tstringContexts[$finalTokens[$lastNotEmptyToken]['code']]) === true) { + $preserveTstring = true; - if (isset($context[$finalTokens[$lastNotEmptyToken]['code']]) === true) { - // Special case for syntax like: return new self - // where self should not be a string. + // Special case for syntax like: return new self/new parent + // where self/parent should not be a string. + $tokenContentLower = strtolower($token[1]); if ($finalTokens[$lastNotEmptyToken]['code'] === T_NEW - && strtolower($token[1]) === 'self' + && ($tokenContentLower === 'self' || $tokenContentLower === 'parent') ) { - $finalTokens[$newStackPtr] = [ - 'content' => $token[1], - 'code' => T_SELF, - 'type' => 'T_SELF', - ]; - } else { - $finalTokens[$newStackPtr] = [ - 'content' => $token[1], - 'code' => T_STRING, - 'type' => 'T_STRING', - ]; + $preserveTstring = false; + } + } else if ($finalTokens[$lastNotEmptyToken]['content'] === '&') { + // Function names for functions declared to return by reference. + for ($i = ($lastNotEmptyToken - 1); $i >= 0; $i--) { + if (isset(Util\Tokens::$emptyTokens[$finalTokens[$i]['code']]) === true) { + continue; + } + + if ($finalTokens[$i]['code'] === T_FUNCTION) { + $preserveTstring = true; + } + + break; + } + } else { + // Keywords with special PHPCS token when used as a function call. + for ($i = ($stackPtr + 1); $i < $numTokens; $i++) { + if (is_array($tokens[$i]) === true + && isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === true + ) { + continue; + } + + if ($tokens[$i][0] === '(') { + $preserveTstring = true; + } + + break; } + }//end if + + if ($preserveTstring === true) { + $finalTokens[$newStackPtr] = [ + 'code' => T_STRING, + 'type' => 'T_STRING', + 'content' => $token[1], + ]; $newStackPtr++; continue; - }//end if + } }//end if $newToken = null; @@ -1532,74 +2216,100 @@ function return types. We want to keep the parenthesis map clean, // Convert colons that are actually the ELSE component of an // inline IF statement. if (empty($insideInlineIf) === false && $newToken['code'] === T_COLON) { - // Make sure this isn't the return type separator of a closure. $isInlineIf = true; + + // Make sure this isn't a named parameter label. + // Get the previous non-empty token. for ($i = ($stackPtr - 1); $i > 0; $i--) { if (is_array($tokens[$i]) === false - || ($tokens[$i][0] !== T_DOC_COMMENT - && $tokens[$i][0] !== T_COMMENT - && $tokens[$i][0] !== T_WHITESPACE) + || isset(Util\Tokens::$emptyTokens[$tokens[$i][0]]) === false ) { break; } } - if ($tokens[$i] === ')') { - $parenCount = 1; - for ($i--; $i > 0; $i--) { - if ($tokens[$i] === '(') { - $parenCount--; - if ($parenCount === 0) { + if ($tokens[$i][0] === T_PARAM_NAME) { + $isInlineIf = false; + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* token is parameter label, not T_INLINE_ELSE".PHP_EOL; + } + } + + if ($isInlineIf === true) { + // Make sure this isn't a return type separator. + for ($i = ($stackPtr - 1); $i > 0; $i--) { + if (is_array($tokens[$i]) === false + || ($tokens[$i][0] !== T_DOC_COMMENT + && $tokens[$i][0] !== T_COMMENT + && $tokens[$i][0] !== T_WHITESPACE) + ) { + break; + } + } + + if ($tokens[$i] === ')') { + $parenCount = 1; + for ($i--; $i > 0; $i--) { + if ($tokens[$i] === '(') { + $parenCount--; + if ($parenCount === 0) { + break; + } + } else if ($tokens[$i] === ')') { + $parenCount++; + } + } + + // We've found the open parenthesis, so if the previous + // non-empty token is FUNCTION or USE, this is a return type. + // Note that we need to skip T_STRING tokens here as these + // can be function names. + for ($i--; $i > 0; $i--) { + if (is_array($tokens[$i]) === false + || ($tokens[$i][0] !== T_DOC_COMMENT + && $tokens[$i][0] !== T_COMMENT + && $tokens[$i][0] !== T_WHITESPACE + && $tokens[$i][0] !== T_STRING) + ) { break; } - } else if ($tokens[$i] === ')') { - $parenCount++; } - } - // We've found the open parenthesis, so if the previous - // non-empty token is FUNCTION or USE, this is a closure. - for ($i--; $i > 0; $i--) { + if ($tokens[$i][0] === T_FUNCTION || $tokens[$i][0] === T_FN || $tokens[$i][0] === T_USE) { + $isInlineIf = false; + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* token is return type, not T_INLINE_ELSE".PHP_EOL; + } + } + }//end if + }//end if + + // Check to see if this is a CASE or DEFAULT opener. + if ($isInlineIf === true) { + $inlineIfToken = $insideInlineIf[(count($insideInlineIf) - 1)]; + for ($i = $stackPtr; $i > $inlineIfToken; $i--) { + if (is_array($tokens[$i]) === true + && ($tokens[$i][0] === T_CASE + || $tokens[$i][0] === T_DEFAULT) + ) { + $isInlineIf = false; + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* token is T_CASE or T_DEFAULT opener, not T_INLINE_ELSE".PHP_EOL; + } + + break; + } + if (is_array($tokens[$i]) === false - || ($tokens[$i][0] !== T_DOC_COMMENT - && $tokens[$i][0] !== T_COMMENT - && $tokens[$i][0] !== T_WHITESPACE) + && ($tokens[$i] === ';' + || $tokens[$i] === '{' + || $tokens[$i] === '}') ) { break; } - } - - if ($tokens[$i][0] === T_FUNCTION || $tokens[$i][0] === T_FN || $tokens[$i][0] === T_USE) { - $isInlineIf = false; - if (PHP_CODESNIFFER_VERBOSITY > 1) { - echo "\t\t* token is function return type, not T_INLINE_ELSE".PHP_EOL; - } - } + }//end for }//end if - // Check to see if this is a CASE or DEFAULT opener. - $inlineIfToken = $insideInlineIf[(count($insideInlineIf) - 1)]; - for ($i = $stackPtr; $i > $inlineIfToken; $i--) { - if (is_array($tokens[$i]) === true - && ($tokens[$i][0] === T_CASE - || $tokens[$i][0] === T_DEFAULT) - ) { - $isInlineIf = false; - if (PHP_CODESNIFFER_VERBOSITY > 1) { - echo "\t\t* token is T_CASE or T_DEFAULT opener, not T_INLINE_ELSE".PHP_EOL; - } - - break; - } - - if (is_array($tokens[$i]) === false - && ($tokens[$i] === ';' - || $tokens[$i] === '{') - ) { - break; - } - } - if ($isInlineIf === true) { array_pop($insideInlineIf); $newToken['code'] = T_INLINE_ELSE; @@ -1611,41 +2321,37 @@ 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 ($i !== $numTokens && $tokens[$i] !== '(') { + $newToken['code'] = T_STRING; + $newToken['type'] = 'T_STRING'; + } } // This is a special case when checking PHP 5.5+ code in PHP < 5.5 // where "finally" should be T_FINALLY instead of T_STRING. if ($newToken['code'] === T_STRING && strtolower($newToken['content']) === 'finally' + && $finalTokens[$lastNotEmptyToken]['code'] === T_CLOSE_CURLY_BRACKET ) { $newToken['code'] = T_FINALLY; $newToken['type'] = 'T_FINALLY'; } - // This is a special case for the PHP 5.5 classname::class syntax - // where "class" should be T_STRING instead of T_CLASS. - if (($newToken['code'] === T_CLASS - || $newToken['code'] === T_FUNCTION) - && $finalTokens[$lastNotEmptyToken]['code'] === T_DOUBLE_COLON - ) { - $newToken['code'] = T_STRING; - $newToken['type'] = 'T_STRING'; - } - // This is a special case for PHP 5.6 use function and use const // where "function" and "const" should be T_STRING instead of T_FUNCTION // and T_CONST. @@ -1706,6 +2412,8 @@ protected function processAdditional() echo "\t*** START ADDITIONAL PHP PROCESSING ***".PHP_EOL; } + $this->createAttributesNestingMap(); + $numTokens = count($this->tokens); for ($i = ($numTokens - 1); $i >= 0; $i--) { // Check for any unset scope conditions due to alternate IF/ENDIF syntax. @@ -1821,14 +2529,19 @@ protected function processAdditional() if (isset($this->tokens[$x]) === true && $this->tokens[$x]['code'] === T_OPEN_PARENTHESIS) { $ignore = Util\Tokens::$emptyTokens; $ignore += [ - T_STRING => T_STRING, - T_ARRAY => T_ARRAY, - T_COLON => T_COLON, - T_NS_SEPARATOR => T_NS_SEPARATOR, - T_NULLABLE => T_NULLABLE, - T_CALLABLE => T_CALLABLE, - T_PARENT => T_PARENT, - T_SELF => T_SELF, + T_ARRAY => T_ARRAY, + T_CALLABLE => T_CALLABLE, + T_COLON => T_COLON, + T_NAMESPACE => T_NAMESPACE, + T_NS_SEPARATOR => T_NS_SEPARATOR, + T_NULL => T_NULL, + T_NULLABLE => T_NULLABLE, + T_PARENT => T_PARENT, + T_SELF => T_SELF, + T_STATIC => T_STATIC, + T_STRING => T_STRING, + T_TYPE_UNION => T_TYPE_UNION, + T_TYPE_INTERSECTION => T_TYPE_INTERSECTION, ]; $closer = $this->tokens[$x]['parenthesis_closer']; @@ -1855,17 +2568,59 @@ protected function processAdditional() $lastEndToken = null; for ($scopeCloser = ($arrow + 1); $scopeCloser < $numTokens; $scopeCloser++) { + // Arrow function closer should never be shared with the closer of a match + // control structure. + if (isset($this->tokens[$scopeCloser]['scope_closer'], $this->tokens[$scopeCloser]['scope_condition']) === true + && $scopeCloser === $this->tokens[$scopeCloser]['scope_closer'] + && $this->tokens[$this->tokens[$scopeCloser]['scope_condition']]['code'] === T_MATCH + ) { + if ($arrow < $this->tokens[$scopeCloser]['scope_condition']) { + // Match in return value of arrow function. Move on to the next token. + continue; + } + + // Arrow function as return value for the last match case without trailing comma. + if ($lastEndToken !== null) { + $scopeCloser = $lastEndToken; + break; + } + + for ($lastNonEmpty = ($scopeCloser - 1); $lastNonEmpty > $arrow; $lastNonEmpty--) { + if (isset(Util\Tokens::$emptyTokens[$this->tokens[$lastNonEmpty]['code']]) === false) { + $scopeCloser = $lastNonEmpty; + break 2; + } + } + } + if (isset($endTokens[$this->tokens[$scopeCloser]['code']]) === true) { if ($lastEndToken !== null - && $this->tokens[$scopeCloser]['code'] === T_CLOSE_PARENTHESIS - && $this->tokens[$scopeCloser]['parenthesis_opener'] < $arrow + && ((isset($this->tokens[$scopeCloser]['parenthesis_opener']) === true + && $this->tokens[$scopeCloser]['parenthesis_opener'] < $arrow) + || (isset($this->tokens[$scopeCloser]['bracket_opener']) === true + && $this->tokens[$scopeCloser]['bracket_opener'] < $arrow)) ) { - $scopeCloser = $lastEndToken; + for ($lastNonEmpty = ($scopeCloser - 1); $lastNonEmpty > $arrow; $lastNonEmpty--) { + if (isset(Util\Tokens::$emptyTokens[$this->tokens[$lastNonEmpty]['code']]) === false) { + $scopeCloser = $lastNonEmpty; + break; + } + } } break; } + if ($inTernary === false + && isset($this->tokens[$scopeCloser]['scope_closer'], $this->tokens[$scopeCloser]['scope_condition']) === true + && $scopeCloser === $this->tokens[$scopeCloser]['scope_closer'] + && $this->tokens[$this->tokens[$scopeCloser]['scope_condition']]['code'] === T_FN + ) { + // Found a nested arrow function that already has the closer set and is in + // the same scope as us, so we can use its closer. + break; + } + if (isset($this->tokens[$scopeCloser]['scope_closer']) === true && $this->tokens[$scopeCloser]['code'] !== T_INLINE_ELSE && $this->tokens[$scopeCloser]['code'] !== T_END_HEREDOC @@ -1970,9 +2725,12 @@ protected function processAdditional() T_CLOSE_PARENTHESIS => T_CLOSE_PARENTHESIS, T_VARIABLE => T_VARIABLE, T_OBJECT_OPERATOR => T_OBJECT_OPERATOR, + T_NULLSAFE_OBJECT_OPERATOR => T_NULLSAFE_OBJECT_OPERATOR, T_STRING => T_STRING, T_CONSTANT_ENCAPSED_STRING => T_CONSTANT_ENCAPSED_STRING, + T_DOUBLE_QUOTED_STRING => T_DOUBLE_QUOTED_STRING, ]; + $allowed += Util\Tokens::$magicConstants; for ($x = ($i - 1); $x >= 0; $x--) { // If we hit a scope opener, the statement has ended @@ -1984,13 +2742,18 @@ protected function processAdditional() } if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === false) { - if (isset($allowed[$this->tokens[$x]['code']]) === false) { + // Allow for control structures without braces. + if (($this->tokens[$x]['code'] === T_CLOSE_PARENTHESIS + && isset($this->tokens[$x]['parenthesis_owner']) === true + && isset(Util\Tokens::$scopeOpeners[$this->tokens[$this->tokens[$x]['parenthesis_owner']]['code']]) === true) + || isset($allowed[$this->tokens[$x]['code']]) === false + ) { $isShortArray = true; } break; } - } + }//end for if ($isShortArray === true) { $this->tokens[$i]['code'] = T_OPEN_SHORT_ARRAY; @@ -2007,6 +2770,262 @@ protected function processAdditional() } } + continue; + } else if ($this->tokens[$i]['code'] === T_MATCH) { + if (isset($this->tokens[$i]['scope_opener'], $this->tokens[$i]['scope_closer']) === false) { + // Not a match expression after all. + $this->tokens[$i]['code'] = T_STRING; + $this->tokens[$i]['type'] = 'T_STRING'; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* token $i changed from T_MATCH to T_STRING".PHP_EOL; + } + + if (isset($this->tokens[$i]['parenthesis_opener'], $this->tokens[$i]['parenthesis_closer']) === true) { + $opener = $this->tokens[$i]['parenthesis_opener']; + $closer = $this->tokens[$i]['parenthesis_closer']; + unset( + $this->tokens[$opener]['parenthesis_owner'], + $this->tokens[$closer]['parenthesis_owner'] + ); + unset( + $this->tokens[$i]['parenthesis_opener'], + $this->tokens[$i]['parenthesis_closer'], + $this->tokens[$i]['parenthesis_owner'] + ); + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* cleaned parenthesis of token $i *".PHP_EOL; + } + } + } else { + // Retokenize the double arrows for match expression cases to `T_MATCH_ARROW`. + $searchFor = [ + T_OPEN_CURLY_BRACKET => T_OPEN_CURLY_BRACKET, + T_OPEN_SQUARE_BRACKET => T_OPEN_SQUARE_BRACKET, + T_OPEN_PARENTHESIS => T_OPEN_PARENTHESIS, + T_OPEN_SHORT_ARRAY => T_OPEN_SHORT_ARRAY, + T_DOUBLE_ARROW => T_DOUBLE_ARROW, + ]; + $searchFor += Util\Tokens::$scopeOpeners; + + for ($x = ($this->tokens[$i]['scope_opener'] + 1); $x < $this->tokens[$i]['scope_closer']; $x++) { + if (isset($searchFor[$this->tokens[$x]['code']]) === false) { + continue; + } + + if (isset($this->tokens[$x]['scope_closer']) === true) { + $x = $this->tokens[$x]['scope_closer']; + continue; + } + + if (isset($this->tokens[$x]['parenthesis_closer']) === true) { + $x = $this->tokens[$x]['parenthesis_closer']; + continue; + } + + if (isset($this->tokens[$x]['bracket_closer']) === true) { + $x = $this->tokens[$x]['bracket_closer']; + continue; + } + + // This must be a double arrow, but make sure anyhow. + if ($this->tokens[$x]['code'] === T_DOUBLE_ARROW) { + $this->tokens[$x]['code'] = T_MATCH_ARROW; + $this->tokens[$x]['type'] = 'T_MATCH_ARROW'; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo "\t\t* token $x changed from T_DOUBLE_ARROW to T_MATCH_ARROW".PHP_EOL; + } + } + }//end for + }//end if + + continue; + } else if ($this->tokens[$i]['code'] === T_BITWISE_OR + || $this->tokens[$i]['code'] === T_BITWISE_AND + ) { + /* + Convert "|" to T_TYPE_UNION or leave as T_BITWISE_OR. + Convert "&" to T_TYPE_INTERSECTION or leave as T_BITWISE_AND. + */ + + $allowed = [ + T_STRING => T_STRING, + T_CALLABLE => T_CALLABLE, + T_SELF => T_SELF, + T_PARENT => T_PARENT, + T_STATIC => T_STATIC, + T_FALSE => T_FALSE, + T_NULL => T_NULL, + T_NAMESPACE => T_NAMESPACE, + T_NS_SEPARATOR => T_NS_SEPARATOR, + ]; + + $suspectedType = null; + $typeTokenCount = 0; + + for ($x = ($i + 1); $x < $numTokens; $x++) { + if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true) { + continue; + } + + if (isset($allowed[$this->tokens[$x]['code']]) === true) { + ++$typeTokenCount; + continue; + } + + if ($typeTokenCount > 0 + && ($this->tokens[$x]['code'] === T_BITWISE_AND + || $this->tokens[$x]['code'] === T_ELLIPSIS) + ) { + // Skip past reference and variadic indicators for parameter types. + continue; + } + + if ($this->tokens[$x]['code'] === T_VARIABLE) { + // Parameter/Property defaults can not contain variables, so this could be a type. + $suspectedType = 'property or parameter'; + break; + } + + if ($this->tokens[$x]['code'] === T_DOUBLE_ARROW) { + // Possible arrow function. + $suspectedType = 'return'; + break; + } + + if ($this->tokens[$x]['code'] === T_SEMICOLON) { + // Possible abstract method or interface method. + $suspectedType = 'return'; + break; + } + + if ($this->tokens[$x]['code'] === T_OPEN_CURLY_BRACKET + && isset($this->tokens[$x]['scope_condition']) === true + && $this->tokens[$this->tokens[$x]['scope_condition']]['code'] === T_FUNCTION + ) { + $suspectedType = 'return'; + } + + break; + }//end for + + if ($typeTokenCount === 0 || isset($suspectedType) === false) { + // Definitely not a union or intersection type, move on. + continue; + } + + $typeTokenCount = 0; + $typeOperators = [$i]; + $confirmed = false; + + for ($x = ($i - 1); $x >= 0; $x--) { + if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true) { + continue; + } + + if (isset($allowed[$this->tokens[$x]['code']]) === true) { + ++$typeTokenCount; + continue; + } + + // Union and intersection types can't use the nullable operator, but be tolerant to parse errors. + if ($typeTokenCount > 0 && $this->tokens[$x]['code'] === T_NULLABLE) { + continue; + } + + if ($this->tokens[$x]['code'] === T_BITWISE_OR || $this->tokens[$x]['code'] === T_BITWISE_AND) { + $typeOperators[] = $x; + continue; + } + + if ($suspectedType === 'return' && $this->tokens[$x]['code'] === T_COLON) { + $confirmed = true; + break; + } + + 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_READONLY) + ) { + // This will also confirm constructor property promotion parameters, but that's fine. + $confirmed = true; + } + + break; + }//end for + + if ($confirmed === false + && $suspectedType === 'property or parameter' + && isset($this->tokens[$i]['nested_parenthesis']) === true + ) { + $parens = $this->tokens[$i]['nested_parenthesis']; + $last = end($parens); + + if (isset($this->tokens[$last]['parenthesis_owner']) === true + && $this->tokens[$this->tokens[$last]['parenthesis_owner']]['code'] === T_FUNCTION + ) { + $confirmed = true; + } else { + // No parenthesis owner set, this may be an arrow function which has not yet + // had additional processing done. + if (isset($this->tokens[$last]['parenthesis_opener']) === true) { + for ($x = ($this->tokens[$last]['parenthesis_opener'] - 1); $x >= 0; $x--) { + if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true) { + continue; + } + + break; + } + + if ($this->tokens[$x]['code'] === T_FN) { + for (--$x; $x >= 0; $x--) { + if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === true + || $this->tokens[$x]['code'] === T_BITWISE_AND + ) { + continue; + } + + break; + } + + if ($this->tokens[$x]['code'] !== T_FUNCTION) { + $confirmed = true; + } + } + }//end if + }//end if + + unset($parens, $last); + }//end if + + if ($confirmed === false) { + // Not a union or intersection type after all, move on. + continue; + } + + foreach ($typeOperators as $x) { + if ($this->tokens[$x]['code'] === T_BITWISE_OR) { + $this->tokens[$x]['code'] = T_TYPE_UNION; + $this->tokens[$x]['type'] = 'T_TYPE_UNION'; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + $line = $this->tokens[$x]['line']; + echo "\t* token $x on line $line changed from T_BITWISE_OR to T_TYPE_UNION".PHP_EOL; + } + } else { + $this->tokens[$x]['code'] = T_TYPE_INTERSECTION; + $this->tokens[$x]['type'] = 'T_TYPE_INTERSECTION'; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + $line = $this->tokens[$x]['line']; + echo "\t* token $x on line $line changed from T_BITWISE_AND to T_TYPE_INTERSECTION".PHP_EOL; + } + } + } + continue; } else if ($this->tokens[$i]['code'] === T_STATIC) { for ($x = ($i - 1); $x > 0; $x--) { @@ -2037,12 +3056,7 @@ protected function processAdditional() } } - $context = [ - T_OBJECT_OPERATOR => true, - T_NS_SEPARATOR => true, - T_PAAMAYIM_NEKUDOTAYIM => true, - ]; - if (isset($context[$this->tokens[$x]['code']]) === true) { + if (isset($this->tstringContexts[$this->tokens[$x]['code']]) === true) { if (PHP_CODESNIFFER_VERBOSITY > 1) { $line = $this->tokens[$i]['line']; $type = $this->tokens[$i]['type']; @@ -2052,25 +3066,6 @@ protected function processAdditional() $this->tokens[$i]['code'] = T_STRING; $this->tokens[$i]['type'] = 'T_STRING'; } - } else if ($this->tokens[$i]['code'] === T_CONST) { - // Context sensitive keywords support. - for ($x = ($i + 1); $i < $numTokens; $x++) { - if (isset(Util\Tokens::$emptyTokens[$this->tokens[$x]['code']]) === false) { - // Non-whitespace content. - break; - } - } - - if ($this->tokens[$x]['code'] !== T_STRING) { - if (PHP_CODESNIFFER_VERBOSITY > 1) { - $line = $this->tokens[$x]['line']; - $type = $this->tokens[$x]['type']; - echo "\t* token $x on line $line changed from $type to T_STRING".PHP_EOL; - } - - $this->tokens[$x]['code'] = T_STRING; - $this->tokens[$x]['type'] = 'T_STRING'; - } }//end if if (($this->tokens[$i]['code'] !== T_CASE @@ -2408,4 +3403,141 @@ public static function resolveSimpleToken($token) }//end resolveSimpleToken() + /** + * Finds a "closer" token (closing parenthesis or square bracket for example) + * Handle parenthesis balancing while searching for closing token + * + * @param array $tokens The list of tokens to iterate searching the closing token (as returned by token_get_all) + * @param int $start The starting position + * @param string|string[] $openerTokens The opening character + * @param string $closerChar The closing character + * + * @return int|null The position of the closing token, if found. NULL otherwise. + */ + private function findCloser(array &$tokens, $start, $openerTokens, $closerChar) + { + $numTokens = count($tokens); + $stack = [0]; + $closer = null; + $openerTokens = (array) $openerTokens; + + for ($x = $start; $x < $numTokens; $x++) { + if (in_array($tokens[$x], $openerTokens, true) === true + || (is_array($tokens[$x]) === true && in_array($tokens[$x][1], $openerTokens, true) === true) + ) { + $stack[] = $x; + } else if ($tokens[$x] === $closerChar) { + array_pop($stack); + if (empty($stack) === true) { + $closer = $x; + break; + } + } + } + + return $closer; + + }//end findCloser() + + + /** + * PHP 8 attributes parser for PHP < 8 + * Handles single-line and multiline attributes. + * + * @param array $tokens The original array of tokens (as returned by token_get_all) + * @param int $stackPtr The current position in token array + * + * @return array|null The array of parsed attribute tokens + */ + private function parsePhpAttribute(array &$tokens, $stackPtr) + { + + $token = $tokens[$stackPtr]; + + $commentBody = substr($token[1], 2); + $subTokens = @token_get_all(' $subToken) { + if (is_array($subToken) === true + && $subToken[0] === T_COMMENT + && strpos($subToken[1], '#[') === 0 + ) { + $reparsed = $this->parsePhpAttribute($subTokens, $i); + if ($reparsed !== null) { + array_splice($subTokens, $i, 1, $reparsed); + } else { + $subToken[0] = T_ATTRIBUTE; + } + } + } + + array_splice($subTokens, 0, 1, [[T_ATTRIBUTE, '#[']]); + + // Go looking for the close bracket. + $bracketCloser = $this->findCloser($subTokens, 1, '[', ']'); + if (PHP_VERSION_ID < 80000 && $bracketCloser === null) { + foreach (array_slice($tokens, ($stackPtr + 1)) as $token) { + if (is_array($token) === true) { + $commentBody .= $token[1]; + } else { + $commentBody .= $token; + } + } + + $subTokens = @token_get_all('findCloser($subTokens, 1, '[', ']'); + if ($bracketCloser !== null) { + array_splice($tokens, ($stackPtr + 1), count($tokens), array_slice($subTokens, ($bracketCloser + 1))); + $subTokens = array_slice($subTokens, 0, ($bracketCloser + 1)); + } + } + + if ($bracketCloser === null) { + return null; + } + + return $subTokens; + + }//end parsePhpAttribute() + + + /** + * Creates a map for the attributes tokens that surround other tokens. + * + * @return void + */ + private function createAttributesNestingMap() + { + $map = []; + for ($i = 0; $i < $this->numTokens; $i++) { + if (isset($this->tokens[$i]['attribute_opener']) === true + && $i === $this->tokens[$i]['attribute_opener'] + ) { + if (empty($map) === false) { + $this->tokens[$i]['nested_attributes'] = $map; + } + + if (isset($this->tokens[$i]['attribute_closer']) === true) { + $map[$this->tokens[$i]['attribute_opener']] + = $this->tokens[$i]['attribute_closer']; + } + } else if (isset($this->tokens[$i]['attribute_closer']) === true + && $i === $this->tokens[$i]['attribute_closer'] + ) { + array_pop($map); + if (empty($map) === false) { + $this->tokens[$i]['nested_attributes'] = $map; + } + } else { + if (empty($map) === false) { + $this->tokens[$i]['nested_attributes'] = $map; + } + }//end if + }//end for + + }//end createAttributesNestingMap() + + }//end class diff --git a/src/Tokenizers/Tokenizer.php b/src/Tokenizers/Tokenizer.php index 82b2b9cc78..0e00bf7f22 100644 --- a/src/Tokenizers/Tokenizer.php +++ b/src/Tokenizers/Tokenizer.php @@ -194,6 +194,8 @@ private function createPositionMap() T_DOUBLE_QUOTED_STRING => true, T_HEREDOC => true, T_NOWDOC => true, + T_END_HEREDOC => true, + T_END_NOWDOC => true, T_INLINE_HTML => true, ]; @@ -250,7 +252,7 @@ private function createPositionMap() || $this->tokens[$i]['code'] === T_DOC_COMMENT_TAG || ($inTests === true && $this->tokens[$i]['code'] === T_INLINE_HTML) ) { - $commentText = ltrim($this->tokens[$i]['content'], " \t/*"); + $commentText = ltrim($this->tokens[$i]['content'], " \t/*#"); $commentText = rtrim($commentText, " */\t\r\n"); $commentTextLower = strtolower($commentText); if (strpos($commentText, '@codingStandards') !== false) { @@ -424,10 +426,10 @@ private function createPositionMap() $disabledSniffs = []; $additionalText = substr($commentText, 14); - if ($additionalText === false) { + if (empty($additionalText) === true) { $ignoring = ['.all' => true]; } else { - $parts = explode(',', substr($commentText, 13)); + $parts = explode(',', $additionalText); foreach ($parts as $sniffCode) { $sniffCode = trim($sniffCode); $disabledSniffs[$sniffCode] = true; @@ -459,10 +461,10 @@ private function createPositionMap() $enabledSniffs = []; $additionalText = substr($commentText, 13); - if ($additionalText === false) { + if (empty($additionalText) === true) { $ignoring = null; } else { - $parts = explode(',', substr($commentText, 13)); + $parts = explode(',', $additionalText); foreach ($parts as $sniffCode) { $sniffCode = trim($sniffCode); $enabledSniffs[$sniffCode] = true; @@ -520,10 +522,10 @@ private function createPositionMap() $ignoreRules = []; $additionalText = substr($commentText, 13); - if ($additionalText === false) { + if (empty($additionalText) === true) { $ignoreRules = ['.all' => true]; } else { - $parts = explode(',', substr($commentText, 13)); + $parts = explode(',', $additionalText); foreach ($parts as $sniffCode) { $ignoreRules[trim($sniffCode)] = true; } @@ -638,25 +640,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 @@ -740,6 +730,40 @@ private function createTokenMap() $this->tokens[$i]['parenthesis_closer'] = $i; $this->tokens[$opener]['parenthesis_closer'] = $i; }//end if + } else if ($this->tokens[$i]['code'] === T_ATTRIBUTE) { + $openers[] = $i; + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo str_repeat("\t", count($openers)); + echo "=> Found attribute opener at $i".PHP_EOL; + } + + $this->tokens[$i]['attribute_opener'] = $i; + $this->tokens[$i]['attribute_closer'] = null; + } else if ($this->tokens[$i]['code'] === T_ATTRIBUTE_END) { + $numOpeners = count($openers); + if ($numOpeners !== 0) { + $opener = array_pop($openers); + if (isset($this->tokens[$opener]['attribute_opener']) === true) { + $this->tokens[$opener]['attribute_closer'] = $i; + + if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo str_repeat("\t", (count($openers) + 1)); + echo "=> Found attribute closer at $i for $opener".PHP_EOL; + } + + for ($x = ($opener + 1); $x <= $i; ++$x) { + if (isset($this->tokens[$x]['attribute_closer']) === true) { + continue; + } + + $this->tokens[$x]['attribute_opener'] = $opener; + $this->tokens[$x]['attribute_closer'] = $i; + } + } else if (PHP_CODESNIFFER_VERBOSITY > 1) { + echo str_repeat("\t", (count($openers) + 1)); + echo "=> Found unowned attribute closer at $i for $opener".PHP_EOL; + } + }//end if }//end if /* @@ -1111,6 +1135,13 @@ private function recurseScopeMap($stackPtr, $depth=1, &$ignore=0) continue; } + if ($tokenType === T_NAMESPACE) { + // PHP namespace keywords are special because they can be + // used as blocks but also inline as operators. + // So if we find them nested inside another opener, just skip them. + continue; + } + if ($tokenType === T_FUNCTION && $this->tokens[$stackPtr]['code'] !== T_FUNCTION ) { @@ -1291,11 +1322,12 @@ private function recurseScopeMap($stackPtr, $depth=1, &$ignore=0) // a new statement, it isn't a scope opener. $disallowed = Util\Tokens::$assignmentTokens; $disallowed += [ - T_DOLLAR => true, - T_VARIABLE => true, - T_OBJECT_OPERATOR => true, - T_COMMA => true, - T_OPEN_PARENTHESIS => true, + T_DOLLAR => true, + T_VARIABLE => true, + T_OBJECT_OPERATOR => true, + T_NULLSAFE_OBJECT_OPERATOR => true, + T_COMMA => true, + T_OPEN_PARENTHESIS => true, ]; if (isset($disallowed[$this->tokens[$x]['code']]) === true) { diff --git a/src/Util/Cache.php b/src/Util/Cache.php index b7819c91eb..68abef59c4 100644 --- a/src/Util/Cache.php +++ b/src/Util/Cache.php @@ -95,22 +95,25 @@ public static function load(Ruleset $ruleset, Config $config) // hash. This ensures that core PHPCS changes will also invalidate the cache. // Note that we ignore sniffs here, and any files that don't affect // the outcome of the run. - $di = new \RecursiveDirectoryIterator($installDir); + $di = new \RecursiveDirectoryIterator( + $installDir, + (\FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::SKIP_DOTS) + ); $filter = new \RecursiveCallbackFilterIterator( $di, function ($file, $key, $iterator) { - // Skip hidden files. + // Skip non-php files. $filename = $file->getFilename(); - if (substr($filename, 0, 1) === '.') { + if ($file->isFile() === true && substr($filename, -4) !== '.php') { return false; } - $filePath = Common::realpath($file->getPathname()); + $filePath = Common::realpath($key); if ($filePath === false) { return false; } - if (is_dir($filePath) === true + if ($iterator->hasChildren() === true && ($filename === 'Standards' || $filename === 'Exceptions' || $filename === 'Reports' diff --git a/src/Util/Common.php b/src/Util/Common.php index f939fc76de..ce7967cc33 100644 --- a/src/Util/Common.php +++ b/src/Util/Common.php @@ -48,6 +48,34 @@ public static function isPharFile($path) }//end isPharFile() + /** + * Checks if a file is readable. + * + * Addresses PHP bug related to reading files from network drives on Windows. + * e.g. when using WSL2. + * + * @param string $path The path to the file. + * + * @return boolean + */ + public static function isReadable($path) + { + if (@is_readable($path) === true) { + return true; + } + + if (@file_exists($path) === true && @is_file($path) === true) { + $f = @fopen($path, 'rb'); + if (fclose($f) === true) { + return true; + } + } + + return false; + + }//end isReadable() + + /** * CodeSniffer alternative for realpath. * @@ -211,6 +239,28 @@ public static function isStdinATTY() }//end isStdinATTY() + /** + * Escape a path to a system command. + * + * @param string $cmd The path to the system command. + * + * @return string + */ + public static function escapeshellcmd($cmd) + { + $cmd = escapeshellcmd($cmd); + + if (stripos(PHP_OS, 'WIN') === 0) { + // Spaces are not escaped by escapeshellcmd on Windows, but need to be + // for the command to be able to execute. + $cmd = preg_replace('`(?= 48 && $ascii <= 57) { - // The character is a number, so it cant be a capital. + // The character is a number, so it can't be a capital. $isCaps = false; } else { if (strtoupper($string[$i]) === $string[$i]) { diff --git a/src/Util/Standards.php b/src/Util/Standards.php index 50f58f0294..65e5d6cb3c 100644 --- a/src/Util/Standards.php +++ b/src/Util/Standards.php @@ -180,7 +180,8 @@ public static function getInstalledStandards( // Check if the installed dir is actually a standard itself. $csFile = $standardsDir.'/ruleset.xml'; if (is_file($csFile) === true) { - $installedStandards[] = basename($standardsDir); + $basename = basename($standardsDir); + $installedStandards[$basename] = $basename; continue; } @@ -190,6 +191,7 @@ public static function getInstalledStandards( } $di = new \DirectoryIterator($standardsDir); + $standardsInDir = []; foreach ($di as $file) { if ($file->isDir() === true && $file->isDot() === false) { $filename = $file->getFilename(); @@ -202,10 +204,13 @@ public static function getInstalledStandards( // Valid coding standard dirs include a ruleset. $csFile = $file->getPathname().'/ruleset.xml'; if (is_file($csFile) === true) { - $installedStandards[] = $filename; + $standardsInDir[$filename] = $filename; } } } + + natsort($standardsInDir); + $installedStandards += $standardsInDir; }//end foreach return $installedStandards; diff --git a/src/Util/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 d11b193629..bb1fb2ca99 100644 --- a/src/Util/Tokens.php +++ b/src/Util/Tokens.php @@ -74,7 +74,14 @@ define('T_CLOSE_USE_GROUP', 'PHPCS_T_CLOSE_USE_GROUP'); define('T_ZSR', 'PHPCS_T_ZSR'); define('T_ZSR_EQUAL', 'PHPCS_T_ZSR_EQUAL'); -define('T_FN_ARROW', 'T_FN_ARROW'); +define('T_FN_ARROW', 'PHPCS_T_FN_ARROW'); +define('T_TYPE_UNION', 'PHPCS_T_TYPE_UNION'); +define('T_PARAM_NAME', 'PHPCS_T_PARAM_NAME'); +define('T_MATCH_ARROW', 'PHPCS_T_MATCH_ARROW'); +define('T_MATCH_DEFAULT', 'PHPCS_T_MATCH_DEFAULT'); +define('T_ATTRIBUTE_END', 'PHPCS_T_ATTRIBUTE_END'); +define('T_ENUM_CASE', 'PHPCS_T_ENUM_CASE'); +define('T_TYPE_INTERSECTION', 'PHPCS_T_TYPE_INTERSECTION'); // Some PHP 5.5 tokens, replicated for lower versions. if (defined('T_FINALLY') === false) { @@ -124,6 +131,48 @@ define('T_FN', 'PHPCS_T_FN'); } +// Some PHP 8.0 tokens, replicated for lower versions. +if (defined('T_NULLSAFE_OBJECT_OPERATOR') === false) { + define('T_NULLSAFE_OBJECT_OPERATOR', 'PHPCS_T_NULLSAFE_OBJECT_OPERATOR'); +} + +if (defined('T_NAME_QUALIFIED') === false) { + define('T_NAME_QUALIFIED', 'PHPCS_T_NAME_QUALIFIED'); +} + +if (defined('T_NAME_FULLY_QUALIFIED') === false) { + define('T_NAME_FULLY_QUALIFIED', 'PHPCS_T_NAME_FULLY_QUALIFIED'); +} + +if (defined('T_NAME_RELATIVE') === false) { + define('T_NAME_RELATIVE', 'PHPCS_T_NAME_RELATIVE'); +} + +if (defined('T_MATCH') === false) { + define('T_MATCH', 'PHPCS_T_MATCH'); +} + +if (defined('T_ATTRIBUTE') === false) { + 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'); +} + +if (defined('T_ENUM') === false) { + define('T_ENUM', 'PHPCS_T_ENUM'); +} + // Tokens used for parsing doc blocks. define('T_DOC_COMMENT_STAR', 'PHPCS_T_DOC_COMMENT_STAR'); define('T_DOC_COMMENT_WHITESPACE', 'PHPCS_T_DOC_COMMENT_WHITESPACE'); @@ -151,6 +200,7 @@ final class Tokens T_CLASS => 1000, T_INTERFACE => 1000, T_TRAIT => 1000, + T_ENUM => 1000, T_NAMESPACE => 1000, T_FUNCTION => 100, T_CLOSURE => 100, @@ -170,6 +220,7 @@ final class Tokens T_CATCH => 50, T_FINALLY => 50, T_SWITCH => 50, + T_MATCH => 50, T_SELF => 25, T_PARENT => 25, @@ -362,6 +413,7 @@ final class Tokens T_ELSEIF => T_ELSEIF, T_CATCH => T_CATCH, T_DECLARE => T_DECLARE, + T_MATCH => T_MATCH, ]; /** @@ -374,6 +426,7 @@ final class Tokens T_ANON_CLASS => T_ANON_CLASS, T_INTERFACE => T_INTERFACE, T_TRAIT => T_TRAIT, + T_ENUM => T_ENUM, T_NAMESPACE => T_NAMESPACE, T_FUNCTION => T_FUNCTION, T_CLOSURE => T_CLOSURE, @@ -394,6 +447,7 @@ final class Tokens T_PROPERTY => T_PROPERTY, T_OBJECT => T_OBJECT, T_USE => T_USE, + T_MATCH => T_MATCH, ]; /** @@ -574,6 +628,7 @@ final class Tokens T_UNSET => T_UNSET, T_EMPTY => T_EMPTY, T_SELF => T_SELF, + T_PARENT => T_PARENT, T_STATIC => T_STATIC, ]; @@ -587,6 +642,105 @@ final class Tokens T_ANON_CLASS => T_ANON_CLASS, T_INTERFACE => T_INTERFACE, T_TRAIT => T_TRAIT, + T_ENUM => T_ENUM, + ]; + + /** + * Tokens representing PHP magic constants. + * + * @var array => + * + * @link https://www.php.net/language.constants.predefined PHP Manual on magic constants + */ + public static $magicConstants = [ + T_CLASS_C => T_CLASS_C, + T_DIR => T_DIR, + T_FILE => T_FILE, + T_FUNC_C => T_FUNC_C, + T_LINE => T_LINE, + T_METHOD_C => T_METHOD_C, + T_NS_C => T_NS_C, + T_TRAIT_C => T_TRAIT_C, + ]; + + /** + * Tokens representing context sensitive keywords in PHP. + * + * @var array + * + * https://wiki.php.net/rfc/context_sensitive_lexer + */ + public static $contextSensitiveKeywords = [ + T_ABSTRACT => T_ABSTRACT, + T_ARRAY => T_ARRAY, + T_AS => T_AS, + T_BREAK => T_BREAK, + T_CALLABLE => T_CALLABLE, + T_CASE => T_CASE, + T_CATCH => T_CATCH, + T_CLASS => T_CLASS, + T_CLONE => T_CLONE, + T_CONST => T_CONST, + T_CONTINUE => T_CONTINUE, + T_DECLARE => T_DECLARE, + T_DEFAULT => T_DEFAULT, + T_DO => T_DO, + T_ECHO => T_ECHO, + T_ELSE => T_ELSE, + T_ELSEIF => T_ELSEIF, + T_EMPTY => T_EMPTY, + T_ENDDECLARE => T_ENDDECLARE, + T_ENDFOR => T_ENDFOR, + T_ENDFOREACH => T_ENDFOREACH, + T_ENDIF => T_ENDIF, + T_ENDSWITCH => T_ENDSWITCH, + T_ENDWHILE => T_ENDWHILE, + T_ENUM => T_ENUM, + T_EVAL => T_EVAL, + T_EXIT => T_EXIT, + T_EXTENDS => T_EXTENDS, + T_FINAL => T_FINAL, + T_FINALLY => T_FINALLY, + T_FN => T_FN, + T_FOR => T_FOR, + T_FOREACH => T_FOREACH, + T_FUNCTION => T_FUNCTION, + T_GLOBAL => T_GLOBAL, + T_GOTO => T_GOTO, + T_IF => T_IF, + T_IMPLEMENTS => T_IMPLEMENTS, + T_INCLUDE => T_INCLUDE, + T_INCLUDE_ONCE => T_INCLUDE_ONCE, + T_INSTANCEOF => T_INSTANCEOF, + T_INSTEADOF => T_INSTEADOF, + T_INTERFACE => T_INTERFACE, + T_ISSET => T_ISSET, + T_LIST => T_LIST, + T_LOGICAL_AND => T_LOGICAL_AND, + T_LOGICAL_OR => T_LOGICAL_OR, + T_LOGICAL_XOR => T_LOGICAL_XOR, + T_MATCH => T_MATCH, + T_NAMESPACE => T_NAMESPACE, + T_NEW => T_NEW, + T_PRINT => T_PRINT, + T_PRIVATE => T_PRIVATE, + T_PROTECTED => T_PROTECTED, + T_PUBLIC => T_PUBLIC, + T_READONLY => T_READONLY, + T_REQUIRE => T_REQUIRE, + T_REQUIRE_ONCE => T_REQUIRE_ONCE, + T_RETURN => T_RETURN, + T_STATIC => T_STATIC, + T_SWITCH => T_SWITCH, + T_THROW => T_THROW, + T_TRAIT => T_TRAIT, + T_TRY => T_TRY, + T_UNSET => T_UNSET, + T_USE => T_USE, + T_VAR => T_VAR, + T_WHILE => T_WHILE, + T_YIELD => T_YIELD, + T_YIELD_FROM => T_YIELD_FROM, ]; diff --git a/tests/AllTests.php b/tests/AllTests.php index 4ed001dfed..9d099c1e3d 100644 --- a/tests/AllTests.php +++ b/tests/AllTests.php @@ -9,16 +9,13 @@ namespace PHP_CodeSniffer\Tests; -$GLOBALS['PHP_CODESNIFFER_PEAR'] = false; - -if (is_file(__DIR__.'/../autoload.php') === true) { +if ($GLOBALS['PHP_CODESNIFFER_PEAR'] === false) { include_once 'Core/AllTests.php'; include_once 'Standards/AllSniffs.php'; } else { include_once 'CodeSniffer/Core/AllTests.php'; include_once 'CodeSniffer/Standards/AllSniffs.php'; include_once 'FileList.php'; - $GLOBALS['PHP_CODESNIFFER_PEAR'] = true; } // PHPUnit 7 made the TestSuite run() method incompatible with diff --git a/tests/Core/Autoloader/DetermineLoadedClassTest.php b/tests/Core/Autoloader/DetermineLoadedClassTest.php new file mode 100644 index 0000000000..c0f38fa6f1 --- /dev/null +++ b/tests/Core/Autoloader/DetermineLoadedClassTest.php @@ -0,0 +1,119 @@ + + * @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\Tests\Core\Autoloader; + +use PHP_CodeSniffer\Autoload; +use PHPUnit\Framework\TestCase; + +class DetermineLoadedClassTest extends TestCase +{ + + + /** + * Load the test files. + * + * @return void + */ + public static function setUpBeforeClass() + { + include __DIR__.'/TestFiles/Sub/C.inc'; + + }//end setUpBeforeClass() + + + /** + * Test for when class list is ordered. + * + * @return void + */ + public function testOrdered() + { + $classesBeforeLoad = [ + 'classes' => [], + 'interfaces' => [], + 'traits' => [], + ]; + + $classesAfterLoad = [ + 'classes' => [ + 'PHP_CodeSniffer\Tests\Core\Autoloader\A', + 'PHP_CodeSniffer\Tests\Core\Autoloader\B', + 'PHP_CodeSniffer\Tests\Core\Autoloader\C', + 'PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C', + ], + 'interfaces' => [], + 'traits' => [], + ]; + + $className = Autoload::determineLoadedClass($classesBeforeLoad, $classesAfterLoad); + $this->assertEquals('PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C', $className); + + }//end testOrdered() + + + /** + * Test for when class list is out of order. + * + * @return void + */ + public function testUnordered() + { + $classesBeforeLoad = [ + 'classes' => [], + 'interfaces' => [], + 'traits' => [], + ]; + + $classesAfterLoad = [ + 'classes' => [ + 'PHP_CodeSniffer\Tests\Core\Autoloader\A', + 'PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C', + 'PHP_CodeSniffer\Tests\Core\Autoloader\C', + 'PHP_CodeSniffer\Tests\Core\Autoloader\B', + ], + 'interfaces' => [], + 'traits' => [], + ]; + + $className = Autoload::determineLoadedClass($classesBeforeLoad, $classesAfterLoad); + $this->assertEquals('PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C', $className); + + $classesAfterLoad = [ + 'classes' => [ + 'PHP_CodeSniffer\Tests\Core\Autoloader\A', + 'PHP_CodeSniffer\Tests\Core\Autoloader\C', + 'PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C', + 'PHP_CodeSniffer\Tests\Core\Autoloader\B', + ], + 'interfaces' => [], + 'traits' => [], + ]; + + $className = Autoload::determineLoadedClass($classesBeforeLoad, $classesAfterLoad); + $this->assertEquals('PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C', $className); + + $classesAfterLoad = [ + 'classes' => [ + 'PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C', + 'PHP_CodeSniffer\Tests\Core\Autoloader\A', + 'PHP_CodeSniffer\Tests\Core\Autoloader\C', + 'PHP_CodeSniffer\Tests\Core\Autoloader\B', + ], + 'interfaces' => [], + 'traits' => [], + ]; + + $className = Autoload::determineLoadedClass($classesBeforeLoad, $classesAfterLoad); + $this->assertEquals('PHP_CodeSniffer\Tests\Core\Autoloader\Sub\C', $className); + + }//end testUnordered() + + +}//end class diff --git a/tests/Core/Autoloader/TestFiles/A.inc b/tests/Core/Autoloader/TestFiles/A.inc new file mode 100644 index 0000000000..c1433718bb --- /dev/null +++ b/tests/Core/Autoloader/TestFiles/A.inc @@ -0,0 +1,3 @@ + + * @copyright 2006-2023 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\Config; + +use PHP_CodeSniffer\Config; +use PHPUnit\Framework\TestCase; +use ReflectionProperty; + +class ReportWidthTest extends TestCase +{ + + + /** + * Set static properties in the Config class to prevent tests influencing each other. + * + * @before + * + * @return void + */ + public static function cleanConfig() + { + // Set to the property's default value to clear out potentially set values from other tests. + self::setStaticProperty('executablePaths', []); + + // Set to a usable value to circumvent Config trying to find a phpcs.xml config file. + self::setStaticProperty('overriddenDefaults', ['standards' => ['PSR1']]); + + // Set to values which prevent the test-runner user's `CodeSniffer.conf` file + // from being read and influencing the tests. + self::setStaticProperty('configData', []); + self::setStaticProperty('configDataFile', ''); + + }//end cleanConfig() + + + /** + * Clean up after each finished test. + * + * @after + * + * @return void + */ + public function resetConfig() + { + $_SERVER['argv'] = []; + + }//end resetConfig() + + + /** + * Reset the static properties in the Config class to their true defaults to prevent this class + * from unfluencing other tests. + * + * @afterClass + * + * @return void + */ + public static function resetConfigToDefaults() + { + self::setStaticProperty('overriddenDefaults', []); + self::setStaticProperty('executablePaths', []); + self::setStaticProperty('configData', null); + self::setStaticProperty('configDataFile', null); + $_SERVER['argv'] = []; + + }//end resetConfigToDefaults() + + + /** + * Test that report width without overrules will always be set to a non-0 positive integer. + * + * @return void + */ + public function testReportWidthDefault() + { + $config = new Config(); + + // Can't test the exact value as "auto" will resolve differently depending on the machine running the tests. + $this->assertTrue(is_int($config->reportWidth), 'Report width is not an integer'); + $this->assertGreaterThan(0, $config->reportWidth, 'Report width is not greater than 0'); + + }//end testReportWidthDefault() + + + /** + * Test that the report width will be set to a non-0 positive integer when not found in the CodeSniffer.conf file. + * + * @return void + */ + public function testReportWidthWillBeSetFromAutoWhenNotFoundInConfFile() + { + $phpCodeSnifferConfig = [ + 'default_standard' => 'PSR2', + 'show_warnings' => '0', + ]; + + $this->setStaticProperty('configData', $phpCodeSnifferConfig); + + $config = new Config(); + + // Can't test the exact value as "auto" will resolve differently depending on the machine running the tests. + $this->assertTrue(is_int($config->reportWidth), 'Report width is not an integer'); + $this->assertGreaterThan(0, $config->reportWidth, 'Report width is not greater than 0'); + + }//end testReportWidthWillBeSetFromAutoWhenNotFoundInConfFile() + + + /** + * Test that the report width will be set correctly when found in the CodeSniffer.conf file. + * + * @return void + */ + public function testReportWidthCanBeSetFromConfFile() + { + $phpCodeSnifferConfig = [ + 'default_standard' => 'PSR2', + 'report_width' => '120', + ]; + + $this->setStaticProperty('configData', $phpCodeSnifferConfig); + + $config = new Config(); + $this->assertSame(120, $config->reportWidth); + + }//end testReportWidthCanBeSetFromConfFile() + + + /** + * Test that the report width will be set correctly when passed as a CLI argument. + * + * @return void + */ + public function testReportWidthCanBeSetFromCLI() + { + $_SERVER['argv'] = [ + 'phpcs', + '--report-width=100', + ]; + + $config = new Config(); + $this->assertSame(100, $config->reportWidth); + + }//end testReportWidthCanBeSetFromCLI() + + + /** + * Test that the report width will be set correctly when multiple report widths are passed on the CLI. + * + * @return void + */ + public function testReportWidthWhenSetFromCLIFirstValuePrevails() + { + $_SERVER['argv'] = [ + 'phpcs', + '--report-width=100', + '--report-width=200', + ]; + + $config = new Config(); + $this->assertSame(100, $config->reportWidth); + + }//end testReportWidthWhenSetFromCLIFirstValuePrevails() + + + /** + * Test that a report width passed as a CLI argument will overrule a report width set in a CodeSniffer.conf file. + * + * @return void + */ + public function testReportWidthSetFromCLIOverrulesConfFile() + { + $phpCodeSnifferConfig = [ + 'default_standard' => 'PSR2', + 'report_format' => 'summary', + 'show_warnings' => '0', + 'show_progress' => '1', + 'report_width' => '120', + ]; + + $this->setStaticProperty('configData', $phpCodeSnifferConfig); + + $cliArgs = [ + 'phpcs', + '--report-width=180', + ]; + + $config = new Config($cliArgs); + $this->assertSame(180, $config->reportWidth); + + }//end testReportWidthSetFromCLIOverrulesConfFile() + + + /** + * Test that the report width will be set to a non-0 positive integer when set to "auto". + * + * @return void + */ + public function testReportWidthInputHandlingForAuto() + { + $config = new Config(); + $config->reportWidth = 'auto'; + + // Can't test the exact value as "auto" will resolve differently depending on the machine running the tests. + $this->assertTrue(is_int($config->reportWidth), 'Report width is not an integer'); + $this->assertGreaterThan(0, $config->reportWidth, 'Report width is not greater than 0'); + + }//end testReportWidthInputHandlingForAuto() + + + /** + * Test that the report width will be set correctly for various types of input. + * + * @param mixed $input Input value received. + * @param int $expected Expected report width. + * + * @dataProvider dataReportWidthInputHandling + * + * @return void + */ + public function testReportWidthInputHandling($input, $expected) + { + $config = new Config(); + $config->reportWidth = $input; + + $this->assertSame($expected, $config->reportWidth); + + }//end testReportWidthInputHandling() + + + /** + * Data provider. + * + * @return array + */ + public function dataReportWidthInputHandling() + { + return [ + 'No value (empty string)' => [ + 'value' => '', + 'expected' => Config::DEFAULT_REPORT_WIDTH, + ], + 'Value: invalid input type null' => [ + 'value' => null, + 'expected' => Config::DEFAULT_REPORT_WIDTH, + ], + 'Value: invalid input type false' => [ + 'value' => false, + 'expected' => Config::DEFAULT_REPORT_WIDTH, + ], + 'Value: invalid input type float' => [ + 'value' => 100.50, + 'expected' => Config::DEFAULT_REPORT_WIDTH, + ], + 'Value: invalid string value "invalid"' => [ + 'value' => 'invalid', + 'expected' => Config::DEFAULT_REPORT_WIDTH, + ], + 'Value: invalid string value, non-integer string "50.25"' => [ + 'value' => '50.25', + 'expected' => Config::DEFAULT_REPORT_WIDTH, + ], + 'Value: valid numeric string value' => [ + 'value' => '250', + 'expected' => 250, + ], + 'Value: valid int value' => [ + 'value' => 220, + 'expected' => 220, + ], + 'Value: negative int value becomes positive int' => [ + 'value' => -180, + 'expected' => 180, + ], + ]; + + }//end dataReportWidthInputHandling() + + + /** + * Helper function to set a static property on the Config class. + * + * @param string $name The name of the property to set. + * @param mixed $value The value to set the propert to. + * + * @return void + */ + public static function setStaticProperty($name, $value) + { + $property = new ReflectionProperty('PHP_CodeSniffer\Config', $name); + $property->setAccessible(true); + $property->setValue($value); + $property->setAccessible(false); + + }//end setStaticProperty() + + +}//end class diff --git a/tests/Core/ErrorSuppressionTest.php b/tests/Core/ErrorSuppressionTest.php index f031b334bb..7181613c62 100644 --- a/tests/Core/ErrorSuppressionTest.php +++ b/tests/Core/ErrorSuppressionTest.php @@ -21,1245 +21,1227 @@ class ErrorSuppressionTest extends TestCase /** * Test suppressing a single error. * + * @param string $before Annotation to place before the code. + * @param string $after Annotation to place after the code. + * @param int $expectedErrors Optional. Number of errors expected. + * Defaults to 0. + * + * @dataProvider dataSuppressError + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap + * * @return void */ - public function testSuppressError() + public function testSuppressError($before, $after, $expectedErrors=0) { - $config = new Config(); - $config->standards = ['Generic']; - $config->sniffs = ['Generic.PHP.LowerCaseConstant']; + static $config, $ruleset; - $ruleset = new Ruleset($config); + if (isset($config, $ruleset) === false) { + $config = new Config(); + $config->standards = ['Generic']; + $config->sniffs = ['Generic.PHP.LowerCaseConstant']; - // Process without suppression. - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); + $ruleset = new Ruleset($config); + } - // Process with inline comment suppression. - $content = 'process(); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); + $this->assertSame($expectedErrors, $file->getErrorCount()); + $this->assertCount($expectedErrors, $file->getErrors()); - // Process with multi-line inline comment suppression, tab-indented. - $content = 'process(); + }//end testSuppressError() - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - // Process with inline @ comment suppression. - $content = 'process(); + /** + * Data provider. + * + * @see testSuppressError() + * + * @return array + */ + public function dataSuppressError() + { + return [ + 'no suppression' => [ + 'before' => '', + 'after' => '', + 'expectedErrors' => 1, + ], + + // Inline slash comments. + 'disable/enable: slash comment' => [ + 'before' => '// phpcs:disable'.PHP_EOL, + 'after' => '// phpcs:enable', + ], + 'disable/enable: multi-line slash comment, tab indented' => [ + 'before' => "\t".'// For reasons'.PHP_EOL."\t".'// phpcs:disable'.PHP_EOL."\t", + 'after' => "\t".'// phpcs:enable', + ], + 'disable/enable: slash comment, with @' => [ + 'before' => '// @phpcs:disable'.PHP_EOL, + 'after' => '// @phpcs:enable', + ], + 'disable/enable: slash comment, mixed case' => [ + 'before' => '// PHPCS:Disable'.PHP_EOL, + 'after' => '// pHPcs:enabLE', + ], + + // Inline hash comments. + 'disable/enable: hash comment' => [ + 'before' => '# phpcs:disable'.PHP_EOL, + 'after' => '# phpcs:enable', + ], + 'disable/enable: multi-line hash comment, tab indented' => [ + 'before' => "\t".'# For reasons'.PHP_EOL."\t".'# phpcs:disable'.PHP_EOL."\t", + 'after' => "\t".'# phpcs:enable', + ], + 'disable/enable: hash comment, with @' => [ + 'before' => '# @phpcs:disable'.PHP_EOL, + 'after' => '# @phpcs:enable', + ], + 'disable/enable: hash comment, mixed case' => [ + 'before' => '# PHPCS:Disable'.PHP_EOL, + 'after' => '# pHPcs:enabLE', + ], + + // Inline star (block) comments. + 'disable/enable: star comment' => [ + 'before' => '/* phpcs:disable */'.PHP_EOL, + 'after' => '/* phpcs:enable */', + ], + 'disable/enable: multi-line star comment' => [ + 'before' => '/*'.PHP_EOL.' phpcs:disable'.PHP_EOL.' */'.PHP_EOL, + 'after' => '/*'.PHP_EOL.' phpcs:enable'.PHP_EOL.' */', + ], + 'disable/enable: multi-line star comment, each line starred' => [ + 'before' => '/*'.PHP_EOL.' * phpcs:disable'.PHP_EOL.' */'.PHP_EOL, + 'after' => '/*'.PHP_EOL.' * phpcs:enable'.PHP_EOL.' */', + ], + 'disable/enable: multi-line star comment, each line starred, tab indented' => [ + 'before' => "\t".'/*'.PHP_EOL."\t".' * phpcs:disable'.PHP_EOL."\t".' */'.PHP_EOL."\t", + 'after' => "\t".'/*'.PHP_EOL.' * phpcs:enable'.PHP_EOL.' */', + ], + + // Docblock comments. + 'disable/enable: single line docblock comment' => [ + 'before' => '/** phpcs:disable */'.PHP_EOL, + 'after' => '/** phpcs:enable */', + ], + + // Deprecated syntax. + 'old style: slash comment' => [ + 'before' => '// @codingStandardsIgnoreStart'.PHP_EOL, + 'after' => '// @codingStandardsIgnoreEnd', + ], + 'old style: star comment' => [ + 'before' => '/* @codingStandardsIgnoreStart */'.PHP_EOL, + 'after' => '/* @codingStandardsIgnoreEnd */', + ], + 'old style: multi-line star comment' => [ + 'before' => '/*'.PHP_EOL.' @codingStandardsIgnoreStart'.PHP_EOL.' */'.PHP_EOL, + 'after' => '/*'.PHP_EOL.' @codingStandardsIgnoreEnd'.PHP_EOL.' */', + ], + 'old style: single line docblock comment' => [ + 'before' => '/** @codingStandardsIgnoreStart */'.PHP_EOL, + 'after' => '/** @codingStandardsIgnoreEnd */', + ], + ]; - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); + }//end dataSuppressError() - // Process with inline comment suppression mixed case. - $content = 'process(); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); + /** + * Test suppressing 1 out of 2 errors. + * + * @param string $before Annotation to place before the code. + * @param string $between Annotation to place between the code. + * @param int $expectedErrors Optional. Number of errors expected. + * Defaults to 1. + * + * @dataProvider dataSuppressSomeErrors + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap + * + * @return void + */ + public function testSuppressSomeErrors($before, $between, $expectedErrors=1) + { + static $config, $ruleset; - // Process with inline comment suppression (deprecated syntax). - $content = 'process(); + if (isset($config, $ruleset) === false) { + $config = new Config(); + $config->standards = ['Generic']; + $config->sniffs = ['Generic.PHP.LowerCaseConstant']; - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); + $ruleset = new Ruleset($config); + } - // Process with block comment suppression. - $content = 'process(); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); + $this->assertSame($expectedErrors, $file->getErrorCount()); + $this->assertCount($expectedErrors, $file->getErrors()); - // Process with multi-line block comment suppression. - $content = 'process(); + }//end testSuppressSomeErrors() - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - // Process with multi-line block comment suppression, each line starred. - $content = 'process(); + /** + * Data provider. + * + * @see testSuppressSomeErrors() + * + * @return array + */ + public function dataSuppressSomeErrors() + { + return [ + 'no suppression' => [ + 'before' => '', + 'between' => '', + 'expectedErrors' => 2, + ], + + // With suppression. + 'disable/enable: slash comment' => [ + 'before' => '// phpcs:disable', + 'between' => '// phpcs:enable', + ], + 'disable/enable: slash comment, with @' => [ + 'before' => '// @phpcs:disable', + 'between' => '// @phpcs:enable', + ], + 'disable/enable: hash comment' => [ + 'before' => '# phpcs:disable', + 'between' => '# phpcs:enable', + ], + 'disable/enable: hash comment, with @' => [ + 'before' => '# @phpcs:disable', + 'between' => '# @phpcs:enable', + ], + 'disable/enable: single line docblock comment' => [ + 'before' => '/** phpcs:disable */', + 'between' => '/** phpcs:enable */', + ], + + // Deprecated syntax. + 'old style: slash comment' => [ + 'before' => '// @codingStandardsIgnoreStart', + 'between' => '// @codingStandardsIgnoreEnd', + ], + 'old style: single line docblock comment' => [ + 'before' => '/** @codingStandardsIgnoreStart */', + 'between' => '/** @codingStandardsIgnoreEnd */', + ], + ]; - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); + }//end dataSuppressSomeErrors() - // Process with multi-line block comment suppression, tab-indented. - $content = 'process(); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); + /** + * Test suppressing a single warning. + * + * @param string $before Annotation to place before the code. + * @param string $after Annotation to place after the code. + * @param int $expectedWarnings Optional. Number of warnings expected. + * Defaults to 0. + * + * @dataProvider dataSuppressWarning + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap + * + * @return void + */ + public function testSuppressWarning($before, $after, $expectedWarnings=0) + { + static $config, $ruleset; - // Process with block comment suppression (deprecated syntax). - $content = 'process(); + if (isset($config, $ruleset) === false) { + $config = new Config(); + $config->standards = ['Generic']; + $config->sniffs = ['Generic.Commenting.Todo']; - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); + $ruleset = new Ruleset($config); + } - // Process with multi-line block comment suppression (deprecated syntax). - $content = 'process(); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); + $this->assertSame($expectedWarnings, $file->getWarningCount()); + $this->assertCount($expectedWarnings, $file->getWarnings()); - // Process with a docblock suppression. - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); + }//end testSuppressWarning() - // Process with a docblock suppression (deprecated syntax). - $content = 'process(); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); + /** + * Data provider. + * + * @see testSuppressWarning() + * + * @return array + */ + public function dataSuppressWarning() + { + return [ + 'no suppression' => [ + 'before' => '', + 'after' => '', + 'expectedWarnings' => 1, + ], + + // With suppression. + 'disable/enable: slash comment' => [ + 'before' => '// phpcs:disable', + 'after' => '// phpcs:enable', + ], + 'disable/enable: slash comment, with @' => [ + 'before' => '// @phpcs:disable', + 'after' => '// @phpcs:enable', + ], + 'disable/enable: single line docblock comment' => [ + 'before' => '/** phpcs:disable */', + 'after' => '/** phpcs:enable */', + ], + + // Deprecated syntax. + 'old style: slash comment' => [ + 'before' => '// @codingStandardsIgnoreStart', + 'after' => '// @codingStandardsIgnoreEnd', + ], + 'old style: single line docblock comment' => [ + 'before' => '/** @codingStandardsIgnoreStart */', + 'after' => '/** @codingStandardsIgnoreEnd */', + ], + ]; - }//end testSuppressError() + }//end dataSuppressWarning() /** - * Test suppressing 1 out of 2 errors. + * Test suppressing a single error using a single line ignore. + * + * @param string $before Annotation to place before the code. + * @param string $after Optional. Annotation to place after the code. + * Defaults to an empty string. + * @param int $expectedErrors Optional. Number of errors expected. + * Defaults to 1. + * + * @dataProvider dataSuppressLine + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap * * @return void */ - public function testSuppressSomeErrors() + public function testSuppressLine($before, $after='', $expectedErrors=1) { - $config = new Config(); - $config->standards = ['Generic']; - $config->sniffs = ['Generic.PHP.LowerCaseConstant']; + static $config, $ruleset; - $ruleset = new Ruleset($config); + if (isset($config, $ruleset) === false) { + $config = new Config(); + $config->standards = ['Generic']; + $config->sniffs = ['Generic.PHP.LowerCaseConstant']; - // Process without suppression. - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(2, $numErrors); - $this->assertCount(2, $errors); - - // Process with suppression. - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); + $ruleset = new Ruleset($config); + } - // Process with @ suppression. - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - - // Process with suppression (deprecated syntax). - $content = 'process(); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); + $this->assertSame($expectedErrors, $file->getErrorCount()); + $this->assertCount($expectedErrors, $file->getErrors()); - // Process with a PHPDoc block suppression. - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); + }//end testSuppressLine() - // Process with a PHPDoc block suppression (deprecated syntax). - $content = 'process(); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); + /** + * Data provider. + * + * @see testSuppressLine() + * + * @return array + */ + public function dataSuppressLine() + { + return [ + 'no suppression' => [ + 'before' => '', + 'after' => '', + 'expectedErrors' => 2, + ], + + // With suppression on line before. + 'ignore: line before, slash comment' => ['before' => '// phpcs:ignore'], + 'ignore: line before, slash comment, with @' => ['before' => '// @phpcs:ignore'], + 'ignore: line before, hash comment' => ['before' => '# phpcs:ignore'], + 'ignore: line before, hash comment, with @' => ['before' => '# @phpcs:ignore'], + 'ignore: line before, star comment' => ['before' => '/* phpcs:ignore */'], + 'ignore: line before, star comment, with @' => ['before' => '/* @phpcs:ignore */'], + + // With suppression as trailing comment on code line. + 'ignore: end of line, slash comment' => [ + 'before' => '', + 'after' => ' // phpcs:ignore', + ], + 'ignore: end of line, slash comment, with @' => [ + 'before' => '', + 'after' => ' // @phpcs:ignore', + ], + 'ignore: end of line, hash comment' => [ + 'before' => '', + 'after' => ' # phpcs:ignore', + ], + 'ignore: end of line, hash comment, with @' => [ + 'before' => '', + 'after' => ' # @phpcs:ignore', + ], + + // Deprecated syntax. + 'old style: line before, slash comment' => ['before' => '// @codingStandardsIgnoreLine'], + 'old style: end of line, slash comment' => [ + 'before' => '', + 'after' => ' // @codingStandardsIgnoreLine', + ], + ]; - }//end testSuppressSomeErrors() + }//end dataSuppressLine() /** - * Test suppressing a single warning. + * Test suppressing a single error using a single line ignore in the middle of a line. + * + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap * * @return void */ - public function testSuppressWarning() + public function testSuppressLineMidLine() { $config = new Config(); $config->standards = ['Generic']; - $config->sniffs = ['Generic.Commenting.Todo']; + $config->sniffs = ['Generic.PHP.LowerCaseConstant']; $ruleset = new Ruleset($config); - // Process without suppression. - $content = 'process(); - - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(1, $numWarnings); - $this->assertCount(1, $warnings); - - // Process with suppression. - $content = 'process(); - - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - // Process with @ suppression. - $content = 'process(); - - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - // Process with suppression (deprecated syntax). - $content = 'process(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); + $this->assertSame(0, $file->getErrorCount()); + $this->assertCount(0, $file->getErrors()); - // Process with a docblock suppression. - $content = 'process(); - - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - // Process with a docblock suppression (deprecated syntax). - $content = 'process(); - - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - }//end testSuppressWarning() + }//end testSuppressLineMidLine() /** - * Test suppressing a single error using a single line ignore. + * Test suppressing a single error using a single line ignore within a docblock. + * + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap * * @return void */ - public function testSuppressLine() + public function testSuppressLineWithinDocblock() { $config = new Config(); $config->standards = ['Generic']; - $config->sniffs = [ - 'Generic.PHP.LowerCaseConstant', - 'Generic.Files.LineLength', - ]; + $config->sniffs = ['Generic.Files.LineLength']; $ruleset = new Ruleset($config); - // Process without suppression. - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(2, $numErrors); - $this->assertCount(2, $errors); - - // Process with suppression on line before. - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - - // Process with @ suppression on line before. - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - - // Process with suppression on line before. - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - - // Process with @ suppression on line before. - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - - // Process with suppression on line before (deprecated syntax). - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - // Process with @ suppression on line before inside docblock. - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - - // Process with suppression on same line. - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - - // Process with @ suppression on same line. - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - - // Process with suppression on same line (deprecated syntax). - $content = 'process(); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); + $this->assertSame(0, $file->getErrorCount()); + $this->assertCount(0, $file->getErrors()); - }//end testSuppressLine() + }//end testSuppressLineWithinDocblock() /** * Test that using a single line ignore does not interfere with other suppressions. * + * @param string $before Annotation to place before the code. + * @param string $after Annotation to place after the code. + * + * @dataProvider dataNestedSuppressLine + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap + * * @return void */ - public function testNestedSuppressLine() + public function testNestedSuppressLine($before, $after) { - $config = new Config(); - $config->standards = ['Generic']; - $config->sniffs = ['Generic.PHP.LowerCaseConstant']; - - $ruleset = new Ruleset($config); + static $config, $ruleset; - // Process with disable/enable suppression and no single line suppression. - $content = 'process(); + if (isset($config, $ruleset) === false) { + $config = new Config(); + $config->standards = ['Generic']; + $config->sniffs = ['Generic.PHP.LowerCaseConstant']; - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); + $ruleset = new Ruleset($config); + } - // Process with disable/enable @ suppression and no single line suppression. - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - - // Process with disable/enable suppression and no single line suppression (deprecated syntax). - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - - // Process with line suppression nested within disable/enable suppression. - $content = 'process(); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); + $this->assertSame(0, $file->getErrorCount()); + $this->assertCount(0, $file->getErrors()); - // Process with line @ suppression nested within disable/enable @ suppression. - $content = 'process(); + }//end testNestedSuppressLine() - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - // Process with line suppression nested within disable/enable suppression (deprecated syntax). - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); + /** + * Data provider. + * + * @see testNestedSuppressLine() + * + * @return array + */ + public function dataNestedSuppressLine() + { + return [ + // Process with disable/enable suppression and no single line suppression. + 'disable/enable: slash comment, no single line suppression' => [ + 'before' => '// phpcs:disable', + 'after' => '// phpcs:enable', + ], + 'disable/enable: slash comment, with @, no single line suppression' => [ + 'before' => '// @phpcs:disable', + 'after' => '// @phpcs:enable', + ], + 'disable/enable: hash comment, no single line suppression' => [ + 'before' => '# phpcs:disable', + 'after' => '# phpcs:enable', + ], + 'old style: slash comment, no single line suppression' => [ + 'before' => '// @codingStandardsIgnoreStart', + 'after' => '// @codingStandardsIgnoreEnd', + ], + + // Process with line suppression nested within disable/enable suppression. + 'disable/enable: slash comment, next line nested single line suppression' => [ + 'before' => '// phpcs:disable'.PHP_EOL.'// phpcs:ignore', + 'after' => '// phpcs:enable', + ], + 'disable/enable: slash comment, with @, next line nested single line suppression' => [ + 'before' => '// @phpcs:disable'.PHP_EOL.'// @phpcs:ignore', + 'after' => '// @phpcs:enable', + ], + 'disable/enable: hash comment, next line nested single line suppression' => [ + 'before' => '# @phpcs:disable'.PHP_EOL.'# @phpcs:ignore', + 'after' => '# @phpcs:enable', + ], + 'old style: slash comment, next line nested single line suppression' => [ + 'before' => '// @codingStandardsIgnoreStart'.PHP_EOL.'// @codingStandardsIgnoreLine', + 'after' => '// @codingStandardsIgnoreEnd', + ], + ]; - }//end testNestedSuppressLine() + }//end dataNestedSuppressLine() /** * Test suppressing a scope opener. * + * @param string $before Annotation to place before the scope opener. + * @param string $after Annotation to place after the scope opener. + * @param int $expectedErrors Optional. Number of errors expected. + * Defaults to 0. + * + * @dataProvider dataSuppressScope + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap + * * @return void */ - public function testSuppressScope() + public function testSuppressScope($before, $after, $expectedErrors=0) { - $config = new Config(); - $config->standards = ['PEAR']; - $config->sniffs = ['PEAR.NamingConventions.ValidVariableName']; - - $ruleset = new Ruleset($config); - - // Process without suppression. - $content = 'foo();'.PHP_EOL.'}'.PHP_EOL.'}'; - $file = new DummyFile($content, $ruleset, $config); - $file->process(); + static $config, $ruleset; - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - - // Process with suppression. - $content = 'foo();'.PHP_EOL.'}'.PHP_EOL.'}'; - $file = new DummyFile($content, $ruleset, $config); - $file->process(); + if (isset($config, $ruleset) === false) { + $config = new Config(); + $config->standards = ['PEAR']; + $config->sniffs = ['PEAR.Functions.FunctionDeclaration']; - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); + $ruleset = new Ruleset($config); + } - // Process with suppression. - $content = 'foo();'.PHP_EOL.'}'.PHP_EOL.'}'; - $file = new DummyFile($content, $ruleset, $config); - $file->process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - - // Process with suppression (deprecated syntax). - $content = 'foo();'.PHP_EOL.'}'.PHP_EOL.'}'; + $content = 'foo(); + } +} +EOD; $file = new DummyFile($content, $ruleset, $config); $file->process(); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - - // Process with a docblock suppression. - $content = 'foo();'.PHP_EOL.'}'.PHP_EOL.'}'; - $file = new DummyFile($content, $ruleset, $config); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - - // Process with a docblock @ suppression. - $content = 'foo();'.PHP_EOL.'}'.PHP_EOL.'}'; - $file = new DummyFile($content, $ruleset, $config); + $this->assertSame($expectedErrors, $file->getErrorCount()); + $this->assertCount($expectedErrors, $file->getErrors()); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); + }//end testSuppressScope() - // Process with a docblock suppression (deprecated syntax). - $content = 'foo();'.PHP_EOL.'}'.PHP_EOL.'}'; - $file = new DummyFile($content, $ruleset, $config); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); + /** + * Data provider. + * + * @see testSuppressScope() + * + * @return array + */ + public function dataSuppressScope() + { + return [ + 'no suppression' => [ + 'before' => '', + 'after' => '', + 'expectedErrors' => 1, + ], + + // Process with suppression. + 'disable/enable: slash comment' => [ + 'before' => '//phpcs:disable', + 'after' => '//phpcs:enable', + ], + 'disable/enable: slash comment, with @' => [ + 'before' => '//@phpcs:disable', + 'after' => '//@phpcs:enable', + ], + 'disable/enable: hash comment' => [ + 'before' => '#phpcs:disable', + 'after' => '#phpcs:enable', + ], + 'disable/enable: single line docblock comment' => [ + 'before' => '/** phpcs:disable */', + 'after' => '/** phpcs:enable */', + ], + 'disable/enable: single line docblock comment, with @' => [ + 'before' => '/** @phpcs:disable */', + 'after' => '/** @phpcs:enable */', + ], + + // Deprecated syntax. + 'old style: start/end, slash comment' => [ + 'before' => '//@codingStandardsIgnoreStart', + 'after' => '//@codingStandardsIgnoreEnd', + ], + 'old style: start/end, single line docblock comment' => [ + 'before' => '/** @codingStandardsIgnoreStart */', + 'after' => '/** @codingStandardsIgnoreEnd */', + ], + ]; - }//end testSuppressScope() + }//end dataSuppressScope() /** * Test suppressing a whole file. * + * @param string $before Annotation to place before the code. + * @param string $after Optional. Annotation to place after the code. + * Defaults to an empty string. + * @param int $expectedWarnings Optional. Number of warnings expected. + * Defaults to 0. + * + * @dataProvider dataSuppressFile + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap + * * @return void */ - public function testSuppressFile() + public function testSuppressFile($before, $after='', $expectedWarnings=0) { - $config = new Config(); - $config->standards = ['Generic']; - $config->sniffs = ['Generic.Commenting.Todo']; - - $ruleset = new Ruleset($config); - - // Process without suppression. - $content = 'process(); - - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(1, $numWarnings); - $this->assertCount(1, $warnings); - - // Process with suppression. - $content = 'process(); + static $config, $ruleset; - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); + if (isset($config, $ruleset) === false) { + $config = new Config(); + $config->standards = ['Generic']; + $config->sniffs = ['Generic.Commenting.Todo']; - // Process with @ suppression. - $content = 'process(); - - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - // Process with suppression (deprecated syntax). - $content = 'process(); - - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); + $ruleset = new Ruleset($config); + } - // Process mixed case. - $content = 'process(); - - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - // Process late comment. - $content = 'process(); - - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - // Process late comment (deprecated syntax). - $content = 'process(); - - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - // Process with a block comment suppression. - $content = 'process(); - - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - // Process with a multi-line block comment suppression. - $content = 'process(); - - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - // Process with a block comment suppression (deprecated syntax). - $content = 'process(); - - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - // Process with a multi-line block comment suppression (deprecated syntax). - $content = 'process(); - - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - // Process with docblock suppression. - $content = 'process(); - - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - // Process with docblock suppression (deprecated syntax). - $content = 'process(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); + $this->assertSame($expectedWarnings, $file->getWarningCount()); + $this->assertCount($expectedWarnings, $file->getWarnings()); }//end testSuppressFile() /** - * Test disabling specific sniffs. + * Data provider. * - * @return void + * @see testSuppressFile() + * + * @return array */ - public function testDisableSelected() + public function dataSuppressFile() { - $config = new Config(); - $config->standards = ['Generic']; - $config->sniffs = [ - 'Generic.PHP.LowerCaseConstant', - 'Generic.Commenting.Todo', + return [ + 'no suppression' => [ + 'before' => '', + 'after' => '', + 'expectedErrors' => 1, + ], + + // Process with suppression. + 'ignoreFile: start of file, slash comment' => ['before' => '// phpcs:ignoreFile'], + 'ignoreFile: start of file, slash comment, with @' => ['before' => '// @phpcs:ignoreFile'], + 'ignoreFile: start of file, slash comment, mixed case' => ['before' => '// PHPCS:Ignorefile'], + 'ignoreFile: start of file, hash comment' => ['before' => '# phpcs:ignoreFile'], + 'ignoreFile: start of file, hash comment, with @' => ['before' => '# @phpcs:ignoreFile'], + 'ignoreFile: start of file, single-line star comment' => ['before' => '/* phpcs:ignoreFile */'], + 'ignoreFile: start of file, multi-line star comment' => [ + 'before' => '/*'.PHP_EOL.' phpcs:ignoreFile'.PHP_EOL.' */', + ], + 'ignoreFile: start of file, single-line docblock comment' => ['before' => '/** phpcs:ignoreFile */'], + + // Process late comment. + 'ignoreFile: late comment, slash comment' => [ + 'before' => '', + 'after' => '// phpcs:ignoreFile', + ], + + // Deprecated syntax. + 'old style: start of file, slash comment' => ['before' => '// @codingStandardsIgnoreFile'], + 'old style: start of file, single-line star comment' => ['before' => '/* @codingStandardsIgnoreFile */'], + 'old style: start of file, multi-line star comment' => [ + 'before' => '/*'.PHP_EOL.' @codingStandardsIgnoreFile'.PHP_EOL.' */', + ], + 'old style: start of file, single-line docblock comment' => ['before' => '/** @codingStandardsIgnoreFile */'], + + // Deprecated syntax, late comment. + 'old style: late comment, slash comment' => [ + 'before' => '', + 'after' => '// @codingStandardsIgnoreFile', + ], ]; - $ruleset = new Ruleset($config); + }//end dataSuppressFile() - // Suppress a single sniff. - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - // Suppress multiple sniffs. - $content = 'process(); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - // Suppress adding sniffs. - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - // Suppress a category of sniffs. - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - // Suppress a whole standard. - $content = 'process(); + /** + * Test disabling specific sniffs. + * + * @param string $before Annotation to place before the code. + * @param int $expectedErrors Optional. Number of errors expected. + * Defaults to 0. + * @param int $expectedWarnings Optional. Number of warnings expected. + * Defaults to 0. + * + * @dataProvider dataDisableSelected + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap + * + * @return void + */ + public function testDisableSelected($before, $expectedErrors=0, $expectedWarnings=0) + { + static $config, $ruleset; - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - // Suppress using docblocks. - $content = 'process(); + if (isset($config, $ruleset) === false) { + $config = new Config(); + $config->standards = ['Generic']; + $config->sniffs = [ + 'Generic.PHP.LowerCaseConstant', + 'Generic.Commenting.Todo', + ]; - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - $content = 'process(); + $ruleset = new Ruleset($config); + } - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - // Suppress wrong category using docblocks. - $content = 'process(); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - $this->assertEquals(1, $numWarnings); - $this->assertCount(1, $warnings); - - $content = 'process(); + $this->assertSame($expectedErrors, $file->getErrorCount()); + $this->assertCount($expectedErrors, $file->getErrors()); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - $this->assertEquals(1, $numWarnings); - $this->assertCount(1, $warnings); + $this->assertSame($expectedWarnings, $file->getWarningCount()); + $this->assertCount($expectedWarnings, $file->getWarnings()); }//end testDisableSelected() /** - * Test re-enabling specific sniffs that have been disabled. + * Data provider. * - * @return void + * @see testDisableSelected() + * + * @return array */ - public function testEnableSelected() + public function dataDisableSelected() { - $config = new Config(); - $config->standards = ['Generic']; - $config->sniffs = [ - 'Generic.PHP.LowerCaseConstant', - 'Generic.Commenting.Todo', + return [ + // Single sniff. + 'disable: single sniff' => [ + 'before' => '// phpcs:disable Generic.Commenting.Todo', + 'expectedErrors' => 1, + ], + 'disable: single sniff with reason' => [ + 'before' => '# phpcs:disable Generic.Commenting.Todo -- for reasons', + 'expectedErrors' => 1, + ], + 'disable: single sniff, docblock' => [ + 'before' => '/**'.PHP_EOL.' * phpcs:disable Generic.Commenting.Todo'.PHP_EOL.' */ ', + 'expectedErrors' => 1, + ], + 'disable: single sniff, docblock, with @' => [ + 'before' => '/**'.PHP_EOL.' * @phpcs:disable Generic.Commenting.Todo'.PHP_EOL.' */ ', + 'expectedErrors' => 1, + ], + + // Multiple sniffs. + 'disable: multiple sniffs in one comment' => ['before' => '// phpcs:disable Generic.Commenting.Todo,Generic.PHP.LowerCaseConstant'], + 'disable: multiple sniff in multiple comments' => [ + 'before' => '// phpcs:disable Generic.Commenting.Todo'.PHP_EOL.'// phpcs:disable Generic.PHP.LowerCaseConstant', + ], + + // Selectiveness variations. + 'disable: complete category' => [ + 'before' => '// phpcs:disable Generic.Commenting', + 'expectedErrors' => 1, + ], + 'disable: whole standard' => ['before' => '// phpcs:disable Generic'], + 'disable: single errorcode' => [ + 'before' => '# @phpcs:disable Generic.Commenting.Todo.TaskFound', + 'expectedErrors' => 1, + ], + 'disable: single errorcode and a category' => ['before' => '// phpcs:disable Generic.PHP.LowerCaseConstant.Found,Generic.Commenting'], + + // Wrong category/sniff/code. + 'disable: wrong error code and category' => [ + 'before' => '/**'.PHP_EOL.' * phpcs:disable Generic.PHP.LowerCaseConstant.Upper,Generic.Comments'.PHP_EOL.' */ ', + 'expectedErrors' => 1, + 'expectedWarnings' => 1, + ], + 'disable: wrong category, docblock' => [ + 'before' => '/**'.PHP_EOL.' * phpcs:disable Generic.Files'.PHP_EOL.' */ ', + 'expectedErrors' => 1, + 'expectedWarnings' => 1, + ], + 'disable: wrong category, docblock, with @' => [ + 'before' => '/**'.PHP_EOL.' * @phpcs:disable Generic.Files'.PHP_EOL.' */ ', + 'expectedErrors' => 1, + 'expectedWarnings' => 1, + ], ]; - $ruleset = new Ruleset($config); + }//end dataDisableSelected() - // Suppress a single sniff and re-enable. - $content = 'process(); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - $this->assertEquals(1, $numWarnings); - $this->assertCount(1, $warnings); - - // Suppress multiple sniffs and re-enable. - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - $this->assertEquals(1, $numWarnings); - $this->assertCount(1, $warnings); - - // Suppress multiple sniffs and re-enable one. - $content = 'process(); - - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - $this->assertEquals(1, $numWarnings); - $this->assertCount(1, $warnings); - - // Suppress a category of sniffs and re-enable. - $content = 'process(); + /** + * Test re-enabling specific sniffs that have been disabled. + * + * @param string $code Code pattern to check. + * @param int $expectedErrors Number of errors expected. + * @param int $expectedWarnings Number of warnings expected. + * + * @dataProvider dataEnableSelected + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap + * + * @return void + */ + public function testEnableSelected($code, $expectedErrors, $expectedWarnings) + { + static $config, $ruleset; - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - $this->assertEquals(1, $numWarnings); - $this->assertCount(1, $warnings); - - // Suppress a whole standard and re-enable. - $content = 'process(); + if (isset($config, $ruleset) === false) { + $config = new Config(); + $config->standards = ['Generic']; + $config->sniffs = [ + 'Generic.PHP.LowerCaseConstant', + 'Generic.Commenting.Todo', + ]; - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - $this->assertEquals(1, $numWarnings); - $this->assertCount(1, $warnings); - - // Suppress a whole standard and re-enable a category. - $content = 'process(); + $ruleset = new Ruleset($config); + } - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - $this->assertEquals(1, $numWarnings); - $this->assertCount(1, $warnings); - - // Suppress a category and re-enable a whole standard. - $content = 'process(); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - $this->assertEquals(1, $numWarnings); - $this->assertCount(1, $warnings); - - // Suppress a sniff and re-enable a category. - $content = 'process(); + $this->assertSame($expectedErrors, $file->getErrorCount()); + $this->assertCount($expectedErrors, $file->getErrors()); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - $this->assertEquals(1, $numWarnings); - $this->assertCount(1, $warnings); - - // Suppress a whole standard and re-enable a sniff. - $content = 'process(); + $this->assertSame($expectedWarnings, $file->getWarningCount()); + $this->assertCount($expectedWarnings, $file->getWarnings()); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - $this->assertEquals(1, $numWarnings); - $this->assertCount(1, $warnings); - - // Suppress a whole standard and re-enable and re-disable a sniff. - $content = 'process(); + }//end testEnableSelected() - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - $this->assertEquals(2, $numWarnings); - $this->assertCount(2, $warnings); - - // Suppress a whole standard and re-enable 2 specific sniffs independently. - $content = 'process(); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - $this->assertEquals(2, $numWarnings); - $this->assertCount(2, $warnings); + /** + * Data provider. + * + * @see testEnableSelected() + * + * @return array + */ + public function dataEnableSelected() + { + return [ + 'disable/enable: a single sniff' => [ + 'code' => ' + // phpcs:disable Generic.Commenting.Todo + $var = FALSE; + //TODO: write some code + // phpcs:enable Generic.Commenting.Todo + //TODO: write some code', + 'expectedErrors' => 1, + 'expectedWarnings' => 1, + ], + 'disable/enable: multiple sniffs' => [ + 'code' => ' + // phpcs:disable Generic.Commenting.Todo,Generic.PHP.LowerCaseConstant + $var = FALSE; + //TODO: write some code + // phpcs:enable Generic.Commenting.Todo,Generic.PHP.LowerCaseConstant + //TODO: write some code + $var = FALSE;', + 'expectedErrors' => 1, + 'expectedWarnings' => 1, + ], + 'disable: multiple sniffs; enable: one' => [ + 'code' => ' + # phpcs:disable Generic.Commenting.Todo,Generic.PHP.LowerCaseConstant + $var = FALSE; + //TODO: write some code + # phpcs:enable Generic.Commenting.Todo + //TODO: write some code + $var = FALSE;', + 'expectedErrors' => 0, + 'expectedWarnings' => 1, + ], + 'disable/enable: complete category' => [ + 'code' => ' + // phpcs:disable Generic.Commenting + $var = FALSE; + //TODO: write some code + // phpcs:enable Generic.Commenting + //TODO: write some code', + 'expectedErrors' => 1, + 'expectedWarnings' => 1, + ], + 'disable/enable: whole standard' => [ + 'code' => ' + // phpcs:disable Generic + $var = FALSE; + //TODO: write some code + // phpcs:enable Generic + //TODO: write some code', + 'expectedErrors' => 0, + 'expectedWarnings' => 1, + ], + 'disable: whole standard; enable: category from the standard' => [ + 'code' => ' + // phpcs:disable Generic + $var = FALSE; + //TODO: write some code + // phpcs:enable Generic.Commenting + //TODO: write some code', + 'expectedErrors' => 0, + 'expectedWarnings' => 1, + ], + 'disable: a category; enable: the whole standard containing the category' => [ + 'code' => ' + # phpcs:disable Generic.Commenting + $var = FALSE; + //TODO: write some code + # phpcs:enable Generic + //TODO: write some code', + 'expectedErrors' => 1, + 'expectedWarnings' => 1, + ], + 'disable: single sniff; enable: the category containing the sniff' => [ + 'code' => ' + // phpcs:disable Generic.Commenting.Todo + $var = FALSE; + //TODO: write some code + // phpcs:enable Generic.Commenting + //TODO: write some code', + 'expectedErrors' => 1, + 'expectedWarnings' => 1, + ], + 'disable: whole standard; enable: single sniff from the standard' => [ + 'code' => ' + // phpcs:disable Generic + $var = FALSE; + //TODO: write some code + // phpcs:enable Generic.Commenting.Todo + //TODO: write some code', + 'expectedErrors' => 0, + 'expectedWarnings' => 1, + ], + 'disable: whole standard; enable: single sniff from the standard; disable: that same sniff; enable: everything' => [ + 'code' => ' + // phpcs:disable Generic + $var = FALSE; + //TODO: write some code + // phpcs:enable Generic.Commenting.Todo + //TODO: write some code + // phpcs:disable Generic.Commenting.Todo + //TODO: write some code + // phpcs:enable + //TODO: write some code', + 'expectedErrors' => 0, + 'expectedWarnings' => 2, + ], + 'disable: whole standard; enable: single sniff from the standard; enable: other sniff from the standard' => [ + 'code' => ' + // phpcs:disable Generic + $var = FALSE; + //TODO: write some code + // phpcs:enable Generic.Commenting.Todo + //TODO: write some code + $var = FALSE; + // phpcs:enable Generic.PHP.LowerCaseConstant + //TODO: write some code + $var = FALSE;', + 'expectedErrors' => 1, + 'expectedWarnings' => 2, + ], + ]; - }//end testEnableSelected() + }//end dataEnableSelected() /** * Test ignoring specific sniffs. * + * @param string $before Annotation to place before the code. + * @param int $expectedErrors Number of errors expected. + * @param int $expectedWarnings Number of warnings expected. + * + * @dataProvider dataIgnoreSelected + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap + * * @return void */ - public function testIgnoreSelected() + public function testIgnoreSelected($before, $expectedErrors, $expectedWarnings) { - $config = new Config(); - $config->standards = ['Generic']; - $config->sniffs = [ - 'Generic.PHP.LowerCaseConstant', - 'Generic.Commenting.Todo', - ]; + static $config, $ruleset; - $ruleset = new Ruleset($config); + if (isset($config, $ruleset) === false) { + $config = new Config(); + $config->standards = ['Generic']; + $config->sniffs = [ + 'Generic.PHP.LowerCaseConstant', + 'Generic.Commenting.Todo', + ]; - // No suppression. - $content = 'process(); + $ruleset = new Ruleset($config); + } - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(2, $numErrors); - $this->assertCount(2, $errors); - $this->assertEquals(2, $numWarnings); - $this->assertCount(2, $warnings); - - // Suppress a single sniff. - $content = 'process(); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(2, $numErrors); - $this->assertCount(2, $errors); - $this->assertEquals(1, $numWarnings); - $this->assertCount(1, $warnings); - - // Suppress multiple sniffs. - $content = 'process(); + $this->assertSame($expectedErrors, $file->getErrorCount()); + $this->assertCount($expectedErrors, $file->getErrors()); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - $this->assertEquals(1, $numWarnings); - $this->assertCount(1, $warnings); - - // Add to suppression. - $content = 'process(); + $this->assertSame($expectedWarnings, $file->getWarningCount()); + $this->assertCount($expectedWarnings, $file->getWarnings()); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - // Suppress a category of sniffs. - $content = 'process(); + }//end testIgnoreSelected() - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(2, $numErrors); - $this->assertCount(2, $errors); - $this->assertEquals(1, $numWarnings); - $this->assertCount(1, $warnings); - - // Suppress a whole standard. - $content = 'process(); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - $this->assertEquals(1, $numWarnings); - $this->assertCount(1, $warnings); + /** + * Data provider. + * + * @see testIgnoreSelected() + * + * @return array + */ + public function dataIgnoreSelected() + { + return [ + 'no suppression' => [ + 'before' => '', + 'expectedErrors' => 2, + 'expectedWarnings' => 2, + ], + + // With suppression. + 'ignore: single sniff' => [ + 'before' => '// phpcs:ignore Generic.Commenting.Todo', + 'expectedErrors' => 2, + 'expectedWarnings' => 1, + ], + 'ignore: multiple sniffs' => [ + 'before' => '// phpcs:ignore Generic.Commenting.Todo,Generic.PHP.LowerCaseConstant', + 'expectedErrors' => 1, + 'expectedWarnings' => 1, + ], + 'disable: single sniff; ignore: single sniff' => [ + 'before' => '// phpcs:disable Generic.Commenting.Todo'.PHP_EOL.'// phpcs:ignore Generic.PHP.LowerCaseConstant', + 'expectedErrors' => 1, + 'expectedWarnings' => 0, + ], + 'ignore: category of sniffs' => [ + 'before' => '# phpcs:ignore Generic.Commenting', + 'expectedErrors' => 2, + 'expectedWarnings' => 1, + ], + 'ignore: whole standard' => [ + 'before' => '// phpcs:ignore Generic', + 'expectedErrors' => 1, + 'expectedWarnings' => 1, + ], + ]; - }//end testIgnoreSelected() + }//end dataIgnoreSelected() /** * Test ignoring specific sniffs. * + * @param string $code Code pattern to check. + * @param int $expectedErrors Number of errors expected. + * @param int $expectedWarnings Number of warnings expected. + * + * @dataProvider dataCommenting + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap + * * @return void */ - public function testCommenting() + public function testCommenting($code, $expectedErrors, $expectedWarnings) { - $config = new Config(); - $config->standards = ['Generic']; - $config->sniffs = [ - 'Generic.PHP.LowerCaseConstant', - 'Generic.Commenting.Todo', - ]; + static $config, $ruleset; - $ruleset = new Ruleset($config); + if (isset($config, $ruleset) === false) { + $config = new Config(); + $config->standards = ['Generic']; + $config->sniffs = [ + 'Generic.PHP.LowerCaseConstant', + 'Generic.Commenting.Todo', + ]; - // Suppress a single sniff. - $content = 'process(); + $ruleset = new Ruleset($config); + } - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(2, $numErrors); - $this->assertCount(2, $errors); - $this->assertEquals(1, $numWarnings); - $this->assertCount(1, $warnings); - - // Suppress a single sniff and re-enable. - $content = 'process(); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - $this->assertEquals(1, $numWarnings); - $this->assertCount(1, $warnings); - - // Suppress a single sniff using block comments. - $content = 'process(); + $this->assertSame($expectedErrors, $file->getErrorCount()); + $this->assertCount($expectedErrors, $file->getErrors()); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(1, $numErrors); - $this->assertCount(1, $errors); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - - // Suppress a single sniff with a multi-line comment. - $content = 'process(); + $this->assertSame($expectedWarnings, $file->getWarningCount()); + $this->assertCount($expectedWarnings, $file->getWarnings()); - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(2, $numErrors); - $this->assertCount(2, $errors); - $this->assertEquals(1, $numWarnings); - $this->assertCount(1, $warnings); - - // Ignore an enable before a disable. - $content = 'process(); + }//end testCommenting() - $errors = $file->getErrors(); - $numErrors = $file->getErrorCount(); - $warnings = $file->getWarnings(); - $numWarnings = $file->getWarningCount(); - $this->assertEquals(0, $numErrors); - $this->assertCount(0, $errors); - $this->assertEquals(0, $numWarnings); - $this->assertCount(0, $warnings); - }//end testCommenting() + /** + * Data provider. + * + * @see testCommenting() + * + * @return array + */ + public function dataCommenting() + { + return [ + 'ignore: single sniff' => [ + 'code' => ' + // phpcs:ignore Generic.Commenting.Todo -- Because reasons + $var = FALSE; //TODO: write some code + $var = FALSE; //TODO: write some code', + 'expectedErrors' => 2, + 'expectedWarnings' => 1, + ], + 'disable: single sniff; enable: same sniff - test whitespace handling around reason delimiter' => [ + 'code' => ' + // phpcs:disable Generic.Commenting.Todo --Because reasons + $var = FALSE; + //TODO: write some code + // phpcs:enable Generic.Commenting.Todo -- Because reasons + //TODO: write some code', + 'expectedErrors' => 1, + 'expectedWarnings' => 1, + ], + 'disable: single sniff, multi-line comment' => [ + 'code' => ' + /* + Disable some checks + phpcs:disable Generic.Commenting.Todo + */ + $var = FALSE; + //TODO: write some code', + 'expectedErrors' => 1, + 'expectedWarnings' => 0, + ], + 'ignore: single sniff, multi-line slash comment' => [ + 'code' => ' + // Turn off a check for the next line of code. + // phpcs:ignore Generic.Commenting.Todo + $var = FALSE; //TODO: write some code + $var = FALSE; //TODO: write some code', + 'expectedErrors' => 2, + 'expectedWarnings' => 1, + ], + 'enable before disable, sniff not in standard' => [ + 'code' => ' + // phpcs:enable Generic.PHP.NoSilencedErrors -- Because reasons + $var = @delete( $filename ); + ', + 'expectedErrors' => 0, + 'expectedWarnings' => 0, + ], + ]; + + }//end dataCommenting() }//end class diff --git a/tests/Core/File/FindEndOfStatementTest.inc b/tests/Core/File/FindEndOfStatementTest.inc index 1d72d7d741..8351679891 100644 --- a/tests/Core/File/FindEndOfStatementTest.inc +++ b/tests/Core/File/FindEndOfStatementTest.inc @@ -39,6 +39,8 @@ $a = [ /* testStaticArrowFunction */ static fn ($a) => $a; +return 0; + /* testArrowFunctionReturnValue */ fn(): array => [a($a, $b)]; @@ -52,4 +54,52 @@ $foo = foo( fn() => [$row[0], $row[3]] ); -return 0; +$match = match ($a) { + /* testMatchCase */ + 1 => 'foo', + /* testMatchDefault */ + default => 'bar' +}; + +$match = match ($a) { + /* testMatchMultipleCase */ + 1, 2, => $a * $b, + /* testMatchDefaultComma */ + default, => 'something' +}; + +match ($pressedKey) { + /* testMatchFunctionCall */ + Key::RETURN_ => save($value, $user) +}; + +$result = match (true) { + /* testMatchFunctionCallArm */ + str_contains($text, 'Welcome') || str_contains($text, 'Hello') => 'en', + str_contains($text, 'Bienvenue') || str_contains($text, 'Bonjour') => 'fr', + default => 'pl' +}; + +/* testMatchClosure */ +$result = match ($key) { + 1 => function($a, $b) {}, + 2 => function($b, $c) {}, +}; + +/* testMatchArray */ +$result = match ($key) { + 1 => [1,2,3], + 2 => [1 => one(), 2 => two()], +}; + +/* testNestedMatch */ +$result = match ($key) { + 1 => match ($key) { + 1 => 'one', + 2 => 'two', + }, + 2 => match ($key) { + 1 => 'two', + 2 => 'one', + }, +}; diff --git a/tests/Core/File/FindEndOfStatementTest.php b/tests/Core/File/FindEndOfStatementTest.php index 1fbc8a6871..7bff26b566 100644 --- a/tests/Core/File/FindEndOfStatementTest.php +++ b/tests/Core/File/FindEndOfStatementTest.php @@ -237,4 +237,179 @@ public function testArrowFunctionWithArrayAsArgument() }//end testArrowFunctionWithArrayAsArgument() + /** + * Test simple match expression case. + * + * @return void + */ + public function testMatchCase() + { + $start = $this->getTargetToken('/* testMatchCase */', T_LNUMBER); + $found = self::$phpcsFile->findEndOfStatement($start); + + $this->assertSame(($start + 5), $found); + + $start = $this->getTargetToken('/* testMatchCase */', T_CONSTANT_ENCAPSED_STRING); + $found = self::$phpcsFile->findEndOfStatement($start); + + $this->assertSame(($start + 1), $found); + + }//end testMatchCase() + + + /** + * Test simple match expression default case. + * + * @return void + */ + public function testMatchDefault() + { + $start = $this->getTargetToken('/* testMatchDefault */', T_MATCH_DEFAULT); + $found = self::$phpcsFile->findEndOfStatement($start); + + $this->assertSame(($start + 4), $found); + + $start = $this->getTargetToken('/* testMatchDefault */', T_CONSTANT_ENCAPSED_STRING); + $found = self::$phpcsFile->findEndOfStatement($start); + + $this->assertSame($start, $found); + + }//end testMatchDefault() + + + /** + * Test multiple comma-separated match expression case values. + * + * @return void + */ + public function testMatchMultipleCase() + { + $start = $this->getTargetToken('/* testMatchMultipleCase */', T_LNUMBER); + $found = self::$phpcsFile->findEndOfStatement($start); + $this->assertSame(($start + 13), $found); + + $start += 6; + $found = self::$phpcsFile->findEndOfStatement($start); + $this->assertSame(($start + 7), $found); + + }//end testMatchMultipleCase() + + + /** + * Test match expression default case with trailing comma. + * + * @return void + */ + public function testMatchDefaultComma() + { + $start = $this->getTargetToken('/* testMatchDefaultComma */', T_MATCH_DEFAULT); + $found = self::$phpcsFile->findEndOfStatement($start); + + $this->assertSame(($start + 5), $found); + + }//end testMatchDefaultComma() + + + /** + * Test match expression with function call. + * + * @return void + */ + public function testMatchFunctionCall() + { + $start = $this->getTargetToken('/* testMatchFunctionCall */', T_STRING); + $found = self::$phpcsFile->findEndOfStatement($start); + + $this->assertSame(($start + 12), $found); + + $start += 8; + $found = self::$phpcsFile->findEndOfStatement($start); + + $this->assertSame(($start + 1), $found); + + }//end testMatchFunctionCall() + + + /** + * Test match expression with function call in the arm. + * + * @return void + */ + public function testMatchFunctionCallArm() + { + // Check the first case. + $start = $this->getTargetToken('/* testMatchFunctionCallArm */', T_STRING); + $found = self::$phpcsFile->findEndOfStatement($start); + + $this->assertSame(($start + 21), $found); + + // Check the second case. + $start += 24; + $found = self::$phpcsFile->findEndOfStatement($start); + + $this->assertSame(($start + 21), $found); + + }//end testMatchFunctionCallArm() + + + /** + * Test match expression with closure. + * + * @return void + */ + public function testMatchClosure() + { + $start = $this->getTargetToken('/* testMatchClosure */', T_LNUMBER); + $found = self::$phpcsFile->findEndOfStatement($start); + + $this->assertSame(($start + 14), $found); + + $start += 17; + $found = self::$phpcsFile->findEndOfStatement($start); + + $this->assertSame(($start + 14), $found); + + }//end testMatchClosure() + + + /** + * Test match expression with array declaration. + * + * @return void + */ + public function testMatchArray() + { + $start = $this->getTargetToken('/* testMatchArray */', T_LNUMBER); + $found = self::$phpcsFile->findEndOfStatement($start); + + $this->assertSame(($start + 11), $found); + + $start += 14; + $found = self::$phpcsFile->findEndOfStatement($start); + + $this->assertSame(($start + 22), $found); + + }//end testMatchArray() + + + /** + * Test nested match expressions. + * + * @return void + */ + public function testNestedMatch() + { + $start = $this->getTargetToken('/* testNestedMatch */', T_LNUMBER); + $found = self::$phpcsFile->findEndOfStatement($start); + + $this->assertSame(($start + 30), $found); + + $start += 21; + $found = self::$phpcsFile->findEndOfStatement($start); + + $this->assertSame(($start + 5), $found); + + }//end testNestedMatch() + + }//end class diff --git a/tests/Core/File/FindImplementedInterfaceNamesTest.inc b/tests/Core/File/FindImplementedInterfaceNamesTest.inc index 3885b27e1d..44c0f64321 100644 --- a/tests/Core/File/FindImplementedInterfaceNamesTest.inc +++ b/tests/Core/File/FindImplementedInterfaceNamesTest.inc @@ -24,3 +24,12 @@ class testFECNClassThatExtendsAndImplements extends testFECNClass implements Int /* testClassThatImplementsAndExtends */ class testFECNClassThatImplementsAndExtends implements \InterfaceA, InterfaceB extends testFECNClass {} + +/* testBackedEnumWithoutImplements */ +enum Suit:string {} + +/* testEnumImplements */ +enum Suit implements Colorful {} + +/* testBackedEnumImplements */ +enum Suit: string implements Colorful, \Deck {} diff --git a/tests/Core/File/FindImplementedInterfaceNamesTest.php b/tests/Core/File/FindImplementedInterfaceNamesTest.php index 834e083202..2395032897 100644 --- a/tests/Core/File/FindImplementedInterfaceNamesTest.php +++ b/tests/Core/File/FindImplementedInterfaceNamesTest.php @@ -27,7 +27,7 @@ class FindImplementedInterfaceNamesTest extends AbstractMethodUnitTest */ public function testFindImplementedInterfaceNames($identifier, $expected) { - $OOToken = $this->getTargetToken($identifier, [T_CLASS, T_ANON_CLASS, T_INTERFACE]); + $OOToken = $this->getTargetToken($identifier, [T_CLASS, T_ANON_CLASS, T_INTERFACE, T_ENUM]); $result = self::$phpcsFile->findImplementedInterfaceNames($OOToken); $this->assertSame($expected, $result); @@ -81,6 +81,21 @@ public function dataImplementedInterface() 'InterfaceB', ], ], + [ + '/* testBackedEnumWithoutImplements */', + false, + ], + [ + '/* testEnumImplements */', + ['Colorful'], + ], + [ + '/* testBackedEnumImplements */', + [ + 'Colorful', + '\Deck', + ], + ], ]; }//end dataImplementedInterface() diff --git a/tests/Core/File/FindStartOfStatementTest.inc b/tests/Core/File/FindStartOfStatementTest.inc new file mode 100644 index 0000000000..ce9dfad3f9 --- /dev/null +++ b/tests/Core/File/FindStartOfStatementTest.inc @@ -0,0 +1,123 @@ + $foo + $bar, 'b' => true]; + +/* testUseGroup */ +use Vendor\Package\{ClassA as A, ClassB, ClassC as C}; + +$a = [ + /* testArrowFunctionArrayValue */ + 'a' => fn() => return 1, + 'b' => fn() => return 1, +]; + +/* testStaticArrowFunction */ +static fn ($a) => $a; + +/* testArrowFunctionReturnValue */ +fn(): array => [a($a, $b)]; + +/* testArrowFunctionAsArgument */ +$foo = foo( + fn() => bar() +); + +/* testArrowFunctionWithArrayAsArgument */ +$foo = foo( + fn() => [$row[0], $row[3]] +); + +$match = match ($a) { + /* testMatchCase */ + 1 => 'foo', + /* testMatchDefault */ + default => 'bar' +}; + +$match = match ($a) { + /* testMatchMultipleCase */ + 1, 2, => $a * $b, + /* testMatchDefaultComma */ + default, => 'something' +}; + +match ($pressedKey) { + /* testMatchFunctionCall */ + Key::RETURN_ => save($value, $user) +}; + +$result = match (true) { + /* testMatchFunctionCallArm */ + str_contains($text, 'Welcome') || str_contains($text, 'Hello') => 'en', + str_contains($text, 'Bienvenue') || str_contains($text, 'Bonjour') => 'fr', + default => 'pl' +}; + +/* testMatchClosure */ +$result = match ($key) { + 1 => function($a, $b) {}, + 2 => function($b, $c) {}, +}; + +/* testMatchArray */ +$result = match ($key) { + 1 => [1,2,3], + 2 => [1 => one($a, $b), 2 => two($b, $c)], + 3 => [], +}; + +/* testNestedMatch */ +$result = match ($key) { + 1 => match ($key) { + 1 => 'one', + 2 => 'two', + }, + 2 => match ($key) { + 1 => 'two', + 2 => 'one', + }, +}; + +return 0; + +/* testOpenTag */ +?> +

Test

+', foo(), ''; + +/* testOpenTagWithEcho */ +?> +

Test

+', foo(), ''; diff --git a/tests/Core/File/FindStartOfStatementTest.php b/tests/Core/File/FindStartOfStatementTest.php new file mode 100644 index 0000000000..ff859eca1c --- /dev/null +++ b/tests/Core/File/FindStartOfStatementTest.php @@ -0,0 +1,503 @@ + + * @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\Tests\Core\File; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class FindStartOfStatementTest extends AbstractMethodUnitTest +{ + + + /** + * Test a simple assignment. + * + * @return void + */ + public function testSimpleAssignment() + { + $start = $this->getTargetToken('/* testSimpleAssignment */', T_SEMICOLON); + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 5), $found); + + }//end testSimpleAssignment() + + + /** + * Test a function call. + * + * @return void + */ + public function testFunctionCall() + { + $start = $this->getTargetToken('/* testFunctionCall */', T_CLOSE_PARENTHESIS); + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 6), $found); + + }//end testFunctionCall() + + + /** + * Test a function call. + * + * @return void + */ + public function testFunctionCallArgument() + { + $start = $this->getTargetToken('/* testFunctionCallArgument */', T_VARIABLE, '$b'); + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame($start, $found); + + }//end testFunctionCallArgument() + + + /** + * Test a direct call to a control structure. + * + * @return void + */ + public function testControlStructure() + { + $start = $this->getTargetToken('/* testControlStructure */', T_CLOSE_CURLY_BRACKET); + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 6), $found); + + }//end testControlStructure() + + + /** + * Test the assignment of a closure. + * + * @return void + */ + public function testClosureAssignment() + { + $start = $this->getTargetToken('/* testClosureAssignment */', T_CLOSE_CURLY_BRACKET); + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 12), $found); + + }//end testClosureAssignment() + + + /** + * Test using a heredoc in a function argument. + * + * @return void + */ + public function testHeredocFunctionArg() + { + // Find the start of the function. + $start = $this->getTargetToken('/* testHeredocFunctionArg */', T_SEMICOLON); + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 10), $found); + + // Find the start of the heredoc. + $start -= 4; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 4), $found); + + // Find the start of the last arg. + $start += 2; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame($start, $found); + + }//end testHeredocFunctionArg() + + + /** + * Test parts of a switch statement. + * + * @return void + */ + public function testSwitch() + { + // Find the start of the switch. + $start = $this->getTargetToken('/* testSwitch */', T_CLOSE_CURLY_BRACKET); + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 47), $found); + + // Find the start of default case. + $start -= 5; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 6), $found); + + // Find the start of the second case. + $start -= 12; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 5), $found); + + // Find the start of the first case. + $start -= 13; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 8), $found); + + // Test inside the first case. + $start--; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 1), $found); + + }//end testSwitch() + + + /** + * Test statements that are array values. + * + * @return void + */ + public function testStatementAsArrayValue() + { + // Test short array syntax. + $start = $this->getTargetToken('/* testStatementAsArrayValue */', T_STRING, 'Datetime'); + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 2), $found); + + // Test long array syntax. + $start += 12; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 2), $found); + + // Test same statement outside of array. + $start++; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 9), $found); + + // Test with an array index. + $start += 17; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 5), $found); + + }//end testStatementAsArrayValue() + + + /** + * Test a use group. + * + * @return void + */ + public function testUseGroup() + { + $start = $this->getTargetToken('/* testUseGroup */', T_SEMICOLON); + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 23), $found); + + }//end testUseGroup() + + + /** + * Test arrow function as array value. + * + * @return void + */ + public function testArrowFunctionArrayValue() + { + $start = $this->getTargetToken('/* testArrowFunctionArrayValue */', T_COMMA); + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 9), $found); + + }//end testArrowFunctionArrayValue() + + + /** + * Test static arrow function. + * + * @return void + */ + public function testStaticArrowFunction() + { + $start = $this->getTargetToken('/* testStaticArrowFunction */', T_SEMICOLON); + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 11), $found); + + }//end testStaticArrowFunction() + + + /** + * Test arrow function with return value. + * + * @return void + */ + public function testArrowFunctionReturnValue() + { + $start = $this->getTargetToken('/* testArrowFunctionReturnValue */', T_SEMICOLON); + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 18), $found); + + }//end testArrowFunctionReturnValue() + + + /** + * Test arrow function used as a function argument. + * + * @return void + */ + public function testArrowFunctionAsArgument() + { + $start = $this->getTargetToken('/* testArrowFunctionAsArgument */', T_FN); + $start += 8; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 8), $found); + + }//end testArrowFunctionAsArgument() + + + /** + * Test arrow function with arrays used as a function argument. + * + * @return void + */ + public function testArrowFunctionWithArrayAsArgument() + { + $start = $this->getTargetToken('/* testArrowFunctionWithArrayAsArgument */', T_FN); + $start += 17; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 17), $found); + + }//end testArrowFunctionWithArrayAsArgument() + + + /** + * Test simple match expression case. + * + * @return void + */ + public function testMatchCase() + { + $start = $this->getTargetToken('/* testMatchCase */', T_COMMA); + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 1), $found); + + }//end testMatchCase() + + + /** + * Test simple match expression default case. + * + * @return void + */ + public function testMatchDefault() + { + $start = $this->getTargetToken('/* testMatchDefault */', T_CONSTANT_ENCAPSED_STRING, "'bar'"); + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame($start, $found); + + }//end testMatchDefault() + + + /** + * Test multiple comma-separated match expression case values. + * + * @return void + */ + public function testMatchMultipleCase() + { + $start = $this->getTargetToken('/* testMatchMultipleCase */', T_MATCH_ARROW); + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 6), $found); + + $start += 6; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 4), $found); + + }//end testMatchMultipleCase() + + + /** + * Test match expression default case with trailing comma. + * + * @return void + */ + public function testMatchDefaultComma() + { + $start = $this->getTargetToken('/* testMatchDefaultComma */', T_MATCH_ARROW); + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 3), $found); + + $start += 2; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame($start, $found); + + }//end testMatchDefaultComma() + + + /** + * Test match expression with function call. + * + * @return void + */ + public function testMatchFunctionCall() + { + $start = $this->getTargetToken('/* testMatchFunctionCall */', T_CLOSE_PARENTHESIS); + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 6), $found); + + }//end testMatchFunctionCall() + + + /** + * Test match expression with function call in the arm. + * + * @return void + */ + public function testMatchFunctionCallArm() + { + // Check the first case. + $start = $this->getTargetToken('/* testMatchFunctionCallArm */', T_MATCH_ARROW); + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 18), $found); + + // Check the second case. + $start += 24; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 18), $found); + + }//end testMatchFunctionCallArm() + + + /** + * Test match expression with closure. + * + * @return void + */ + public function testMatchClosure() + { + $start = $this->getTargetToken('/* testMatchClosure */', T_LNUMBER); + $start += 14; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 10), $found); + + $start += 17; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 10), $found); + + }//end testMatchClosure() + + + /** + * Test match expression with array declaration. + * + * @return void + */ + public function testMatchArray() + { + // Start of first case statement. + $start = $this->getTargetToken('/* testMatchArray */', T_LNUMBER); + $found = self::$phpcsFile->findStartOfStatement($start); + $this->assertSame($start, $found); + + // Comma after first statement. + $start += 11; + $found = self::$phpcsFile->findStartOfStatement($start); + $this->assertSame(($start - 7), $found); + + // Start of second case statement. + $start += 3; + $found = self::$phpcsFile->findStartOfStatement($start); + $this->assertSame($start, $found); + + // Comma after first statement. + $start += 30; + $found = self::$phpcsFile->findStartOfStatement($start); + $this->assertSame(($start - 26), $found); + + }//end testMatchArray() + + + /** + * Test nested match expressions. + * + * @return void + */ + public function testNestedMatch() + { + $start = $this->getTargetToken('/* testNestedMatch */', T_LNUMBER); + $start += 30; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 26), $found); + + $start -= 4; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 1), $found); + + $start -= 3; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 2), $found); + + }//end testNestedMatch() + + + /** + * Test PHP open tag. + * + * @return void + */ + public function testOpenTag() + { + $start = $this->getTargetToken('/* testOpenTag */', T_OPEN_TAG); + $start += 2; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 1), $found); + + }//end testOpenTag() + + + /** + * Test PHP short open echo tag. + * + * @return void + */ + public function testOpenTagWithEcho() + { + $start = $this->getTargetToken('/* testOpenTagWithEcho */', T_OPEN_TAG_WITH_ECHO); + $start += 3; + $found = self::$phpcsFile->findStartOfStatement($start); + + $this->assertSame(($start - 1), $found); + + }//end testOpenTagWithEcho() + + +}//end class diff --git a/tests/Core/File/GetClassPropertiesTest.inc b/tests/Core/File/GetClassPropertiesTest.inc new file mode 100644 index 0000000000..2490a09657 --- /dev/null +++ b/tests/Core/File/GetClassPropertiesTest.inc @@ -0,0 +1,58 @@ + + * @copyright 2022 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\File; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class GetClassPropertiesTest extends AbstractMethodUnitTest +{ + + + /** + * Test receiving an expected exception when a non class token is passed. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $tokenType The type of token to look for after the marker. + * + * @dataProvider dataNotAClassException + * + * @expectedException PHP_CodeSniffer\Exceptions\RuntimeException + * @expectedExceptionMessage $stackPtr must be of type T_CLASS + * + * @return void + */ + public function testNotAClassException($testMarker, $tokenType) + { + $target = $this->getTargetToken($testMarker, $tokenType); + self::$phpcsFile->getClassProperties($target); + + }//end testNotAClassException() + + + /** + * Data provider. + * + * @see testNotAClassException() For the array format. + * + * @return array + */ + public function dataNotAClassException() + { + return [ + 'interface' => [ + '/* testNotAClass */', + \T_INTERFACE, + ], + 'anon-class' => [ + '/* testAnonClass */', + \T_ANON_CLASS, + ], + 'enum' => [ + '/* testEnum */', + \T_ENUM, + ], + ]; + + }//end dataNotAClassException() + + + /** + * Test retrieving the properties for a class declaration. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param array $expected Expected function output. + * + * @dataProvider dataGetClassProperties + * + * @return void + */ + public function testGetClassProperties($testMarker, $expected) + { + $class = $this->getTargetToken($testMarker, \T_CLASS); + $result = self::$phpcsFile->getClassProperties($class); + $this->assertSame($expected, $result); + + }//end testGetClassProperties() + + + /** + * Data provider. + * + * @see testGetClassProperties() For the array format. + * + * @return array + */ + public function dataGetClassProperties() + { + return [ + 'no-properties' => [ + '/* testClassWithoutProperties */', + [ + 'is_abstract' => false, + 'is_final' => false, + 'is_readonly' => false, + ], + ], + 'abstract' => [ + '/* testAbstractClass */', + [ + 'is_abstract' => true, + 'is_final' => false, + 'is_readonly' => false, + ], + ], + 'final' => [ + '/* testFinalClass */', + [ + 'is_abstract' => false, + 'is_final' => true, + 'is_readonly' => false, + ], + ], + 'readonly' => [ + '/* testReadonlyClass */', + [ + 'is_abstract' => false, + 'is_final' => false, + 'is_readonly' => true, + ], + ], + 'final-readonly' => [ + '/* testFinalReadonlyClass */', + [ + 'is_abstract' => false, + 'is_final' => true, + 'is_readonly' => true, + ], + ], + 'readonly-final' => [ + '/* testReadonlyFinalClass */', + [ + 'is_abstract' => false, + 'is_final' => true, + 'is_readonly' => true, + ], + ], + 'abstract-readonly' => [ + '/* testAbstractReadonlyClass */', + [ + 'is_abstract' => true, + 'is_final' => false, + 'is_readonly' => true, + ], + ], + 'readonly-abstract' => [ + '/* testReadonlyAbstractClass */', + [ + 'is_abstract' => true, + 'is_final' => false, + 'is_readonly' => true, + ], + ], + 'comments-and-new-lines' => [ + '/* testWithCommentsAndNewLines */', + [ + 'is_abstract' => true, + 'is_final' => false, + 'is_readonly' => false, + ], + ], + 'no-properties-with-docblock' => [ + '/* testWithDocblockWithoutProperties */', + [ + 'is_abstract' => false, + 'is_final' => false, + 'is_readonly' => false, + ], + ], + 'abstract-final-parse-error' => [ + '/* testParseErrorAbstractFinal */', + [ + 'is_abstract' => true, + 'is_final' => true, + 'is_readonly' => false, + ], + ], + ]; + + }//end dataGetClassProperties() + + +}//end class diff --git a/tests/Core/File/GetMemberPropertiesTest.inc b/tests/Core/File/GetMemberPropertiesTest.inc index af52ff75df..f40b6021f4 100644 --- a/tests/Core/File/GetMemberPropertiesTest.inc +++ b/tests/Core/File/GetMemberPropertiesTest.inc @@ -179,3 +179,126 @@ function_call( 'param', new class { /* testNestedMethodParam 2 */ public function __construct( $open, $post_id ) {} }, 10, 2 ); + +class PHP8Mixed { + /* testPHP8MixedTypeHint */ + public static miXed $mixed; + + /* testPHP8MixedTypeHintNullable */ + // Intentional fatal error - nullability is not allowed with mixed, but that's not the concern of the method. + private ?mixed $nullableMixed; +} + +class NSOperatorInType { + /* testNamespaceOperatorTypeHint */ + public ?namespace\Name $prop; +} + +$anon = class() { + /* testPHP8UnionTypesSimple */ + public int|float $unionTypeSimple; + + /* testPHP8UnionTypesTwoClasses */ + private MyClassA|\Package\MyClassB $unionTypesTwoClasses; + + /* testPHP8UnionTypesAllBaseTypes */ + protected array|bool|int|float|NULL|object|string $unionTypesAllBaseTypes; + + /* testPHP8UnionTypesAllPseudoTypes */ + // Intentional fatal error - mixing types which cannot be combined, but that's not the concern of the method. + var false|mixed|self|parent|iterable|Resource $unionTypesAllPseudoTypes; + + /* testPHP8UnionTypesIllegalTypes */ + // Intentional fatal error - types which are not allowed for properties, but that's not the concern of the method. + public callable|static|void $unionTypesIllegalTypes; + + /* testPHP8UnionTypesNullable */ + // Intentional fatal error - nullability is not allowed with union types, but that's not the concern of the method. + public ?int|float $unionTypesNullable; + + /* testPHP8PseudoTypeNull */ + // Intentional fatal error - null pseudotype is only allowed in union types, but that's not the concern of the method. + public null $pseudoTypeNull; + + /* testPHP8PseudoTypeFalse */ + // Intentional fatal error - false pseudotype is only allowed in union types, but that's not the concern of the method. + public false $pseudoTypeFalse; + + /* testPHP8PseudoTypeFalseAndBool */ + // Intentional fatal error - false pseudotype is not allowed in combination with bool, but that's not the concern of the method. + public bool|FALSE $pseudoTypeFalseAndBool; + + /* testPHP8ObjectAndClass */ + // Intentional fatal error - object is not allowed in combination with class name, but that's not the concern of the method. + public object|ClassName $objectAndClass; + + /* testPHP8PseudoTypeIterableAndArray */ + // Intentional fatal error - iterable pseudotype is not allowed in combination with array or Traversable, but that's not the concern of the method. + public iterable|array|Traversable $pseudoTypeIterableAndArray; + + /* testPHP8DuplicateTypeInUnionWhitespaceAndComment */ + // Intentional fatal error - duplicate types are not allowed in union types, but that's not the concern of the method. + public int |string| /*comment*/ INT $duplicateTypeInUnion; + + /* testPHP81Readonly */ + public readonly int $readonly; + + /* testPHP81ReadonlyWithNullableType */ + public readonly ?array $array; + + /* testPHP81ReadonlyWithUnionType */ + public readonly string|int $readonlyWithUnionType; + + /* testPHP81ReadonlyWithUnionTypeWithNull */ + protected ReadOnly string|null $readonlyWithUnionTypeWithNull; + + /* testPHP81OnlyReadonlyWithUnionType */ + readonly string|int $onlyReadonly; +}; + +$anon = class { + /* testPHP8PropertySingleAttribute */ + #[PropertyWithAttribute] + public string $foo; + + /* testPHP8PropertyMultipleAttributes */ + #[PropertyWithAttribute(foo: 'bar'), MyAttribute] + protected ?int|float $bar; + + /* testPHP8PropertyMultilineAttribute */ + #[ + PropertyWithAttribute(/* comment */ 'baz') + ] + private mixed $baz; +}; + +enum Suit +{ + /* testEnumProperty */ + protected $anonymous; +} + +enum Direction implements ArrayAccess +{ + case Up; + case Down; + + /* testEnumMethodParamNotProperty */ + public function offsetGet($val) { ... } +} + +$anon = class() { + /* testPHP81IntersectionTypes */ + public Foo&Bar $intersectionType; + + /* testPHP81MoreIntersectionTypes */ + public Foo&Bar&Baz $moreIntersectionTypes; + + /* testPHP81IllegalIntersectionTypes */ + // Intentional fatal error - types which are not allowed for intersection type, but that's not the concern of the method. + public int&string $illegalIntersectionType; + + /* testPHP81NulltableIntersectionType */ + // Intentional fatal error - nullability is not allowed with intersection type, but that's not the concern of the method. + public ?Foo&Bar $nullableIntersectionType; +}; diff --git a/tests/Core/File/GetMemberPropertiesTest.php b/tests/Core/File/GetMemberPropertiesTest.php index a4bff22133..934d8e2890 100644 --- a/tests/Core/File/GetMemberPropertiesTest.php +++ b/tests/Core/File/GetMemberPropertiesTest.php @@ -51,6 +51,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -61,6 +62,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => false, + 'is_readonly' => false, 'type' => '?int', 'nullable_type' => true, ], @@ -71,6 +73,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -81,6 +84,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'string', 'nullable_type' => false, ], @@ -91,6 +95,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -101,6 +106,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'bool', 'nullable_type' => false, ], @@ -111,6 +117,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -121,6 +128,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'array', 'nullable_type' => false, ], @@ -131,6 +139,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -141,6 +150,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => true, + 'is_readonly' => false, 'type' => '?string', 'nullable_type' => true, ], @@ -151,6 +161,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -161,6 +172,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => false, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -171,6 +183,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -181,6 +194,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -191,6 +205,18 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, + 'type' => '', + 'nullable_type' => false, + ], + ], + [ + '/* testNoPrefix */', + [ + 'scope' => 'public', + 'scope_specified' => false, + 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -201,6 +227,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -211,6 +238,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -221,6 +249,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -231,6 +260,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'float', 'nullable_type' => false, ], @@ -241,6 +271,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => 'float', 'nullable_type' => false, ], @@ -251,6 +282,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '?string', 'nullable_type' => true, ], @@ -261,26 +293,18 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '?string', 'nullable_type' => true, ], ], - [ - '/* testNoPrefix */', - [ - 'scope' => 'public', - 'scope_specified' => false, - 'is_static' => false, - 'type' => '', - 'nullable_type' => false, - ], - ], [ '/* testGroupProtectedStatic 1 */', [ 'scope' => 'protected', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -291,6 +315,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -301,6 +326,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -311,6 +337,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -321,6 +348,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -331,6 +359,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -341,6 +370,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -351,6 +381,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -361,6 +392,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -371,6 +403,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -381,6 +414,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '?array', 'nullable_type' => true, ], @@ -391,6 +425,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '\MyNamespace\MyClass', 'nullable_type' => false, ], @@ -401,6 +436,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '?ClassName', 'nullable_type' => true, ], @@ -411,6 +447,7 @@ public function dataGetMemberProperties() 'scope' => 'protected', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '?Folder\ClassName', 'nullable_type' => true, ], @@ -421,6 +458,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '\MyNamespace\MyClass\Foo', 'nullable_type' => false, ], @@ -431,6 +469,7 @@ public function dataGetMemberProperties() 'scope' => 'private', 'scope_specified' => true, 'is_static' => true, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -445,6 +484,7 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], @@ -455,10 +495,309 @@ public function dataGetMemberProperties() 'scope' => 'public', 'scope_specified' => true, 'is_static' => false, + 'is_readonly' => false, 'type' => '', 'nullable_type' => false, ], ], + [ + '/* testPHP8MixedTypeHint */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => true, + 'is_readonly' => false, + 'type' => 'miXed', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP8MixedTypeHintNullable */', + [ + 'scope' => 'private', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => false, + 'type' => '?mixed', + 'nullable_type' => true, + ], + ], + [ + '/* testNamespaceOperatorTypeHint */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => false, + 'type' => '?namespace\Name', + 'nullable_type' => true, + ], + ], + [ + '/* testPHP8UnionTypesSimple */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => false, + 'type' => 'int|float', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP8UnionTypesTwoClasses */', + [ + 'scope' => 'private', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => false, + 'type' => 'MyClassA|\Package\MyClassB', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP8UnionTypesAllBaseTypes */', + [ + 'scope' => 'protected', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => false, + 'type' => 'array|bool|int|float|NULL|object|string', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP8UnionTypesAllPseudoTypes */', + [ + 'scope' => 'public', + 'scope_specified' => false, + 'is_static' => false, + 'is_readonly' => false, + 'type' => 'false|mixed|self|parent|iterable|Resource', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP8UnionTypesIllegalTypes */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => false, + // Missing static, but that's OK as not an allowed syntax. + 'type' => 'callable||void', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP8UnionTypesNullable */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => false, + 'type' => '?int|float', + 'nullable_type' => true, + ], + ], + [ + '/* testPHP8PseudoTypeNull */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => false, + 'type' => 'null', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP8PseudoTypeFalse */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => false, + 'type' => 'false', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP8PseudoTypeFalseAndBool */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => false, + 'type' => 'bool|FALSE', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP8ObjectAndClass */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => false, + 'type' => 'object|ClassName', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP8PseudoTypeIterableAndArray */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => false, + 'type' => 'iterable|array|Traversable', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP8DuplicateTypeInUnionWhitespaceAndComment */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => false, + 'type' => 'int|string|INT', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP81Readonly */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => true, + 'type' => 'int', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP81ReadonlyWithNullableType */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => true, + 'type' => '?array', + 'nullable_type' => true, + ], + ], + [ + '/* testPHP81ReadonlyWithUnionType */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => true, + 'type' => 'string|int', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP81ReadonlyWithUnionTypeWithNull */', + [ + 'scope' => 'protected', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => true, + 'type' => 'string|null', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP81OnlyReadonlyWithUnionType */', + [ + 'scope' => 'public', + 'scope_specified' => false, + 'is_static' => false, + 'is_readonly' => true, + 'type' => 'string|int', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP8PropertySingleAttribute */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => false, + 'type' => 'string', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP8PropertyMultipleAttributes */', + [ + 'scope' => 'protected', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => false, + 'type' => '?int|float', + 'nullable_type' => true, + ], + ], + [ + '/* testPHP8PropertyMultilineAttribute */', + [ + 'scope' => 'private', + 'scope_specified' => true, + 'is_static' => false, + 'is_readonly' => false, + 'type' => 'mixed', + 'nullable_type' => false, + ], + ], + [ + '/* testEnumProperty */', + [], + ], + [ + '/* testPHP81IntersectionTypes */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'type' => 'Foo&Bar', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP81MoreIntersectionTypes */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'type' => 'Foo&Bar&Baz', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP81IllegalIntersectionTypes */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'type' => 'int&string', + 'nullable_type' => false, + ], + ], + [ + '/* testPHP81NulltableIntersectionType */', + [ + 'scope' => 'public', + 'scope_specified' => true, + 'is_static' => false, + 'type' => '?Foo&Bar', + 'nullable_type' => true, + ], + ], ]; }//end dataGetMemberProperties() @@ -500,6 +839,7 @@ public function dataNotClassProperty() ['/* testGlobalVariable */'], ['/* testNestedMethodParam 1 */'], ['/* testNestedMethodParam 2 */'], + ['/* testEnumMethodParamNotProperty */'], ]; }//end dataNotClassProperty() diff --git a/tests/Core/File/GetMethodParametersTest.inc b/tests/Core/File/GetMethodParametersTest.inc index 86eff8dd4a..c0ef6e0621 100644 --- a/tests/Core/File/GetMethodParametersTest.inc +++ b/tests/Core/File/GetMethodParametersTest.inc @@ -31,3 +31,139 @@ function myFunction($a = 10 & 20) {} /* testArrowFunction */ fn(int $a, ...$b) => $b; + +/* testPHP8MixedTypeHint */ +function mixedTypeHint(mixed &...$var1) {} + +/* testPHP8MixedTypeHintNullable */ +// Intentional fatal error - nullability is not allowed with mixed, but that's not the concern of the method. +function mixedTypeHintNullable(?Mixed $var1) {} + +/* testNamespaceOperatorTypeHint */ +function namespaceOperatorTypeHint(?namespace\Name $var1) {} + +/* testPHP8UnionTypesSimple */ +function unionTypeSimple(int|float $number, self|parent &...$obj) {} + +/* testPHP8UnionTypesWithSpreadOperatorAndReference */ +function globalFunctionWithSpreadAndReference(float|null &$paramA, string|int ...$paramB) {} + +/* testPHP8UnionTypesSimpleWithBitwiseOrInDefault */ +$fn = fn(int|float $var = CONSTANT_A | CONSTANT_B) => $var; + +/* testPHP8UnionTypesTwoClasses */ +function unionTypesTwoClasses(MyClassA|\Package\MyClassB $var) {} + +/* testPHP8UnionTypesAllBaseTypes */ +function unionTypesAllBaseTypes(array|bool|callable|int|float|null|object|string $var) {} + +/* testPHP8UnionTypesAllPseudoTypes */ +// Intentional fatal error - mixing types which cannot be combined, but that's not the concern of the method. +function unionTypesAllPseudoTypes(false|mixed|self|parent|iterable|Resource $var) {} + +/* testPHP8UnionTypesNullable */ +// Intentional fatal error - nullability is not allowed with union types, but that's not the concern of the method. +$closure = function (?int|float $number) {}; + +/* testPHP8PseudoTypeNull */ +// Intentional fatal error - null pseudotype is only allowed in union types, but that's not the concern of the method. +function pseudoTypeNull(null $var = null) {} + +/* testPHP8PseudoTypeFalse */ +// Intentional fatal error - false pseudotype is only allowed in union types, but that's not the concern of the method. +function pseudoTypeFalse(false $var = false) {} + +/* testPHP8PseudoTypeFalseAndBool */ +// Intentional fatal error - false pseudotype is not allowed in combination with bool, but that's not the concern of the method. +function pseudoTypeFalseAndBool(bool|false $var = false) {} + +/* testPHP8ObjectAndClass */ +// Intentional fatal error - object is not allowed in combination with class name, but that's not the concern of the method. +function objectAndClass(object|ClassName $var) {} + +/* testPHP8PseudoTypeIterableAndArray */ +// Intentional fatal error - iterable pseudotype is not allowed in combination with array or Traversable, but that's not the concern of the method. +function pseudoTypeIterableAndArray(iterable|array|Traversable $var) {} + +/* testPHP8DuplicateTypeInUnionWhitespaceAndComment */ +// Intentional fatal error - duplicate types are not allowed in union types, but that's not the concern of the method. +function duplicateTypeInUnion( int | string /*comment*/ | INT $var) {} + +class ConstructorPropertyPromotionNoTypes { + /* testPHP8ConstructorPropertyPromotionNoTypes */ + public function __construct( + public $x = 0.0, + protected $y = '', + private $z = null, + ) {} +} + +class ConstructorPropertyPromotionWithTypes { + /* testPHP8ConstructorPropertyPromotionWithTypes */ + public function __construct(protected float|int $x, public ?string &$y = 'test', private mixed $z) {} +} + +class ConstructorPropertyPromotionAndNormalParams { + /* testPHP8ConstructorPropertyPromotionAndNormalParam */ + public function __construct(public int $promotedProp, ?int $normalArg) {} +} + +class ConstructorPropertyPromotionWithReadOnly { + /* testPHP81ConstructorPropertyPromotionWithReadOnly */ + public function __construct(public readonly ?int $promotedProp, readonly private string|bool &$promotedToo) {} +} + +class ConstructorPropertyPromotionWithOnlyReadOnly { + /* testPHP81ConstructorPropertyPromotionWithOnlyReadOnly */ + public function __construct(readonly Foo&Bar $promotedProp, readonly ?bool $promotedToo,) {} +} + +/* testPHP8ConstructorPropertyPromotionGlobalFunction */ +// Intentional fatal error. Property promotion not allowed in non-constructor, but that's not the concern of this method. +function globalFunction(private $x) {} + +abstract class ConstructorPropertyPromotionAbstractMethod { + /* testPHP8ConstructorPropertyPromotionAbstractMethod */ + // Intentional fatal error. + // 1. Property promotion not allowed in abstract method, but that's not the concern of this method. + // 2. Variadic arguments not allowed in property promotion, but that's not the concern of this method. + // 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, + ) {} +} + +/* testPHP8IntersectionTypes */ +function intersectionTypes(Foo&Bar $obj1, Boo&Bar $obj2) {} + +/* testPHP81IntersectionTypesWithSpreadOperatorAndReference */ +function globalFunctionWithSpreadAndReference(Boo&Bar &$paramA, Foo&Bar ...$paramB) {} + +/* testPHP81MoreIntersectionTypes */ +function moreIntersectionTypes(MyClassA&\Package\MyClassB&\Package\MyClassC $var) {} + +/* testPHP81IllegalIntersectionTypes */ +// Intentional fatal error - simple types are not allowed with intersection types, but that's not the concern of the method. +$closure = function (string&int $numeric_string) {}; + +/* testPHP81NullableIntersectionTypes */ +// Intentional fatal error - nullability is not allowed with intersection types, but that's not the concern of the method. +$closure = function (?Foo&Bar $object) {}; diff --git a/tests/Core/File/GetMethodParametersTest.php b/tests/Core/File/GetMethodParametersTest.php index 1e8595da5c..b5bd61813c 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', @@ -59,6 +61,87 @@ public function testArrayHint() }//end testArrayHint() + /** + * Verify variable. + * + * @return void + */ + public function testVariable() + { + $expected = []; + $expected[0] = [ + 'name' => '$var', + 'content' => '$var', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => '', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testVariable() + + + /** + * Verify default value parsing with a single function param. + * + * @return void + */ + public function testSingleDefaultValue() + { + $expected = []; + $expected[0] = [ + 'name' => '$var1', + 'content' => '$var1=self::CONSTANT', + 'has_attributes' => false, + 'default' => 'self::CONSTANT', + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => '', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testSingleDefaultValue() + + + /** + * Verify default value parsing. + * + * @return void + */ + public function testDefaultValues() + { + $expected = []; + $expected[0] = [ + 'name' => '$var1', + 'content' => '$var1=1', + 'has_attributes' => false, + 'default' => '1', + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => '', + 'nullable_type' => false, + ]; + $expected[1] = [ + 'name' => '$var2', + 'content' => "\$var2='value'", + 'has_attributes' => false, + 'default' => "'value'", + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => '', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testDefaultValues() + + /** * Verify type hint parsing. * @@ -70,6 +153,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 +163,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 +186,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 +209,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 +219,7 @@ public function testNullableTypeHint() $expected[1] = [ 'name' => '$var2', 'content' => '?\bar $var2', + 'has_attributes' => false, 'pass_by_reference' => false, 'variable_length' => false, 'type_hint' => '?\bar', @@ -144,16 +232,18 @@ public function testNullableTypeHint() /** - * Verify variable. + * Verify "bitwise and" in default value !== pass-by-reference. * * @return void */ - public function testVariable() + public function testBitwiseAndConstantExpressionDefaultValue() { $expected = []; $expected[0] = [ - 'name' => '$var', - 'content' => '$var', + 'name' => '$a', + 'content' => '$a = 10 & 20', + 'default' => '10 & 20', + 'has_attributes' => false, 'pass_by_reference' => false, 'variable_length' => false, 'type_hint' => '', @@ -162,116 +252,913 @@ public function testVariable() $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); - }//end testVariable() + }//end testBitwiseAndConstantExpressionDefaultValue() /** - * Verify default value parsing with a single function param. + * Verify that arrow functions are supported. * * @return void */ - public function testSingleDefaultValue() + public function testArrowFunction() { $expected = []; $expected[0] = [ - 'name' => '$var1', - 'content' => '$var1=self::CONSTANT', - 'default' => 'self::CONSTANT', + 'name' => '$a', + 'content' => 'int $a', + 'has_attributes' => false, 'pass_by_reference' => false, 'variable_length' => false, + 'type_hint' => 'int', + 'nullable_type' => false, + ]; + + $expected[1] = [ + 'name' => '$b', + 'content' => '...$b', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => true, 'type_hint' => '', 'nullable_type' => false, ]; $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); - }//end testSingleDefaultValue() + }//end testArrowFunction() /** - * Verify default value parsing. + * Verify recognition of PHP8 mixed type declaration. * * @return void */ - public function testDefaultValues() + public function testPHP8MixedTypeHint() { $expected = []; $expected[0] = [ 'name' => '$var1', - 'content' => '$var1=1', - 'default' => '1', + 'content' => 'mixed &...$var1', + 'has_attributes' => false, + 'pass_by_reference' => true, + 'variable_length' => true, + 'type_hint' => 'mixed', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8MixedTypeHint() + + + /** + * Verify recognition of PHP8 mixed type declaration with nullability. + * + * @return void + */ + public function testPHP8MixedTypeHintNullable() + { + $expected = []; + $expected[0] = [ + 'name' => '$var1', + 'content' => '?Mixed $var1', + 'has_attributes' => false, 'pass_by_reference' => false, 'variable_length' => false, - 'type_hint' => '', + 'type_hint' => '?Mixed', + 'nullable_type' => true, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8MixedTypeHintNullable() + + + /** + * Verify recognition of type declarations using the namespace operator. + * + * @return void + */ + public function testNamespaceOperatorTypeHint() + { + $expected = []; + $expected[0] = [ + 'name' => '$var1', + 'content' => '?namespace\Name $var1', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => '?namespace\Name', + 'nullable_type' => true, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testNamespaceOperatorTypeHint() + + + /** + * Verify recognition of PHP8 union type declaration. + * + * @return void + */ + public function testPHP8UnionTypesSimple() + { + $expected = []; + $expected[0] = [ + 'name' => '$number', + 'content' => 'int|float $number', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => 'int|float', 'nullable_type' => false, ]; $expected[1] = [ - 'name' => '$var2', - 'content' => "\$var2='value'", - 'default' => "'value'", + 'name' => '$obj', + 'content' => 'self|parent &...$obj', + 'has_attributes' => false, + 'pass_by_reference' => true, + 'variable_length' => true, + 'type_hint' => 'self|parent', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8UnionTypesSimple() + + + /** + * Verify recognition of PHP8 union type declaration when the variable has either a spread operator or a reference. + * + * @return void + */ + public function testPHP8UnionTypesWithSpreadOperatorAndReference() + { + $expected = []; + $expected[0] = [ + 'name' => '$paramA', + 'content' => 'float|null &$paramA', + 'has_attributes' => false, + 'pass_by_reference' => true, + 'variable_length' => false, + 'type_hint' => 'float|null', + 'nullable_type' => false, + ]; + $expected[1] = [ + 'name' => '$paramB', + 'content' => 'string|int ...$paramB', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => true, + 'type_hint' => 'string|int', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8UnionTypesWithSpreadOperatorAndReference() + + + /** + * Verify recognition of PHP8 union type declaration with a bitwise or in the default value. + * + * @return void + */ + public function testPHP8UnionTypesSimpleWithBitwiseOrInDefault() + { + $expected = []; + $expected[0] = [ + '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' => '', + 'type_hint' => 'int|float', 'nullable_type' => false, ]; $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); - }//end testDefaultValues() + }//end testPHP8UnionTypesSimpleWithBitwiseOrInDefault() /** - * Verify "bitwise and" in default value !== pass-by-reference. + * Verify recognition of PHP8 union type declaration with two classes. * * @return void */ - public function testBitwiseAndConstantExpressionDefaultValue() + public function testPHP8UnionTypesTwoClasses() { $expected = []; $expected[0] = [ - 'name' => '$a', - 'content' => '$a = 10 & 20', - 'default' => '10 & 20', + 'name' => '$var', + 'content' => 'MyClassA|\Package\MyClassB $var', + 'has_attributes' => false, 'pass_by_reference' => false, 'variable_length' => false, - 'type_hint' => '', + 'type_hint' => 'MyClassA|\Package\MyClassB', 'nullable_type' => false, ]; $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); - }//end testBitwiseAndConstantExpressionDefaultValue() + }//end testPHP8UnionTypesTwoClasses() /** - * Verify that arrow functions are supported. + * Verify recognition of PHP8 union type declaration with all base types. * * @return void */ - public function testArrowFunction() + public function testPHP8UnionTypesAllBaseTypes() { $expected = []; $expected[0] = [ - 'name' => '$a', - 'content' => 'int $a', + 'name' => '$var', + 'content' => 'array|bool|callable|int|float|null|object|string $var', + 'has_attributes' => false, 'pass_by_reference' => false, 'variable_length' => false, - 'type_hint' => 'int', + 'type_hint' => 'array|bool|callable|int|float|null|object|string', 'nullable_type' => false, ]; - $expected[1] = [ - 'name' => '$b', - 'content' => '...$b', + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8UnionTypesAllBaseTypes() + + + /** + * Verify recognition of PHP8 union type declaration with all pseudo types. + * + * @return void + */ + public function testPHP8UnionTypesAllPseudoTypes() + { + $expected = []; + $expected[0] = [ + 'name' => '$var', + 'content' => 'false|mixed|self|parent|iterable|Resource $var', + 'has_attributes' => false, 'pass_by_reference' => false, - 'variable_length' => true, - 'type_hint' => '', + 'variable_length' => false, + 'type_hint' => 'false|mixed|self|parent|iterable|Resource', 'nullable_type' => false, ]; $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); - }//end testArrowFunction() + }//end testPHP8UnionTypesAllPseudoTypes() + + + /** + * Verify recognition of PHP8 union type declaration with (illegal) nullability. + * + * @return void + */ + public function testPHP8UnionTypesNullable() + { + $expected = []; + $expected[0] = [ + 'name' => '$number', + 'content' => '?int|float $number', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => '?int|float', + 'nullable_type' => true, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8UnionTypesNullable() + + + /** + * Verify recognition of PHP8 type declaration with (illegal) single type null. + * + * @return void + */ + public function testPHP8PseudoTypeNull() + { + $expected = []; + $expected[0] = [ + 'name' => '$var', + 'content' => 'null $var = null', + 'default' => 'null', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => 'null', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8PseudoTypeNull() + + + /** + * Verify recognition of PHP8 type declaration with (illegal) single type false. + * + * @return void + */ + public function testPHP8PseudoTypeFalse() + { + $expected = []; + $expected[0] = [ + 'name' => '$var', + 'content' => 'false $var = false', + 'default' => 'false', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => 'false', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8PseudoTypeFalse() + + + /** + * Verify recognition of PHP8 type declaration with (illegal) type false combined with type bool. + * + * @return void + */ + public function testPHP8PseudoTypeFalseAndBool() + { + $expected = []; + $expected[0] = [ + 'name' => '$var', + 'content' => 'bool|false $var = false', + 'default' => 'false', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => 'bool|false', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8PseudoTypeFalseAndBool() + + + /** + * Verify recognition of PHP8 type declaration with (illegal) type object combined with a class name. + * + * @return void + */ + public function testPHP8ObjectAndClass() + { + $expected = []; + $expected[0] = [ + 'name' => '$var', + 'content' => 'object|ClassName $var', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => 'object|ClassName', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8ObjectAndClass() + + + /** + * Verify recognition of PHP8 type declaration with (illegal) type iterable combined with array/Traversable. + * + * @return void + */ + public function testPHP8PseudoTypeIterableAndArray() + { + $expected = []; + $expected[0] = [ + 'name' => '$var', + 'content' => 'iterable|array|Traversable $var', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => 'iterable|array|Traversable', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8PseudoTypeIterableAndArray() + + + /** + * Verify recognition of PHP8 type declaration with (illegal) duplicate types. + * + * @return void + */ + public function testPHP8DuplicateTypeInUnionWhitespaceAndComment() + { + $expected = []; + $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', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8DuplicateTypeInUnionWhitespaceAndComment() + + + /** + * Verify recognition of PHP8 constructor property promotion without type declaration, with defaults. + * + * @return void + */ + public function testPHP8ConstructorPropertyPromotionNoTypes() + { + $expected = []; + $expected[0] = [ + 'name' => '$x', + 'content' => 'public $x = 0.0', + 'default' => '0.0', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => '', + 'nullable_type' => false, + 'property_visibility' => 'public', + 'property_readonly' => false, + ]; + $expected[1] = [ + 'name' => '$y', + 'content' => 'protected $y = \'\'', + 'default' => "''", + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => '', + 'nullable_type' => false, + 'property_visibility' => 'protected', + 'property_readonly' => false, + ]; + $expected[2] = [ + 'name' => '$z', + 'content' => 'private $z = null', + 'default' => 'null', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => '', + 'nullable_type' => false, + 'property_visibility' => 'private', + 'property_readonly' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8ConstructorPropertyPromotionNoTypes() + + + /** + * Verify recognition of PHP8 constructor property promotion with type declarations. + * + * @return void + */ + public function testPHP8ConstructorPropertyPromotionWithTypes() + { + $expected = []; + $expected[0] = [ + 'name' => '$x', + 'content' => 'protected float|int $x', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => 'float|int', + 'nullable_type' => false, + 'property_visibility' => 'protected', + 'property_readonly' => false, + ]; + $expected[1] = [ + 'name' => '$y', + 'content' => 'public ?string &$y = \'test\'', + 'default' => "'test'", + 'has_attributes' => false, + 'pass_by_reference' => true, + 'variable_length' => false, + 'type_hint' => '?string', + 'nullable_type' => true, + 'property_visibility' => 'public', + 'property_readonly' => false, + ]; + $expected[2] = [ + 'name' => '$z', + 'content' => 'private mixed $z', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => 'mixed', + 'nullable_type' => false, + 'property_visibility' => 'private', + 'property_readonly' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8ConstructorPropertyPromotionWithTypes() + + + /** + * Verify recognition of PHP8 constructor with both property promotion as well as normal parameters. + * + * @return void + */ + public function testPHP8ConstructorPropertyPromotionAndNormalParam() + { + $expected = []; + $expected[0] = [ + 'name' => '$promotedProp', + 'content' => 'public int $promotedProp', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => 'int', + 'nullable_type' => false, + 'property_visibility' => 'public', + 'property_readonly' => false, + ]; + $expected[1] = [ + 'name' => '$normalArg', + 'content' => '?int $normalArg', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => '?int', + 'nullable_type' => true, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8ConstructorPropertyPromotionAndNormalParam() + + + /** + * Verify recognition of PHP8 constructor with property promotion using PHP 8.1 readonly keyword. + * + * @return void + */ + public function testPHP81ConstructorPropertyPromotionWithReadOnly() + { + $expected = []; + $expected[0] = [ + 'name' => '$promotedProp', + 'content' => 'public readonly ?int $promotedProp', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => '?int', + 'nullable_type' => true, + 'property_visibility' => 'public', + 'property_readonly' => true, + ]; + $expected[1] = [ + 'name' => '$promotedToo', + 'content' => 'readonly private string|bool &$promotedToo', + 'has_attributes' => false, + 'pass_by_reference' => true, + 'variable_length' => false, + 'type_hint' => 'string|bool', + 'nullable_type' => false, + 'property_visibility' => 'private', + 'property_readonly' => true, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81ConstructorPropertyPromotionWithReadOnly() + + + /** + * Verify recognition of PHP8 constructor with property promotion using PHP 8.1 readonly + * keyword without explicit visibility. + * + * @return void + */ + public function testPHP81ConstructorPropertyPromotionWithOnlyReadOnly() + { + $expected = []; + $expected[0] = [ + 'name' => '$promotedProp', + 'content' => 'readonly Foo&Bar $promotedProp', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => 'Foo&Bar', + 'nullable_type' => false, + 'property_visibility' => 'public', + 'property_readonly' => true, + ]; + $expected[1] = [ + 'name' => '$promotedToo', + 'content' => 'readonly ?bool $promotedToo', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => '?bool', + 'nullable_type' => true, + 'property_visibility' => 'public', + 'property_readonly' => true, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81ConstructorPropertyPromotionWithOnlyReadOnly() + + + /** + * Verify behaviour when a non-constructor function uses PHP 8 property promotion syntax. + * + * @return void + */ + public function testPHP8ConstructorPropertyPromotionGlobalFunction() + { + $expected = []; + $expected[0] = [ + 'name' => '$x', + 'content' => 'private $x', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => '', + 'nullable_type' => false, + 'property_visibility' => 'private', + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8ConstructorPropertyPromotionGlobalFunction() + + + /** + * Verify behaviour when an abstract constructor uses PHP 8 property promotion syntax. + * + * @return void + */ + public function testPHP8ConstructorPropertyPromotionAbstractMethod() + { + $expected = []; + $expected[0] = [ + 'name' => '$y', + 'content' => 'public callable $y', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => 'callable', + 'nullable_type' => false, + 'property_visibility' => 'public', + ]; + $expected[1] = [ + 'name' => '$x', + 'content' => 'private ...$x', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => true, + 'type_hint' => '', + 'nullable_type' => false, + 'property_visibility' => 'private', + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//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() + + + /** + * Verify recognition of PHP8.1 intersection type declaration. + * + * @return void + */ + public function testPHP8IntersectionTypes() + { + $expected = []; + $expected[0] = [ + 'name' => '$obj1', + 'content' => 'Foo&Bar $obj1', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => 'Foo&Bar', + 'nullable_type' => false, + ]; + $expected[1] = [ + 'name' => '$obj2', + 'content' => 'Boo&Bar $obj2', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => 'Boo&Bar', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8IntersectionTypes() + + + /** + * Verify recognition of PHP8 intersection type declaration when the variable has either a spread operator or a reference. + * + * @return void + */ + public function testPHP81IntersectionTypesWithSpreadOperatorAndReference() + { + $expected = []; + $expected[0] = [ + 'name' => '$paramA', + 'content' => 'Boo&Bar &$paramA', + 'has_attributes' => false, + 'pass_by_reference' => true, + 'variable_length' => false, + 'type_hint' => 'Boo&Bar', + 'nullable_type' => false, + ]; + $expected[1] = [ + 'name' => '$paramB', + 'content' => 'Foo&Bar ...$paramB', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => true, + 'type_hint' => 'Foo&Bar', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81IntersectionTypesWithSpreadOperatorAndReference() + + + /** + * Verify recognition of PHP8.1 intersection type declaration with more types. + * + * @return void + */ + public function testPHP81MoreIntersectionTypes() + { + $expected = []; + $expected[0] = [ + 'name' => '$var', + 'content' => 'MyClassA&\Package\MyClassB&\Package\MyClassC $var', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => 'MyClassA&\Package\MyClassB&\Package\MyClassC', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81MoreIntersectionTypes() + + + /** + * Verify recognition of PHP8.1 intersection type declaration with illegal simple types. + * + * @return void + */ + public function testPHP81IllegalIntersectionTypes() + { + $expected = []; + $expected[0] = [ + 'name' => '$numeric_string', + 'content' => 'string&int $numeric_string', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => 'string&int', + 'nullable_type' => false, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81IllegalIntersectionTypes() + + + /** + * Verify recognition of PHP8.1 intersection type declaration with (illegal) nullability. + * + * @return void + */ + public function testPHP81NullableIntersectionTypes() + { + $expected = []; + $expected[0] = [ + 'name' => '$object', + 'content' => '?Foo&Bar $object', + 'has_attributes' => false, + 'pass_by_reference' => false, + 'variable_length' => false, + 'type_hint' => '?Foo&Bar', + 'nullable_type' => true, + ]; + + $this->getMethodParametersTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81NullableIntersectionTypes() /** @@ -284,7 +1171,7 @@ public function testArrowFunction() */ private function getMethodParametersTestHelper($commentString, $expected) { - $function = $this->getTargetToken($commentString, [T_FUNCTION, T_FN]); + $function = $this->getTargetToken($commentString, [T_FUNCTION, T_CLOSURE, T_FN]); $found = self::$phpcsFile->getMethodParameters($function); $this->assertArraySubset($expected, $found, true); diff --git a/tests/Core/File/GetMethodPropertiesTest.inc b/tests/Core/File/GetMethodPropertiesTest.inc index b04202fcd2..0c592369a5 100644 --- a/tests/Core/File/GetMethodPropertiesTest.inc +++ b/tests/Core/File/GetMethodPropertiesTest.inc @@ -33,7 +33,7 @@ class MyClass { /* testMessyNullableReturnMethod */ public function myFunction() /* comment - */ : + */ : /* comment */ ? //comment array {} @@ -73,3 +73,80 @@ class ReturnMe { return $this; } } + +/* testPHP8MixedTypeHint */ +function mixedTypeHint() :mixed {} + +/* testPHP8MixedTypeHintNullable */ +// Intentional fatal error - nullability is not allowed with mixed, but that's not the concern of the method. +function mixedTypeHintNullable(): ?mixed {} + +/* testNamespaceOperatorTypeHint */ +function namespaceOperatorTypeHint() : ?namespace\Name {} + +/* testPHP8UnionTypesSimple */ +function unionTypeSimple($number) : int|float {} + +/* testPHP8UnionTypesTwoClasses */ +$fn = fn($var): MyClassA|\Package\MyClassB => $var; + +/* testPHP8UnionTypesAllBaseTypes */ +function unionTypesAllBaseTypes() : array|bool|callable|int|float|null|Object|string {} + +/* testPHP8UnionTypesAllPseudoTypes */ +// Intentional fatal error - mixing types which cannot be combined, but that's not the concern of the method. +function unionTypesAllPseudoTypes($var) : false|MIXED|self|parent|static|iterable|Resource|void {} + +/* testPHP8UnionTypesNullable */ +// Intentional fatal error - nullability is not allowed with union types, but that's not the concern of the method. +$closure = function () use($a) :?int|float {}; + +/* testPHP8PseudoTypeNull */ +// Intentional fatal error - null pseudotype is only allowed in union types, but that's not the concern of the method. +function pseudoTypeNull(): null {} + +/* testPHP8PseudoTypeFalse */ +// Intentional fatal error - false pseudotype is only allowed in union types, but that's not the concern of the method. +function pseudoTypeFalse(): false {} + +/* testPHP8PseudoTypeFalseAndBool */ +// Intentional fatal error - false pseudotype is not allowed in combination with bool, but that's not the concern of the method. +function pseudoTypeFalseAndBool(): bool|false {} + +/* testPHP8ObjectAndClass */ +// Intentional fatal error - object is not allowed in combination with class name, but that's not the concern of the method. +function objectAndClass(): object|ClassName {} + +/* testPHP8PseudoTypeIterableAndArray */ +// Intentional fatal error - iterable pseudotype is not allowed in combination with array or Traversable, but that's not the concern of the method. +interface FooBar { + public function pseudoTypeIterableAndArray(): iterable|array|Traversable; +} + +/* testPHP8DuplicateTypeInUnionWhitespaceAndComment */ +// Intentional fatal error - duplicate types are not allowed in union types, but that's not the concern of the method. +function duplicateTypeInUnion(): int | /*comment*/ string | INT {} + +/* testPHP81NeverType */ +function never(): never {} + +/* testPHP81NullableNeverType */ +// Intentional fatal error - nullability is not allowed with never, but that's not the concern of the method. +function nullableNever(): ?never {} + +/* testPHP8IntersectionTypes */ +function intersectionTypes(): Foo&Bar {} + +/* testPHP81MoreIntersectionTypes */ +function moreIntersectionTypes(): MyClassA&\Package\MyClassB&\Package\MyClassC {} + +/* testPHP81IntersectionArrowFunction */ +$fn = fn($var): MyClassA&\Package\MyClassB => $var; + +/* testPHP81IllegalIntersectionTypes */ +// Intentional fatal error - simple types are not allowed with intersection types, but that's not the concern of the method. +$closure = function (): string&int {}; + +/* testPHP81NullableIntersectionTypes */ +// Intentional fatal error - nullability is not allowed with intersection types, but that's not the concern of the method. +$closure = function (): ?Foo&Bar {}; diff --git a/tests/Core/File/GetMethodPropertiesTest.php b/tests/Core/File/GetMethodPropertiesTest.php index b801962637..66f4eea3ea 100644 --- a/tests/Core/File/GetMethodPropertiesTest.php +++ b/tests/Core/File/GetMethodPropertiesTest.php @@ -406,6 +406,489 @@ public function testReturnTypeStatic() }//end testReturnTypeStatic() + /** + * Test a function with return type "mixed". + * + * @return void + */ + public function testPHP8MixedTypeHint() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'mixed', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8MixedTypeHint() + + + /** + * Test a function with return type "mixed" and nullability. + * + * @return void + */ + public function testPHP8MixedTypeHintNullable() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => '?mixed', + 'nullable_return_type' => true, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8MixedTypeHintNullable() + + + /** + * Test a function with return type using the namespace operator. + * + * @return void + */ + public function testNamespaceOperatorTypeHint() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => '?namespace\Name', + 'nullable_return_type' => true, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testNamespaceOperatorTypeHint() + + + /** + * Verify recognition of PHP8 union type declaration. + * + * @return void + */ + public function testPHP8UnionTypesSimple() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'int|float', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8UnionTypesSimple() + + + /** + * Verify recognition of PHP8 union type declaration with two classes. + * + * @return void + */ + public function testPHP8UnionTypesTwoClasses() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'MyClassA|\Package\MyClassB', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8UnionTypesTwoClasses() + + + /** + * Verify recognition of PHP8 union type declaration with all base types. + * + * @return void + */ + public function testPHP8UnionTypesAllBaseTypes() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'array|bool|callable|int|float|null|Object|string', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8UnionTypesAllBaseTypes() + + + /** + * Verify recognition of PHP8 union type declaration with all pseudo types. + * + * @return void + */ + public function testPHP8UnionTypesAllPseudoTypes() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'false|MIXED|self|parent|static|iterable|Resource|void', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8UnionTypesAllPseudoTypes() + + + /** + * Verify recognition of PHP8 union type declaration with (illegal) nullability. + * + * @return void + */ + public function testPHP8UnionTypesNullable() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => '?int|float', + 'nullable_return_type' => true, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8UnionTypesNullable() + + + /** + * Verify recognition of PHP8 type declaration with (illegal) single type null. + * + * @return void + */ + public function testPHP8PseudoTypeNull() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'null', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8PseudoTypeNull() + + + /** + * Verify recognition of PHP8 type declaration with (illegal) single type false. + * + * @return void + */ + public function testPHP8PseudoTypeFalse() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'false', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8PseudoTypeFalse() + + + /** + * Verify recognition of PHP8 type declaration with (illegal) type false combined with type bool. + * + * @return void + */ + public function testPHP8PseudoTypeFalseAndBool() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'bool|false', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8PseudoTypeFalseAndBool() + + + /** + * Verify recognition of PHP8 type declaration with (illegal) type object combined with a class name. + * + * @return void + */ + public function testPHP8ObjectAndClass() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'object|ClassName', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8ObjectAndClass() + + + /** + * Verify recognition of PHP8 type declaration with (illegal) type iterable combined with array/Traversable. + * + * @return void + */ + public function testPHP8PseudoTypeIterableAndArray() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => true, + 'return_type' => 'iterable|array|Traversable', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => false, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8PseudoTypeIterableAndArray() + + + /** + * Verify recognition of PHP8 type declaration with (illegal) duplicate types. + * + * @return void + */ + public function testPHP8DuplicateTypeInUnionWhitespaceAndComment() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'int|string|INT', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8DuplicateTypeInUnionWhitespaceAndComment() + + + /** + * Verify recognition of PHP8.1 type "never". + * + * @return void + */ + public function testPHP81NeverType() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'never', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81NeverType() + + + /** + * Verify recognition of PHP8.1 type "never" with (illegal) nullability. + * + * @return void + */ + public function testPHP81NullableNeverType() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => '?never', + 'nullable_return_type' => true, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81NullableNeverType() + + + /** + * Verify recognition of PHP8.1 intersection type declaration. + * + * @return void + */ + public function testPHP8IntersectionTypes() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'Foo&Bar', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP8IntersectionTypes() + + + /** + * Verify recognition of PHP8.1 intersection type declaration with more types. + * + * @return void + */ + public function testPHP81MoreIntersectionTypes() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'MyClassA&\Package\MyClassB&\Package\MyClassC', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81MoreIntersectionTypes() + + + /** + * Verify recognition of PHP8.1 intersection type declaration in arrow function. + * + * @return void + */ + public function testPHP81IntersectionArrowFunction() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'MyClassA&\Package\MyClassB', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81IntersectionArrowFunction() + + + /** + * Verify recognition of PHP8.1 intersection type declaration with illegal simple types. + * + * @return void + */ + public function testPHP81IllegalIntersectionTypes() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => 'string&int', + 'nullable_return_type' => false, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81IllegalIntersectionTypes() + + + /** + * Verify recognition of PHP8.1 intersection type declaration with (illegal) nullability. + * + * @return void + */ + public function testPHP81NullableIntersectionTypes() + { + $expected = [ + 'scope' => 'public', + 'scope_specified' => false, + 'return_type' => '?Foo&Bar', + 'nullable_return_type' => true, + 'is_abstract' => false, + 'is_final' => false, + 'is_static' => false, + 'has_body' => true, + ]; + + $this->getMethodPropertiesTestHelper('/* '.__FUNCTION__.' */', $expected); + + }//end testPHP81NullableIntersectionTypes() + + /** * Test helper. * diff --git a/tests/Core/File/IsReferenceTest.inc b/tests/Core/File/IsReferenceTest.inc index c8f5dc2243..f71e2639d8 100644 --- a/tests/Core/File/IsReferenceTest.inc +++ b/tests/Core/File/IsReferenceTest.inc @@ -12,7 +12,7 @@ $a = [ $something & $somethingElse ]; $a = [ $first, $something & self::$somethingElse ]; /* testBitwiseAndD */ -$a = array $first, $something & $somethingElse ); +$a = array( $first, $something & $somethingElse ); /* testBitwiseAndE */ $a = [ 'a' => $first, 'b' => $something & $somethingElse ]; @@ -51,7 +51,7 @@ function myFunction(array &$one) {} $closure = function (\MyClass &$one) {}; /* testFunctionPassByReferenceG */ -$closure = function myFunc($param, &...$moreParams) {}; +$closure = function ($param, &...$moreParams) {}; /* testForeachValueByReference */ foreach( $array as $key => &$value ) {} @@ -140,5 +140,11 @@ $closure = function() use (&$var){}; /* testArrowFunctionReturnByReference */ fn&($x) => $x; +/* testArrowFunctionPassByReferenceA */ +$fn = fn(array &$one) => 1; + +/* testArrowFunctionPassByReferenceB */ +$fn = fn($param, &...$moreParams) => 1; + /* testClosureReturnByReference */ $closure = function &($param) use ($value) {}; diff --git a/tests/Core/File/IsReferenceTest.php b/tests/Core/File/IsReferenceTest.php index d6c37c6c1f..ea2dddbad3 100644 --- a/tests/Core/File/IsReferenceTest.php +++ b/tests/Core/File/IsReferenceTest.php @@ -16,7 +16,7 @@ class IsReferenceTest extends AbstractMethodUnitTest /** - * Test a class that extends another. + * Test correctly identifying whether a "bitwise and" token is a reference or not. * * @param string $identifier Comment which precedes the test case. * @param bool $expected Expected function output. @@ -228,6 +228,14 @@ public function dataIsReference() '/* testArrowFunctionReturnByReference */', true, ], + [ + '/* testArrowFunctionPassByReferenceA */', + true, + ], + [ + '/* testArrowFunctionPassByReferenceB */', + true, + ], [ '/* testClosureReturnByReference */', true, diff --git a/tests/Core/Filters/Filter/AcceptTest.php b/tests/Core/Filters/Filter/AcceptTest.php index 5d928c38b8..2c9f57cd39 100644 --- a/tests/Core/Filters/Filter/AcceptTest.php +++ b/tests/Core/Filters/Filter/AcceptTest.php @@ -111,9 +111,13 @@ public function dataExcludePatterns() '/path/to/src/Main.php', '/path/to/src/Something/Main.php', '/path/to/src/Somethingelse/Main.php', + '/path/to/src/SomethingelseEvenLonger/Main.php', '/path/to/src/Other/Main.php', ], - ['/path/to/src/Main.php'], + [ + '/path/to/src/Main.php', + '/path/to/src/SomethingelseEvenLonger/Main.php', + ], ], // Test ignoring standard/sniff specific exclude patterns. diff --git a/tests/Core/Ruleset/Fixtures/Sniffs/Category/SetPropertyAllowedAsDeclaredSniff.php b/tests/Core/Ruleset/Fixtures/Sniffs/Category/SetPropertyAllowedAsDeclaredSniff.php new file mode 100644 index 0000000000..7eeed212f3 --- /dev/null +++ b/tests/Core/Ruleset/Fixtures/Sniffs/Category/SetPropertyAllowedAsDeclaredSniff.php @@ -0,0 +1,28 @@ +magic[$name] = $value; + } + + public function __get($name) + { + if (isset($this->magic[$name])) { + return $this->magic[$name]; + } + + return null; + } + + public function register() + { + return [T_WHITESPACE]; + } + + public function process(File $phpcsFile, $stackPtr) + { + // Do something. + } +} diff --git a/tests/Core/Ruleset/Fixtures/Sniffs/Category/SetPropertyAllowedViaStdClassSniff.php b/tests/Core/Ruleset/Fixtures/Sniffs/Category/SetPropertyAllowedViaStdClassSniff.php new file mode 100644 index 0000000000..6ffffad3e2 --- /dev/null +++ b/tests/Core/Ruleset/Fixtures/Sniffs/Category/SetPropertyAllowedViaStdClassSniff.php @@ -0,0 +1,26 @@ +assertObjectHasAttribute('sniffCodes', self::$ruleset); - $this->assertCount(14, self::$ruleset->sniffCodes); + $this->assertCount(48, self::$ruleset->sniffCodes); }//end testHasSniffCodes() /** - * Test that sniffs are correctly registered, independently on the syntax used to include the sniff. + * Test that sniffs are correctly registered, independently of the syntax used to include the sniff. * * @param string $key Expected array key. * @param string $value Expected array value. @@ -147,6 +147,54 @@ public function testRegisteredSniffCodes($key, $value) public function dataRegisteredSniffCodes() { return [ + [ + 'PSR2.Classes.ClassDeclaration', + 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Classes\ClassDeclarationSniff', + ], + [ + 'PSR2.Classes.PropertyDeclaration', + 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Classes\PropertyDeclarationSniff', + ], + [ + 'PSR2.ControlStructures.ControlStructureSpacing', + 'PHP_CodeSniffer\Standards\PSR2\Sniffs\ControlStructures\ControlStructureSpacingSniff', + ], + [ + 'PSR2.ControlStructures.ElseIfDeclaration', + 'PHP_CodeSniffer\Standards\PSR2\Sniffs\ControlStructures\ElseIfDeclarationSniff', + ], + [ + 'PSR2.ControlStructures.SwitchDeclaration', + 'PHP_CodeSniffer\Standards\PSR2\Sniffs\ControlStructures\SwitchDeclarationSniff', + ], + [ + 'PSR2.Files.ClosingTag', + 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Files\ClosingTagSniff', + ], + [ + 'PSR2.Files.EndFileNewline', + 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Files\EndFileNewlineSniff', + ], + [ + 'PSR2.Methods.FunctionCallSignature', + 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Methods\FunctionCallSignatureSniff', + ], + [ + 'PSR2.Methods.FunctionClosingBrace', + 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Methods\FunctionClosingBraceSniff', + ], + [ + 'PSR2.Methods.MethodDeclaration', + 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Methods\MethodDeclarationSniff', + ], + [ + 'PSR2.Namespaces.NamespaceDeclaration', + 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Namespaces\NamespaceDeclarationSniff', + ], + [ + 'PSR2.Namespaces.UseDeclaration', + 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Namespaces\UseDeclarationSniff', + ], [ 'PSR1.Classes.ClassDeclaration', 'PHP_CodeSniffer\Standards\PSR1\Sniffs\Classes\ClassDeclarationSniff', @@ -180,8 +228,100 @@ public function dataRegisteredSniffCodes() 'PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions\UpperCaseConstantNameSniff', ], [ - 'Zend.NamingConventions.ValidVariableName', - 'PHP_CodeSniffer\Standards\Zend\Sniffs\NamingConventions\ValidVariableNameSniff', + 'Generic.Files.LineEndings', + 'PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineEndingsSniff', + ], + [ + 'Generic.Files.LineLength', + 'PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineLengthSniff', + ], + [ + 'Squiz.WhiteSpace.SuperfluousWhitespace', + 'PHP_CodeSniffer\Standards\Squiz\Sniffs\WhiteSpace\SuperfluousWhitespaceSniff', + ], + [ + 'Generic.Formatting.DisallowMultipleStatements', + 'PHP_CodeSniffer\Standards\Generic\Sniffs\Formatting\DisallowMultipleStatementsSniff', + ], + [ + 'Generic.WhiteSpace.ScopeIndent', + 'PHP_CodeSniffer\Standards\Generic\Sniffs\WhiteSpace\ScopeIndentSniff', + ], + [ + 'Generic.WhiteSpace.DisallowTabIndent', + 'PHP_CodeSniffer\Standards\Generic\Sniffs\WhiteSpace\DisallowTabIndentSniff', + ], + [ + 'Generic.PHP.LowerCaseKeyword', + 'PHP_CodeSniffer\Standards\Generic\Sniffs\PHP\LowerCaseKeywordSniff', + ], + [ + 'Generic.PHP.LowerCaseConstant', + 'PHP_CodeSniffer\Standards\Generic\Sniffs\PHP\LowerCaseConstantSniff', + ], + [ + 'Squiz.Scope.MethodScope', + 'PHP_CodeSniffer\Standards\Squiz\Sniffs\Scope\MethodScopeSniff', + ], + [ + 'Squiz.WhiteSpace.ScopeKeywordSpacing', + 'PHP_CodeSniffer\Standards\Squiz\Sniffs\WhiteSpace\ScopeKeywordSpacingSniff', + ], + [ + 'Squiz.Functions.FunctionDeclaration', + 'PHP_CodeSniffer\Standards\Squiz\Sniffs\Functions\FunctionDeclarationSniff', + ], + [ + 'Squiz.Functions.LowercaseFunctionKeywords', + 'PHP_CodeSniffer\Standards\Squiz\Sniffs\Functions\LowercaseFunctionKeywordsSniff', + ], + [ + 'Squiz.Functions.FunctionDeclarationArgumentSpacing', + 'PHP_CodeSniffer\Standards\Squiz\Sniffs\Functions\FunctionDeclarationArgumentSpacingSniff', + ], + [ + 'PEAR.Functions.ValidDefaultValue', + 'PHP_CodeSniffer\Standards\PEAR\Sniffs\Functions\ValidDefaultValueSniff', + ], + [ + 'Squiz.Functions.MultiLineFunctionDeclaration', + 'PHP_CodeSniffer\Standards\Squiz\Sniffs\Functions\MultiLineFunctionDeclarationSniff', + ], + [ + 'Generic.Functions.FunctionCallArgumentSpacing', + 'PHP_CodeSniffer\Standards\Generic\Sniffs\Functions\FunctionCallArgumentSpacingSniff', + ], + [ + 'Squiz.ControlStructures.ControlSignature', + 'PHP_CodeSniffer\Standards\Squiz\Sniffs\ControlStructures\ControlSignatureSniff', + ], + [ + 'Squiz.WhiteSpace.ControlStructureSpacing', + 'PHP_CodeSniffer\Standards\Squiz\Sniffs\WhiteSpace\ControlStructureSpacingSniff', + ], + [ + 'Squiz.WhiteSpace.ScopeClosingBrace', + 'PHP_CodeSniffer\Standards\Squiz\Sniffs\WhiteSpace\ScopeClosingBraceSniff', + ], + [ + 'Squiz.ControlStructures.ForEachLoopDeclaration', + 'PHP_CodeSniffer\Standards\Squiz\Sniffs\ControlStructures\ForEachLoopDeclarationSniff', + ], + [ + 'Squiz.ControlStructures.ForLoopDeclaration', + 'PHP_CodeSniffer\Standards\Squiz\Sniffs\ControlStructures\ForLoopDeclarationSniff', + ], + [ + 'Squiz.ControlStructures.LowercaseDeclaration', + 'PHP_CodeSniffer\Standards\Squiz\Sniffs\ControlStructures\LowercaseDeclarationSniff', + ], + [ + 'Generic.ControlStructures.InlineControlStructure', + 'PHP_CodeSniffer\Standards\Generic\Sniffs\ControlStructures\InlineControlStructureSniff', + ], + [ + 'PSR12.Operators.OperatorSpacing', + 'PHP_CodeSniffer\Standards\PSR12\Sniffs\Operators\OperatorSpacingSniff', ], [ 'Generic.Arrays.ArrayIndent', @@ -191,10 +331,6 @@ public function dataRegisteredSniffCodes() 'Generic.Metrics.CyclomaticComplexity', 'PHP_CodeSniffer\Standards\Generic\Sniffs\Metrics\CyclomaticComplexitySniff', ], - [ - 'Generic.Files.LineLength', - 'PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineLengthSniff', - ], [ 'Generic.NamingConventions.CamelCapsFunctionName', 'PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions\CamelCapsFunctionNameSniff', @@ -242,49 +378,54 @@ public function testSettingProperties($sniffClass, $propertyName, $expectedValue public function dataSettingProperties() { return [ - 'ClassDeclarationSniff' => [ - 'PHP_CodeSniffer\Standards\PSR1\Sniffs\Classes\ClassDeclarationSniff', - 'setforallsniffs', - true, + 'Set property for complete standard: PSR2 ClassDeclaration' => [ + 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Classes\ClassDeclarationSniff', + 'indent', + '20', ], - 'SideEffectsSniff' => [ - 'PHP_CodeSniffer\Standards\PSR1\Sniffs\Files\SideEffectsSniff', - 'setforallsniffs', - true, + 'Set property for complete standard: PSR2 SwitchDeclaration' => [ + 'PHP_CodeSniffer\Standards\PSR2\Sniffs\ControlStructures\SwitchDeclarationSniff', + 'indent', + '20', ], - 'ValidVariableNameSniff' => [ - 'PHP_CodeSniffer\Standards\Zend\Sniffs\NamingConventions\ValidVariableNameSniff', - 'setforallincategory', - true, + 'Set property for complete standard: PSR2 FunctionCallSignature' => [ + 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Methods\FunctionCallSignatureSniff', + 'indent', + '20', ], - 'ArrayIndentSniff' => [ + 'Set property for complete category: PSR12 OperatorSpacing' => [ + 'PHP_CodeSniffer\Standards\PSR12\Sniffs\Operators\OperatorSpacingSniff', + 'ignoreSpacingBeforeAssignments', + false, + ], + 'Set property for individual sniff: Generic ArrayIndent' => [ 'PHP_CodeSniffer\Standards\Generic\Sniffs\Arrays\ArrayIndentSniff', 'indent', '2', ], - 'LineLengthSniff' => [ + 'Set property for individual sniff using sniff file inclusion: Generic LineLength' => [ 'PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineLengthSniff', 'lineLimit', '10', ], - 'CamelCapsFunctionNameSniff' => [ + 'Set property for individual sniff using sniff file inclusion: CamelCapsFunctionName' => [ 'PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions\CamelCapsFunctionNameSniff', 'strict', false, ], - 'NestingLevelSniff-nestingLevel' => [ + 'Set property for individual sniff via included ruleset: NestingLevel - nestingLevel' => [ 'PHP_CodeSniffer\Standards\Generic\Sniffs\Metrics\NestingLevelSniff', 'nestingLevel', '2', ], - 'NestingLevelSniff-setforsniffsinincludedruleset' => [ + 'Set property for all sniffs in an included ruleset: NestingLevel - absoluteNestingLevel' => [ 'PHP_CodeSniffer\Standards\Generic\Sniffs\Metrics\NestingLevelSniff', - 'setforsniffsinincludedruleset', + 'absoluteNestingLevel', true, ], // Testing that setting a property at error code level does *not* work. - 'CyclomaticComplexitySniff' => [ + 'Set property for error code will not change the sniff property value: CyclomaticComplexity' => [ 'PHP_CodeSniffer\Standards\Generic\Sniffs\Metrics\CyclomaticComplexitySniff', 'complexity', 10, @@ -294,4 +435,53 @@ public function dataSettingProperties() }//end dataSettingProperties() + /** + * Test that setting properties for standards, categories on sniffs which don't support the property will + * silently ignore the property and not set it. + * + * @param string $sniffClass The name of the sniff class. + * @param string $propertyName The name of the property which should not be set. + * + * @dataProvider dataSettingInvalidPropertiesOnStandardsAndCategoriesSilentlyFails + * + * @return void + */ + public function testSettingInvalidPropertiesOnStandardsAndCategoriesSilentlyFails($sniffClass, $propertyName) + { + $this->assertObjectHasAttribute('sniffs', self::$ruleset, 'Ruleset does not have property sniffs'); + $this->assertArrayHasKey($sniffClass, self::$ruleset->sniffs, 'Sniff class '.$sniffClass.' not listed in registered sniffs'); + + $sniffObject = self::$ruleset->sniffs[$sniffClass]; + $this->assertObjectNotHasAttribute($propertyName, $sniffObject, 'Property '.$propertyName.' registered for sniff '.$sniffClass.' which does not support it'); + + }//end testSettingInvalidPropertiesOnStandardsAndCategoriesSilentlyFails() + + + /** + * Data provider. + * + * @see self::testSettingInvalidPropertiesOnStandardsAndCategoriesSilentlyFails() + * + * @return array + */ + public function dataSettingInvalidPropertiesOnStandardsAndCategoriesSilentlyFails() + { + return [ + 'Set property for complete standard: PSR2 ClassDeclaration' => [ + 'PHP_CodeSniffer\Standards\PSR1\Sniffs\Classes\ClassDeclarationSniff', + 'setforallsniffs', + ], + 'Set property for complete standard: PSR2 FunctionCallSignature' => [ + 'PHP_CodeSniffer\Standards\PSR2\Sniffs\Methods\FunctionCallSignatureSniff', + 'setforallsniffs', + ], + 'Set property for complete category: PSR12 OperatorSpacing' => [ + 'PHP_CodeSniffer\Standards\PSR12\Sniffs\Operators\OperatorSpacingSniff', + 'setforallincategory', + ], + ]; + + }//end dataSettingInvalidPropertiesOnStandardsAndCategoriesSilentlyFails() + + }//end class diff --git a/tests/Core/Ruleset/RuleInclusionTest.xml b/tests/Core/Ruleset/RuleInclusionTest.xml index 06ce040e71..e1812bbba5 100644 --- a/tests/Core/Ruleset/RuleInclusionTest.xml +++ b/tests/Core/Ruleset/RuleInclusionTest.xml @@ -1,15 +1,17 @@ - + + - + + @@ -38,8 +40,9 @@ + - + diff --git a/tests/Core/Ruleset/SetPropertyAllowedAsDeclaredTest.xml b/tests/Core/Ruleset/SetPropertyAllowedAsDeclaredTest.xml new file mode 100644 index 0000000000..21dba9da8f --- /dev/null +++ b/tests/Core/Ruleset/SetPropertyAllowedAsDeclaredTest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/tests/Core/Ruleset/SetPropertyAllowedViaMagicMethodTest.xml b/tests/Core/Ruleset/SetPropertyAllowedViaMagicMethodTest.xml new file mode 100644 index 0000000000..03424db26e --- /dev/null +++ b/tests/Core/Ruleset/SetPropertyAllowedViaMagicMethodTest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/tests/Core/Ruleset/SetPropertyAllowedViaStdClassTest.xml b/tests/Core/Ruleset/SetPropertyAllowedViaStdClassTest.xml new file mode 100644 index 0000000000..4e4fd59e65 --- /dev/null +++ b/tests/Core/Ruleset/SetPropertyAllowedViaStdClassTest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/tests/Core/Ruleset/SetPropertyAppliesPropertyToMultipleSniffsInCategoryTest.xml b/tests/Core/Ruleset/SetPropertyAppliesPropertyToMultipleSniffsInCategoryTest.xml new file mode 100644 index 0000000000..cb4f489d37 --- /dev/null +++ b/tests/Core/Ruleset/SetPropertyAppliesPropertyToMultipleSniffsInCategoryTest.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/Core/Ruleset/SetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForCategoryTest.xml b/tests/Core/Ruleset/SetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForCategoryTest.xml new file mode 100644 index 0000000000..044414c0f5 --- /dev/null +++ b/tests/Core/Ruleset/SetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForCategoryTest.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/Core/Ruleset/SetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForStandardTest.xml b/tests/Core/Ruleset/SetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForStandardTest.xml new file mode 100644 index 0000000000..759c6c655a --- /dev/null +++ b/tests/Core/Ruleset/SetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForStandardTest.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/Core/Ruleset/SetPropertyNotAllowedViaAttributeTest.xml b/tests/Core/Ruleset/SetPropertyNotAllowedViaAttributeTest.xml new file mode 100644 index 0000000000..d9f808e383 --- /dev/null +++ b/tests/Core/Ruleset/SetPropertyNotAllowedViaAttributeTest.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/tests/Core/Ruleset/SetPropertyThrowsErrorOnInvalidPropertyTest.xml b/tests/Core/Ruleset/SetPropertyThrowsErrorOnInvalidPropertyTest.xml new file mode 100644 index 0000000000..435601affb --- /dev/null +++ b/tests/Core/Ruleset/SetPropertyThrowsErrorOnInvalidPropertyTest.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/Core/Ruleset/SetSniffPropertyTest.php b/tests/Core/Ruleset/SetSniffPropertyTest.php new file mode 100644 index 0000000000..9dbfc7bc88 --- /dev/null +++ b/tests/Core/Ruleset/SetSniffPropertyTest.php @@ -0,0 +1,413 @@ + + * @copyright 2022 Juliette Reinders Folmer. All rights reserved. + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Ruleset; + +use PHP_CodeSniffer\Config; +use PHP_CodeSniffer\Ruleset; +use PHPUnit\Framework\TestCase; + +/** + * These tests specifically focus on the changes made to work around the PHP 8.2 dynamic properties deprecation. + * + * @covers \PHP_CodeSniffer\Ruleset::setSniffProperty + */ +class SetSniffPropertyTest extends TestCase +{ + + + /** + * Initialize the test. + * + * @return void + */ + public function setUp() + { + if ($GLOBALS['PHP_CODESNIFFER_PEAR'] === true) { + // PEAR installs test and sniff files into different locations + // so these tests will not pass as they directly reference files + // by relative location. + $this->markTestSkipped('Test cannot run from a PEAR install'); + } + + }//end setUp() + + + /** + * Test that setting a property via the ruleset works in all situations which allow for it. + * + * @param string $name Name of the test. Used for the sniff name, the ruleset file name etc. + * + * @dataProvider dataSniffPropertiesGetSetWhenAllowed + * + * @return void + */ + public function testSniffPropertiesGetSetWhenAllowed($name) + { + $sniffCode = "Fixtures.Category.{$name}"; + $sniffClass = 'Fixtures\Sniffs\Category\\'.$name.'Sniff'; + $properties = [ + 'arbitrarystring' => 'arbitraryvalue', + 'arbitraryarray' => [ + 'mykey' => 'myvalue', + 'otherkey' => 'othervalue', + ], + ]; + + // Set up the ruleset. + $standard = __DIR__."/{$name}Test.xml"; + $config = new Config(["--standard=$standard"]); + $ruleset = new Ruleset($config); + + // Verify that the sniff has been registered. + $this->assertGreaterThan(0, count($ruleset->sniffCodes), 'No sniff codes registered'); + + // Verify that our target sniff has been registered. + $this->assertArrayHasKey($sniffCode, $ruleset->sniffCodes, 'Target sniff not registered'); + $this->assertSame($sniffClass, $ruleset->sniffCodes[$sniffCode], 'Target sniff not registered with the correct class'); + + // Test that the property as declared in the ruleset has been set on the sniff. + $this->assertArrayHasKey($sniffClass, $ruleset->sniffs, 'Sniff class not listed in registered sniffs'); + + $sniffObject = $ruleset->sniffs[$sniffClass]; + foreach ($properties as $name => $expectedValue) { + $this->assertSame($expectedValue, $sniffObject->$name, 'Property value not set to expected value'); + } + + }//end testSniffPropertiesGetSetWhenAllowed() + + + /** + * Data provider. + * + * @see self::testSniffPropertiesGetSetWhenAllowed() + * + * @return array + */ + public function dataSniffPropertiesGetSetWhenAllowed() + { + return [ + 'Property allowed as explicitly declared' => ['SetPropertyAllowedAsDeclared'], + 'Property allowed as sniff extends stdClass' => ['SetPropertyAllowedViaStdClass'], + 'Property allowed as sniff has magic __set() method' => ['SetPropertyAllowedViaMagicMethod'], + ]; + + }//end dataSniffPropertiesGetSetWhenAllowed() + + + /** + * Test that setting a property for a category will apply it correctly to those sniffs which support the + * property, but won't apply it to sniffs which don't. + * + * Note: this test intentionally uses the `PEAR.Functions` category as two sniffs in that category + * have a public property with the same name (`indent`) and one sniff doesn't, which makes it a great + * test case for this. + * + * @return void + */ + public function testSetPropertyAppliesPropertyToMultipleSniffsInCategory() + { + $propertyName = 'indent'; + $expectedValue = '10'; + + // Set up the ruleset. + $standard = __DIR__.'/SetPropertyAppliesPropertyToMultipleSniffsInCategoryTest.xml'; + $config = new Config(["--standard=$standard"]); + $ruleset = new Ruleset($config); + + // Test that the two sniffs which support the property have received the value. + $sniffClass = 'PHP_CodeSniffer\Standards\PEAR\Sniffs\Functions\FunctionCallSignatureSniff'; + $this->assertArrayHasKey($sniffClass, $ruleset->sniffs, 'Sniff class '.$sniffClass.' not listed in registered sniffs'); + $sniffObject = $ruleset->sniffs[$sniffClass]; + $this->assertSame($expectedValue, $sniffObject->$propertyName, 'Property value not set to expected value for '.$sniffClass); + + $sniffClass = 'PHP_CodeSniffer\Standards\PEAR\Sniffs\Functions\FunctionDeclarationSniff'; + $this->assertArrayHasKey($sniffClass, $ruleset->sniffs, 'Sniff class '.$sniffClass.' not listed in registered sniffs'); + $sniffObject = $ruleset->sniffs[$sniffClass]; + $this->assertSame($expectedValue, $sniffObject->$propertyName, 'Property value not set to expected value for '.$sniffClass); + + // Test that the property doesn't get set for the one sniff which doesn't support the property. + $sniffClass = 'PHP_CodeSniffer\Standards\PEAR\Sniffs\Functions\ValidDefaultValueSniff'; + $this->assertArrayHasKey($sniffClass, $ruleset->sniffs, 'Sniff class '.$sniffClass.' not listed in registered sniffs'); + $sniffObject = $ruleset->sniffs[$sniffClass]; + $this->assertObjectNotHasAttribute($propertyName, $sniffObject, 'Property registered for sniff '.$sniffClass.' which does not support it'); + + }//end testSetPropertyAppliesPropertyToMultipleSniffsInCategory() + + + /** + * Test that attempting to set a non-existent property directly on a sniff will throw an error + * when the sniff does not explicitly declare the property, extends stdClass or has magic methods. + * + * @return void + */ + public function testSetPropertyThrowsErrorOnInvalidProperty() + { + $exceptionClass = 'PHP_CodeSniffer\Exceptions\RuntimeException'; + $exceptionMsg = 'Ruleset invalid. Property "indentation" does not exist on sniff Generic.Arrays.ArrayIndent'; + if (method_exists($this, 'expectException') === true) { + $this->expectException($exceptionClass); + $this->expectExceptionMessage($exceptionMsg); + } else { + // PHPUnit < 5.2.0. + $this->setExpectedException($exceptionClass, $exceptionMsg); + } + + // Set up the ruleset. + $standard = __DIR__.'/SetPropertyThrowsErrorOnInvalidPropertyTest.xml'; + $config = new Config(["--standard=$standard"]); + $ruleset = new Ruleset($config); + + }//end testSetPropertyThrowsErrorOnInvalidProperty() + + + /** + * Test that attempting to set a non-existent property directly on a sniff will throw an error + * when the sniff does not explicitly declare the property, extends stdClass or has magic methods, + * even though the sniff has the PHP 8.2 `#[AllowDynamicProperties]` attribute set. + * + * @return void + */ + public function testSetPropertyThrowsErrorWhenPropertyOnlyAllowedViaAttribute() + { + $exceptionClass = 'PHP_CodeSniffer\Exceptions\RuntimeException'; + $exceptionMsg = 'Ruleset invalid. Property "arbitrarystring" does not exist on sniff Fixtures.Category.SetPropertyNotAllowedViaAttribute'; + if (method_exists($this, 'expectException') === true) { + $this->expectException($exceptionClass); + $this->expectExceptionMessage($exceptionMsg); + } else { + // PHPUnit < 5.2.0. + $this->setExpectedException($exceptionClass, $exceptionMsg); + } + + // Set up the ruleset. + $standard = __DIR__.'/SetPropertyNotAllowedViaAttributeTest.xml'; + $config = new Config(["--standard=$standard"]); + $ruleset = new Ruleset($config); + + }//end testSetPropertyThrowsErrorWhenPropertyOnlyAllowedViaAttribute() + + + /** + * Test that attempting to set a non-existent property on a sniff when the property directive is + * for the whole standard, does not yield an error. + * + * @doesNotPerformAssertions + * + * @return void + */ + public function testSetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForStandard() + { + // Set up the ruleset. + $standard = __DIR__.'/SetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForStandardTest.xml'; + $config = new Config(["--standard=$standard"]); + $ruleset = new Ruleset($config); + + }//end testSetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForStandard() + + + /** + * Test that attempting to set a non-existent property on a sniff when the property directive is + * for a whole category, does not yield an error. + * + * @doesNotPerformAssertions + * + * @return void + */ + public function testSetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForCategory() + { + // Set up the ruleset. + $standard = __DIR__.'/SetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForCategoryTest.xml'; + $config = new Config(["--standard=$standard"]); + $ruleset = new Ruleset($config); + + }//end testSetPropertyDoesNotThrowErrorOnInvalidPropertyWhenSetForCategory() + + + /** + * Test that setting a property via a direct call to the Ruleset::setSniffProperty() method + * sets the property correctly when using the new $settings array format. + * + * @return void + */ + public function testDirectCallWithNewArrayFormatSetsProperty() + { + $name = 'SetPropertyAllowedAsDeclared'; + $sniffCode = "Fixtures.Category.{$name}"; + $sniffClass = 'Fixtures\Sniffs\Category\\'.$name.'Sniff'; + + // Set up the ruleset. + $standard = __DIR__."/{$name}Test.xml"; + $config = new Config(["--standard=$standard"]); + $ruleset = new Ruleset($config); + + $propertyName = 'arbitrarystring'; + $propertyValue = 'new value'; + + $ruleset->setSniffProperty( + $sniffClass, + $propertyName, + [ + 'scope' => 'sniff', + 'value' => $propertyValue, + ] + ); + + // Verify that the sniff has been registered. + $this->assertGreaterThan(0, count($ruleset->sniffCodes), 'No sniff codes registered'); + + // Verify that our target sniff has been registered. + $this->assertArrayHasKey($sniffCode, $ruleset->sniffCodes, 'Target sniff not registered'); + $this->assertSame($sniffClass, $ruleset->sniffCodes[$sniffCode], 'Target sniff not registered with the correct class'); + + // Test that the property as declared in the ruleset has been set on the sniff. + $this->assertArrayHasKey($sniffClass, $ruleset->sniffs, 'Sniff class not listed in registered sniffs'); + + $sniffObject = $ruleset->sniffs[$sniffClass]; + $this->assertSame($propertyValue, $sniffObject->$propertyName, 'Property value not set to expected value'); + + }//end testDirectCallWithNewArrayFormatSetsProperty() + + + /** + * Test that setting a property via a direct call to the Ruleset::setSniffProperty() method + * sets the property correctly when using the old $settings array format. + * + * Tested by silencing the deprecation notice as otherwise the test would fail on the deprecation notice. + * + * @param mixed $propertyValue Value for the property to set. + * + * @dataProvider dataDirectCallWithOldArrayFormatSetsProperty + * + * @return void + */ + public function testDirectCallWithOldArrayFormatSetsProperty($propertyValue) + { + $name = 'SetPropertyAllowedAsDeclared'; + $sniffCode = "Fixtures.Category.{$name}"; + $sniffClass = 'Fixtures\Sniffs\Category\\'.$name.'Sniff'; + + // Set up the ruleset. + $standard = __DIR__."/{$name}Test.xml"; + $config = new Config(["--standard=$standard"]); + $ruleset = new Ruleset($config); + + $propertyName = 'arbitrarystring'; + + @$ruleset->setSniffProperty( + $sniffClass, + $propertyName, + $propertyValue + ); + + // Verify that the sniff has been registered. + $this->assertGreaterThan(0, count($ruleset->sniffCodes), 'No sniff codes registered'); + + // Verify that our target sniff has been registered. + $this->assertArrayHasKey($sniffCode, $ruleset->sniffCodes, 'Target sniff not registered'); + $this->assertSame($sniffClass, $ruleset->sniffCodes[$sniffCode], 'Target sniff not registered with the correct class'); + + // Test that the property as declared in the ruleset has been set on the sniff. + $this->assertArrayHasKey($sniffClass, $ruleset->sniffs, 'Sniff class not listed in registered sniffs'); + + $sniffObject = $ruleset->sniffs[$sniffClass]; + $this->assertSame($propertyValue, $sniffObject->$propertyName, 'Property value not set to expected value'); + + }//end testDirectCallWithOldArrayFormatSetsProperty() + + + /** + * Data provider. + * + * @see self::testDirectCallWithOldArrayFormatSetsProperty() + * + * @return array + */ + public function dataDirectCallWithOldArrayFormatSetsProperty() + { + return [ + 'Property value is not an array (boolean)' => [false], + 'Property value is not an array (string)' => ['a string'], + 'Property value is an empty array' => [[]], + 'Property value is an array without keys' => [ + [ + 'value', + false, + ], + ], + 'Property value is an array without the "scope" or "value" keys' => [ + [ + 'key1' => 'value', + 'key2' => false, + ], + ], + 'Property value is an array without the "scope" key' => [ + [ + 'key1' => 'value', + 'value' => true, + ], + ], + 'Property value is an array without the "value" key' => [ + [ + 'scope' => 'value', + 'key2' => 1234, + ], + ], + ]; + + }//end dataDirectCallWithOldArrayFormatSetsProperty() + + + /** + * Test that setting a property via a direct call to the Ruleset::setSniffProperty() method + * throws a deprecation notice when using the old $settings array format. + * + * Note: as PHPUnit stops as soon as it sees the deprecation notice, the setting of the property + * value is not tested here. + * + * @return void + */ + public function testDirectCallWithOldArrayFormatThrowsDeprecationNotice() + { + $exceptionClass = 'PHPUnit\Framework\Error\Deprecated'; + if (class_exists($exceptionClass) === false) { + $exceptionClass = 'PHPUnit_Framework_Error_Deprecated'; + } + + $exceptionMsg = 'the format of the $settings parameter has changed from (mixed) $value to array(\'scope\' => \'sniff|standard\', \'value\' => $value). Please update your integration code. See PR #3629 for more information.'; + + if (method_exists($this, 'expectException') === true) { + $this->expectException($exceptionClass); + $this->expectExceptionMessage($exceptionMsg); + } else { + // PHPUnit < 5.2.0. + $this->setExpectedException($exceptionClass, $exceptionMsg); + } + + $name = 'SetPropertyAllowedAsDeclared'; + $sniffCode = "Fixtures.Category.{$name}"; + $sniffClass = 'Fixtures\Sniffs\Category\\'.$name.'Sniff'; + + // Set up the ruleset. + $standard = __DIR__."/{$name}Test.xml"; + $config = new Config(["--standard=$standard"]); + $ruleset = new Ruleset($config); + + $propertyName = 'arbitrarystring'; + + $ruleset->setSniffProperty( + $sniffClass, + 'arbitrarystring', + ['key' => 'value'] + ); + + }//end testDirectCallWithOldArrayFormatThrowsDeprecationNotice() + + +}//end class 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/AttributesTest.inc b/tests/Core/Tokenizer/AttributesTest.inc new file mode 100644 index 0000000000..e539adf8a7 --- /dev/null +++ b/tests/Core/Tokenizer/AttributesTest.inc @@ -0,0 +1,90 @@ + 'foobar'])] +function attribute_with_params_on_function_test() {} + +/* testAttributeWithShortClosureParameter */ +#[AttributeWithParams(static fn ($value) => ! $value)] +function attribute_with_short_closure_param_test() {} + +/* testTwoAttributeOnTheSameLine */ +#[CustomAttribute] #[AttributeWithParams('foo')] +function two_attribute_on_same_line_test() {} + +/* testAttributeAndCommentOnTheSameLine */ +#[CustomAttribute] // This is a comment +function attribute_and_line_comment_on_same_line_test() {} + +/* testAttributeGrouping */ +#[CustomAttribute, AttributeWithParams('foo'), AttributeWithParams('foo', bar: ['bar' => 'foobar'])] +function attribute_grouping_test() {} + +/* testAttributeMultiline */ +#[ + CustomAttribute, + AttributeWithParams('foo'), + AttributeWithParams('foo', bar: ['bar' => 'foobar']) +] +function attribute_multiline_test() {} + +/* testAttributeMultilineWithComment */ +#[ + CustomAttribute, // comment + AttributeWithParams(/* another comment */ 'foo'), + AttributeWithParams('foo', bar: ['bar' => 'foobar']) +] +function attribute_multiline_with_comment_test() {} + +/* testSingleAttributeOnParameter */ +function single_attribute_on_parameter_test(#[ParamAttribute] int $param) {} + +/* testMultipleAttributesOnParameter */ +function multiple_attributes_on_parameter_test(#[ParamAttribute, AttributeWithParams(/* another comment */ 'foo')] int $param) {} + +/* testFqcnAttribute */ +#[Boo\QualifiedName, \Foo\FullyQualifiedName('foo')] +function fqcn_attrebute_test() {} + +/* testNestedAttributes */ +#[Boo\QualifiedName(fn (#[AttributeOne('boo')] $value) => (string) $value)] +function nested_attributes_test() {} + +/* testMultilineAttributesOnParameter */ +function multiline_attributes_on_parameter_test(#[ + AttributeWithParams( + 'foo' + ) + ] int $param) {} + +/* testAttributeContainingTextLookingLikeCloseTag */ +#[DeprecationReason('reason: ')] +function attribute_containing_text_looking_like_close_tag() {} + +/* testAttributeContainingMultilineTextLookingLikeCloseTag */ +#[DeprecationReason( + 'reason: ' +)] +function attribute_containing_mulitline_text_looking_like_close_tag() {} + +/* testInvalidAttribute */ +#[ThisIsNotAnAttribute +function invalid_attribute_test() {} diff --git a/tests/Core/Tokenizer/AttributesTest.php b/tests/Core/Tokenizer/AttributesTest.php new file mode 100644 index 0000000000..8ac826f2f6 --- /dev/null +++ b/tests/Core/Tokenizer/AttributesTest.php @@ -0,0 +1,658 @@ + + * @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 AttributesTest extends AbstractMethodUnitTest +{ + + + /** + * Test that attributes are parsed correctly. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int $length The number of tokens between opener and closer. + * @param array $tokenCodes The codes of tokens inside the attributes. + * + * @dataProvider dataAttribute + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * @covers PHP_CodeSniffer\Tokenizers\PHP::findCloser + * @covers PHP_CodeSniffer\Tokenizers\PHP::parsePhpAttribute + * + * @return void + */ + public function testAttribute($testMarker, $length, $tokenCodes) + { + $tokens = self::$phpcsFile->getTokens(); + + $attribute = $this->getTargetToken($testMarker, T_ATTRIBUTE); + $this->assertArrayHasKey('attribute_closer', $tokens[$attribute]); + + $closer = $tokens[$attribute]['attribute_closer']; + $this->assertSame(($attribute + $length), $closer); + + $this->assertSame(T_ATTRIBUTE_END, $tokens[$closer]['code']); + + $this->assertSame($tokens[$attribute]['attribute_opener'], $tokens[$closer]['attribute_opener']); + $this->assertSame($tokens[$attribute]['attribute_closer'], $tokens[$closer]['attribute_closer']); + + $map = array_map( + function ($token) use ($attribute, $length) { + $this->assertArrayHasKey('attribute_closer', $token); + $this->assertSame(($attribute + $length), $token['attribute_closer']); + + return $token['code']; + }, + array_slice($tokens, ($attribute + 1), ($length - 1)) + ); + + $this->assertSame($tokenCodes, $map); + + }//end testAttribute() + + + /** + * Data provider. + * + * @see testAttribute() + * + * @return array + */ + public function dataAttribute() + { + return [ + [ + '/* testAttribute */', + 2, + [ T_STRING ], + ], + [ + '/* testAttributeWithParams */', + 7, + [ + T_STRING, + T_OPEN_PARENTHESIS, + T_STRING, + T_DOUBLE_COLON, + T_STRING, + T_CLOSE_PARENTHESIS, + ], + ], + [ + '/* testAttributeWithNamedParam */', + 10, + [ + T_STRING, + T_OPEN_PARENTHESIS, + T_PARAM_NAME, + T_COLON, + T_WHITESPACE, + T_STRING, + T_DOUBLE_COLON, + T_STRING, + T_CLOSE_PARENTHESIS, + ], + ], + [ + '/* testAttributeOnFunction */', + 2, + [ T_STRING ], + ], + [ + '/* testAttributeOnFunctionWithParams */', + 17, + [ + T_STRING, + T_OPEN_PARENTHESIS, + T_CONSTANT_ENCAPSED_STRING, + T_COMMA, + T_WHITESPACE, + T_PARAM_NAME, + T_COLON, + T_WHITESPACE, + T_OPEN_SHORT_ARRAY, + T_CONSTANT_ENCAPSED_STRING, + T_WHITESPACE, + T_DOUBLE_ARROW, + T_WHITESPACE, + T_CONSTANT_ENCAPSED_STRING, + T_CLOSE_SHORT_ARRAY, + T_CLOSE_PARENTHESIS, + ], + ], + [ + '/* testAttributeWithShortClosureParameter */', + 17, + [ + T_STRING, + T_OPEN_PARENTHESIS, + T_STATIC, + T_WHITESPACE, + T_FN, + T_WHITESPACE, + T_OPEN_PARENTHESIS, + T_VARIABLE, + T_CLOSE_PARENTHESIS, + T_WHITESPACE, + T_FN_ARROW, + T_WHITESPACE, + T_BOOLEAN_NOT, + T_WHITESPACE, + T_VARIABLE, + T_CLOSE_PARENTHESIS, + ], + ], + [ + '/* testAttributeGrouping */', + 26, + [ + T_STRING, + T_COMMA, + T_WHITESPACE, + T_STRING, + T_OPEN_PARENTHESIS, + T_CONSTANT_ENCAPSED_STRING, + T_CLOSE_PARENTHESIS, + T_COMMA, + T_WHITESPACE, + T_STRING, + T_OPEN_PARENTHESIS, + T_CONSTANT_ENCAPSED_STRING, + T_COMMA, + T_WHITESPACE, + T_PARAM_NAME, + T_COLON, + T_WHITESPACE, + T_OPEN_SHORT_ARRAY, + T_CONSTANT_ENCAPSED_STRING, + T_WHITESPACE, + T_DOUBLE_ARROW, + T_WHITESPACE, + T_CONSTANT_ENCAPSED_STRING, + T_CLOSE_SHORT_ARRAY, + T_CLOSE_PARENTHESIS, + ], + ], + [ + '/* testAttributeMultiline */', + 31, + [ + T_WHITESPACE, + T_WHITESPACE, + T_STRING, + T_COMMA, + T_WHITESPACE, + T_WHITESPACE, + T_STRING, + T_OPEN_PARENTHESIS, + T_CONSTANT_ENCAPSED_STRING, + T_CLOSE_PARENTHESIS, + T_COMMA, + T_WHITESPACE, + T_WHITESPACE, + T_STRING, + T_OPEN_PARENTHESIS, + T_CONSTANT_ENCAPSED_STRING, + T_COMMA, + T_WHITESPACE, + T_PARAM_NAME, + T_COLON, + T_WHITESPACE, + T_OPEN_SHORT_ARRAY, + T_CONSTANT_ENCAPSED_STRING, + T_WHITESPACE, + T_DOUBLE_ARROW, + T_WHITESPACE, + T_CONSTANT_ENCAPSED_STRING, + T_CLOSE_SHORT_ARRAY, + T_CLOSE_PARENTHESIS, + T_WHITESPACE, + ], + ], + [ + '/* testFqcnAttribute */', + 13, + [ + T_STRING, + T_NS_SEPARATOR, + T_STRING, + T_COMMA, + T_WHITESPACE, + T_NS_SEPARATOR, + T_STRING, + T_NS_SEPARATOR, + T_STRING, + T_OPEN_PARENTHESIS, + T_CONSTANT_ENCAPSED_STRING, + T_CLOSE_PARENTHESIS, + ], + ], + ]; + + }//end dataAttribute() + + + /** + * Test that multiple attributes on the same line are parsed correctly. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * @covers PHP_CodeSniffer\Tokenizers\PHP::findCloser + * @covers PHP_CodeSniffer\Tokenizers\PHP::parsePhpAttribute + * + * @return void + */ + public function testTwoAttributesOnTheSameLine() + { + $tokens = self::$phpcsFile->getTokens(); + + $attribute = $this->getTargetToken('/* testTwoAttributeOnTheSameLine */', T_ATTRIBUTE); + $this->assertArrayHasKey('attribute_closer', $tokens[$attribute]); + + $closer = $tokens[$attribute]['attribute_closer']; + $this->assertSame(T_WHITESPACE, $tokens[($closer + 1)]['code']); + $this->assertSame(T_ATTRIBUTE, $tokens[($closer + 2)]['code']); + $this->assertArrayHasKey('attribute_closer', $tokens[($closer + 2)]); + + }//end testTwoAttributesOnTheSameLine() + + + /** + * Test that attribute followed by a line comment is parsed correctly. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * @covers PHP_CodeSniffer\Tokenizers\PHP::findCloser + * @covers PHP_CodeSniffer\Tokenizers\PHP::parsePhpAttribute + * + * @return void + */ + public function testAttributeAndLineComment() + { + $tokens = self::$phpcsFile->getTokens(); + + $attribute = $this->getTargetToken('/* testAttributeAndCommentOnTheSameLine */', T_ATTRIBUTE); + $this->assertArrayHasKey('attribute_closer', $tokens[$attribute]); + + $closer = $tokens[$attribute]['attribute_closer']; + $this->assertSame(T_WHITESPACE, $tokens[($closer + 1)]['code']); + $this->assertSame(T_COMMENT, $tokens[($closer + 2)]['code']); + + }//end testAttributeAndLineComment() + + + /** + * Test that attributes on function declaration parameters are parsed correctly. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int $position The token position (starting from T_FUNCTION) of T_ATTRIBUTE token. + * @param int $length The number of tokens between opener and closer. + * @param array $tokenCodes The codes of tokens inside the attributes. + * + * @dataProvider dataAttributeOnParameters + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * @covers PHP_CodeSniffer\Tokenizers\PHP::findCloser + * @covers PHP_CodeSniffer\Tokenizers\PHP::parsePhpAttribute + * + * @return void + */ + public function testAttributeOnParameters($testMarker, $position, $length, array $tokenCodes) + { + $tokens = self::$phpcsFile->getTokens(); + + $function = $this->getTargetToken($testMarker, T_FUNCTION); + $attribute = ($function + $position); + + $this->assertSame(T_ATTRIBUTE, $tokens[$attribute]['code']); + $this->assertArrayHasKey('attribute_closer', $tokens[$attribute]); + + $this->assertSame(($attribute + $length), $tokens[$attribute]['attribute_closer']); + + $closer = $tokens[$attribute]['attribute_closer']; + $this->assertSame(T_WHITESPACE, $tokens[($closer + 1)]['code']); + $this->assertSame(T_STRING, $tokens[($closer + 2)]['code']); + $this->assertSame('int', $tokens[($closer + 2)]['content']); + + $this->assertSame(T_VARIABLE, $tokens[($closer + 4)]['code']); + $this->assertSame('$param', $tokens[($closer + 4)]['content']); + + $map = array_map( + function ($token) use ($attribute, $length) { + $this->assertArrayHasKey('attribute_closer', $token); + $this->assertSame(($attribute + $length), $token['attribute_closer']); + + return $token['code']; + }, + array_slice($tokens, ($attribute + 1), ($length - 1)) + ); + + $this->assertSame($tokenCodes, $map); + + }//end testAttributeOnParameters() + + + /** + * Data provider. + * + * @see testAttributeOnParameters() + * + * @return array + */ + public function dataAttributeOnParameters() + { + return [ + [ + '/* testSingleAttributeOnParameter */', + 4, + 2, + [T_STRING], + ], + [ + '/* testMultipleAttributesOnParameter */', + 4, + 10, + [ + T_STRING, + T_COMMA, + T_WHITESPACE, + T_STRING, + T_OPEN_PARENTHESIS, + T_COMMENT, + T_WHITESPACE, + T_CONSTANT_ENCAPSED_STRING, + T_CLOSE_PARENTHESIS, + ], + ], + [ + '/* testMultilineAttributesOnParameter */', + 4, + 13, + [ + T_WHITESPACE, + T_WHITESPACE, + T_STRING, + T_OPEN_PARENTHESIS, + T_WHITESPACE, + T_WHITESPACE, + T_CONSTANT_ENCAPSED_STRING, + T_WHITESPACE, + T_WHITESPACE, + T_CLOSE_PARENTHESIS, + T_WHITESPACE, + T_WHITESPACE, + ], + ], + ]; + + }//end dataAttributeOnParameters() + + + /** + * Test that an attribute containing text which looks like a PHP close tag is tokenized correctly. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param int $length The number of tokens between opener and closer. + * @param array $expectedTokensAttribute The codes of tokens inside the attributes. + * @param array $expectedTokensAfter The codes of tokens after the attributes. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::parsePhpAttribute + * + * @dataProvider dataAttributeOnTextLookingLikeCloseTag + * + * @return void + */ + public function testAttributeContainingTextLookingLikeCloseTag($testMarker, $length, array $expectedTokensAttribute, array $expectedTokensAfter) + { + $tokens = self::$phpcsFile->getTokens(); + + $attribute = $this->getTargetToken($testMarker, T_ATTRIBUTE); + + $this->assertSame('T_ATTRIBUTE', $tokens[$attribute]['type']); + $this->assertArrayHasKey('attribute_closer', $tokens[$attribute]); + + $closer = $tokens[$attribute]['attribute_closer']; + $this->assertSame(($attribute + $length), $closer); + $this->assertSame(T_ATTRIBUTE_END, $tokens[$closer]['code']); + $this->assertSame('T_ATTRIBUTE_END', $tokens[$closer]['type']); + + $this->assertSame($tokens[$attribute]['attribute_opener'], $tokens[$closer]['attribute_opener']); + $this->assertSame($tokens[$attribute]['attribute_closer'], $tokens[$closer]['attribute_closer']); + + $i = ($attribute + 1); + foreach ($expectedTokensAttribute as $item) { + list($expectedType, $expectedContents) = $item; + $this->assertSame($expectedType, $tokens[$i]['type']); + $this->assertSame($expectedContents, $tokens[$i]['content']); + $this->assertArrayHasKey('attribute_opener', $tokens[$i]); + $this->assertArrayHasKey('attribute_closer', $tokens[$i]); + ++$i; + } + + $i = ($closer + 1); + foreach ($expectedTokensAfter as $expectedCode) { + $this->assertSame($expectedCode, $tokens[$i]['code']); + ++$i; + } + + }//end testAttributeContainingTextLookingLikeCloseTag() + + + /** + * Data provider. + * + * @see dataAttributeOnTextLookingLikeCloseTag() + * + * @return array + */ + public function dataAttributeOnTextLookingLikeCloseTag() + { + return [ + [ + '/* testAttributeContainingTextLookingLikeCloseTag */', + 5, + [ + [ + 'T_STRING', + 'DeprecationReason', + ], + [ + 'T_OPEN_PARENTHESIS', + '(', + ], + [ + 'T_CONSTANT_ENCAPSED_STRING', + "'reason: '", + ], + [ + 'T_CLOSE_PARENTHESIS', + ')', + ], + [ + 'T_ATTRIBUTE_END', + ']', + ], + ], + [ + T_WHITESPACE, + T_FUNCTION, + T_WHITESPACE, + T_STRING, + T_OPEN_PARENTHESIS, + T_CLOSE_PARENTHESIS, + T_WHITESPACE, + T_OPEN_CURLY_BRACKET, + T_CLOSE_CURLY_BRACKET, + ], + ], + [ + '/* testAttributeContainingMultilineTextLookingLikeCloseTag */', + 8, + [ + [ + 'T_STRING', + 'DeprecationReason', + ], + [ + 'T_OPEN_PARENTHESIS', + '(', + ], + [ + 'T_WHITESPACE', + "\n", + ], + [ + 'T_WHITESPACE', + " ", + ], + [ + 'T_CONSTANT_ENCAPSED_STRING', + "'reason: '", + ], + [ + 'T_WHITESPACE', + "\n", + ], + [ + 'T_CLOSE_PARENTHESIS', + ')', + ], + [ + 'T_ATTRIBUTE_END', + ']', + ], + ], + [ + T_WHITESPACE, + T_FUNCTION, + T_WHITESPACE, + T_STRING, + T_OPEN_PARENTHESIS, + T_CLOSE_PARENTHESIS, + T_WHITESPACE, + T_OPEN_CURLY_BRACKET, + T_CLOSE_CURLY_BRACKET, + ], + ], + ]; + + }//end dataAttributeOnTextLookingLikeCloseTag() + + + /** + * Test that invalid attribute (or comment starting with #[ and without ]) are parsed correctly. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * @covers PHP_CodeSniffer\Tokenizers\PHP::findCloser + * @covers PHP_CodeSniffer\Tokenizers\PHP::parsePhpAttribute + * + * @return void + */ + public function testInvalidAttribute() + { + $tokens = self::$phpcsFile->getTokens(); + + $attribute = $this->getTargetToken('/* testInvalidAttribute */', T_ATTRIBUTE); + + $this->assertArrayHasKey('attribute_closer', $tokens[$attribute]); + $this->assertNull($tokens[$attribute]['attribute_closer']); + + }//end testInvalidAttribute() + + + /** + * Test that nested attributes are parsed correctly. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * @covers PHP_CodeSniffer\Tokenizers\PHP::findCloser + * @covers PHP_CodeSniffer\Tokenizers\PHP::parsePhpAttribute + * + * @return void + */ + public function testNestedAttributes() + { + $tokens = self::$phpcsFile->getTokens(); + $tokenCodes = [ + T_STRING, + T_NS_SEPARATOR, + T_STRING, + T_OPEN_PARENTHESIS, + T_FN, + T_WHITESPACE, + T_OPEN_PARENTHESIS, + T_ATTRIBUTE, + T_STRING, + T_OPEN_PARENTHESIS, + T_CONSTANT_ENCAPSED_STRING, + T_CLOSE_PARENTHESIS, + T_ATTRIBUTE_END, + T_WHITESPACE, + T_VARIABLE, + T_CLOSE_PARENTHESIS, + T_WHITESPACE, + T_FN_ARROW, + T_WHITESPACE, + T_STRING_CAST, + T_WHITESPACE, + T_VARIABLE, + T_CLOSE_PARENTHESIS, + ]; + + $attribute = $this->getTargetToken('/* testNestedAttributes */', T_ATTRIBUTE); + $this->assertArrayHasKey('attribute_closer', $tokens[$attribute]); + + $closer = $tokens[$attribute]['attribute_closer']; + $this->assertSame(($attribute + 24), $closer); + + $this->assertSame(T_ATTRIBUTE_END, $tokens[$closer]['code']); + + $this->assertSame($tokens[$attribute]['attribute_opener'], $tokens[$closer]['attribute_opener']); + $this->assertSame($tokens[$attribute]['attribute_closer'], $tokens[$closer]['attribute_closer']); + + $this->assertArrayNotHasKey('nested_attributes', $tokens[$attribute]); + $this->assertArrayHasKey('nested_attributes', $tokens[($attribute + 8)]); + $this->assertSame([$attribute => ($attribute + 24)], $tokens[($attribute + 8)]['nested_attributes']); + + $test = function (array $tokens, $length, $nestedMap) use ($attribute) { + foreach ($tokens as $token) { + $this->assertArrayHasKey('attribute_closer', $token); + $this->assertSame(($attribute + $length), $token['attribute_closer']); + $this->assertSame($nestedMap, $token['nested_attributes']); + } + }; + + $test(array_slice($tokens, ($attribute + 1), 7), 24, [$attribute => $attribute + 24]); + $test(array_slice($tokens, ($attribute + 8), 1), 8 + 5, [$attribute => $attribute + 24]); + + // Length here is 8 (nested attribute offset) + 5 (real length). + $test( + array_slice($tokens, ($attribute + 9), 4), + 8 + 5, + [ + $attribute => $attribute + 24, + $attribute + 8 => $attribute + 13, + ] + ); + + $test(array_slice($tokens, ($attribute + 13), 1), 8 + 5, [$attribute => $attribute + 24]); + $test(array_slice($tokens, ($attribute + 14), 10), 24, [$attribute => $attribute + 24]); + + $map = array_map( + static function ($token) { + return $token['code']; + }, + array_slice($tokens, ($attribute + 1), 23) + ); + + $this->assertSame($tokenCodes, $map); + + }//end testNestedAttributes() + + +}//end class diff --git a/tests/Core/Tokenizer/BackfillEnumTest.inc b/tests/Core/Tokenizer/BackfillEnumTest.inc new file mode 100644 index 0000000000..82bfe24fd5 --- /dev/null +++ b/tests/Core/Tokenizer/BackfillEnumTest.inc @@ -0,0 +1,95 @@ +enum = 'foo'; + } +} + +/* testEnumUsedAsFunctionName */ +function enum() +{ +} + +/* testDeclarationContainingComment */ +enum /* comment */ Name +{ + case SOME_CASE; +} + +enum /* testEnumUsedAsEnumName */ Enum +{ +} + +/* testEnumUsedAsNamespaceName */ +namespace Enum; +/* testEnumUsedAsPartOfNamespaceName */ +namespace My\Enum\Collection; +/* testEnumUsedInObjectInitialization */ +$obj = new Enum; +/* testEnumAsFunctionCall */ +$var = enum($a, $b); +/* testEnumAsFunctionCallWithNamespace */ +var = namespace\enum(); +/* testClassConstantFetchWithEnumAsClassName */ +echo Enum::CONSTANT; +/* testClassConstantFetchWithEnumAsConstantName */ +echo ClassName::ENUM; + +/* testParseErrorMissingName */ +enum { + case SOME_CASE; +} + +/* testParseErrorLiveCoding */ +// This must be the last test in the file. +enum diff --git a/tests/Core/Tokenizer/BackfillEnumTest.php b/tests/Core/Tokenizer/BackfillEnumTest.php new file mode 100644 index 0000000000..33cff3a2c7 --- /dev/null +++ b/tests/Core/Tokenizer/BackfillEnumTest.php @@ -0,0 +1,229 @@ + + * @copyright 2021 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class BackfillEnumTest extends AbstractMethodUnitTest +{ + + + /** + * Test that the "enum" keyword is tokenized as such. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $testContent The token content to look for. + * @param int $openerOffset Offset to find expected scope opener. + * @param int $closerOffset Offset to find expected scope closer. + * + * @dataProvider dataEnums + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testEnums($testMarker, $testContent, $openerOffset, $closerOffset) + { + $tokens = self::$phpcsFile->getTokens(); + + $enum = $this->getTargetToken($testMarker, [T_ENUM, T_STRING], $testContent); + + $this->assertSame(T_ENUM, $tokens[$enum]['code']); + $this->assertSame('T_ENUM', $tokens[$enum]['type']); + + $this->assertArrayHasKey('scope_condition', $tokens[$enum]); + $this->assertArrayHasKey('scope_opener', $tokens[$enum]); + $this->assertArrayHasKey('scope_closer', $tokens[$enum]); + + $this->assertSame($enum, $tokens[$enum]['scope_condition']); + + $scopeOpener = $tokens[$enum]['scope_opener']; + $scopeCloser = $tokens[$enum]['scope_closer']; + + $expectedScopeOpener = ($enum + $openerOffset); + $expectedScopeCloser = ($enum + $closerOffset); + + $this->assertSame($expectedScopeOpener, $scopeOpener); + $this->assertArrayHasKey('scope_condition', $tokens[$scopeOpener]); + $this->assertArrayHasKey('scope_opener', $tokens[$scopeOpener]); + $this->assertArrayHasKey('scope_closer', $tokens[$scopeOpener]); + $this->assertSame($enum, $tokens[$scopeOpener]['scope_condition']); + $this->assertSame($scopeOpener, $tokens[$scopeOpener]['scope_opener']); + $this->assertSame($scopeCloser, $tokens[$scopeOpener]['scope_closer']); + + $this->assertSame($expectedScopeCloser, $scopeCloser); + $this->assertArrayHasKey('scope_condition', $tokens[$scopeCloser]); + $this->assertArrayHasKey('scope_opener', $tokens[$scopeCloser]); + $this->assertArrayHasKey('scope_closer', $tokens[$scopeCloser]); + $this->assertSame($enum, $tokens[$scopeCloser]['scope_condition']); + $this->assertSame($scopeOpener, $tokens[$scopeCloser]['scope_opener']); + $this->assertSame($scopeCloser, $tokens[$scopeCloser]['scope_closer']); + + }//end testEnums() + + + /** + * Data provider. + * + * @see testEnums() + * + * @return array + */ + public function dataEnums() + { + return [ + [ + '/* testPureEnum */', + 'enum', + 4, + 12, + ], + [ + '/* testBackedIntEnum */', + 'enum', + 7, + 29, + ], + [ + '/* testBackedStringEnum */', + 'enum', + 8, + 30, + ], + [ + '/* testComplexEnum */', + 'enum', + 11, + 72, + ], + [ + '/* testEnumWithEnumAsClassName */', + 'enum', + 6, + 7, + ], + [ + '/* testEnumIsCaseInsensitive */', + 'EnUm', + 4, + 5, + ], + [ + '/* testDeclarationContainingComment */', + 'enum', + 6, + 14, + ], + ]; + + }//end dataEnums() + + + /** + * Test that "enum" when not used as the keyword is still tokenized as `T_STRING`. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $testContent The token content to look for. + * + * @dataProvider dataNotEnums + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testNotEnums($testMarker, $testContent) + { + $tokens = self::$phpcsFile->getTokens(); + + $target = $this->getTargetToken($testMarker, [T_ENUM, T_STRING], $testContent); + $this->assertSame(T_STRING, $tokens[$target]['code']); + $this->assertSame('T_STRING', $tokens[$target]['type']); + + }//end testNotEnums() + + + /** + * Data provider. + * + * @see testNotEnums() + * + * @return array + */ + public function dataNotEnums() + { + return [ + [ + '/* testEnumAsClassNameAfterEnumKeyword */', + 'Enum', + ], + [ + '/* testEnumUsedAsClassName */', + 'Enum', + ], + [ + '/* testEnumUsedAsClassConstantName */', + 'ENUM', + ], + [ + '/* testEnumUsedAsMethodName */', + 'enum', + ], + [ + '/* testEnumUsedAsPropertyName */', + 'enum', + ], + [ + '/* testEnumUsedAsFunctionName */', + 'enum', + ], + [ + '/* testEnumUsedAsEnumName */', + 'Enum', + ], + [ + '/* testEnumUsedAsNamespaceName */', + 'Enum', + ], + [ + '/* testEnumUsedAsPartOfNamespaceName */', + 'Enum', + ], + [ + '/* testEnumUsedInObjectInitialization */', + 'Enum', + ], + [ + '/* testEnumAsFunctionCall */', + 'enum', + ], + [ + '/* testEnumAsFunctionCallWithNamespace */', + 'enum', + ], + [ + '/* testClassConstantFetchWithEnumAsClassName */', + 'Enum', + ], + [ + '/* testClassConstantFetchWithEnumAsConstantName */', + 'ENUM', + ], + [ + '/* testParseErrorMissingName */', + 'enum', + ], + [ + '/* testParseErrorLiveCoding */', + 'enum', + ], + ]; + + }//end dataNotEnums() + + +}//end class diff --git a/tests/Core/Tokenizer/BackfillExplicitOctalNotationTest.inc b/tests/Core/Tokenizer/BackfillExplicitOctalNotationTest.inc new file mode 100644 index 0000000000..eb907ed390 --- /dev/null +++ b/tests/Core/Tokenizer/BackfillExplicitOctalNotationTest.inc @@ -0,0 +1,31 @@ + + * @copyright 2019 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class BackfillExplicitOctalNotationTest extends AbstractMethodUnitTest +{ + + + /** + * Test that explicitly-defined octal values are tokenized as a single number and not as a number and a string. + * + * @param string $marker The comment which prefaces the target token in the test file. + * @param string $value The expected content of the token + * @param int|string $nextToken The expected next token. + * @param string $nextContent The expected content of the next token. + * + * @dataProvider dataExplicitOctalNotation + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testExplicitOctalNotation($marker, $value, $nextToken, $nextContent) + { + $tokens = self::$phpcsFile->getTokens(); + + $number = $this->getTargetToken($marker, [T_LNUMBER]); + + $this->assertSame($value, $tokens[$number]['content'], 'Content of integer token does not match expectation'); + + $this->assertSame($nextToken, $tokens[($number + 1)]['code'], 'Next token is not the expected type, but '.$tokens[($number + 1)]['type']); + $this->assertSame($nextContent, $tokens[($number + 1)]['content'], 'Next token did not have the expected contents'); + + }//end testExplicitOctalNotation() + + + /** + * Data provider. + * + * @see testExplicitOctalNotation() + * + * @return array + */ + public function dataExplicitOctalNotation() + { + return [ + [ + 'marker' => '/* testExplicitOctal */', + 'value' => '0o137041', + 'nextToken' => T_SEMICOLON, + 'nextContent' => ';', + ], + [ + 'marker' => '/* testExplicitOctalCapitalised */', + 'value' => '0O137041', + 'nextToken' => T_SEMICOLON, + 'nextContent' => ';', + ], + [ + 'marker' => '/* testExplicitOctalWithNumericSeparator */', + 'value' => '0o137_041', + 'nextToken' => T_SEMICOLON, + 'nextContent' => ';', + ], + [ + 'marker' => '/* testInvalid1 */', + 'value' => '0', + 'nextToken' => T_STRING, + 'nextContent' => 'o_137', + ], + [ + 'marker' => '/* testInvalid2 */', + 'value' => '0', + 'nextToken' => T_STRING, + 'nextContent' => 'O_41', + ], + [ + 'marker' => '/* testInvalid3 */', + 'value' => '0', + 'nextToken' => T_STRING, + 'nextContent' => 'o91', + ], + [ + 'marker' => '/* testInvalid4 */', + 'value' => '0O2', + 'nextToken' => T_LNUMBER, + 'nextContent' => '82', + ], + [ + 'marker' => '/* testInvalid5 */', + 'value' => '0o2', + 'nextToken' => T_LNUMBER, + 'nextContent' => '8_2', + ], + [ + 'marker' => '/* testInvalid6 */', + 'value' => '0o2', + 'nextToken' => T_STRING, + 'nextContent' => '_82', + ], + [ + 'marker' => '/* testInvalid7 */', + 'value' => '0', + 'nextToken' => T_STRING, + 'nextContent' => 'o', + ], + ]; + + }//end dataExplicitOctalNotation() + + +}//end class diff --git a/tests/Core/Tokenizer/BackfillFnTokenTest.inc b/tests/Core/Tokenizer/BackfillFnTokenTest.inc index 7714a78680..13f165b77f 100644 --- a/tests/Core/Tokenizer/BackfillFnTokenTest.inc +++ b/tests/Core/Tokenizer/BackfillFnTokenTest.inc @@ -23,6 +23,9 @@ function fn() {} /* testNestedOuter */ $fn = fn($x) => /* testNestedInner */ fn($y) => $x * $y + $z; +/* testNestedSharedCloserOuter */ +$foo = foo(fn() => /* testNestedSharedCloserInner */ fn() => bar() === true); + /* testFunctionCall */ $extended = fn($c) => $callable($factory($c), $c); @@ -40,6 +43,9 @@ $extended = fn($c) => $callable(function() { echo 'done'; }, $c); +/* testArrayIndex */ +$found = in_array_cb($needle, $haystack, fn($array, $needle) => $array[2] === $needle); + $result = array_map( /* testReturnType */ static fn(int $number) : int => $number + 1, @@ -57,12 +63,20 @@ $a = [ 'a' => fn() => return 1, ]; +/* testArrayValueNoTrailingComma */ +$a = [ + 'a' => fn() => foo() +]; + /* testYield */ $a = fn($x) => yield 'k' => $x; /* testNullableNamespace */ $a = fn(?\DateTime $x) : ?\DateTime => $x; +/* testNamespaceOperatorInTypes */ +$fn = fn(namespace\Foo $a) : ?namespace\Foo => $a; + /* testSelfReturnType */ fn(self $a) : self => $a; @@ -75,9 +89,61 @@ fn(callable $a) : callable => $a; /* testArrayReturnType */ fn(array $a) : array => $a; +/* testStaticReturnType */ +fn(array $a) : static => $a; + +/* testUnionParamType */ +$arrowWithUnionParam = fn(int|float $param) : SomeClass => new SomeClass($param); + +/* testUnionReturnType */ +$arrowWithUnionReturn = fn($param) : int|float => $param | 10; + /* testTernary */ $fn = fn($a) => $a ? /* testTernaryThen */ fn() : string => 'a' : /* testTernaryElse */ fn() : string => 'b'; +/* testTernaryWithTypes */ +$fn = fn(int|null $a) : array|null => $a ? null : []; + +function matchInArrow($x) { + /* testWithMatchValue */ + $fn = fn($x) => match(true) { + 1, 2, 3, 4, 5 => 'foo', + default => 'bar', + }; +} + +function matchInArrowAndMore($x) { + /* testWithMatchValueAndMore */ + $fn = fn($x) => match(true) { + 1, 2, 3, 4, 5 => 'foo', + default => 'bar', + } . 'suffix'; +} + +function arrowFunctionInMatchWithTrailingComma($x) { + return match ($x) { + /* testInMatchNotLastValue */ + 1 => fn($y) => callMe($y), + /* testInMatchLastValueWithTrailingComma */ + default => fn($y) => callThem($y), + }; +} + +function arrowFunctionInMatchNoTrailingComma1($x) { + return match ($x) { + 1 => fn($y) => callMe($y), + /* testInMatchLastValueNoTrailingComma1 */ + default => fn($y) => callThem($y) + }; +} + +function arrowFunctionInMatchNoTrailingComma2($x) { + return match ($x) { + /* testInMatchLastValueNoTrailingComma2 */ + default => fn($y) => 5 * $y + }; +} + /* testConstantDeclaration */ const FN = 'a'; @@ -124,6 +190,9 @@ $a = MyNS\Sub\Fn($param); /* testNonArrowNamespaceOperatorFunctionCall */ $a = namespace\fn($param); +/* testNonArrowFunctionNameWithUnionTypes */ +function fn(int|float $param) : string|null {} + /* testLiveCoding */ // Intentional parse error. This has to be the last test in the file. $fn = fn diff --git a/tests/Core/Tokenizer/BackfillFnTokenTest.php b/tests/Core/Tokenizer/BackfillFnTokenTest.php index 4dbb8b43c4..4d0f4c0649 100644 --- a/tests/Core/Tokenizer/BackfillFnTokenTest.php +++ b/tests/Core/Tokenizer/BackfillFnTokenTest.php @@ -24,22 +24,10 @@ class BackfillFnTokenTest extends AbstractMethodUnitTest */ public function testSimple() { - $tokens = self::$phpcsFile->getTokens(); - foreach (['/* testStandard */', '/* testMixedCase */'] as $comment) { $token = $this->getTargetToken($comment, T_FN); $this->backfillHelper($token); - - $this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 12), 'Scope closer is not the semicolon token'); - - $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 12), 'Opener scope closer is not the semicolon token'); - - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 12), 'Closer scope closer is not the semicolon token'); + $this->scopePositionTestHelper($token, 5, 12); } }//end testSimple() @@ -54,21 +42,9 @@ public function testSimple() */ public function testWhitespace() { - $tokens = self::$phpcsFile->getTokens(); - $token = $this->getTargetToken('/* testWhitespace */', T_FN); $this->backfillHelper($token); - - $this->assertSame($tokens[$token]['scope_opener'], ($token + 6), 'Scope opener is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 13), 'Scope closer is not the semicolon token'); - - $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 6), 'Opener scope opener is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 13), 'Opener scope closer is not the semicolon token'); - - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 6), 'Closer scope opener is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 13), 'Closer scope closer is not the semicolon token'); + $this->scopePositionTestHelper($token, 6, 13); }//end testWhitespace() @@ -82,21 +58,9 @@ public function testWhitespace() */ public function testComment() { - $tokens = self::$phpcsFile->getTokens(); - $token = $this->getTargetToken('/* testComment */', T_FN); $this->backfillHelper($token); - - $this->assertSame($tokens[$token]['scope_opener'], ($token + 8), 'Scope opener is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 15), 'Scope closer is not the semicolon token'); - - $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 8), 'Opener scope opener is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 15), 'Opener scope closer is not the semicolon token'); - - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 8), 'Closer scope opener is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 15), 'Closer scope closer is not the semicolon token'); + $this->scopePositionTestHelper($token, 8, 15); }//end testComment() @@ -110,21 +74,9 @@ public function testComment() */ public function testHeredoc() { - $tokens = self::$phpcsFile->getTokens(); - $token = $this->getTargetToken('/* testHeredoc */', T_FN); $this->backfillHelper($token); - - $this->assertSame($tokens[$token]['scope_opener'], ($token + 4), 'Scope opener is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 9), 'Scope closer is not the semicolon token'); - - $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 4), 'Opener scope opener is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 9), 'Opener scope closer is not the semicolon token'); - - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 4), 'Closer scope opener is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 9), 'Closer scope closer is not the semicolon token'); + $this->scopePositionTestHelper($token, 4, 9); }//end testHeredoc() @@ -138,21 +90,9 @@ public function testHeredoc() */ public function testNestedOuter() { - $tokens = self::$phpcsFile->getTokens(); - $token = $this->getTargetToken('/* testNestedOuter */', T_FN); $this->backfillHelper($token); - - $this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 25), 'Scope closer is not the semicolon token'); - - $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 25), 'Opener scope closer is not the semicolon token'); - - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 25), 'Closer scope closer is not the semicolon token'); + $this->scopePositionTestHelper($token, 5, 25); }//end testNestedOuter() @@ -169,46 +109,72 @@ public function testNestedInner() $tokens = self::$phpcsFile->getTokens(); $token = $this->getTargetToken('/* testNestedInner */', T_FN); - $this->backfillHelper($token); + $this->backfillHelper($token, true); - $this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 16), 'Scope closer is not the semicolon token'); + $expectedScopeOpener = ($token + 5); + $expectedScopeCloser = ($token + 16); + + $this->assertSame($expectedScopeOpener, $tokens[$token]['scope_opener'], 'Scope opener is not the arrow token'); + $this->assertSame($expectedScopeCloser, $tokens[$token]['scope_closer'], 'Scope closer is not the semicolon token'); $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 16), 'Opener scope closer is not the semicolon token'); + $this->assertSame($expectedScopeOpener, $tokens[$opener]['scope_opener'], 'Opener scope opener is not the arrow token'); + $this->assertSame($expectedScopeCloser, $tokens[$opener]['scope_closer'], 'Opener scope closer is not the semicolon token'); - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 16), 'Closer scope closer is not the semicolon token'); + $closer = $tokens[$token]['scope_closer']; + $this->assertSame(($token - 4), $tokens[$closer]['scope_opener'], 'Closer scope opener is not the arrow token of the "outer" arrow function (shared scope closer)'); + $this->assertSame($expectedScopeCloser, $tokens[$closer]['scope_closer'], 'Closer scope closer is not the semicolon token'); }//end testNestedInner() /** - * Test arrow functions that call functions. + * Test nested arrow functions with a shared closer. * * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional * * @return void */ - public function testFunctionCall() + public function testNestedSharedCloser() { $tokens = self::$phpcsFile->getTokens(); - $token = $this->getTargetToken('/* testFunctionCall */', T_FN); + $token = $this->getTargetToken('/* testNestedSharedCloserOuter */', T_FN); $this->backfillHelper($token); + $this->scopePositionTestHelper($token, 4, 20); - $this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 17), 'Scope closer is not the semicolon token'); + $token = $this->getTargetToken('/* testNestedSharedCloserInner */', T_FN); + $this->backfillHelper($token, true); + + $expectedScopeOpener = ($token + 4); + $expectedScopeCloser = ($token + 12); + + $this->assertSame($expectedScopeOpener, $tokens[$token]['scope_opener'], 'Scope opener for "inner" arrow function is not the arrow token'); + $this->assertSame($expectedScopeCloser, $tokens[$token]['scope_closer'], 'Scope closer for "inner" arrow function is not the TRUE token'); $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 17), 'Opener scope closer is not the semicolon token'); + $this->assertSame($expectedScopeOpener, $tokens[$opener]['scope_opener'], 'Opener scope opener for "inner" arrow function is not the arrow token'); + $this->assertSame($expectedScopeCloser, $tokens[$opener]['scope_closer'], 'Opener scope closer for "inner" arrow function is not the semicolon token'); + + $closer = $tokens[$token]['scope_closer']; + $this->assertSame(($token - 4), $tokens[$closer]['scope_opener'], 'Closer scope opener for "inner" arrow function is not the arrow token of the "outer" arrow function (shared scope closer)'); + $this->assertSame($expectedScopeCloser, $tokens[$closer]['scope_closer'], 'Closer scope closer for "inner" arrow function is not the TRUE token'); + + }//end testNestedSharedCloser() - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 17), 'Closer scope closer is not the semicolon token'); + + /** + * Test arrow functions that call functions. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testFunctionCall() + { + $token = $this->getTargetToken('/* testFunctionCall */', T_FN); + $this->backfillHelper($token); + $this->scopePositionTestHelper($token, 5, 17); }//end testFunctionCall() @@ -222,21 +188,9 @@ public function testFunctionCall() */ public function testChainedFunctionCall() { - $tokens = self::$phpcsFile->getTokens(); - $token = $this->getTargetToken('/* testChainedFunctionCall */', T_FN); $this->backfillHelper($token); - - $this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 12), 'Scope closer is not the bracket token'); - - $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 12), 'Opener scope closer is not the bracket token'); - - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 12), 'Closer scope closer is not the bracket token'); + $this->scopePositionTestHelper($token, 5, 12, 'bracket'); }//end testChainedFunctionCall() @@ -250,21 +204,9 @@ public function testChainedFunctionCall() */ public function testFunctionArgument() { - $tokens = self::$phpcsFile->getTokens(); - $token = $this->getTargetToken('/* testFunctionArgument */', T_FN); $this->backfillHelper($token); - - $this->assertSame($tokens[$token]['scope_opener'], ($token + 8), 'Scope opener is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 15), 'Scope closer is not the comma token'); - - $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 8), 'Opener scope opener is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 15), 'Opener scope closer is not the comma token'); - - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 8), 'Closer scope opener is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 15), 'Closer scope closer is not the comma token'); + $this->scopePositionTestHelper($token, 8, 15, 'comma'); }//end testFunctionArgument() @@ -278,23 +220,27 @@ public function testFunctionArgument() */ public function testClosure() { - $tokens = self::$phpcsFile->getTokens(); - $token = $this->getTargetToken('/* testClosure */', T_FN); $this->backfillHelper($token); + $this->scopePositionTestHelper($token, 5, 60, 'comma'); - $this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 60), 'Scope closer is not the comma token'); + }//end testClosure() - $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 60), 'Opener scope closer is not the comma token'); - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 60), 'Closer scope closer is not the comma token'); + /** + * Test arrow functions using an array index. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testArrayIndex() + { + $token = $this->getTargetToken('/* testArrayIndex */', T_FN); + $this->backfillHelper($token); + $this->scopePositionTestHelper($token, 8, 17, 'comma'); - }//end testClosure() + }//end testArrayIndex() /** @@ -306,21 +252,9 @@ public function testClosure() */ public function testReturnType() { - $tokens = self::$phpcsFile->getTokens(); - $token = $this->getTargetToken('/* testReturnType */', T_FN); $this->backfillHelper($token); - - $this->assertSame($tokens[$token]['scope_opener'], ($token + 11), 'Scope opener is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 18), 'Scope closer is not the comma token'); - - $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 11), 'Opener scope opener is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 18), 'Opener scope closer is not the comma token'); - - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 11), 'Closer scope opener is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 18), 'Closer scope closer is not the comma token'); + $this->scopePositionTestHelper($token, 11, 18, 'comma'); }//end testReturnType() @@ -334,21 +268,9 @@ public function testReturnType() */ public function testReference() { - $tokens = self::$phpcsFile->getTokens(); - $token = $this->getTargetToken('/* testReference */', T_FN); $this->backfillHelper($token); - - $this->assertSame($tokens[$token]['scope_opener'], ($token + 6), 'Scope opener is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 9), 'Scope closer is not the semicolon token'); - - $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 6), 'Opener scope opener is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 9), 'Opener scope closer is not the semicolon token'); - - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 6), 'Closer scope opener is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 9), 'Closer scope closer is not the semicolon token'); + $this->scopePositionTestHelper($token, 6, 9); }//end testReference() @@ -362,21 +284,9 @@ public function testReference() */ public function testGrouped() { - $tokens = self::$phpcsFile->getTokens(); - $token = $this->getTargetToken('/* testGrouped */', T_FN); $this->backfillHelper($token); - - $this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 8), 'Scope closer is not the semicolon token'); - - $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 8), 'Opener scope closer is not the semicolon token'); - - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 8), 'Closer scope closer is not the semicolon token'); + $this->scopePositionTestHelper($token, 5, 8); }//end testGrouped() @@ -390,23 +300,27 @@ public function testGrouped() */ public function testArrayValue() { - $tokens = self::$phpcsFile->getTokens(); - $token = $this->getTargetToken('/* testArrayValue */', T_FN); $this->backfillHelper($token); + $this->scopePositionTestHelper($token, 4, 9, 'comma'); - $this->assertSame($tokens[$token]['scope_opener'], ($token + 4), 'Scope opener is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 9), 'Scope closer is not the comma token'); + }//end testArrayValue() - $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 4), 'Opener scope opener is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 9), 'Opener scope closer is not the comma token'); - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 4), 'Closer scope opener is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 9), 'Closer scope closer is not the comma token'); + /** + * Test arrow functions that are used as array values with no trailing comma. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testArrayValueNoTrailingComma() + { + $token = $this->getTargetToken('/* testArrayValueNoTrailingComma */', T_FN); + $this->backfillHelper($token); + $this->scopePositionTestHelper($token, 4, 8, 'closing parenthesis'); - }//end testArrayValue() + }//end testArrayValueNoTrailingComma() /** @@ -418,21 +332,9 @@ public function testArrayValue() */ public function testYield() { - $tokens = self::$phpcsFile->getTokens(); - $token = $this->getTargetToken('/* testYield */', T_FN); $this->backfillHelper($token); - - $this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 14), 'Scope closer is not the semicolon token'); - - $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 14), 'Opener scope closer is not the semicolon token'); - - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 14), 'Closer scope closer is not the semicolon token'); + $this->scopePositionTestHelper($token, 5, 14); }//end testYield() @@ -446,27 +348,31 @@ public function testYield() */ public function testNullableNamespace() { - $tokens = self::$phpcsFile->getTokens(); - $token = $this->getTargetToken('/* testNullableNamespace */', T_FN); $this->backfillHelper($token); + $this->scopePositionTestHelper($token, 15, 18); - $this->assertSame($tokens[$token]['scope_opener'], ($token + 15), 'Scope opener is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 18), 'Scope closer is not the semicolon token'); + }//end testNullableNamespace() - $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 15), 'Opener scope opener is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 18), 'Opener scope closer is not the semicolon token'); - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 15), 'Closer scope opener is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 18), 'Closer scope closer is not the semicolon token'); + /** + * Test arrow functions that use the namespace operator in the return type. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testNamespaceOperatorInTypes() + { + $token = $this->getTargetToken('/* testNamespaceOperatorInTypes */', T_FN); + $this->backfillHelper($token); + $this->scopePositionTestHelper($token, 16, 19); - }//end testNullableNamespace() + }//end testNamespaceOperatorInTypes() /** - * Test arrow functions that use self/parent/callable return types. + * Test arrow functions that use self/parent/callable/array/static return types. * * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional * @@ -481,27 +387,63 @@ public function testKeywordReturnTypes() 'Parent', 'Callable', 'Array', + 'Static', ]; foreach ($testMarkers as $marker) { $token = $this->getTargetToken('/* test'.$marker.'ReturnType */', T_FN); $this->backfillHelper($token); - $this->assertSame($tokens[$token]['scope_opener'], ($token + 11), "Scope opener is not the arrow token (for $marker)"); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 14), "Scope closer is not the semicolon token(for $marker)"); + $expectedScopeOpener = ($token + 11); + $expectedScopeCloser = ($token + 14); + + $this->assertSame($expectedScopeOpener, $tokens[$token]['scope_opener'], "Scope opener is not the arrow token (for $marker)"); + $this->assertSame($expectedScopeCloser, $tokens[$token]['scope_closer'], "Scope closer is not the semicolon token(for $marker)"); $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 11), "Opener scope opener is not the arrow token(for $marker)"); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 14), "Opener scope closer is not the semicolon token(for $marker)"); + $this->assertSame($expectedScopeOpener, $tokens[$opener]['scope_opener'], "Opener scope opener is not the arrow token(for $marker)"); + $this->assertSame($expectedScopeCloser, $tokens[$opener]['scope_closer'], "Opener scope closer is not the semicolon token(for $marker)"); - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 11), "Closer scope opener is not the arrow token(for $marker)"); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 14), "Closer scope closer is not the semicolon token(for $marker)"); + $closer = $tokens[$token]['scope_closer']; + $this->assertSame($expectedScopeOpener, $tokens[$closer]['scope_opener'], "Closer scope opener is not the arrow token(for $marker)"); + $this->assertSame($expectedScopeCloser, $tokens[$closer]['scope_closer'], "Closer scope closer is not the semicolon token(for $marker)"); } }//end testKeywordReturnTypes() + /** + * Test arrow function with a union parameter type. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testUnionParamType() + { + $token = $this->getTargetToken('/* testUnionParamType */', T_FN); + $this->backfillHelper($token); + $this->scopePositionTestHelper($token, 13, 21); + + }//end testUnionParamType() + + + /** + * Test arrow function with a union return type. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testUnionReturnType() + { + $token = $this->getTargetToken('/* testUnionReturnType */', T_FN); + $this->backfillHelper($token); + $this->scopePositionTestHelper($token, 11, 18); + + }//end testUnionReturnType() + + /** * Test arrow functions used in ternary operators. * @@ -515,73 +457,178 @@ public function testTernary() $token = $this->getTargetToken('/* testTernary */', T_FN); $this->backfillHelper($token); - - $this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 40), 'Scope closer is not the semicolon token'); - - $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 40), 'Opener scope closer is not the semicolon token'); - - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 40), 'Closer scope closer is not the semicolon token'); + $this->scopePositionTestHelper($token, 5, 40); $token = $this->getTargetToken('/* testTernaryThen */', T_FN); $this->backfillHelper($token); - $this->assertSame($tokens[$token]['scope_opener'], ($token + 8), 'Scope opener for THEN is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 12), 'Scope closer for THEN is not the semicolon token'); + $expectedScopeOpener = ($token + 8); + $expectedScopeCloser = ($token + 12); + + $this->assertSame($expectedScopeOpener, $tokens[$token]['scope_opener'], 'Scope opener for THEN is not the arrow token'); + $this->assertSame($expectedScopeCloser, $tokens[$token]['scope_closer'], 'Scope closer for THEN is not the semicolon token'); $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 8), 'Opener scope opener for THEN is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 12), 'Opener scope closer for THEN is not the semicolon token'); + $this->assertSame($expectedScopeOpener, $tokens[$opener]['scope_opener'], 'Opener scope opener for THEN is not the arrow token'); + $this->assertSame($expectedScopeCloser, $tokens[$opener]['scope_closer'], 'Opener scope closer for THEN is not the semicolon token'); - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 8), 'Closer scope opener for THEN is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 12), 'Closer scope closer for THEN is not the semicolon token'); + $closer = $tokens[$token]['scope_closer']; + $this->assertSame($expectedScopeOpener, $tokens[$closer]['scope_opener'], 'Closer scope opener for THEN is not the arrow token'); + $this->assertSame($expectedScopeCloser, $tokens[$closer]['scope_closer'], 'Closer scope closer for THEN is not the semicolon token'); $token = $this->getTargetToken('/* testTernaryElse */', T_FN); - $this->backfillHelper($token); + $this->backfillHelper($token, true); - $this->assertSame($tokens[$token]['scope_opener'], ($token + 8), 'Scope opener for ELSE is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 11), 'Scope closer for ELSE is not the semicolon token'); + $expectedScopeOpener = ($token + 8); + $expectedScopeCloser = ($token + 11); + + $this->assertSame($expectedScopeOpener, $tokens[$token]['scope_opener'], 'Scope opener for ELSE is not the arrow token'); + $this->assertSame($expectedScopeCloser, $tokens[$token]['scope_closer'], 'Scope closer for ELSE is not the semicolon token'); $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 8), 'Opener scope opener for ELSE is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 11), 'Opener scope closer for ELSE is not the semicolon token'); + $this->assertSame($expectedScopeOpener, $tokens[$opener]['scope_opener'], 'Opener scope opener for ELSE is not the arrow token'); + $this->assertSame($expectedScopeCloser, $tokens[$opener]['scope_closer'], 'Opener scope closer for ELSE is not the semicolon token'); - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 8), 'Closer scope opener for ELSE is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 11), 'Closer scope closer for ELSE is not the semicolon token'); + $closer = $tokens[$token]['scope_closer']; + $this->assertSame(($token - 24), $tokens[$closer]['scope_opener'], 'Closer scope opener for ELSE is not the arrow token of the "outer" arrow function (shared scope closer)'); + $this->assertSame($expectedScopeCloser, $tokens[$closer]['scope_closer'], 'Closer scope closer for ELSE is not the semicolon token'); }//end testTernary() /** - * Test arrow function nested within a method declaration. + * Test typed arrow functions used in ternary operators. * * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional * * @return void */ - public function testNestedInMethod() + public function testTernaryWithTypes() { $tokens = self::$phpcsFile->getTokens(); - $token = $this->getTargetToken('/* testNestedInMethod */', T_FN); + $token = $this->getTargetToken('/* testTernaryWithTypes */', T_FN); $this->backfillHelper($token); + $this->scopePositionTestHelper($token, 15, 27); - $this->assertSame($tokens[$token]['scope_opener'], ($token + 5), 'Scope opener is not the arrow token'); - $this->assertSame($tokens[$token]['scope_closer'], ($token + 17), 'Scope closer is not the semicolon token'); + }//end testTernaryWithTypes() - $opener = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$opener]['scope_opener'], ($token + 5), 'Opener scope opener is not the arrow token'); - $this->assertSame($tokens[$opener]['scope_closer'], ($token + 17), 'Opener scope closer is not the semicolon token'); - $closer = $tokens[$token]['scope_opener']; - $this->assertSame($tokens[$closer]['scope_opener'], ($token + 5), 'Closer scope opener is not the arrow token'); - $this->assertSame($tokens[$closer]['scope_closer'], ($token + 17), 'Closer scope closer is not the semicolon token'); + /** + * Test arrow function returning a match control structure. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testWithMatchValue() + { + $token = $this->getTargetToken('/* testWithMatchValue */', T_FN); + $this->backfillHelper($token); + $this->scopePositionTestHelper($token, 5, 44); + + }//end testWithMatchValue() + + + /** + * Test arrow function returning a match control structure with something behind it. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testWithMatchValueAndMore() + { + $token = $this->getTargetToken('/* testWithMatchValueAndMore */', T_FN); + $this->backfillHelper($token); + $this->scopePositionTestHelper($token, 5, 48); + + }//end testWithMatchValueAndMore() + + + /** + * Test match control structure returning arrow functions. + * + * @param string $testMarker The comment prefacing the target token. + * @param int $openerOffset The expected offset of the scope opener in relation to the testMarker. + * @param int $closerOffset The expected offset of the scope closer in relation to the testMarker. + * @param string $expectedCloserType The type of token expected for the scope closer. + * @param string $expectedCloserFriendlyName A friendly name for the type of token expected for the scope closer + * to be used in the error message for failing tests. + * + * @dataProvider dataInMatchValue + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testInMatchValue($testMarker, $openerOffset, $closerOffset, $expectedCloserType, $expectedCloserFriendlyName) + { + $tokens = self::$phpcsFile->getTokens(); + + $token = $this->getTargetToken($testMarker, T_FN); + $this->backfillHelper($token); + $this->scopePositionTestHelper($token, $openerOffset, $closerOffset, $expectedCloserFriendlyName); + + $this->assertSame($expectedCloserType, $tokens[($token + $closerOffset)]['type'], 'Mismatched scope closer type'); + + }//end testInMatchValue() + + + /** + * Data provider. + * + * @see testInMatchValue() + * + * @return array + */ + public function dataInMatchValue() + { + return [ + 'not_last_value' => [ + '/* testInMatchNotLastValue */', + 5, + 11, + 'T_COMMA', + 'comma', + ], + 'last_value_with_trailing_comma' => [ + '/* testInMatchLastValueWithTrailingComma */', + 5, + 11, + 'T_COMMA', + 'comma', + ], + 'last_value_without_trailing_comma_1' => [ + '/* testInMatchLastValueNoTrailingComma1 */', + 5, + 10, + 'T_CLOSE_PARENTHESIS', + 'close parenthesis', + ], + 'last_value_without_trailing_comma_2' => [ + '/* testInMatchLastValueNoTrailingComma2 */', + 5, + 11, + 'T_VARIABLE', + '$y variable', + ], + ]; + + }//end dataInMatchValue() + + + /** + * Test arrow function nested within a method declaration. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testNestedInMethod() + { + $token = $this->getTargetToken('/* testNestedInMethod */', T_FN); + $this->backfillHelper($token); + $this->scopePositionTestHelper($token, 5, 17); }//end testNestedInMethod() @@ -652,15 +699,16 @@ public function dataNotAnArrowFunction() 'Fn', ], ['/* testNonArrowObjectMethodCall */'], - [ - '/* testNonArrowNamespacedFunctionCall */', - 'Fn', - ], [ '/* testNonArrowObjectMethodCallUpper */', 'FN', ], + [ + '/* testNonArrowNamespacedFunctionCall */', + 'Fn', + ], ['/* testNonArrowNamespaceOperatorFunctionCall */'], + ['/* testNonArrowFunctionNameWithUnionTypes */'], ['/* testLiveCoding */'], ]; @@ -670,11 +718,16 @@ public function dataNotAnArrowFunction() /** * Helper function to check that all token keys are correctly set for T_FN tokens. * - * @param string $token The T_FN token to check. + * @param int $token The T_FN token to check. + * @param bool $skipScopeCloserCheck Whether to skip the scope closer check. + * This should be set to "true" when testing nested arrow functions, + * where the "inner" arrow function shares a scope closer with the + * "outer" arrow function, as the 'scope_condition' for the scope closer + * of the "inner" arrow function will point to the "outer" arrow function. * * @return void */ - private function backfillHelper($token) + private function backfillHelper($token, $skipScopeCloserCheck=false) { $tokens = self::$phpcsFile->getTokens(); @@ -692,12 +745,16 @@ private function backfillHelper($token) $this->assertTrue(array_key_exists('scope_opener', $tokens[$opener]), 'Opener scope opener is not set'); $this->assertTrue(array_key_exists('scope_closer', $tokens[$opener]), 'Opener scope closer is not set'); $this->assertSame($tokens[$opener]['scope_condition'], $token, 'Opener scope condition is not the T_FN token'); + $this->assertSame(T_FN_ARROW, $tokens[$opener]['code'], 'Arrow scope opener not tokenized as T_FN_ARROW (code)'); + $this->assertSame('T_FN_ARROW', $tokens[$opener]['type'], 'Arrow scope opener not tokenized as T_FN_ARROW (type)'); - $closer = $tokens[$token]['scope_opener']; + $closer = $tokens[$token]['scope_closer']; $this->assertTrue(array_key_exists('scope_condition', $tokens[$closer]), 'Closer scope condition is not set'); $this->assertTrue(array_key_exists('scope_opener', $tokens[$closer]), 'Closer scope opener is not set'); $this->assertTrue(array_key_exists('scope_closer', $tokens[$closer]), 'Closer scope closer is not set'); - $this->assertSame($tokens[$closer]['scope_condition'], $token, 'Closer scope condition is not the T_FN token'); + if ($skipScopeCloserCheck === false) { + $this->assertSame($tokens[$closer]['scope_condition'], $token, 'Closer scope condition is not the T_FN token'); + } $opener = $tokens[$token]['parenthesis_opener']; $this->assertTrue(array_key_exists('parenthesis_owner', $tokens[$opener]), 'Opening parenthesis owner is not set'); @@ -710,4 +767,36 @@ private function backfillHelper($token) }//end backfillHelper() + /** + * Helper function to check that the scope opener/closer positions are correctly set for T_FN tokens. + * + * @param int $token The T_FN token to check. + * @param int $openerOffset The expected offset of the scope opener in relation to + * the fn keyword. + * @param int $closerOffset The expected offset of the scope closer in relation to + * the fn keyword. + * @param string $expectedCloserType Optional. The type of token expected for the scope closer. + * + * @return void + */ + private function scopePositionTestHelper($token, $openerOffset, $closerOffset, $expectedCloserType='semicolon') + { + $tokens = self::$phpcsFile->getTokens(); + $expectedScopeOpener = ($token + $openerOffset); + $expectedScopeCloser = ($token + $closerOffset); + + $this->assertSame($expectedScopeOpener, $tokens[$token]['scope_opener'], 'Scope opener is not the arrow token'); + $this->assertSame($expectedScopeCloser, $tokens[$token]['scope_closer'], 'Scope closer is not the '.$expectedCloserType.' token'); + + $opener = $tokens[$token]['scope_opener']; + $this->assertSame($expectedScopeOpener, $tokens[$opener]['scope_opener'], 'Opener scope opener is not the arrow token'); + $this->assertSame($expectedScopeCloser, $tokens[$opener]['scope_closer'], 'Opener scope closer is not the '.$expectedCloserType.' token'); + + $closer = $tokens[$token]['scope_closer']; + $this->assertSame($expectedScopeOpener, $tokens[$closer]['scope_opener'], 'Closer scope opener is not the arrow token'); + $this->assertSame($expectedScopeCloser, $tokens[$closer]['scope_closer'], 'Closer scope closer is not the '.$expectedCloserType.' token'); + + }//end scopePositionTestHelper() + + }//end class diff --git a/tests/Core/Tokenizer/BackfillMatchTokenTest.inc b/tests/Core/Tokenizer/BackfillMatchTokenTest.inc new file mode 100644 index 0000000000..095a4d7dfc --- /dev/null +++ b/tests/Core/Tokenizer/BackfillMatchTokenTest.inc @@ -0,0 +1,319 @@ + 'Zero', + 1 => 'One', + 2 => 'Two', + }; +} + +function matchNoTrailingComma($bool) { + /* testMatchNoTrailingComma */ + echo match ($bool) { + true => "true\n", + false => "false\n" + }; +} + +function matchWithDefault($i) { + /* testMatchWithDefault */ + return match ($i) { + 1 => 1, + 2 => 2, + default => 'default', + }; +} + +function matchExpressionInCondition($i) { + /* testMatchExpressionInCondition */ + return match (true) { + $i >= 50 => '50+', + $i >= 40 => '40-50', + $i >= 30 => '30-40', + $i >= 20 => '20-30', + $i >= 10 => '10-20', + default => '0-10', + }; +} + +function matchMultiCase($day) { + /* testMatchMultiCase */ + return match ($day) { + 1, 7 => false, + 2, 3, 4, 5, 6 => true, + }; +} + +function matchMultiCaseTrailingCommaInCase($bool) { + /* testMatchMultiCaseTrailingCommaInCase */ + echo match ($bool) { + false, + 0, + => "false\n", + true, + 1, + => "true\n", + default, + => "not bool\n", + }; +} + +assert((function () { + /* testMatchInClosureNotLowercase */ + Match ('foo') { + 'foo', 'bar' => false, + 'baz' => 'a', + default => 'b', + }; +})()); + +function matchInArrowFunction($x) { + /* testMatchInArrowFunction */ + $fn = fn($x) => match(true) { + 1, 2, 3, 4, 5 => 'foo', + default => 'bar', + }; +} + +function arrowFunctionInMatchNoTrailingComma($x) { + /* testArrowFunctionInMatchNoTrailingComma */ + return match ($x) { + 1 => fn($y) => callMe($y), + default => fn($y) => callThem($y) + }; +} + +/* testMatchInFunctionCallParamNotLowercase */ +var_dump(MATCH ( 'foo' ) { + 'foo' => dump_and_return('foo'), + 'bar' => dump_and_return('bar'), +}); + +/* testMatchInMethodCallParam */ +Test::usesValue(match(true) { true => $i }); + +/* testMatchDiscardResult */ +match (1) { + 1 => print "Executed\n", +}; + +/* testMatchWithDuplicateConditionsWithComments */ +echo match /*comment*/ ( $value /*comment*/ ) { + // Comment. + 2, 1 => '2, 1', + 1 => 1, + 3 => 3, + 4 => 4, + 5 => 5, +}; + +/* testNestedMatchOuter */ +$x = match ($y) { + /* testNestedMatchInner */ + default => match ($z) { 1 => 1 }, +}; + +/* testMatchInTernaryCondition */ +$x = match ($test) { 1 => 'a', 2 => 'b' } ? + /* testMatchInTernaryThen */ match ($test) { 1 => 'a', 2 => 'b' } : + /* testMatchInTernaryElse */ match ($notTest) { 3 => 'a', 4 => 'b' }; + +/* testMatchInArrayValue */ +$array = array( + match ($test) { 1 => 'a', 2 => 'b' }, +); + +/* testMatchInArrayKey */ +$array = [ + match ($test) { 1 => 'a', 2 => 'b' } => 'dynamic keys, woho!', +]; + +/* testMatchreturningArray */ +$matcher = match ($x) { + 0 => array( 0 => 1, 'a' => 2, 'b' => 3 ), + 1 => [1, 2, 3], + 2 => array( 1, [1, 2, 3], 2, 3), + 3 => [ 0 => 1, 'a' => array(1, 2, 3), 'b' => 2, 3], +}; + +/* testSwitchContainingMatch */ +switch ($something) { + /* testMatchWithDefaultNestedInSwitchCase1 */ + case 'foo': + $var = [1, 2, 3]; + $var = match ($i) { + 1 => 1, + default => 'default', + }; + continue 2; + + /* testMatchWithDefaultNestedInSwitchCase2 */ + case 'bar' ; + $i = callMe($a, $b); + return match ($i) { + 1 => 1, + default => 'default', + }; + + /* testMatchWithDefaultNestedInSwitchDefault */ + default: + echo 'something', match ($i) { + 1 => 1, + default => 'default', + }; + break; +} + +/* testMatchContainingSwitch */ +$x = match ($y) { + 5, 8 => function($z) { + /* testSwitchNestedInMatch1 */ + switch($z) { + case 'a': + $var = [1, 2, 3]; + return 'a'; + /* testSwitchDefaultNestedInMatchCase */ + default: + $i = callMe($a, $b); + return 'default1'; + } + }, + default => function($z) { + /* testSwitchNestedInMatch2 */ + switch($z) { + case 'a'; + $i = callMe($a, $b); + return 'b'; + /* testSwitchDefaultNestedInMatchDefault */ + default; + $var = [1, 2, 3]; + return 'default2'; + } + } +}; + +/* testMatchNoCases */ +// Intentional fatal error. +$x = match (true) {}; + +/* testMatchMultiDefault */ +// Intentional fatal error. +echo match (1) { + default => 'foo', + 1 => 'bar', + 2 => 'baz', + default => 'qux', +}; + +/* testNoMatchStaticMethodCall */ +$a = Foo::match($param); + +/* testNoMatchClassConstantAccess */ +$a = MyClass::MATCH; + +/* testNoMatchClassConstantArrayAccessMixedCase */ +$a = MyClass::Match[$a]; + +/* testNoMatchMethodCall */ +$a = $obj->match($param); + +/* testNoMatchMethodCallUpper */ +$a = $obj??->MATCH()->chain($param); + +/* testNoMatchPropertyAccess */ +$a = $obj->match; + +/* testNoMatchNamespacedFunctionCall */ +// Intentional fatal error. +$a = MyNS\Sub\match($param); + +/* testNoMatchNamespaceOperatorFunctionCall */ +// Intentional fatal error. +$a = namespace\match($param); + +interface MatchInterface { + /* testNoMatchInterfaceMethodDeclaration */ + public static function match($param); +} + +class MatchClass { + /* testNoMatchClassConstantDeclarationLower */ + const match = 'a'; + + /* testNoMatchClassMethodDeclaration */ + public static function match($param) { + /* testNoMatchPropertyAssignment */ + $this->match = 'a'; + } +} + +/* testNoMatchClassInstantiation */ +$obj = new Match(); + +$anon = new class() { + /* testNoMatchAnonClassMethodDeclaration */ + protected function maTCH($param) { + } +}; + +/* testNoMatchClassDeclaration */ +// Intentional fatal error. Match is now a reserved keyword. +class Match {} + +/* testNoMatchInterfaceDeclaration */ +// Intentional fatal error. Match is now a reserved keyword. +interface Match {} + +/* testNoMatchTraitDeclaration */ +// Intentional fatal error. Match is now a reserved keyword. +trait Match {} + +/* testNoMatchConstantDeclaration */ +// Intentional fatal error. Match is now a reserved keyword. +const MATCH = '1'; + +/* testNoMatchFunctionDeclaration */ +// Intentional fatal error. Match is now a reserved keyword. +function match() {} + +/* testNoMatchNamespaceDeclaration */ +// Intentional fatal error. Match is now a reserved keyword. +namespace Match {} + +/* testNoMatchExtendedClassDeclaration */ +// Intentional fatal error. Match is now a reserved keyword. +class Foo extends Match {} + +/* testNoMatchImplementedClassDeclaration */ +// Intentional fatal error. Match is now a reserved keyword. +class Bar implements Match {} + +/* testNoMatchInUseStatement */ +// Intentional fatal error in PHP < 8. Match is now a reserved keyword. +use Match\me; + +function brokenMatchNoCurlies($x) { + /* testNoMatchMissingCurlies */ + // Intentional fatal error. New control structure is not supported without curly braces. + return match ($x) + 0 => 'Zero', + 1 => 'One', + 2 => 'Two', + ; +} + +function brokenMatchAlternativeSyntax($x) { + /* testNoMatchAlternativeSyntax */ + // Intentional fatal error. Alternative syntax is not supported. + return match ($x) : + 0 => 'Zero', + 1 => 'One', + 2 => 'Two', + endmatch; +} + +/* testLiveCoding */ +// Intentional parse error. This has to be the last test in the file. +echo match diff --git a/tests/Core/Tokenizer/BackfillMatchTokenTest.php b/tests/Core/Tokenizer/BackfillMatchTokenTest.php new file mode 100644 index 0000000000..80f909acdf --- /dev/null +++ b/tests/Core/Tokenizer/BackfillMatchTokenTest.php @@ -0,0 +1,529 @@ + + * @copyright 2020-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; +use PHP_CodeSniffer\Util\Tokens; + +class BackfillMatchTokenTest extends AbstractMethodUnitTest +{ + + + /** + * Test tokenization of match expressions. + * + * @param string $testMarker The comment prefacing the target token. + * @param int $openerOffset The expected offset of the scope opener in relation to the testMarker. + * @param int $closerOffset The expected offset of the scope closer in relation to the testMarker. + * @param string $testContent The token content to look for. + * + * @dataProvider dataMatchExpression + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testMatchExpression($testMarker, $openerOffset, $closerOffset, $testContent='match') + { + $tokens = self::$phpcsFile->getTokens(); + + $token = $this->getTargetToken($testMarker, [T_STRING, T_MATCH], $testContent); + $tokenArray = $tokens[$token]; + + $this->assertSame(T_MATCH, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_MATCH (code)'); + $this->assertSame('T_MATCH', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_MATCH (type)'); + + $this->scopeTestHelper($token, $openerOffset, $closerOffset); + $this->parenthesisTestHelper($token); + + }//end testMatchExpression() + + + /** + * Data provider. + * + * @see testMatchExpression() + * + * @return array + */ + public function dataMatchExpression() + { + return [ + 'simple_match' => [ + '/* testMatchSimple */', + 6, + 33, + ], + 'no_trailing_comma' => [ + '/* testMatchNoTrailingComma */', + 6, + 24, + ], + 'with_default_case' => [ + '/* testMatchWithDefault */', + 6, + 33, + ], + 'expression_in_condition' => [ + '/* testMatchExpressionInCondition */', + 6, + 77, + ], + 'multicase' => [ + '/* testMatchMultiCase */', + 6, + 40, + ], + 'multicase_trailing_comma_in_case' => [ + '/* testMatchMultiCaseTrailingCommaInCase */', + 6, + 47, + ], + 'in_closure_not_lowercase' => [ + '/* testMatchInClosureNotLowercase */', + 6, + 36, + 'Match', + ], + 'in_arrow_function' => [ + '/* testMatchInArrowFunction */', + 5, + 36, + ], + 'arrow_function_in_match_no_trailing_comma' => [ + '/* testArrowFunctionInMatchNoTrailingComma */', + 6, + 44, + ], + 'in_function_call_param_not_lowercase' => [ + '/* testMatchInFunctionCallParamNotLowercase */', + 8, + 32, + 'MATCH', + ], + 'in_method_call_param' => [ + '/* testMatchInMethodCallParam */', + 5, + 13, + ], + 'discard_result' => [ + '/* testMatchDiscardResult */', + 6, + 18, + ], + 'duplicate_conditions_and_comments' => [ + '/* testMatchWithDuplicateConditionsWithComments */', + 12, + 59, + ], + 'nested_match_outer' => [ + '/* testNestedMatchOuter */', + 6, + 33, + ], + 'nested_match_inner' => [ + '/* testNestedMatchInner */', + 6, + 14, + ], + 'ternary_condition' => [ + '/* testMatchInTernaryCondition */', + 6, + 21, + ], + 'ternary_then' => [ + '/* testMatchInTernaryThen */', + 6, + 21, + ], + 'ternary_else' => [ + '/* testMatchInTernaryElse */', + 6, + 21, + ], + 'array_value' => [ + '/* testMatchInArrayValue */', + 6, + 21, + ], + 'array_key' => [ + '/* testMatchInArrayKey */', + 6, + 21, + ], + 'returning_array' => [ + '/* testMatchreturningArray */', + 6, + 125, + ], + 'nested_in_switch_case_1' => [ + '/* testMatchWithDefaultNestedInSwitchCase1 */', + 6, + 25, + ], + 'nested_in_switch_case_2' => [ + '/* testMatchWithDefaultNestedInSwitchCase2 */', + 6, + 25, + ], + 'nested_in_switch_default' => [ + '/* testMatchWithDefaultNestedInSwitchDefault */', + 6, + 25, + ], + 'match_with_nested_switch' => [ + '/* testMatchContainingSwitch */', + 6, + 180, + ], + 'no_cases' => [ + '/* testMatchNoCases */', + 6, + 7, + ], + 'multi_default' => [ + '/* testMatchMultiDefault */', + 6, + 40, + ], + ]; + + }//end dataMatchExpression() + + + /** + * Verify that "match" keywords which are not match control structures get tokenized as T_STRING + * and don't have the extra token array indexes. + * + * @param string $testMarker The comment prefacing the target token. + * @param string $testContent The token content to look for. + * + * @dataProvider dataNotAMatchStructure + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testNotAMatchStructure($testMarker, $testContent='match') + { + $tokens = self::$phpcsFile->getTokens(); + + $token = $this->getTargetToken($testMarker, [T_STRING, T_MATCH], $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('scope_condition', $tokenArray, 'Scope condition is set'); + $this->assertArrayNotHasKey('scope_opener', $tokenArray, 'Scope opener is set'); + $this->assertArrayNotHasKey('scope_closer', $tokenArray, 'Scope closer is set'); + $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'); + + $next = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($token + 1), null, true); + if ($next !== false && $tokens[$next]['code'] === T_OPEN_PARENTHESIS) { + $this->assertArrayNotHasKey('parenthesis_owner', $tokenArray, 'Parenthesis owner is set for opener after'); + } + + }//end testNotAMatchStructure() + + + /** + * Data provider. + * + * @see testNotAMatchStructure() + * + * @return array + */ + public function dataNotAMatchStructure() + { + return [ + 'static_method_call' => ['/* testNoMatchStaticMethodCall */'], + 'class_constant_access' => [ + '/* testNoMatchClassConstantAccess */', + 'MATCH', + ], + 'class_constant_array_access' => [ + '/* testNoMatchClassConstantArrayAccessMixedCase */', + 'Match', + ], + 'method_call' => ['/* testNoMatchMethodCall */'], + 'method_call_uppercase' => [ + '/* testNoMatchMethodCallUpper */', + 'MATCH', + ], + 'property_access' => ['/* testNoMatchPropertyAccess */'], + 'namespaced_function_call' => ['/* testNoMatchNamespacedFunctionCall */'], + 'namespace_operator_function_call' => ['/* testNoMatchNamespaceOperatorFunctionCall */'], + 'interface_method_declaration' => ['/* testNoMatchInterfaceMethodDeclaration */'], + 'class_constant_declaration' => ['/* testNoMatchClassConstantDeclarationLower */'], + 'class_method_declaration' => ['/* testNoMatchClassMethodDeclaration */'], + 'property_assigment' => ['/* testNoMatchPropertyAssignment */'], + 'class_instantiation' => [ + '/* testNoMatchClassInstantiation */', + 'Match', + ], + 'anon_class_method_declaration' => [ + '/* testNoMatchAnonClassMethodDeclaration */', + 'maTCH', + ], + 'class_declaration' => [ + '/* testNoMatchClassDeclaration */', + 'Match', + ], + 'interface_declaration' => [ + '/* testNoMatchInterfaceDeclaration */', + 'Match', + ], + 'trait_declaration' => [ + '/* testNoMatchTraitDeclaration */', + 'Match', + ], + 'constant_declaration' => [ + '/* testNoMatchConstantDeclaration */', + 'MATCH', + ], + 'function_declaration' => ['/* testNoMatchFunctionDeclaration */'], + 'namespace_declaration' => [ + '/* testNoMatchNamespaceDeclaration */', + 'Match', + ], + 'class_extends_declaration' => [ + '/* testNoMatchExtendedClassDeclaration */', + 'Match', + ], + 'class_implements_declaration' => [ + '/* testNoMatchImplementedClassDeclaration */', + 'Match', + ], + 'use_statement' => [ + '/* testNoMatchInUseStatement */', + 'Match', + ], + 'unsupported_inline_control_structure' => ['/* testNoMatchMissingCurlies */'], + 'unsupported_alternative_syntax' => ['/* testNoMatchAlternativeSyntax */'], + 'live_coding' => ['/* testLiveCoding */'], + ]; + + }//end dataNotAMatchStructure() + + + /** + * Verify that the tokenization of switch structures is not affected by the backfill. + * + * @param string $testMarker The comment prefacing the target token. + * @param int $openerOffset The expected offset of the scope opener in relation to the testMarker. + * @param int $closerOffset The expected offset of the scope closer in relation to the testMarker. + * + * @dataProvider dataSwitchExpression + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testSwitchExpression($testMarker, $openerOffset, $closerOffset) + { + $token = $this->getTargetToken($testMarker, T_SWITCH); + + $this->scopeTestHelper($token, $openerOffset, $closerOffset); + $this->parenthesisTestHelper($token); + + }//end testSwitchExpression() + + + /** + * Data provider. + * + * @see testSwitchExpression() + * + * @return array + */ + public function dataSwitchExpression() + { + return [ + 'switch_containing_match' => [ + '/* testSwitchContainingMatch */', + 6, + 174, + ], + 'match_containing_switch_1' => [ + '/* testSwitchNestedInMatch1 */', + 5, + 63, + ], + 'match_containing_switch_2' => [ + '/* testSwitchNestedInMatch2 */', + 5, + 63, + ], + ]; + + }//end dataSwitchExpression() + + + /** + * Verify that the tokenization of a switch case/default structure containing a match structure + * or contained *in* a match structure is not affected by the backfill. + * + * @param string $testMarker The comment prefacing the target token. + * @param int $openerOffset The expected offset of the scope opener in relation to the testMarker. + * @param int $closerOffset The expected offset of the scope closer in relation to the testMarker. + * + * @dataProvider dataSwitchCaseVersusMatch + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testSwitchCaseVersusMatch($testMarker, $openerOffset, $closerOffset) + { + $token = $this->getTargetToken($testMarker, [T_CASE, T_DEFAULT]); + + $this->scopeTestHelper($token, $openerOffset, $closerOffset); + + }//end testSwitchCaseVersusMatch() + + + /** + * Data provider. + * + * @see testSwitchCaseVersusMatch() + * + * @return array + */ + public function dataSwitchCaseVersusMatch() + { + return [ + 'switch_with_nested_match_case_1' => [ + '/* testMatchWithDefaultNestedInSwitchCase1 */', + 3, + 55, + ], + 'switch_with_nested_match_case_2' => [ + '/* testMatchWithDefaultNestedInSwitchCase2 */', + 4, + 21, + ], + 'switch_with_nested_match_default_case' => [ + '/* testMatchWithDefaultNestedInSwitchDefault */', + 1, + 38, + ], + 'match_with_nested_switch_case' => [ + '/* testSwitchDefaultNestedInMatchCase */', + 1, + 18, + ], + 'match_with_nested_switch_default_case' => [ + '/* testSwitchDefaultNestedInMatchDefault */', + 1, + 20, + ], + ]; + + }//end dataSwitchCaseVersusMatch() + + + /** + * Helper function to verify that all scope related array indexes for a control structure + * are set correctly. + * + * @param string $token The control structure token to check. + * @param int $openerOffset The expected offset of the scope opener in relation to + * the control structure token. + * @param int $closerOffset The expected offset of the scope closer in relation to + * the control structure token. + * @param bool $skipScopeCloserCheck Whether to skip the scope closer check. + * This should be set to "true" when testing nested arrow functions, + * where the "inner" arrow function shares a scope closer with the + * "outer" arrow function, as the 'scope_condition' for the scope closer + * of the "inner" arrow function will point to the "outer" arrow function. + * + * @return void + */ + private function scopeTestHelper($token, $openerOffset, $closerOffset, $skipScopeCloserCheck=false) + { + $tokens = self::$phpcsFile->getTokens(); + $tokenArray = $tokens[$token]; + $tokenType = $tokenArray['type']; + $expectedScopeOpener = ($token + $openerOffset); + $expectedScopeCloser = ($token + $closerOffset); + + $this->assertArrayHasKey('scope_condition', $tokenArray, 'Scope condition is not set'); + $this->assertArrayHasKey('scope_opener', $tokenArray, 'Scope opener is not set'); + $this->assertArrayHasKey('scope_closer', $tokenArray, 'Scope closer is not set'); + $this->assertSame($token, $tokenArray['scope_condition'], 'Scope condition is not the '.$tokenType.' token'); + $this->assertSame($expectedScopeOpener, $tokenArray['scope_opener'], 'Scope opener of the '.$tokenType.' token incorrect'); + $this->assertSame($expectedScopeCloser, $tokenArray['scope_closer'], 'Scope closer of the '.$tokenType.' token incorrect'); + + $opener = $tokenArray['scope_opener']; + $this->assertArrayHasKey('scope_condition', $tokens[$opener], 'Opener scope condition is not set'); + $this->assertArrayHasKey('scope_opener', $tokens[$opener], 'Opener scope opener is not set'); + $this->assertArrayHasKey('scope_closer', $tokens[$opener], 'Opener scope closer is not set'); + $this->assertSame($token, $tokens[$opener]['scope_condition'], 'Opener scope condition is not the '.$tokenType.' token'); + $this->assertSame($expectedScopeOpener, $tokens[$opener]['scope_opener'], $tokenType.' opener scope opener token incorrect'); + $this->assertSame($expectedScopeCloser, $tokens[$opener]['scope_closer'], $tokenType.' opener scope closer token incorrect'); + + $closer = $tokenArray['scope_closer']; + $this->assertArrayHasKey('scope_condition', $tokens[$closer], 'Closer scope condition is not set'); + $this->assertArrayHasKey('scope_opener', $tokens[$closer], 'Closer scope opener is not set'); + $this->assertArrayHasKey('scope_closer', $tokens[$closer], 'Closer scope closer is not set'); + if ($skipScopeCloserCheck === false) { + $this->assertSame($token, $tokens[$closer]['scope_condition'], 'Closer scope condition is not the '.$tokenType.' token'); + } + + $this->assertSame($expectedScopeOpener, $tokens[$closer]['scope_opener'], $tokenType.' closer scope opener token incorrect'); + $this->assertSame($expectedScopeCloser, $tokens[$closer]['scope_closer'], $tokenType.' closer scope closer token incorrect'); + + if (($opener + 1) !== $closer) { + for ($i = ($opener + 1); $i < $closer; $i++) { + $this->assertArrayHasKey( + $token, + $tokens[$i]['conditions'], + $tokenType.' condition not added for token belonging to the '.$tokenType.' structure' + ); + } + } + + }//end scopeTestHelper() + + + /** + * Helper function to verify that all parenthesis related array indexes for a control structure + * token are set correctly. + * + * @param int $token The position of the control structure token. + * + * @return void + */ + private function parenthesisTestHelper($token) + { + $tokens = self::$phpcsFile->getTokens(); + $tokenArray = $tokens[$token]; + $tokenType = $tokenArray['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'); + $this->assertSame($token, $tokenArray['parenthesis_owner'], 'Parenthesis owner is not the '.$tokenType.' token'); + + $opener = $tokenArray['parenthesis_opener']; + $this->assertArrayHasKey('parenthesis_owner', $tokens[$opener], 'Opening parenthesis owner is not set'); + $this->assertSame($token, $tokens[$opener]['parenthesis_owner'], 'Opening parenthesis owner is not the '.$tokenType.' token'); + + $closer = $tokenArray['parenthesis_closer']; + $this->assertArrayHasKey('parenthesis_owner', $tokens[$closer], 'Closing parenthesis owner is not set'); + $this->assertSame($token, $tokens[$closer]['parenthesis_owner'], 'Closing parenthesis owner is not the '.$tokenType.' token'); + + }//end parenthesisTestHelper() + + +}//end class diff --git a/tests/Core/Tokenizer/BackfillNumericSeparatorTest.inc b/tests/Core/Tokenizer/BackfillNumericSeparatorTest.inc index eef53f593b..d8559705c3 100644 --- a/tests/Core/Tokenizer/BackfillNumericSeparatorTest.inc +++ b/tests/Core/Tokenizer/BackfillNumericSeparatorTest.inc @@ -34,6 +34,12 @@ $foo = 0b0101_1111; /* testOctal */ $foo = 0137_041; +/* testExplicitOctal */ +$foo = 0o137_041; + +/* testExplicitOctalCapitalised */ +$foo = 0O137_041; + /* testIntMoreThanMax */ $foo = 10_223_372_036_854_775_807; @@ -71,6 +77,12 @@ $testValue = 107_925_284 .88; /* testInvalid10 */ $testValue = 107_925_284/*comment*/.88; +/* testInvalid11 */ +$foo = 0o_137; + +/* testInvalid12 */ +$foo = 0O_41; + /* * Ensure that legitimate calculations are not touched by the backfill. */ diff --git a/tests/Core/Tokenizer/BackfillNumericSeparatorTest.php b/tests/Core/Tokenizer/BackfillNumericSeparatorTest.php index 443cc30ef1..645088fd6c 100644 --- a/tests/Core/Tokenizer/BackfillNumericSeparatorTest.php +++ b/tests/Core/Tokenizer/BackfillNumericSeparatorTest.php @@ -1,6 +1,6 @@ * @copyright 2019 Squiz Pty Ltd (ABN 77 084 670 600) @@ -16,7 +16,7 @@ class BackfillNumericSeparatorTest extends AbstractMethodUnitTest /** - * Test that numbers using numeric seperators are tokenized correctly. + * Test that numbers using numeric separators are tokenized correctly. * * @param array $testData The data required for the specific test case. * @@ -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 */', @@ -145,7 +159,7 @@ public function dataTestBackfill() /** - * Test that numbers using numeric seperators which are considered parse errors and/or + * Test that numbers using numeric separators which are considered parse errors and/or * which aren't relevant to the backfill, do not incorrectly trigger the backfill anyway. * * @param string $testMarker The comment which prefaces the target token in the test file. @@ -322,6 +336,32 @@ public function dataNoBackfill() ], ], ], + [ + '/* testInvalid11 */', + [ + [ + 'code' => T_LNUMBER, + 'content' => '0', + ], + [ + 'code' => T_STRING, + 'content' => 'o_137', + ], + ], + ], + [ + '/* testInvalid12 */', + [ + [ + 'code' => T_LNUMBER, + 'content' => '0', + ], + [ + 'code' => T_STRING, + 'content' => 'O_41', + ], + ], + ], [ '/* testCalc1 */', [ diff --git a/tests/Core/Tokenizer/BackfillReadonlyTest.inc b/tests/Core/Tokenizer/BackfillReadonlyTest.inc new file mode 100644 index 0000000000..eaf0b4b3cc --- /dev/null +++ b/tests/Core/Tokenizer/BackfillReadonlyTest.inc @@ -0,0 +1,100 @@ +readonly = 'foo'; + + /* testReadonlyPropertyInTernaryOperator */ + $isReadonly = $this->readonly ? true : false; + } +} + +/* testReadonlyUsedAsFunctionName */ +function readonly() +{ +} + +/* testReadonlyUsedAsNamespaceName */ +namespace Readonly; +/* testReadonlyUsedAsPartOfNamespaceName */ +namespace My\Readonly\Collection; +/* testReadonlyAsFunctionCall */ +$var = readonly($a, $b); +/* testClassConstantFetchWithReadonlyAsConstantName */ +echo ClassName::READONLY; + +/* testReadonlyUsedAsFunctionCallWithSpaceBetweenKeywordAndParens */ +$var = readonly /* comment */ (); + +/* testParseErrorLiveCoding */ +// This must be the last test in the file. +readonly diff --git a/tests/Core/Tokenizer/BackfillReadonlyTest.php b/tests/Core/Tokenizer/BackfillReadonlyTest.php new file mode 100644 index 0000000000..dddc18ebc2 --- /dev/null +++ b/tests/Core/Tokenizer/BackfillReadonlyTest.php @@ -0,0 +1,236 @@ + + * @copyright 2021 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class BackfillReadonlyTest extends AbstractMethodUnitTest +{ + + + /** + * Test that the "readonly" keyword is tokenized as such. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $testContent The token content to look for. + * + * @dataProvider dataReadonly + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testReadonly($testMarker, $testContent) + { + $tokens = self::$phpcsFile->getTokens(); + + $target = $this->getTargetToken($testMarker, [T_READONLY, T_STRING], $testContent); + $this->assertSame(T_READONLY, $tokens[$target]['code']); + $this->assertSame('T_READONLY', $tokens[$target]['type']); + + }//end testReadonly() + + + /** + * Data provider. + * + * @see testReadonly() + * + * @return array + */ + public function dataReadonly() + { + return [ + [ + '/* testReadonlyProperty */', + 'readonly', + ], + [ + '/* testVarReadonlyProperty */', + 'readonly', + ], + [ + '/* testReadonlyVarProperty */', + 'readonly', + ], + [ + '/* testStaticReadonlyProperty */', + 'readonly', + ], + [ + '/* testReadonlyStaticProperty */', + 'readonly', + ], + [ + '/* testConstReadonlyProperty */', + 'readonly', + ], + [ + '/* testReadonlyPropertyWithoutType */', + 'readonly', + ], + [ + '/* testPublicReadonlyProperty */', + 'readonly', + ], + [ + '/* testProtectedReadonlyProperty */', + 'readonly', + ], + [ + '/* testPrivateReadonlyProperty */', + 'readonly', + ], + [ + '/* testPublicReadonlyPropertyWithReadonlyFirst */', + 'readonly', + ], + [ + '/* testProtectedReadonlyPropertyWithReadonlyFirst */', + 'readonly', + ], + [ + '/* testPrivateReadonlyPropertyWithReadonlyFirst */', + 'readonly', + ], + [ + '/* testReadonlyWithCommentsInDeclaration */', + 'readonly', + ], + [ + '/* testReadonlyWithNullableProperty */', + 'readonly', + ], + [ + '/* testReadonlyNullablePropertyWithUnionTypeHintAndNullFirst */', + 'readonly', + ], + [ + '/* testReadonlyNullablePropertyWithUnionTypeHintAndNullLast */', + 'readonly', + ], + [ + '/* testReadonlyPropertyWithArrayTypeHint */', + 'readonly', + ], + [ + '/* testReadonlyPropertyWithSelfTypeHint */', + 'readonly', + ], + [ + '/* testReadonlyPropertyWithParentTypeHint */', + 'readonly', + ], + [ + '/* testReadonlyPropertyWithFullyQualifiedTypeHint */', + 'readonly', + ], + [ + '/* testReadonlyIsCaseInsensitive */', + 'ReAdOnLy', + ], + [ + '/* testReadonlyConstructorPropertyPromotion */', + 'readonly', + ], + [ + '/* testReadonlyConstructorPropertyPromotionWithReference */', + 'ReadOnly', + ], + [ + '/* testReadonlyPropertyInAnonymousClass */', + 'readonly', + ], + [ + '/* testReadonlyUsedAsFunctionCallWithSpaceBetweenKeywordAndParens */', + 'readonly', + ], + [ + '/* testParseErrorLiveCoding */', + 'readonly', + ], + ]; + + }//end dataReadonly() + + + /** + * Test that "readonly" when not used as the keyword is still tokenized as `T_STRING`. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $testContent The token content to look for. + * + * @dataProvider dataNotReadonly + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testNotReadonly($testMarker, $testContent) + { + $tokens = self::$phpcsFile->getTokens(); + + $target = $this->getTargetToken($testMarker, [T_READONLY, T_STRING], $testContent); + $this->assertSame(T_STRING, $tokens[$target]['code']); + $this->assertSame('T_STRING', $tokens[$target]['type']); + + }//end testNotReadonly() + + + /** + * Data provider. + * + * @see testNotReadonly() + * + * @return array + */ + public function dataNotReadonly() + { + return [ + [ + '/* testReadonlyUsedAsClassConstantName */', + 'READONLY', + ], + [ + '/* testReadonlyUsedAsMethodName */', + 'readonly', + ], + [ + '/* testReadonlyUsedAsPropertyName */', + 'readonly', + ], + [ + '/* testReadonlyPropertyInTernaryOperator */', + 'readonly', + ], + [ + '/* testReadonlyUsedAsFunctionName */', + 'readonly', + ], + [ + '/* testReadonlyUsedAsNamespaceName */', + 'Readonly', + ], + [ + '/* testReadonlyUsedAsPartOfNamespaceName */', + 'Readonly', + ], + [ + '/* testReadonlyAsFunctionCall */', + 'readonly', + ], + [ + '/* testClassConstantFetchWithReadonlyAsConstantName */', + 'READONLY', + ], + ]; + + }//end dataNotReadonly() + + +}//end class diff --git a/tests/Core/Tokenizer/BitwiseOrTest.inc b/tests/Core/Tokenizer/BitwiseOrTest.inc new file mode 100644 index 0000000000..bfdbdc18c1 --- /dev/null +++ b/tests/Core/Tokenizer/BitwiseOrTest.inc @@ -0,0 +1,134 @@ + $param | $int; + +/* testTypeUnionArrowReturnType */ +$arrowWithReturnType = fn ($param) : int|null => $param * 10; + +/* testBitwiseOrInArrayKey */ +$array = array( + A | B => /* testBitwiseOrInArrayValue */ B | C +); + +/* testBitwiseOrInShortArrayKey */ +$array = [ + A | B => /* testBitwiseOrInShortArrayValue */ B | C +]; + +/* testBitwiseOrTryCatch */ +try { +} catch ( ExceptionA | ExceptionB $e ) { +} + +/* testBitwiseOrNonArrowFnFunctionCall */ +$obj->fn($something | $else); + +/* testTypeUnionNonArrowFunctionDeclaration */ +function &fn(int|false $something) {} + +/* testLiveCoding */ +// Intentional parse error. This has to be the last test in the file. +return function( type| diff --git a/tests/Core/Tokenizer/BitwiseOrTest.php b/tests/Core/Tokenizer/BitwiseOrTest.php new file mode 100644 index 0000000000..d56e7340aa --- /dev/null +++ b/tests/Core/Tokenizer/BitwiseOrTest.php @@ -0,0 +1,138 @@ + + * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class BitwiseOrTest extends AbstractMethodUnitTest +{ + + + /** + * Test that non-union type bitwise or tokens are still tokenized as bitwise or. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @dataProvider dataBitwiseOr + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testBitwiseOr($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $opener = $this->getTargetToken($testMarker, [T_BITWISE_OR, T_TYPE_UNION]); + $this->assertSame(T_BITWISE_OR, $tokens[$opener]['code']); + $this->assertSame('T_BITWISE_OR', $tokens[$opener]['type']); + + }//end testBitwiseOr() + + + /** + * Data provider. + * + * @see testBitwiseOr() + * + * @return array + */ + public function dataBitwiseOr() + { + return [ + ['/* testBitwiseOr1 */'], + ['/* testBitwiseOr2 */'], + ['/* testBitwiseOrPropertyDefaultValue */'], + ['/* testBitwiseOrParamDefaultValue */'], + ['/* testBitwiseOr3 */'], + ['/* testBitwiseOrClosureParamDefault */'], + ['/* testBitwiseOrArrowParamDefault */'], + ['/* testBitwiseOrArrowExpression */'], + ['/* testBitwiseOrInArrayKey */'], + ['/* testBitwiseOrInArrayValue */'], + ['/* testBitwiseOrInShortArrayKey */'], + ['/* testBitwiseOrInShortArrayValue */'], + ['/* testBitwiseOrTryCatch */'], + ['/* testBitwiseOrNonArrowFnFunctionCall */'], + ['/* testLiveCoding */'], + ]; + + }//end dataBitwiseOr() + + + /** + * Test that bitwise or tokens when used as part of a union type are tokenized as `T_TYPE_UNION`. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @dataProvider dataTypeUnion + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testTypeUnion($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $opener = $this->getTargetToken($testMarker, [T_BITWISE_OR, T_TYPE_UNION]); + $this->assertSame(T_TYPE_UNION, $tokens[$opener]['code']); + $this->assertSame('T_TYPE_UNION', $tokens[$opener]['type']); + + }//end testTypeUnion() + + + /** + * Data provider. + * + * @see testTypeUnion() + * + * @return array + */ + public function dataTypeUnion() + { + return [ + ['/* testTypeUnionPropertySimple */'], + ['/* testTypeUnionPropertyReverseModifierOrder */'], + ['/* testTypeUnionPropertyMulti1 */'], + ['/* testTypeUnionPropertyMulti2 */'], + ['/* testTypeUnionPropertyMulti3 */'], + ['/* testTypeUnionPropertyNamespaceRelative */'], + ['/* testTypeUnionPropertyPartiallyQualified */'], + ['/* testTypeUnionPropertyFullyQualified */'], + ['/* testTypeUnionPropertyWithReadOnlyKeyword */'], + ['/* testTypeUnionPropertyWithReadOnlyKeywordFirst */'], + ['/* testTypeUnionPropertyWithStaticAndReadOnlyKeywords */'], + ['/* testTypeUnionPropertyWithVarAndReadOnlyKeywords */'], + ['/* testTypeUnionPropertyWithOnlyReadOnlyKeyword */'], + ['/* testTypeUnionParam1 */'], + ['/* testTypeUnionParam2 */'], + ['/* testTypeUnionParam3 */'], + ['/* testTypeUnionParamNamespaceRelative */'], + ['/* testTypeUnionParamPartiallyQualified */'], + ['/* testTypeUnionParamFullyQualified */'], + ['/* testTypeUnionReturnType */'], + ['/* testTypeUnionConstructorPropertyPromotion */'], + ['/* testTypeUnionAbstractMethodReturnType1 */'], + ['/* testTypeUnionAbstractMethodReturnType2 */'], + ['/* testTypeUnionReturnTypeNamespaceRelative */'], + ['/* testTypeUnionReturnPartiallyQualified */'], + ['/* testTypeUnionReturnFullyQualified */'], + ['/* testTypeUnionClosureParamIllegalNullable */'], + ['/* testTypeUnionWithReference */'], + ['/* testTypeUnionWithSpreadOperator */'], + ['/* testTypeUnionClosureReturn */'], + ['/* testTypeUnionArrowParam */'], + ['/* testTypeUnionArrowReturnType */'], + ['/* testTypeUnionNonArrowFunctionDeclaration */'], + ]; + + }//end dataTypeUnion() + + +}//end class diff --git a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc new file mode 100644 index 0000000000..bc98c49f3d --- /dev/null +++ b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.inc @@ -0,0 +1,264 @@ + 'a', + 2 => 'b', + /* testMatchDefaultIsKeyword */ default => 'default', +}; + +$closure = /* testFnIsKeyword */ fn () => 'string'; + +function () { + /* testYieldIsKeyword */ yield $f; + /* testYieldFromIsKeyword */ yield from someFunction(); +}; + +/* testDeclareIsKeyword */ declare(ticks=1): +/* testEndDeclareIsKeyword */ enddeclare; + +if (true /* testAndIsKeyword */ and false /* testOrIsKeyword */ or null /* testXorIsKeyword */ xor 0) { + +} + +$anonymousClass = new /* testAnonymousClassIsKeyword */ class {}; +$anonymousClass2 = new class /* testExtendsInAnonymousClassIsKeyword */ extends SomeParent {}; +$anonymousClass3 = new class /* testImplementsInAnonymousClassIsKeyword */ implements SomeInterface {}; + +$instantiated1 = new /* testClassInstantiationParentIsKeyword */ parent(); +$instantiated2 = new /* testClassInstantiationSelfIsKeyword */ SELF; +$instantiated3 = new /* testClassInstantiationStaticIsKeyword */ static($param); + +class Foo extends /* testNamespaceInNameIsKeyword */ namespace\Exception +{} + +function /* testKeywordAfterFunctionShouldBeString */ eval() {} +function /* testKeywordAfterFunctionByRefShouldBeString */ &switch() {} + +function /* testKeywordSelfAfterFunctionByRefShouldBeString */ &self() {} +function /* testKeywordStaticAfterFunctionByRefShouldBeString */ &static() {} +function /* testKeywordParentAfterFunctionByRefShouldBeString */ &parent() {} +function /* testKeywordFalseAfterFunctionByRefShouldBeString */ &false() {} +function /* testKeywordTrueAfterFunctionByRefShouldBeString */ & true () {} +function /* testKeywordNullAfterFunctionByRefShouldBeString */ &NULL() {} + +/* testKeywordAsFunctionCallNameShouldBeStringSelf */ self(); +/* testKeywordAsFunctionCallNameShouldBeStringStatic */ static(); +$obj-> /* testKeywordAsMethodCallNameShouldBeStringStatic */ static(); +/* testKeywordAsFunctionCallNameShouldBeStringParent */ parent(); +/* testKeywordAsFunctionCallNameShouldBeStringFalse */ false(); +/* testKeywordAsFunctionCallNameShouldBeStringTrue */ True (); +/* testKeywordAsFunctionCallNameShouldBeStringNull */ null /*comment*/ (); + +$instantiated4 = new /* testClassInstantiationFalseIsString */ False(); +$instantiated5 = new /* testClassInstantiationTrueIsString */ true (); +$instantiated6 = new /* testClassInstantiationNullIsString */ null(); + +$function = /* testStaticIsKeywordBeforeClosure */ static function(/* testStaticIsKeywordWhenParamType */ static $param) {}; +$arrow = /* testStaticIsKeywordBeforeArrow */ static fn(): /* testStaticIsKeywordWhenReturnType */ static => 10; + +function standAloneFalseTrueNullTypesAndMore( + /* testFalseIsKeywordAsParamType */ false $paramA, + /* testTrueIsKeywordAsParamType */ true $paramB, + /* testNullIsKeywordAsParamType */ null $paramC, +) /* testFalseIsKeywordAsReturnType */ false | /* testTrueIsKeywordAsReturnType */ true | /* testNullIsKeywordAsReturnType */ null { + if ($a === /* testFalseIsKeywordInComparison */ false + || $a === /* testTrueIsKeywordInComparison */ true + || $a === /* testNullIsKeywordInComparison */ null + ) {} +} diff --git a/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php new file mode 100644 index 0000000000..3f077ca639 --- /dev/null +++ b/tests/Core/Tokenizer/ContextSensitiveKeywordsTest.php @@ -0,0 +1,587 @@ + + * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; +use PHP_CodeSniffer\Util\Tokens; + +class ContextSensitiveKeywordsTest extends AbstractMethodUnitTest +{ + + + /** + * Test that context sensitive keyword is tokenized as string when it should be string. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @dataProvider dataStrings + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testStrings($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $token = $this->getTargetToken($testMarker, (Tokens::$contextSensitiveKeywords + [T_STRING, T_NULL, T_FALSE, T_TRUE, T_PARENT, T_SELF])); + + $this->assertSame(T_STRING, $tokens[$token]['code']); + $this->assertSame('T_STRING', $tokens[$token]['type']); + + }//end testStrings() + + + /** + * Data provider. + * + * @see testStrings() + * + * @return array + */ + public function dataStrings() + { + return [ + ['/* testAbstract */'], + ['/* testArray */'], + ['/* testAs */'], + ['/* testBreak */'], + ['/* testCallable */'], + ['/* testCase */'], + ['/* testCatch */'], + ['/* testClass */'], + ['/* testClone */'], + ['/* testConst */'], + ['/* testContinue */'], + ['/* testDeclare */'], + ['/* testDefault */'], + ['/* testDo */'], + ['/* testEcho */'], + ['/* testElse */'], + ['/* testElseIf */'], + ['/* testEmpty */'], + ['/* testEndDeclare */'], + ['/* testEndFor */'], + ['/* testEndForeach */'], + ['/* testEndIf */'], + ['/* testEndSwitch */'], + ['/* testEndWhile */'], + ['/* testEnum */'], + ['/* testEval */'], + ['/* testExit */'], + ['/* testExtends */'], + ['/* testFinal */'], + ['/* testFinally */'], + ['/* testFn */'], + ['/* testFor */'], + ['/* testForeach */'], + ['/* testFunction */'], + ['/* testGlobal */'], + ['/* testGoto */'], + ['/* testIf */'], + ['/* testImplements */'], + ['/* testInclude */'], + ['/* testIncludeOnce */'], + ['/* testInstanceOf */'], + ['/* testInsteadOf */'], + ['/* testInterface */'], + ['/* testIsset */'], + ['/* testList */'], + ['/* testMatch */'], + ['/* testNamespace */'], + ['/* testNew */'], + ['/* testParent */'], + ['/* testPrint */'], + ['/* testPrivate */'], + ['/* testProtected */'], + ['/* testPublic */'], + ['/* testReadonly */'], + ['/* testRequire */'], + ['/* testRequireOnce */'], + ['/* testReturn */'], + ['/* testSelf */'], + ['/* testStatic */'], + ['/* testSwitch */'], + ['/* testThrows */'], + ['/* testTrait */'], + ['/* testTry */'], + ['/* testUnset */'], + ['/* testUse */'], + ['/* testVar */'], + ['/* testWhile */'], + ['/* testYield */'], + ['/* testYieldFrom */'], + ['/* testAnd */'], + ['/* testOr */'], + ['/* testXor */'], + ['/* testFalse */'], + ['/* testTrue */'], + ['/* testNull */'], + + ['/* testKeywordAfterNamespaceShouldBeString */'], + ['/* testNamespaceNameIsString1 */'], + ['/* testNamespaceNameIsString2 */'], + ['/* testNamespaceNameIsString3 */'], + + ['/* testKeywordAfterFunctionShouldBeString */'], + ['/* testKeywordAfterFunctionByRefShouldBeString */'], + ['/* testKeywordSelfAfterFunctionByRefShouldBeString */'], + ['/* testKeywordStaticAfterFunctionByRefShouldBeString */'], + ['/* testKeywordParentAfterFunctionByRefShouldBeString */'], + ['/* testKeywordFalseAfterFunctionByRefShouldBeString */'], + ['/* testKeywordTrueAfterFunctionByRefShouldBeString */'], + ['/* testKeywordNullAfterFunctionByRefShouldBeString */'], + + ['/* testKeywordAsFunctionCallNameShouldBeStringSelf */'], + ['/* testKeywordAsFunctionCallNameShouldBeStringStatic */'], + ['/* testKeywordAsMethodCallNameShouldBeStringStatic */'], + ['/* testKeywordAsFunctionCallNameShouldBeStringParent */'], + ['/* testKeywordAsFunctionCallNameShouldBeStringFalse */'], + ['/* testKeywordAsFunctionCallNameShouldBeStringTrue */'], + ['/* testKeywordAsFunctionCallNameShouldBeStringNull */'], + + ['/* testClassInstantiationFalseIsString */'], + ['/* testClassInstantiationTrueIsString */'], + ['/* testClassInstantiationNullIsString */'], + ]; + + }//end dataStrings() + + + /** + * Test that context sensitive keyword is tokenized as keyword when it should be keyword. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $expectedTokenType The expected token type. + * + * @dataProvider dataKeywords + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testKeywords($testMarker, $expectedTokenType) + { + $tokens = self::$phpcsFile->getTokens(); + + $token = $this->getTargetToken( + $testMarker, + (Tokens::$contextSensitiveKeywords + [T_ANON_CLASS, T_MATCH_DEFAULT, T_PARENT, T_SELF, T_STRING, T_NULL, T_FALSE, T_TRUE]) + ); + + $this->assertSame(constant($expectedTokenType), $tokens[$token]['code']); + $this->assertSame($expectedTokenType, $tokens[$token]['type']); + + }//end testKeywords() + + + /** + * Data provider. + * + * @see testKeywords() + * + * @return array + */ + public function dataKeywords() + { + return [ + [ + '/* testNamespaceIsKeyword */', + 'T_NAMESPACE', + ], + [ + '/* testAbstractIsKeyword */', + 'T_ABSTRACT', + ], + [ + '/* testClassIsKeyword */', + 'T_CLASS', + ], + [ + '/* testExtendsIsKeyword */', + 'T_EXTENDS', + ], + [ + '/* testImplementsIsKeyword */', + 'T_IMPLEMENTS', + ], + [ + '/* testUseIsKeyword */', + 'T_USE', + ], + [ + '/* testInsteadOfIsKeyword */', + 'T_INSTEADOF', + ], + [ + '/* testAsIsKeyword */', + 'T_AS', + ], + [ + '/* testConstIsKeyword */', + 'T_CONST', + ], + [ + '/* testPrivateIsKeyword */', + 'T_PRIVATE', + ], + [ + '/* testProtectedIsKeyword */', + 'T_PROTECTED', + ], + [ + '/* testPublicIsKeyword */', + 'T_PUBLIC', + ], + [ + '/* testVarIsKeyword */', + 'T_VAR', + ], + [ + '/* testStaticIsKeyword */', + 'T_STATIC', + ], + [ + '/* testReadonlyIsKeyword */', + 'T_READONLY', + ], + [ + '/* testFinalIsKeyword */', + 'T_FINAL', + ], + [ + '/* testFunctionIsKeyword */', + 'T_FUNCTION', + ], + [ + '/* testCallableIsKeyword */', + 'T_CALLABLE', + ], + [ + '/* testSelfIsKeyword */', + 'T_SELF', + ], + [ + '/* testParentIsKeyword */', + 'T_PARENT', + ], + [ + '/* testReturnIsKeyword */', + 'T_RETURN', + ], + + [ + '/* testInterfaceIsKeyword */', + 'T_INTERFACE', + ], + [ + '/* testTraitIsKeyword */', + 'T_TRAIT', + ], + [ + '/* testEnumIsKeyword */', + 'T_ENUM', + ], + + [ + '/* testNewIsKeyword */', + 'T_NEW', + ], + [ + '/* testInstanceOfIsKeyword */', + 'T_INSTANCEOF', + ], + [ + '/* testCloneIsKeyword */', + 'T_CLONE', + ], + + [ + '/* testIfIsKeyword */', + 'T_IF', + ], + [ + '/* testEmptyIsKeyword */', + 'T_EMPTY', + ], + [ + '/* testElseIfIsKeyword */', + 'T_ELSEIF', + ], + [ + '/* testElseIsKeyword */', + 'T_ELSE', + ], + [ + '/* testEndIfIsKeyword */', + 'T_ENDIF', + ], + + [ + '/* testForIsKeyword */', + 'T_FOR', + ], + [ + '/* testEndForIsKeyword */', + 'T_ENDFOR', + ], + + [ + '/* testForeachIsKeyword */', + 'T_FOREACH', + ], + [ + '/* testEndForeachIsKeyword */', + 'T_ENDFOREACH', + ], + + [ + '/* testSwitchIsKeyword */', + 'T_SWITCH', + ], + [ + '/* testCaseIsKeyword */', + 'T_CASE', + ], + [ + '/* testDefaultIsKeyword */', + 'T_DEFAULT', + ], + [ + '/* testEndSwitchIsKeyword */', + 'T_ENDSWITCH', + ], + [ + '/* testBreakIsKeyword */', + 'T_BREAK', + ], + [ + '/* testContinueIsKeyword */', + 'T_CONTINUE', + ], + + [ + '/* testDoIsKeyword */', + 'T_DO', + ], + [ + '/* testWhileIsKeyword */', + 'T_WHILE', + ], + [ + '/* testEndWhileIsKeyword */', + 'T_ENDWHILE', + ], + + [ + '/* testTryIsKeyword */', + 'T_TRY', + ], + [ + '/* testThrowIsKeyword */', + 'T_THROW', + ], + [ + '/* testCatchIsKeyword */', + 'T_CATCH', + ], + [ + '/* testFinallyIsKeyword */', + 'T_FINALLY', + ], + + [ + '/* testGlobalIsKeyword */', + 'T_GLOBAL', + ], + [ + '/* testEchoIsKeyword */', + 'T_ECHO', + ], + [ + '/* testPrintIsKeyword */', + 'T_PRINT', + ], + [ + '/* testDieIsKeyword */', + 'T_EXIT', + ], + [ + '/* testEvalIsKeyword */', + 'T_EVAL', + ], + [ + '/* testExitIsKeyword */', + 'T_EXIT', + ], + [ + '/* testIssetIsKeyword */', + 'T_ISSET', + ], + [ + '/* testUnsetIsKeyword */', + 'T_UNSET', + ], + + [ + '/* testIncludeIsKeyword */', + 'T_INCLUDE', + ], + [ + '/* testIncludeOnceIsKeyword */', + 'T_INCLUDE_ONCE', + ], + [ + '/* testRequireIsKeyword */', + 'T_REQUIRE', + ], + [ + '/* testRequireOnceIsKeyword */', + 'T_REQUIRE_ONCE', + ], + + [ + '/* testListIsKeyword */', + 'T_LIST', + ], + [ + '/* testGotoIsKeyword */', + 'T_GOTO', + ], + [ + '/* testMatchIsKeyword */', + 'T_MATCH', + ], + [ + '/* testMatchDefaultIsKeyword */', + 'T_MATCH_DEFAULT', + ], + [ + '/* testFnIsKeyword */', + 'T_FN', + ], + + [ + '/* testYieldIsKeyword */', + 'T_YIELD', + ], + [ + '/* testYieldFromIsKeyword */', + 'T_YIELD_FROM', + ], + + [ + '/* testDeclareIsKeyword */', + 'T_DECLARE', + ], + [ + '/* testEndDeclareIsKeyword */', + 'T_ENDDECLARE', + ], + + [ + '/* testAndIsKeyword */', + 'T_LOGICAL_AND', + ], + [ + '/* testOrIsKeyword */', + 'T_LOGICAL_OR', + ], + [ + '/* testXorIsKeyword */', + 'T_LOGICAL_XOR', + ], + + [ + '/* testAnonymousClassIsKeyword */', + 'T_ANON_CLASS', + ], + [ + '/* testExtendsInAnonymousClassIsKeyword */', + 'T_EXTENDS', + ], + [ + '/* testImplementsInAnonymousClassIsKeyword */', + 'T_IMPLEMENTS', + ], + [ + '/* testClassInstantiationParentIsKeyword */', + 'T_PARENT', + ], + [ + '/* testClassInstantiationSelfIsKeyword */', + 'T_SELF', + ], + [ + '/* testClassInstantiationStaticIsKeyword */', + 'T_STATIC', + ], + [ + '/* testNamespaceInNameIsKeyword */', + 'T_NAMESPACE', + ], + + [ + '/* testStaticIsKeywordBeforeClosure */', + 'T_STATIC', + ], + [ + '/* testStaticIsKeywordWhenParamType */', + 'T_STATIC', + ], + [ + '/* testStaticIsKeywordBeforeArrow */', + 'T_STATIC', + ], + [ + '/* testStaticIsKeywordWhenReturnType */', + 'T_STATIC', + ], + + [ + '/* testFalseIsKeywordAsParamType */', + 'T_FALSE', + ], + [ + '/* testTrueIsKeywordAsParamType */', + 'T_TRUE', + ], + [ + '/* testNullIsKeywordAsParamType */', + 'T_NULL', + ], + [ + '/* testFalseIsKeywordAsReturnType */', + 'T_FALSE', + ], + [ + '/* testTrueIsKeywordAsReturnType */', + 'T_TRUE', + ], + [ + '/* testNullIsKeywordAsReturnType */', + 'T_NULL', + ], + [ + '/* testFalseIsKeywordInComparison */', + 'T_FALSE', + ], + [ + '/* testTrueIsKeywordInComparison */', + 'T_TRUE', + ], + [ + '/* testNullIsKeywordInComparison */', + 'T_NULL', + ], + ]; + + }//end dataKeywords() + + +}//end class diff --git a/tests/Core/Tokenizer/DefaultKeywordTest.inc b/tests/Core/Tokenizer/DefaultKeywordTest.inc new file mode 100644 index 0000000000..648149d2ff --- /dev/null +++ b/tests/Core/Tokenizer/DefaultKeywordTest.inc @@ -0,0 +1,203 @@ + 1, + 2 => 2, + /* testSimpleMatchDefault */ + default => 'default', + }; +} + +function switchWithDefault($i) { + switch ($i) { + case 1: + return 1; + case 2: + return 2; + /* testSimpleSwitchDefault */ + default: + return 'default'; + } +} + +function switchWithDefaultAndCurlies($i) { + switch ($i) { + case 1: + return 1; + case 2: + return 2; + /* testSimpleSwitchDefaultWithCurlies */ + default: { + return 'default'; + } + } +} + +function matchWithDefaultInSwitch() { + switch ($something) { + case 'foo': + $var = [1, 2, 3]; + $var = match ($i) { + 1 => 1, + /* testMatchDefaultNestedInSwitchCase1 */ + default => 'default', + }; + continue; + + case 'bar' : + $i = callMe($a, $b); + return match ($i) { + 1 => 1, + /* testMatchDefaultNestedInSwitchCase2 */ + default => 'default', + }; + + /* testSwitchDefault */ + default; + echo 'something', match ($i) { + 1, => 1, + /* testMatchDefaultNestedInSwitchDefault */ + default, => 'default', + }; + break; + } +} + +function switchWithDefaultInMatch() { + $x = match ($y) { + 5, 8 => function($z) { + switch($z) { + case 'a'; + $var = [1, 2, 3]; + return 'a'; + /* testSwitchDefaultNestedInMatchCase */ + default: + $var = [1, 2, 3]; + return 'default1'; + } + }, + /* testMatchDefault */ + default => function($z) { + switch($z) { + case 'a': + $i = callMe($a, $b); + return 'b'; + /* testSwitchDefaultNestedInMatchDefault */ + default: + $i = callMe($a, $b); + return 'default2'; + } + } + }; +} + +function shortArrayWithConstantKey() { + $arr = [ + /* testClassConstantAsShortArrayKey */ + SomeClass::DEFAULT => 1, + /* testClassPropertyAsShortArrayKey */ + SomeClass->DEFAULT => 1, + /* testNamespacedConstantAsShortArrayKey */ + // Intentional parse error PHP < 8.0. Reserved keyword used as namespaced constant. + SomeNamespace\DEFAULT => 1, + /* testFQNGlobalConstantAsShortArrayKey */ + // Intentional parse error in PHP < 8.0. Reserved keyword used as global constant. + \DEFAULT => 1, + ]; +} + +function longArrayWithConstantKey() { + $arr = array( + /* testClassConstantAsLongArrayKey */ + SomeClass::DEFAULT => 1, + ); +} + +function yieldWithConstantKey() { + /* testClassConstantAsYieldKey */ + yield SomeClass::DEFAULT => 1; +} + +function longArrayWithConstantKeyNestedInMatch() { + return match($x) { + /* testMatchDefaultWithNestedLongArrayWithClassConstantKey */ + DEFAULT => array( + /* testClassConstantAsLongArrayKeyNestedInMatch */ + SomeClass::DEFAULT => match($x) { + /* testMatchDefaultWithNestedLongArrayWithClassConstantKeyLevelDown */ + DEFAULT => array( + /* testClassConstantAsLongArrayKeyNestedInMatchLevelDown */ + SomeClass::DEFAULT => 1, + ), + }, + ), + }; +} + +function shortArrayWithConstantKeyNestedInMatch() { + return match($x) { + /* testMatchDefaultWithNestedShortArrayWithClassConstantKey */ + DEFAULT => [ + /* testClassConstantAsShortArrayKeyNestedInMatch */ + SomeClass::DEFAULT => match($x) { + /* testMatchDefaultWithNestedShortArrayWithClassConstantKeyLevelDown */ + DEFAULT => [ + /* testClassConstantAsShortArrayKeyNestedInMatchLevelDown */ + SomeClass::DEFAULT => 1, + ], + }, + ], + }; +} + + +function longArrayWithConstantKeyWithNestedMatch() { + return array( + /* testClassConstantAsLongArrayKeyWithNestedMatch */ + SomeClass::DEFAULT => match($x) { + /* testMatchDefaultNestedInLongArray */ + DEFAULT => 'foo' + }, + ); +} + +function shortArrayWithConstantKeyWithNestedMatch() { + return [ + /* testClassConstantAsShortArrayKeyWithNestedMatch */ + SomeClass::DEFAULT => match($x) { + /* testMatchDefaultNestedInShortArray */ + DEFAULT => 'foo' + }, + ]; +} + +function switchWithConstantNonDefault($i) { + switch ($i) { + /* testClassConstantInSwitchCase */ + case SomeClass::DEFAULT: + return 1; + + /* testClassPropertyInSwitchCase */ + case SomeClass->DEFAULT: + return 2; + + /* testNamespacedConstantInSwitchCase */ + // Intentional parse error PHP < 8.0. Reserved keyword used as constant. + case SomeNamespace\DEFAULT: + return 2; + + /* testNamespaceRelativeConstantInSwitchCase */ + // Intentional parse error PHP < 8.0. Reserved keyword used as global constant. + case namespace\DEFAULT: + return 2; + } +} + +class Foo { + /* testClassConstant */ + const DEFAULT = 'foo'; + + /* testMethodDeclaration */ + public function default() {} +} diff --git a/tests/Core/Tokenizer/DefaultKeywordTest.php b/tests/Core/Tokenizer/DefaultKeywordTest.php new file mode 100644 index 0000000000..9a5b061a05 --- /dev/null +++ b/tests/Core/Tokenizer/DefaultKeywordTest.php @@ -0,0 +1,302 @@ + + * @copyright 2020-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 DefaultKeywordTest extends AbstractMethodUnitTest +{ + + + /** + * Test the retokenization of the `default` keyword for match structure to `T_MATCH_DEFAULT`. + * + * Note: Cases and default structures within a match structure do *NOT* get case/default scope + * conditions, in contrast to case and default structures in switch control structures. + * + * @param string $testMarker The comment prefacing the target token. + * @param string $testContent The token content to look for. + * + * @dataProvider dataMatchDefault + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::recurseScopeMap + * + * @return void + */ + public function testMatchDefault($testMarker, $testContent='default') + { + $tokens = self::$phpcsFile->getTokens(); + + $token = $this->getTargetToken($testMarker, [T_MATCH_DEFAULT, T_DEFAULT, T_STRING], $testContent); + $tokenArray = $tokens[$token]; + + $this->assertSame(T_MATCH_DEFAULT, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_MATCH_DEFAULT (code)'); + $this->assertSame('T_MATCH_DEFAULT', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_MATCH_DEFAULT (type)'); + + $this->assertArrayNotHasKey('scope_condition', $tokenArray, 'Scope condition is set'); + $this->assertArrayNotHasKey('scope_opener', $tokenArray, 'Scope opener is set'); + $this->assertArrayNotHasKey('scope_closer', $tokenArray, 'Scope closer is set'); + + }//end testMatchDefault() + + + /** + * Data provider. + * + * @see testMatchDefault() + * + * @return array + */ + public function dataMatchDefault() + { + return [ + 'simple_match_default' => ['/* testSimpleMatchDefault */'], + 'match_default_in_switch_case_1' => ['/* testMatchDefaultNestedInSwitchCase1 */'], + 'match_default_in_switch_case_2' => ['/* testMatchDefaultNestedInSwitchCase2 */'], + 'match_default_in_switch_default' => ['/* testMatchDefaultNestedInSwitchDefault */'], + 'match_default_containing_switch' => ['/* testMatchDefault */'], + + 'match_default_with_nested_long_array_and_default_key' => [ + '/* testMatchDefaultWithNestedLongArrayWithClassConstantKey */', + 'DEFAULT', + ], + 'match_default_with_nested_long_array_and_default_key_2' => [ + '/* testMatchDefaultWithNestedLongArrayWithClassConstantKeyLevelDown */', + 'DEFAULT', + ], + 'match_default_with_nested_short_array_and_default_key' => [ + '/* testMatchDefaultWithNestedShortArrayWithClassConstantKey */', + 'DEFAULT', + ], + 'match_default_with_nested_short_array_and_default_key_2' => [ + '/* testMatchDefaultWithNestedShortArrayWithClassConstantKeyLevelDown */', + 'DEFAULT', + ], + 'match_default_in_long_array' => [ + '/* testMatchDefaultNestedInLongArray */', + 'DEFAULT', + ], + 'match_default_in_short_array' => [ + '/* testMatchDefaultNestedInShortArray */', + 'DEFAULT', + ], + ]; + + }//end dataMatchDefault() + + + /** + * Verify that the retokenization of `T_DEFAULT` tokens in match constructs, doesn't negatively + * impact the tokenization of `T_DEFAULT` tokens in switch control structures. + * + * Note: Cases and default structures within a switch control structure *do* get case/default scope + * conditions. + * + * @param string $testMarker The comment prefacing the target token. + * @param int $openerOffset The expected offset of the scope opener in relation to the testMarker. + * @param int $closerOffset The expected offset of the scope closer in relation to the testMarker. + * @param int|null $conditionStop The expected offset at which tokens stop having T_DEFAULT as a scope condition. + * @param string $testContent The token content to look for. + * + * @dataProvider dataSwitchDefault + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::recurseScopeMap + * + * @return void + */ + public function testSwitchDefault($testMarker, $openerOffset, $closerOffset, $conditionStop=null, $testContent='default') + { + $tokens = self::$phpcsFile->getTokens(); + + $token = $this->getTargetToken($testMarker, [T_MATCH_DEFAULT, T_DEFAULT, T_STRING], $testContent); + $tokenArray = $tokens[$token]; + $expectedScopeOpener = ($token + $openerOffset); + $expectedScopeCloser = ($token + $closerOffset); + + $this->assertSame(T_DEFAULT, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_DEFAULT (code)'); + $this->assertSame('T_DEFAULT', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_DEFAULT (type)'); + + $this->assertArrayHasKey('scope_condition', $tokenArray, 'Scope condition is not set'); + $this->assertArrayHasKey('scope_opener', $tokenArray, 'Scope opener is not set'); + $this->assertArrayHasKey('scope_closer', $tokenArray, 'Scope closer is not set'); + $this->assertSame($token, $tokenArray['scope_condition'], 'Scope condition is not the T_DEFAULT token'); + $this->assertSame($expectedScopeOpener, $tokenArray['scope_opener'], 'Scope opener of the T_DEFAULT token incorrect'); + $this->assertSame($expectedScopeCloser, $tokenArray['scope_closer'], 'Scope closer of the T_DEFAULT token incorrect'); + + $opener = $tokenArray['scope_opener']; + $this->assertArrayHasKey('scope_condition', $tokens[$opener], 'Opener scope condition is not set'); + $this->assertArrayHasKey('scope_opener', $tokens[$opener], 'Opener scope opener is not set'); + $this->assertArrayHasKey('scope_closer', $tokens[$opener], 'Opener scope closer is not set'); + $this->assertSame($token, $tokens[$opener]['scope_condition'], 'Opener scope condition is not the T_DEFAULT token'); + $this->assertSame($expectedScopeOpener, $tokens[$opener]['scope_opener'], 'T_DEFAULT opener scope opener token incorrect'); + $this->assertSame($expectedScopeCloser, $tokens[$opener]['scope_closer'], 'T_DEFAULT opener scope closer token incorrect'); + + $closer = $tokenArray['scope_closer']; + $this->assertArrayHasKey('scope_condition', $tokens[$closer], 'Closer scope condition is not set'); + $this->assertArrayHasKey('scope_opener', $tokens[$closer], 'Closer scope opener is not set'); + $this->assertArrayHasKey('scope_closer', $tokens[$closer], 'Closer scope closer is not set'); + $this->assertSame($token, $tokens[$closer]['scope_condition'], 'Closer scope condition is not the T_DEFAULT token'); + $this->assertSame($expectedScopeOpener, $tokens[$closer]['scope_opener'], 'T_DEFAULT closer scope opener token incorrect'); + $this->assertSame($expectedScopeCloser, $tokens[$closer]['scope_closer'], 'T_DEFAULT closer scope closer token incorrect'); + + if (($opener + 1) !== $closer) { + $end = $closer; + if (isset($conditionStop) === true) { + $end = $conditionStop; + } + + for ($i = ($opener + 1); $i < $end; $i++) { + $this->assertArrayHasKey( + $token, + $tokens[$i]['conditions'], + 'T_DEFAULT condition not added for token belonging to the T_DEFAULT structure' + ); + } + } + + }//end testSwitchDefault() + + + /** + * Data provider. + * + * @see testSwitchDefault() + * + * @return array + */ + public function dataSwitchDefault() + { + return [ + 'simple_switch_default' => [ + '/* testSimpleSwitchDefault */', + 1, + 4, + ], + 'simple_switch_default_with_curlies' => [ + // For a default structure with curly braces, the scope opener + // will be the open curly and the closer the close curly. + // However, scope conditions will not be set for open to close, + // but only for the open token up to the "break/return/continue" etc. + '/* testSimpleSwitchDefaultWithCurlies */', + 3, + 12, + 6, + ], + 'switch_default_toplevel' => [ + '/* testSwitchDefault */', + 1, + 43, + ], + 'switch_default_nested_in_match_case' => [ + '/* testSwitchDefaultNestedInMatchCase */', + 1, + 20, + ], + 'switch_default_nested_in_match_default' => [ + '/* testSwitchDefaultNestedInMatchDefault */', + 1, + 18, + ], + ]; + + }//end dataSwitchDefault() + + + /** + * Verify that the retokenization of `T_DEFAULT` tokens in match constructs, doesn't negatively + * impact the tokenization of `T_STRING` tokens with the contents 'default' which aren't in + * actual fact the default keyword. + * + * @param string $testMarker The comment prefacing the target token. + * @param string $testContent The token content to look for. + * + * @dataProvider dataNotDefaultKeyword + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testNotDefaultKeyword($testMarker, $testContent='DEFAULT') + { + $tokens = self::$phpcsFile->getTokens(); + + $token = $this->getTargetToken($testMarker, [T_MATCH_DEFAULT, T_DEFAULT, 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('scope_condition', $tokenArray, 'Scope condition is set'); + $this->assertArrayNotHasKey('scope_opener', $tokenArray, 'Scope opener is set'); + $this->assertArrayNotHasKey('scope_closer', $tokenArray, 'Scope closer is set'); + + }//end testNotDefaultKeyword() + + + /** + * Data provider. + * + * @see testNotDefaultKeyword() + * + * @return array + */ + public function dataNotDefaultKeyword() + { + return [ + 'class-constant-as-short-array-key' => ['/* testClassConstantAsShortArrayKey */'], + 'class-property-as-short-array-key' => ['/* testClassPropertyAsShortArrayKey */'], + 'namespaced-constant-as-short-array-key' => ['/* testNamespacedConstantAsShortArrayKey */'], + 'fqn-global-constant-as-short-array-key' => ['/* testFQNGlobalConstantAsShortArrayKey */'], + 'class-constant-as-long-array-key' => ['/* testClassConstantAsLongArrayKey */'], + 'class-constant-as-yield-key' => ['/* testClassConstantAsYieldKey */'], + + 'class-constant-as-long-array-key-nested-in-match' => ['/* testClassConstantAsLongArrayKeyNestedInMatch */'], + 'class-constant-as-long-array-key-nested-in-match-2' => ['/* testClassConstantAsLongArrayKeyNestedInMatchLevelDown */'], + 'class-constant-as-short-array-key-nested-in-match' => ['/* testClassConstantAsShortArrayKeyNestedInMatch */'], + 'class-constant-as-short-array-key-nested-in-match-2' => ['/* testClassConstantAsShortArrayKeyNestedInMatchLevelDown */'], + 'class-constant-as-long-array-key-with-nested-match' => ['/* testClassConstantAsLongArrayKeyWithNestedMatch */'], + 'class-constant-as-short-array-key-with-nested-match' => ['/* testClassConstantAsShortArrayKeyWithNestedMatch */'], + + 'class-constant-in-switch-case' => ['/* testClassConstantInSwitchCase */'], + 'class-property-in-switch-case' => ['/* testClassPropertyInSwitchCase */'], + 'namespaced-constant-in-switch-case' => ['/* testNamespacedConstantInSwitchCase */'], + 'namespace-relative-constant-in-switch-case' => ['/* testNamespaceRelativeConstantInSwitchCase */'], + + 'class-constant-declaration' => ['/* testClassConstant */'], + 'class-method-declaration' => [ + '/* testMethodDeclaration */', + 'default', + ], + ]; + + }//end dataNotDefaultKeyword() + + + /** + * Test a specific edge case where a scope opener would be incorrectly set. + * + * @link https://github.com/squizlabs/PHP_CodeSniffer/issues/3326 + * + * @return void + */ + public function testIssue3326() + { + $tokens = self::$phpcsFile->getTokens(); + + $token = $this->getTargetToken('/* testClassConstant */', [T_SEMICOLON]); + $tokenArray = $tokens[$token]; + + $this->assertArrayNotHasKey('scope_condition', $tokenArray, 'Scope condition is set'); + $this->assertArrayNotHasKey('scope_opener', $tokenArray, 'Scope opener is set'); + $this->assertArrayNotHasKey('scope_closer', $tokenArray, 'Scope closer is set'); + + }//end testIssue3326() + + +}//end class diff --git a/tests/Core/Tokenizer/DoubleArrowTest.inc b/tests/Core/Tokenizer/DoubleArrowTest.inc new file mode 100644 index 0000000000..b67b066015 --- /dev/null +++ b/tests/Core/Tokenizer/DoubleArrowTest.inc @@ -0,0 +1,281 @@ + 'Zero', + ); +} + +function simpleShortArray($x) { + return [ + /* testShortArrayArrowSimple */ + 0 => 'Zero', + ]; +} + +function simpleLongList($x) { + list( + /* testLongListArrowSimple */ + 0 => $a, + ) = $x; +} + +function simpleShortList($x) { + [ + /* testShortListArrowSimple */ + 0 => $a, + ] = $x; +} + +function simpleYield($x) { + $i = 0; + foreach (explode("\n", $x) as $line) { + /* testYieldArrowSimple */ + yield ++$i => $line; + } +} + +function simpleForeach($x) { + /* testForeachArrowSimple */ + foreach ($x as $k => $value) {} +} + +function simpleMatch($x) { + return match ($x) { + /* testMatchArrowSimpleSingleCase */ + 0 => 'Zero', + /* testMatchArrowSimpleMultiCase */ + 2, 4, 6 => 'Zero', + /* testMatchArrowSimpleSingleCaseWithTrailingComma */ + 1, => 'Zero', + /* testMatchArrowSimpleMultiCaseWithTrailingComma */ + 3, 5, => 'Zero', + }; +} + +function simpleArrowFunction($y) { + /* testFnArrowSimple */ + return fn ($y) => callMe($y); +} + +function matchNestedInMatch() { + $x = match ($y) { + /* testMatchArrowNestedMatchOuter */ + default, => match ($z) { + /* testMatchArrowNestedMatchInner */ + 1 => 1 + }, + }; +} + +function matchNestedInLongArrayValue() { + $array = array( + /* testLongArrayArrowWithNestedMatchValue1 */ + 'a' => match ($test) { + /* testMatchArrowInLongArrayValue1 */ + 1 => 'a', + /* testMatchArrowInLongArrayValue2 */ + 2 => 'b' + }, + /* testLongArrayArrowWithNestedMatchValue2 */ + $i => match ($test) { + /* testMatchArrowInLongArrayValue3 */ + 1 => 'a', + }, + ); +} + +function matchNestedInShortArrayValue() { + $array = [ + /* testShortArrayArrowWithNestedMatchValue1 */ + 'a' => match ($test) { + /* testMatchArrowInShortArrayValue1 */ + 1 => 'a', + /* testMatchArrowInShortArrayValue2 */ + 2 => 'b' + }, + /* testShortArrayArrowWithNestedMatchValue2 */ + $i => match ($test) { + /* testMatchArrowInShortArrayValue3 */ + 1 => 'a', + }, + ]; +} + +function matchNestedInLongArrayKey() { + $array = array( + match ($test) { /* testMatchArrowInLongArrayKey1 */ 1 => 'a', /* testMatchArrowInLongArrayKey2 */ 2 => 'b' } + /* testLongArrayArrowWithMatchKey */ + => 'dynamic keys, woho!', + ); +} + +function matchNestedInShortArrayKey() { + $array = [ + match ($test) { /* testMatchArrowInShortArrayKey1 */ 1 => 'a', /* testMatchArrowInShortArrayKey2 */ 2 => 'b' } + /* testShortArrayArrowWithMatchKey */ + => 'dynamic keys, woho!', + ]; +} + +function arraysNestedInMatch() { + $matcher = match ($x) { + /* testMatchArrowWithLongArrayBodyWithKeys */ + 0 => array( + /* testLongArrayArrowInMatchBody1 */ + 0 => 1, + /* testLongArrayArrowInMatchBody2 */ + 'a' => 2, + /* testLongArrayArrowInMatchBody3 */ + 'b' => 3 + ), + /* testMatchArrowWithShortArrayBodyWithoutKeys */ + 1 => [1, 2, 3], + /* testMatchArrowWithLongArrayBodyWithoutKeys */ + 2 => array( 1, [1, 2, 3], 2, 3), + /* testMatchArrowWithShortArrayBodyWithKeys */ + 3 => [ + /* testShortArrayArrowInMatchBody1 */ + 0 => 1, + /* testShortArrayArrowInMatchBody2 */ + 'a' => array(1, 2, 3), + /* testShortArrayArrowInMatchBody3 */ + 'b' => 2, + 3 + ], + /* testShortArrayArrowinMatchCase1 */ + [4 => 'a', /* testShortArrayArrowinMatchCase2 */ 5 => 6] + /* testMatchArrowWithShortArrayWithKeysAsCase */ + => 'match with array as case value', + /* testShortArrayArrowinMatchCase3 */ + [4 => 'a'], /* testLongArrayArrowinMatchCase4 */ array(5 => 6), + /* testMatchArrowWithMultipleArraysWithKeysAsCase */ + => 'match with multiple arrays as case value', + }; +} + +function matchNestedInArrowFunction($x) { + /* testFnArrowWithMatchInValue */ + $fn = fn($x) => match(true) { + /* testMatchArrowInFnBody1 */ + 1, 2, 3, 4, 5 => 'foo', + /* testMatchArrowInFnBody2 */ + default => 'bar', + }; +} + +function arrowFunctionsNestedInMatch($x) { + return match ($x) { + /* testMatchArrowWithFnBody1 */ + 1 => /* testFnArrowInMatchBody1 */ fn($y) => callMe($y), + /* testMatchArrowWithFnBody2 */ + default => /* testFnArrowInMatchBody2 */ fn($y) => callThem($y) + }; +} + +function matchShortArrayMismash() { + $array = [ + match ($test) { + /* testMatchArrowInComplexShortArrayKey1 */ + 1 => [ /* testShortArrayArrowInComplexMatchValueinShortArrayKey */ 1 => 'a'], + /* testMatchArrowInComplexShortArrayKey2 */ + 2 => 'b' + /* testShortArrayArrowInComplexMatchArrayMismash */ + } => match ($test) { + /* testMatchArrowInComplexShortArrayValue1 */ + 1 => [ /* testShortArrayArrowInComplexMatchValueinShortArrayValue */ 1 => 'a'], + /* testMatchArrowInComplexShortArrayValue2 */ + 2 => /* testFnArrowInComplexMatchValueInShortArrayValue */ fn($y) => callMe($y) + }, + ]; +} + + +function longListInMatch($x, $y) { + return match($x) { + /* testMatchArrowWithLongListBody */ + 1 => list('a' => $a, /* testLongListArrowInMatchBody */ 'b' => $b, 'c' => list('d' => $c)) = $y, + /* testLongListArrowInMatchCase */ + list('a' => $a, 'b' => $b) = $y /* testMatchArrowWithLongListInCase */ => 'something' + }; +} + +function shortListInMatch($x, $y) { + return match($x) { + /* testMatchArrowWithShortListBody */ + 1 => ['a' => $a, 'b' => $b, 'c' => /* testShortListArrowInMatchBody */ ['d' => $c]] = $y, + /* testShortListArrowInMatchCase */ + ['a' => $a, 'b' => $b] = $y /* testMatchArrowWithShortListInCase */ => 'something' + }; +} + +function matchInLongList() { + /* testMatchArrowInLongListKey */ + list(match($x) {1 => 1, 2 => 2} /* testLongListArrowWithMatchInKey */ => $a) = $array; +} + +function matchInShortList() { + /* testMatchArrowInShortListKey */ + [match($x) {1 => 1, 2 => 2} /* testShortListArrowWithMatchInKey */ => $a] = $array; +} + +function longArrayWithConstantKey() { + $arr = array( + /* testLongArrayArrowWithClassConstantKey */ + SomeClass::DEFAULT => 1, + ); +} + +function shortArrayWithConstantKey() { + $arr = [ + /* testShortArrayArrowWithClassConstantKey */ + SomeClass::DEFAULT => 1, + ]; +} + +function yieldWithConstantKey() { + /* testYieldArrowWithClassConstantKey */ + yield SomeClass::DEFAULT => 1; +} + +function longArrayWithConstantKeyNestedInMatch() { + return match($x) { + /* testMatchArrowWithNestedLongArrayWithClassConstantKey */ + default => array( + /* testLongArrayArrowWithClassConstantKeyNestedInMatch */ + SomeClass::DEFAULT => 1, + ), + }; +} + +function shortArrayWithConstantKeyNestedInMatch() { + return match($x) { + /* testMatchArrowWithNestedShortArrayWithClassConstantKey */ + default => [ + /* testShortArrayArrowWithClassConstantKeyNestedInMatch */ + SomeClass::DEFAULT => 1, + ], + }; +} + + +function longArrayWithConstantKeyWithNestedMatch() { + return array( + /* testLongArrayArrowWithClassConstantKeyWithNestedMatch */ + SomeClass::DEFAULT => match($x) { + /* testMatchArrowNestedInLongArrayWithClassConstantKey */ + default => 'foo' + }, + ); +} + +function shortArrayWithConstantKeyWithNestedMatch() { + return [ + /* testShortArrayArrowWithClassConstantKeyWithNestedMatch */ + SomeClass::DEFAULT => match($x) { + /* testMatchArrowNestedInShortArrayWithClassConstantKey */ + default => 'foo' + }, + ]; +} diff --git a/tests/Core/Tokenizer/DoubleArrowTest.php b/tests/Core/Tokenizer/DoubleArrowTest.php new file mode 100644 index 0000000000..ad1a411ff3 --- /dev/null +++ b/tests/Core/Tokenizer/DoubleArrowTest.php @@ -0,0 +1,237 @@ + + * @copyright 2020-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 DoubleArrowTest extends AbstractMethodUnitTest +{ + + + /** + * Test that "normal" double arrows are correctly tokenized as `T_DOUBLE_ARROW`. + * + * @param string $testMarker The comment prefacing the target token. + * + * @dataProvider dataDoubleArrow + * @coversNothing + * + * @return void + */ + public function testDoubleArrow($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $token = $this->getTargetToken($testMarker, [T_DOUBLE_ARROW, T_MATCH_ARROW, T_FN_ARROW]); + $tokenArray = $tokens[$token]; + + $this->assertSame(T_DOUBLE_ARROW, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_DOUBLE_ARROW (code)'); + $this->assertSame('T_DOUBLE_ARROW', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_DOUBLE_ARROW (type)'); + + }//end testDoubleArrow() + + + /** + * Data provider. + * + * @see testDoubleArrow() + * + * @return array + */ + public function dataDoubleArrow() + { + return [ + 'simple_long_array' => ['/* testLongArrayArrowSimple */'], + 'simple_short_array' => ['/* testShortArrayArrowSimple */'], + 'simple_long_list' => ['/* testLongListArrowSimple */'], + 'simple_short_list' => ['/* testShortListArrowSimple */'], + 'simple_yield' => ['/* testYieldArrowSimple */'], + 'simple_foreach' => ['/* testForeachArrowSimple */'], + + 'long_array_with_match_value_1' => ['/* testLongArrayArrowWithNestedMatchValue1 */'], + 'long_array_with_match_value_2' => ['/* testLongArrayArrowWithNestedMatchValue2 */'], + 'short_array_with_match_value_1' => ['/* testShortArrayArrowWithNestedMatchValue1 */'], + 'short_array_with_match_value_2' => ['/* testShortArrayArrowWithNestedMatchValue2 */'], + + 'long_array_with_match_key' => ['/* testLongArrayArrowWithMatchKey */'], + 'short_array_with_match_key' => ['/* testShortArrayArrowWithMatchKey */'], + + 'long_array_in_match_body_1' => ['/* testLongArrayArrowInMatchBody1 */'], + 'long_array_in_match_body_2' => ['/* testLongArrayArrowInMatchBody2 */'], + 'long_array_in_match_body_3' => ['/* testLongArrayArrowInMatchBody3 */'], + 'short_array_in_match_body_1' => ['/* testShortArrayArrowInMatchBody1 */'], + 'short_array_in_match_body_2' => ['/* testShortArrayArrowInMatchBody2 */'], + 'short_array_in_match_body_3' => ['/* testShortArrayArrowInMatchBody3 */'], + + 'short_array_in_match_case_1' => ['/* testShortArrayArrowinMatchCase1 */'], + 'short_array_in_match_case_2' => ['/* testShortArrayArrowinMatchCase2 */'], + 'short_array_in_match_case_3' => ['/* testShortArrayArrowinMatchCase3 */'], + 'long_array_in_match_case_4' => ['/* testLongArrayArrowinMatchCase4 */'], + + 'in_complex_short_array_key_match_value' => ['/* testShortArrayArrowInComplexMatchValueinShortArrayKey */'], + 'in_complex_short_array_toplevel' => ['/* testShortArrayArrowInComplexMatchArrayMismash */'], + 'in_complex_short_array_value_match_value' => ['/* testShortArrayArrowInComplexMatchValueinShortArrayValue */'], + + 'long_list_in_match_body' => ['/* testLongListArrowInMatchBody */'], + 'long_list_in_match_case' => ['/* testLongListArrowInMatchCase */'], + 'short_list_in_match_body' => ['/* testShortListArrowInMatchBody */'], + 'short_list_in_match_case' => ['/* testShortListArrowInMatchCase */'], + 'long_list_with_match_in_key' => ['/* testLongListArrowWithMatchInKey */'], + 'short_list_with_match_in_key' => ['/* testShortListArrowWithMatchInKey */'], + + 'long_array_with_constant_default_in_key' => ['/* testLongArrayArrowWithClassConstantKey */'], + 'short_array_with_constant_default_in_key' => ['/* testShortArrayArrowWithClassConstantKey */'], + 'yield_with_constant_default_in_key' => ['/* testYieldArrowWithClassConstantKey */'], + + 'long_array_with_default_in_key_in_match' => ['/* testLongArrayArrowWithClassConstantKeyNestedInMatch */'], + 'short_array_with_default_in_key_in_match' => ['/* testShortArrayArrowWithClassConstantKeyNestedInMatch */'], + 'long_array_with_default_in_key_with_match' => ['/* testLongArrayArrowWithClassConstantKeyWithNestedMatch */'], + 'short_array_with_default_in_key_with_match' => ['/* testShortArrayArrowWithClassConstantKeyWithNestedMatch */'], + ]; + + }//end dataDoubleArrow() + + + /** + * Test that double arrows in match expressions which are the demarkation between a case and the return value + * are correctly tokenized as `T_MATCH_ARROW`. + * + * @param string $testMarker The comment prefacing the target token. + * + * @dataProvider dataMatchArrow + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testMatchArrow($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $token = $this->getTargetToken($testMarker, [T_DOUBLE_ARROW, T_MATCH_ARROW, T_FN_ARROW]); + $tokenArray = $tokens[$token]; + + $this->assertSame(T_MATCH_ARROW, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_MATCH_ARROW (code)'); + $this->assertSame('T_MATCH_ARROW', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_MATCH_ARROW (type)'); + + }//end testMatchArrow() + + + /** + * Data provider. + * + * @see testMatchArrow() + * + * @return array + */ + public function dataMatchArrow() + { + return [ + 'single_case' => ['/* testMatchArrowSimpleSingleCase */'], + 'multi_case' => ['/* testMatchArrowSimpleMultiCase */'], + 'single_case_with_trailing_comma' => ['/* testMatchArrowSimpleSingleCaseWithTrailingComma */'], + 'multi_case_with_trailing_comma' => ['/* testMatchArrowSimpleMultiCaseWithTrailingComma */'], + 'match_nested_outer' => ['/* testMatchArrowNestedMatchOuter */'], + 'match_nested_inner' => ['/* testMatchArrowNestedMatchInner */'], + + 'in_long_array_value_1' => ['/* testMatchArrowInLongArrayValue1 */'], + 'in_long_array_value_2' => ['/* testMatchArrowInLongArrayValue2 */'], + 'in_long_array_value_3' => ['/* testMatchArrowInLongArrayValue3 */'], + 'in_short_array_value_1' => ['/* testMatchArrowInShortArrayValue1 */'], + 'in_short_array_value_2' => ['/* testMatchArrowInShortArrayValue2 */'], + 'in_short_array_value_3' => ['/* testMatchArrowInShortArrayValue3 */'], + + 'in_long_array_key_1' => ['/* testMatchArrowInLongArrayKey1 */'], + 'in_long_array_key_2' => ['/* testMatchArrowInLongArrayKey2 */'], + 'in_short_array_key_1' => ['/* testMatchArrowInShortArrayKey1 */'], + 'in_short_array_key_2' => ['/* testMatchArrowInShortArrayKey2 */'], + + 'with_long_array_value_with_keys' => ['/* testMatchArrowWithLongArrayBodyWithKeys */'], + 'with_short_array_value_without_keys' => ['/* testMatchArrowWithShortArrayBodyWithoutKeys */'], + 'with_long_array_value_without_keys' => ['/* testMatchArrowWithLongArrayBodyWithoutKeys */'], + 'with_short_array_value_with_keys' => ['/* testMatchArrowWithShortArrayBodyWithKeys */'], + + 'with_short_array_with_keys_as_case' => ['/* testMatchArrowWithShortArrayWithKeysAsCase */'], + 'with_multiple_arrays_with_keys_as_case' => ['/* testMatchArrowWithMultipleArraysWithKeysAsCase */'], + + 'in_fn_body_case' => ['/* testMatchArrowInFnBody1 */'], + 'in_fn_body_default' => ['/* testMatchArrowInFnBody2 */'], + 'with_fn_body_case' => ['/* testMatchArrowWithFnBody1 */'], + 'with_fn_body_default' => ['/* testMatchArrowWithFnBody2 */'], + + 'in_complex_short_array_key_1' => ['/* testMatchArrowInComplexShortArrayKey1 */'], + 'in_complex_short_array_key_2' => ['/* testMatchArrowInComplexShortArrayKey2 */'], + 'in_complex_short_array_value_1' => ['/* testMatchArrowInComplexShortArrayValue1 */'], + 'in_complex_short_array_value_2' => ['/* testMatchArrowInComplexShortArrayValue2 */'], + + 'with_long_list_in_body' => ['/* testMatchArrowWithLongListBody */'], + 'with_long_list_in_case' => ['/* testMatchArrowWithLongListInCase */'], + 'with_short_list_in_body' => ['/* testMatchArrowWithShortListBody */'], + 'with_short_list_in_case' => ['/* testMatchArrowWithShortListInCase */'], + 'in_long_list_key' => ['/* testMatchArrowInLongListKey */'], + 'in_short_list_key' => ['/* testMatchArrowInShortListKey */'], + + 'with_long_array_value_with_default_key' => ['/* testMatchArrowWithNestedLongArrayWithClassConstantKey */'], + 'with_short_array_value_with_default_key' => ['/* testMatchArrowWithNestedShortArrayWithClassConstantKey */'], + 'in_long_array_value_with_default_key' => ['/* testMatchArrowNestedInLongArrayWithClassConstantKey */'], + 'in_short_array_value_with_default_key' => ['/* testMatchArrowNestedInShortArrayWithClassConstantKey */'], + ]; + + }//end dataMatchArrow() + + + /** + * Test that double arrows used as the scope opener for an arrow function + * are correctly tokenized as `T_FN_ARROW`. + * + * @param string $testMarker The comment prefacing the target token. + * + * @dataProvider dataFnArrow + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testFnArrow($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $token = $this->getTargetToken($testMarker, [T_DOUBLE_ARROW, T_MATCH_ARROW, T_FN_ARROW]); + $tokenArray = $tokens[$token]; + + $this->assertSame(T_FN_ARROW, $tokenArray['code'], 'Token tokenized as '.$tokenArray['type'].', not T_FN_ARROW (code)'); + $this->assertSame('T_FN_ARROW', $tokenArray['type'], 'Token tokenized as '.$tokenArray['type'].', not T_FN_ARROW (type)'); + + }//end testFnArrow() + + + /** + * Data provider. + * + * @see testFnArrow() + * + * @return array + */ + public function dataFnArrow() + { + return [ + 'simple_fn' => ['/* testFnArrowSimple */'], + + 'with_match_as_value' => ['/* testFnArrowWithMatchInValue */'], + 'in_match_value_case' => ['/* testFnArrowInMatchBody1 */'], + 'in_match_value_default' => ['/* testFnArrowInMatchBody2 */'], + + 'in_complex_match_value_in_short_array' => ['/* testFnArrowInComplexMatchValueInShortArrayValue */'], + ]; + + }//end dataFnArrow() + + +}//end class diff --git a/tests/Core/Tokenizer/DoubleQuotedStringTest.inc b/tests/Core/Tokenizer/DoubleQuotedStringTest.inc new file mode 100644 index 0000000000..62535b1e41 --- /dev/null +++ b/tests/Core/Tokenizer/DoubleQuotedStringTest.inc @@ -0,0 +1,52 @@ +bar"; +/* testProperty2 */ +"{$foo->bar}"; + +/* testMethod1 */ +"{$foo->bar()}"; + +/* testClosure1 */ +"{$foo()}"; + +/* testChain1 */ +"{$foo['bar']->baz()()}"; + +/* testVariableVar1 */ +"${$bar}"; +/* testVariableVar2 */ +"${(foo)}"; +/* testVariableVar3 */ +"${foo->bar}"; + +/* testNested1 */ +"${foo["${bar}"]}"; +/* testNested2 */ +"${foo["${bar['baz']}"]}"; +/* testNested3 */ +"${foo->{$baz}}"; +/* testNested4 */ +"${foo->{${'a'}}}"; +/* testNested5 */ +"${foo->{"${'a'}"}}"; + +/* testParseError */ +"${foo["${bar diff --git a/tests/Core/Tokenizer/DoubleQuotedStringTest.php b/tests/Core/Tokenizer/DoubleQuotedStringTest.php new file mode 100644 index 0000000000..cc9fe49ec4 --- /dev/null +++ b/tests/Core/Tokenizer/DoubleQuotedStringTest.php @@ -0,0 +1,136 @@ + + * @copyright 2022 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class DoubleQuotedStringTest extends AbstractMethodUnitTest +{ + + + /** + * Test that double quoted strings contain the complete string. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $expectedContent The expected content of the double quoted string. + * + * @dataProvider dataDoubleQuotedString + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testDoubleQuotedString($testMarker, $expectedContent) + { + $tokens = self::$phpcsFile->getTokens(); + + $target = $this->getTargetToken($testMarker, T_DOUBLE_QUOTED_STRING); + $this->assertSame($expectedContent, $tokens[$target]['content']); + + }//end testDoubleQuotedString() + + + /** + * Data provider. + * + * @see testDoubleQuotedString() + * + * @return array + */ + public function dataDoubleQuotedString() + { + return [ + [ + 'testMarker' => '/* testSimple1 */', + 'expectedContent' => '"$foo"', + ], + [ + 'testMarker' => '/* testSimple2 */', + 'expectedContent' => '"{$foo}"', + ], + [ + 'testMarker' => '/* testSimple3 */', + 'expectedContent' => '"${foo}"', + ], + [ + 'testMarker' => '/* testDIM1 */', + 'expectedContent' => '"$foo[bar]"', + ], + [ + 'testMarker' => '/* testDIM2 */', + 'expectedContent' => '"{$foo[\'bar\']}"', + ], + [ + 'testMarker' => '/* testDIM3 */', + 'expectedContent' => '"${foo[\'bar\']}"', + ], + [ + 'testMarker' => '/* testProperty1 */', + 'expectedContent' => '"$foo->bar"', + ], + [ + 'testMarker' => '/* testProperty2 */', + 'expectedContent' => '"{$foo->bar}"', + ], + [ + 'testMarker' => '/* testMethod1 */', + 'expectedContent' => '"{$foo->bar()}"', + ], + [ + 'testMarker' => '/* testClosure1 */', + 'expectedContent' => '"{$foo()}"', + ], + [ + 'testMarker' => '/* testChain1 */', + 'expectedContent' => '"{$foo[\'bar\']->baz()()}"', + ], + [ + 'testMarker' => '/* testVariableVar1 */', + 'expectedContent' => '"${$bar}"', + ], + [ + 'testMarker' => '/* testVariableVar2 */', + 'expectedContent' => '"${(foo)}"', + ], + [ + 'testMarker' => '/* testVariableVar3 */', + 'expectedContent' => '"${foo->bar}"', + ], + [ + 'testMarker' => '/* testNested1 */', + 'expectedContent' => '"${foo["${bar}"]}"', + ], + [ + 'testMarker' => '/* testNested2 */', + 'expectedContent' => '"${foo["${bar[\'baz\']}"]}"', + ], + [ + 'testMarker' => '/* testNested3 */', + 'expectedContent' => '"${foo->{$baz}}"', + ], + [ + 'testMarker' => '/* testNested4 */', + 'expectedContent' => '"${foo->{${\'a\'}}}"', + ], + [ + 'testMarker' => '/* testNested5 */', + 'expectedContent' => '"${foo->{"${\'a\'}"}}"', + ], + [ + 'testMarker' => '/* testParseError */', + 'expectedContent' => '"${foo["${bar +', + ], + ]; + + }//end dataDoubleQuotedString() + + +}//end class diff --git a/tests/Core/Tokenizer/EnumCaseTest.inc b/tests/Core/Tokenizer/EnumCaseTest.inc new file mode 100644 index 0000000000..13b87242e1 --- /dev/null +++ b/tests/Core/Tokenizer/EnumCaseTest.inc @@ -0,0 +1,95 @@ + + * @copyright 2021 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class EnumCaseTest extends AbstractMethodUnitTest +{ + + + /** + * Test that the enum "case" is converted to T_ENUM_CASE. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @dataProvider dataEnumCases + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::recurseScopeMap + * + * @return void + */ + public function testEnumCases($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $enumCase = $this->getTargetToken($testMarker, [T_ENUM_CASE, T_CASE]); + + $this->assertSame(T_ENUM_CASE, $tokens[$enumCase]['code']); + $this->assertSame('T_ENUM_CASE', $tokens[$enumCase]['type']); + + $this->assertArrayNotHasKey('scope_condition', $tokens[$enumCase], 'Scope condition is set'); + $this->assertArrayNotHasKey('scope_opener', $tokens[$enumCase], 'Scope opener is set'); + $this->assertArrayNotHasKey('scope_closer', $tokens[$enumCase], 'Scope closer is set'); + + }//end testEnumCases() + + + /** + * Data provider. + * + * @see testEnumCases() + * + * @return array + */ + public function dataEnumCases() + { + return [ + ['/* testPureEnumCase */'], + ['/* testBackingIntegerEnumCase */'], + ['/* testBackingStringEnumCase */'], + ['/* testEnumCaseInComplexEnum */'], + ['/* testEnumCaseIsCaseInsensitive */'], + ['/* testEnumCaseAfterSwitch */'], + ['/* testEnumCaseAfterSwitchWithEndSwitch */'], + ]; + + }//end dataEnumCases() + + + /** + * Test that "case" that is not enum case is still tokenized as `T_CASE`. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @dataProvider dataNotEnumCases + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::recurseScopeMap + * + * @return void + */ + public function testNotEnumCases($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $case = $this->getTargetToken($testMarker, [T_ENUM_CASE, T_CASE]); + + $this->assertSame(T_CASE, $tokens[$case]['code']); + $this->assertSame('T_CASE', $tokens[$case]['type']); + + $this->assertArrayHasKey('scope_condition', $tokens[$case], 'Scope condition is not set'); + $this->assertArrayHasKey('scope_opener', $tokens[$case], 'Scope opener is not set'); + $this->assertArrayHasKey('scope_closer', $tokens[$case], 'Scope closer is not set'); + + }//end testNotEnumCases() + + + /** + * Data provider. + * + * @see testNotEnumCases() + * + * @return array + */ + public function dataNotEnumCases() + { + return [ + ['/* testCaseWithSemicolonIsNotEnumCase */'], + ['/* testCaseWithConstantIsNotEnumCase */'], + ['/* testCaseWithConstantAndIdenticalIsNotEnumCase */'], + ['/* testCaseWithAssigmentToConstantIsNotEnumCase */'], + ['/* testIsNotEnumCaseIsCaseInsensitive */'], + ['/* testCaseInSwitchWhenCreatingEnumInSwitch1 */'], + ['/* testCaseInSwitchWhenCreatingEnumInSwitch2 */'], + ]; + + }//end dataNotEnumCases() + + + /** + * Test that "case" that is not enum case is still tokenized as `T_CASE`. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @dataProvider dataKeywordAsEnumCaseNameShouldBeString + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testKeywordAsEnumCaseNameShouldBeString($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $enumCaseName = $this->getTargetToken($testMarker, [T_STRING, T_INTERFACE, T_TRAIT, T_ENUM, T_FUNCTION, T_FALSE, T_DEFAULT, T_ARRAY]); + + $this->assertSame(T_STRING, $tokens[$enumCaseName]['code']); + $this->assertSame('T_STRING', $tokens[$enumCaseName]['type']); + + }//end testKeywordAsEnumCaseNameShouldBeString() + + + /** + * Data provider. + * + * @see testKeywordAsEnumCaseNameShouldBeString() + * + * @return array + */ + public function dataKeywordAsEnumCaseNameShouldBeString() + { + return [ + ['/* testKeywordAsEnumCaseNameShouldBeString1 */'], + ['/* testKeywordAsEnumCaseNameShouldBeString2 */'], + ['/* testKeywordAsEnumCaseNameShouldBeString3 */'], + ['/* testKeywordAsEnumCaseNameShouldBeString4 */'], + ]; + + }//end dataKeywordAsEnumCaseNameShouldBeString() + + +}//end class diff --git a/tests/Core/Tokenizer/FinallyTest.inc b/tests/Core/Tokenizer/FinallyTest.inc new file mode 100644 index 0000000000..e65600b6b4 --- /dev/null +++ b/tests/Core/Tokenizer/FinallyTest.inc @@ -0,0 +1,40 @@ +finally = 'foo'; + } +} diff --git a/tests/Core/Tokenizer/FinallyTest.php b/tests/Core/Tokenizer/FinallyTest.php new file mode 100644 index 0000000000..2b28bc5d30 --- /dev/null +++ b/tests/Core/Tokenizer/FinallyTest.php @@ -0,0 +1,96 @@ + + * @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 FinallyTest extends AbstractMethodUnitTest +{ + + + /** + * Test that the finally keyword is tokenized as such. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @dataProvider dataFinallyKeyword + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testFinallyKeyword($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $target = $this->getTargetToken($testMarker, [T_FINALLY, T_STRING]); + $this->assertSame(T_FINALLY, $tokens[$target]['code']); + $this->assertSame('T_FINALLY', $tokens[$target]['type']); + + }//end testFinallyKeyword() + + + /** + * Data provider. + * + * @see testFinallyKeyword() + * + * @return array + */ + public function dataFinallyKeyword() + { + return [ + ['/* testTryCatchFinally */'], + ['/* testTryFinallyCatch */'], + ['/* testTryFinally */'], + ]; + + }//end dataFinallyKeyword() + + + /** + * Test that 'finally' when not used as the reserved keyword is tokenized as `T_STRING`. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @dataProvider dataFinallyNonKeyword + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testFinallyNonKeyword($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $target = $this->getTargetToken($testMarker, [T_FINALLY, T_STRING]); + $this->assertSame(T_STRING, $tokens[$target]['code']); + $this->assertSame('T_STRING', $tokens[$target]['type']); + + }//end testFinallyNonKeyword() + + + /** + * Data provider. + * + * @see testFinallyNonKeyword() + * + * @return array + */ + public function dataFinallyNonKeyword() + { + return [ + ['/* testFinallyUsedAsClassConstantName */'], + ['/* testFinallyUsedAsMethodName */'], + ['/* testFinallyUsedAsPropertyName */'], + ]; + + }//end dataFinallyNonKeyword() + + +}//end class diff --git a/tests/Core/Tokenizer/GotoLabelTest.inc b/tests/Core/Tokenizer/GotoLabelTest.inc new file mode 100644 index 0000000000..12df5d296b --- /dev/null +++ b/tests/Core/Tokenizer/GotoLabelTest.inc @@ -0,0 +1,56 @@ + +
+ +property: + // Do something. + break; +} + +switch (true) { + /* testNotGotoDeclarationGlobalConstantInTernary */ + case $x === ($cond) ? CONST_A : CONST_B: + // Do something. + break; +} + +/* testNotGotoDeclarationEnumWithType */ +enum Suit: string implements Colorful, CardGame {} diff --git a/tests/Core/Tokenizer/GotoLabelTest.php b/tests/Core/Tokenizer/GotoLabelTest.php new file mode 100644 index 0000000000..0f937cc8b0 --- /dev/null +++ b/tests/Core/Tokenizer/GotoLabelTest.php @@ -0,0 +1,175 @@ + + * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class GotoLabelTest extends AbstractMethodUnitTest +{ + + + /** + * Verify that the label in a goto statement is tokenized as T_STRING. + * + * @param string $testMarker The comment prefacing the target token. + * @param string $testContent The token content to expect. + * + * @dataProvider dataGotoStatement + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testGotoStatement($testMarker, $testContent) + { + $tokens = self::$phpcsFile->getTokens(); + + $label = $this->getTargetToken($testMarker, T_STRING); + + $this->assertInternalType('int', $label); + $this->assertSame($testContent, $tokens[$label]['content']); + + }//end testGotoStatement() + + + /** + * Data provider. + * + * @see testGotoStatement() + * + * @return array + */ + public function dataGotoStatement() + { + return [ + [ + '/* testGotoStatement */', + 'marker', + ], + [ + '/* testGotoStatementInLoop */', + 'end', + ], + ]; + + }//end dataGotoStatement() + + + /** + * Verify that the label in a goto declaration is tokenized as T_GOTO_LABEL. + * + * @param string $testMarker The comment prefacing the target token. + * @param string $testContent The token content to expect. + * + * @dataProvider dataGotoDeclaration + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testGotoDeclaration($testMarker, $testContent) + { + $tokens = self::$phpcsFile->getTokens(); + + $label = $this->getTargetToken($testMarker, T_GOTO_LABEL); + + $this->assertInternalType('int', $label); + $this->assertSame($testContent, $tokens[$label]['content']); + + }//end testGotoDeclaration() + + + /** + * Data provider. + * + * @see testGotoDeclaration() + * + * @return array + */ + public function dataGotoDeclaration() + { + return [ + [ + '/* testGotoDeclaration */', + 'marker:', + ], + [ + '/* testGotoDeclarationOutsideLoop */', + 'end:', + ], + ]; + + }//end dataGotoDeclaration() + + + /** + * Verify that the constant used in a switch - case statement is not confused with a goto label. + * + * @param string $testMarker The comment prefacing the target token. + * @param string $testContent The token content to expect. + * + * @dataProvider dataNotAGotoDeclaration + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testNotAGotoDeclaration($testMarker, $testContent) + { + $tokens = self::$phpcsFile->getTokens(); + $target = $this->getTargetToken($testMarker, [T_GOTO_LABEL, T_STRING], $testContent); + + $this->assertSame(T_STRING, $tokens[$target]['code']); + $this->assertSame('T_STRING', $tokens[$target]['type']); + + }//end testNotAGotoDeclaration() + + + /** + * Data provider. + * + * @see testNotAGotoDeclaration() + * + * @return array + */ + public function dataNotAGotoDeclaration() + { + return [ + [ + '/* testNotGotoDeclarationGlobalConstant */', + 'CONSTANT', + ], + [ + '/* testNotGotoDeclarationNamespacedConstant */', + 'CONSTANT', + ], + [ + '/* testNotGotoDeclarationClassConstant */', + 'CONSTANT', + ], + [ + '/* testNotGotoDeclarationClassProperty */', + 'property', + ], + [ + '/* testNotGotoDeclarationGlobalConstantInTernary */', + 'CONST_A', + ], + [ + '/* testNotGotoDeclarationGlobalConstantInTernary */', + 'CONST_B', + ], + [ + '/* testNotGotoDeclarationEnumWithType */', + 'Suit', + ], + ]; + + }//end dataNotAGotoDeclaration() + + +}//end class diff --git a/tests/Core/Tokenizer/HeredocNowdocCloserTest.inc b/tests/Core/Tokenizer/HeredocNowdocCloserTest.inc new file mode 100644 index 0000000000..a800980b8e --- /dev/null +++ b/tests/Core/Tokenizer/HeredocNowdocCloserTest.inc @@ -0,0 +1,43 @@ + + * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Config; +use PHP_CodeSniffer\Ruleset; +use PHP_CodeSniffer\Files\DummyFile; +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +/** + * Heredoc/nowdoc closer token test. + * + * @requires PHP 7.3 + */ +class HeredocNowdocCloserTest extends AbstractMethodUnitTest +{ + + + /** + * Initialize & tokenize \PHP_CodeSniffer\Files\File with code from the test case file. + * + * {@internal This is a near duplicate of the original method. Only difference is that + * tab replacement is enabled for this test.} + * + * @return void + */ + public static function setUpBeforeClass() + { + $config = new Config(); + $config->standards = ['PSR1']; + $config->tabWidth = 4; + + $ruleset = new Ruleset($config); + + // Default to a file with the same name as the test class. Extension is property based. + $relativeCN = str_replace(__NAMESPACE__, '', get_called_class()); + $relativePath = str_replace('\\', DIRECTORY_SEPARATOR, $relativeCN); + $pathToTestFile = realpath(__DIR__).$relativePath.'.'.static::$fileExtension; + + // Make sure the file gets parsed correctly based on the file type. + $contents = 'phpcs_input_file: '.$pathToTestFile.PHP_EOL; + $contents .= file_get_contents($pathToTestFile); + + self::$phpcsFile = new DummyFile($contents, $ruleset, $config); + self::$phpcsFile->process(); + + }//end setUpBeforeClass() + + + /** + * Verify that leading (indent) whitespace in a heredoc/nowdoc closer token get the tab replacement treatment. + * + * @param string $testMarker The comment prefacing the target token. + * @param array $expected Expectations for the token array. + * + * @dataProvider dataHeredocNowdocCloserTabReplacement + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::createPositionMap + * + * @return void + */ + public function testHeredocNowdocCloserTabReplacement($testMarker, $expected) + { + $tokens = self::$phpcsFile->getTokens(); + + $closer = $this->getTargetToken($testMarker, [T_END_HEREDOC, T_END_NOWDOC]); + + foreach ($expected as $key => $value) { + if ($key === 'orig_content' && $value === null) { + $this->assertArrayNotHasKey($key, $tokens[$closer], "Unexpected 'orig_content' key found in the token array."); + continue; + } + + $this->assertArrayHasKey($key, $tokens[$closer], "Key $key not found in the token array."); + $this->assertSame($value, $tokens[$closer][$key], "Value for key $key does not match expectation."); + } + + }//end testHeredocNowdocCloserTabReplacement() + + + /** + * Data provider. + * + * @see testHeredocNowdocCloserTabReplacement() + * + * @return array + */ + public function dataHeredocNowdocCloserTabReplacement() + { + return [ + [ + 'testMarker' => '/* testHeredocCloserNoIndent */', + 'expected' => [ + 'length' => 3, + 'content' => 'EOD', + 'orig_content' => null, + ], + ], + [ + 'testMarker' => '/* testNowdocCloserNoIndent */', + 'expected' => [ + 'length' => 3, + 'content' => 'EOD', + 'orig_content' => null, + ], + ], + [ + 'testMarker' => '/* testHeredocCloserSpaceIndent */', + 'expected' => [ + 'length' => 7, + 'content' => ' END', + 'orig_content' => null, + ], + ], + [ + 'testMarker' => '/* testNowdocCloserSpaceIndent */', + 'expected' => [ + 'length' => 8, + 'content' => ' END', + 'orig_content' => null, + ], + ], + [ + 'testMarker' => '/* testHeredocCloserTabIndent */', + 'expected' => [ + 'length' => 8, + 'content' => ' END', + 'orig_content' => ' END', + ], + ], + [ + 'testMarker' => '/* testNowdocCloserTabIndent */', + 'expected' => [ + 'length' => 7, + 'content' => ' END', + 'orig_content' => ' END', + ], + ], + ]; + + }//end dataHeredocNowdocCloserTabReplacement() + + +}//end class diff --git a/tests/Core/Tokenizer/HeredocStringTest.inc b/tests/Core/Tokenizer/HeredocStringTest.inc new file mode 100644 index 0000000000..ae43e24a59 --- /dev/null +++ b/tests/Core/Tokenizer/HeredocStringTest.inc @@ -0,0 +1,193 @@ +bar +EOD; + +/* testProperty2 */ +$heredoc = <<<"EOD" +{$foo->bar} +EOD; + +/* testMethod1 */ +$heredoc = <<bar()} +EOD; + +/* testClosure1 */ +$heredoc = <<<"EOD" +{$foo()} +EOD; + +/* testChain1 */ +$heredoc = <<baz()()} +EOD; + +/* testVariableVar1 */ +$heredoc = <<<"EOD" +${$bar} +EOD; + +/* testVariableVar2 */ +$heredoc = <<bar} +EOD; + +/* testNested1 */ +$heredoc = <<{$baz}} +EOD; + +/* testNested4 */ +$heredoc = <<<"EOD" +${foo->{${'a'}}} +EOD; + +/* testNested5 */ +$heredoc = <<{"${'a'}"}} +EOD; + +/* testSimple1Wrapped */ +$heredoc = <<bar Something +EOD; + +/* testProperty2Wrapped */ +$heredoc = <<<"EOD" +Do {$foo->bar} Something +EOD; + +/* testMethod1Wrapped */ +$heredoc = <<bar()} Something +EOD; + +/* testClosure1Wrapped */ +$heredoc = <<<"EOD" +Do {$foo()} Something +EOD; + +/* testChain1Wrapped */ +$heredoc = <<baz()()} Something +EOD; + +/* testVariableVar1Wrapped */ +$heredoc = <<<"EOD" +Do ${$bar} Something +EOD; + +/* testVariableVar2Wrapped */ +$heredoc = <<bar} Something +EOD; + +/* testNested1Wrapped */ +$heredoc = <<{$baz}} Something +EOD; + +/* testNested4Wrapped */ +$heredoc = <<<"EOD" +Do ${foo->{${'a'}}} Something +EOD; + +/* testNested5Wrapped */ +$heredoc = <<{"${'a'}"}} Something +EOD; diff --git a/tests/Core/Tokenizer/HeredocStringTest.php b/tests/Core/Tokenizer/HeredocStringTest.php new file mode 100644 index 0000000000..2c808be906 --- /dev/null +++ b/tests/Core/Tokenizer/HeredocStringTest.php @@ -0,0 +1,153 @@ + + * @copyright 2022 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class HeredocStringTest extends AbstractMethodUnitTest +{ + + + /** + * Test that heredoc strings contain the complete interpolated string. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $expectedContent The expected content of the heredoc string. + * + * @dataProvider dataHeredocString + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testHeredocString($testMarker, $expectedContent) + { + $tokens = self::$phpcsFile->getTokens(); + + $target = $this->getTargetToken($testMarker, T_HEREDOC); + $this->assertSame($expectedContent."\n", $tokens[$target]['content']); + + }//end testHeredocString() + + + /** + * Test that heredoc strings contain the complete interpolated string when combined with other texts. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param string $expectedContent The expected content of the heredoc string. + * + * @dataProvider dataHeredocString + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testHeredocStringWrapped($testMarker, $expectedContent) + { + $tokens = self::$phpcsFile->getTokens(); + + $testMarker = substr($testMarker, 0, -3).'Wrapped */'; + $target = $this->getTargetToken($testMarker, T_HEREDOC); + $this->assertSame('Do '.$expectedContent." Something\n", $tokens[$target]['content']); + + }//end testHeredocStringWrapped() + + + /** + * Data provider. + * + * @see testHeredocString() + * + * @return array + */ + public function dataHeredocString() + { + return [ + [ + 'testMarker' => '/* testSimple1 */', + 'expectedContent' => '$foo', + ], + [ + 'testMarker' => '/* testSimple2 */', + 'expectedContent' => '{$foo}', + ], + [ + 'testMarker' => '/* testSimple3 */', + 'expectedContent' => '${foo}', + ], + [ + 'testMarker' => '/* testDIM1 */', + 'expectedContent' => '$foo[bar]', + ], + [ + 'testMarker' => '/* testDIM2 */', + 'expectedContent' => '{$foo[\'bar\']}', + ], + [ + 'testMarker' => '/* testDIM3 */', + 'expectedContent' => '${foo[\'bar\']}', + ], + [ + 'testMarker' => '/* testProperty1 */', + 'expectedContent' => '$foo->bar', + ], + [ + 'testMarker' => '/* testProperty2 */', + 'expectedContent' => '{$foo->bar}', + ], + [ + 'testMarker' => '/* testMethod1 */', + 'expectedContent' => '{$foo->bar()}', + ], + [ + 'testMarker' => '/* testClosure1 */', + 'expectedContent' => '{$foo()}', + ], + [ + 'testMarker' => '/* testChain1 */', + 'expectedContent' => '{$foo[\'bar\']->baz()()}', + ], + [ + 'testMarker' => '/* testVariableVar1 */', + 'expectedContent' => '${$bar}', + ], + [ + 'testMarker' => '/* testVariableVar2 */', + 'expectedContent' => '${(foo)}', + ], + [ + 'testMarker' => '/* testVariableVar3 */', + 'expectedContent' => '${foo->bar}', + ], + [ + 'testMarker' => '/* testNested1 */', + 'expectedContent' => '${foo["${bar}"]}', + ], + [ + 'testMarker' => '/* testNested2 */', + 'expectedContent' => '${foo["${bar[\'baz\']}"]}', + ], + [ + 'testMarker' => '/* testNested3 */', + 'expectedContent' => '${foo->{$baz}}', + ], + [ + 'testMarker' => '/* testNested4 */', + 'expectedContent' => '${foo->{${\'a\'}}}', + ], + [ + 'testMarker' => '/* testNested5 */', + 'expectedContent' => '${foo->{"${\'a\'}"}}', + ], + ]; + + }//end dataHeredocString() + + +}//end class diff --git a/tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc b/tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc new file mode 100644 index 0000000000..b9c0df24d4 --- /dev/null +++ b/tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.inc @@ -0,0 +1,407 @@ +getPos(skip: false), + count: count(array_or_countable: $array), + value: 50 +); + +array_fill( + start_index: /* testNestedFunctionCallInner1 */ $obj->getPos(skip: false), + count: /* testNestedFunctionCallInner2 */ count(array_or_countable: $array), + value: 50 +); + +/* testNamespaceOperatorFunction */ +namespace\function_name(label:$string, more: false); + +/* testNamespaceRelativeFunction */ +Partially\Qualified\function_name(label:$string, more: false); + +/* testNamespacedFQNFunction */ +\Fully\Qualified\function_name(label: $string, more:false); + +/* testVariableFunction */ +$fn(label: $string, more:false); + +/* testVariableVariableFunction */ +${$fn}(label: $string, more:false); + +/* testMethodCall */ +$obj->methodName(label: $foo, more: $bar); + +/* testVariableMethodCall */ +$obj->{$var}(label: $foo, more: $bar); + +/* testClassInstantiation */ +$obj = new MyClass(label: $string, more:false); + +/* testClassInstantiationSelf */ +$obj = new self(label: $string, more:true); + +/* testClassInstantiationStatic */ +$obj = new static(label: $string, more:false); + +/* testAnonClass */ +$anon = new class(label: $string, more: false) { + public function __construct($label, $more) {} +}; + +function myfoo( $💩💩💩, $Пасха, $_valid) {} +/* testNonAsciiNames */ +foo(💩💩💩: [], Пасха: 'text', _valid: 123); + +/* testMixedPositionalAndNamedArgsWithTernary */ +foo( $cond ? true : false, name: $value2 ); + +/* testNamedArgWithTernary */ +foo( label: $cond ? true : false, more: $cond ? CONSTANT_A : CONSTANT_B ); + +/* testTernaryWithFunctionCallsInThenElse */ +echo $cond ? foo( label: $something ) : foo( more: $something_else ); + +/* testTernaryWithConstantsInThenElse */ +echo $cond ? CONSTANT_NAME : OTHER_CONSTANT; + +switch ($s) { + /* testSwitchCaseWithConstant */ + case MY_CONSTANT: + // Do something. + break; + + /* testSwitchCaseWithClassProperty */ + case $obj->property: + // Do something. + break; + + /* testSwitchDefault */ + default: + // Do something. + break; +} + +/* testTernaryWithClosuresAndReturnTypes */ +$closure = $cond ? function() : bool {return true;} : function() : int {return 123;}; + +/* testTernaryWithArrowFunctionsAndReturnTypes */ +$fn = $cond ? fn() : bool => true : fn() : int => 123; + + +/* testCompileErrorNamedBeforePositional */ +// Not the concern of PHPCS. Should still be handled. +test(param: $bar, $foo); + +/* testDuplicateName1 */ +// Error Exception, but not the concern of PHPCS. Should still be handled. +test(param: 1, /* testDuplicateName2 */ param: 2); + +/* testIncorrectOrderWithVariadic */ +// Error Exception, but not the concern of PHPCS. Should still be handled. +array_fill(start_index: 0, ...[100, 50]); + +/* testCompileErrorIncorrectOrderWithVariadic */ +// Not the concern of PHPCS. Should still be handled. +test(...$values, param: $value); // Compile-time error + +/* testParseErrorNoValue */ +// Not the concern of PHPCS. Should still be handled. +test(param1:, param2:); + +/* testParseErrorDynamicName */ +// Parse error. Ignore. +function_name($variableStoringParamName: $value); + +/* testParseErrorExit */ +// Exit is a language construct, not a function. Named params not supported, handle it anyway. +exit(status: $value); + +/* testParseErrorEmpty */ +// Empty is a language construct, not a function. Named params not supported, handle it anyway. +empty(variable: $value); + +/* testParseErrorEval */ +// Eval is a language construct, not a function. Named params not supported, handle it anyway. +eval(code: $value); + +/* testParseErrorArbitraryParentheses */ +// Parse error. Not named param, handle it anyway. +$calc = (something: $value / $other); + + +/* testReservedKeywordAbstract1 */ +foobar(abstract: $value, /* testReservedKeywordAbstract2 */ abstract: $value); + +/* testReservedKeywordAnd1 */ +foobar(and: $value, /* testReservedKeywordAnd2 */ and: $value); + +/* testReservedKeywordArray1 */ +foobar(array: $value, /* testReservedKeywordArray2 */ array: $value); + +/* testReservedKeywordAs1 */ +foobar(as: $value, /* testReservedKeywordAs2 */ as: $value); + +/* testReservedKeywordBreak1 */ +foobar(break: $value, /* testReservedKeywordBreak2 */ break: $value); + +/* testReservedKeywordCallable1 */ +foobar(callable: $value, /* testReservedKeywordCallable2 */ callable: $value); + +/* testReservedKeywordCase1 */ +foobar(case: $value, /* testReservedKeywordCase2 */ case: $value); + +/* testReservedKeywordCatch1 */ +foobar(catch: $value, /* testReservedKeywordCatch2 */ catch: $value); + +/* testReservedKeywordClass1 */ +foobar(class: $value, /* testReservedKeywordClass2 */ class: $value); + +/* testReservedKeywordClone1 */ +foobar(clone: $value, /* testReservedKeywordClone2 */ clone: $value); + +/* testReservedKeywordConst1 */ +foobar(const: $value, /* testReservedKeywordConst2 */ const: $value); + +/* testReservedKeywordContinue1 */ +foobar(continue: $value, /* testReservedKeywordContinue2 */ continue: $value); + +/* testReservedKeywordDeclare1 */ +foobar(declare: $value, /* testReservedKeywordDeclare2 */ declare: $value); + +/* testReservedKeywordDefault1 */ +foobar(default: $value, /* testReservedKeywordDefault2 */ default: $value); + +/* testReservedKeywordDie1 */ +foobar(die: $value, /* testReservedKeywordDie2 */ die: $value); + +/* testReservedKeywordDo1 */ +foobar(do: $value, /* testReservedKeywordDo2 */ do: $value); + +/* testReservedKeywordEcho1 */ +foobar(echo: $value, /* testReservedKeywordEcho2 */ echo: $value); + +/* testReservedKeywordElse1 */ +foobar(else: $value, /* testReservedKeywordElse2 */ else: $value); + +/* testReservedKeywordElseif1 */ +foobar(elseif: $value, /* testReservedKeywordElseif2 */ elseif: $value); + +/* testReservedKeywordEmpty1 */ +foobar(empty: $value, /* testReservedKeywordEmpty2 */ empty: $value); + +/* testReservedKeywordEnum1 */ +foobar(enum: $value, /* testReservedKeywordEnum2 */ enum: $value); + +/* testReservedKeywordEnddeclare1 */ +foobar(enddeclare: $value, /* testReservedKeywordEnddeclare2 */ enddeclare: $value); + +/* testReservedKeywordEndfor1 */ +foobar(endfor: $value, /* testReservedKeywordEndfor2 */ endfor: $value); + +/* testReservedKeywordEndforeach1 */ +foobar(endforeach: $value, /* testReservedKeywordEndforeach2 */ endforeach: $value); + +/* testReservedKeywordEndif1 */ +foobar(endif: $value, /* testReservedKeywordEndif2 */ endif: $value); + +/* testReservedKeywordEndswitch1 */ +foobar(endswitch: $value, /* testReservedKeywordEndswitch2 */ endswitch: $value); + +/* testReservedKeywordEndwhile1 */ +foobar(endwhile: $value, /* testReservedKeywordEndwhile2 */ endwhile: $value); + +/* testReservedKeywordEval1 */ +foobar(eval: $value, /* testReservedKeywordEval2 */ eval: $value); + +/* testReservedKeywordExit1 */ +foobar(exit: $value, /* testReservedKeywordExit2 */ exit: $value); + +/* testReservedKeywordExtends1 */ +foobar(extends: $value, /* testReservedKeywordExtends2 */ extends: $value); + +/* testReservedKeywordFinal1 */ +foobar(final: $value, /* testReservedKeywordFinal2 */ final: $value); + +/* testReservedKeywordFinally1 */ +foobar(finally: $value, /* testReservedKeywordFinally2 */ finally: $value); + +/* testReservedKeywordFn1 */ +foobar(fn: $value, /* testReservedKeywordFn2 */ fn: $value); + +/* testReservedKeywordFor1 */ +foobar(for: $value, /* testReservedKeywordFor2 */ for: $value); + +/* testReservedKeywordForeach1 */ +foobar(foreach: $value, /* testReservedKeywordForeach2 */ foreach: $value); + +/* testReservedKeywordFunction1 */ +foobar(function: $value, /* testReservedKeywordFunction2 */ function: $value); + +/* testReservedKeywordGlobal1 */ +foobar(global: $value, /* testReservedKeywordGlobal2 */ global: $value); + +/* testReservedKeywordGoto1 */ +foobar(goto: $value, /* testReservedKeywordGoto2 */ goto: $value); + +/* testReservedKeywordIf1 */ +foobar(if: $value, /* testReservedKeywordIf2 */ if: $value); + +/* testReservedKeywordImplements1 */ +foobar(implements: $value, /* testReservedKeywordImplements2 */ implements: $value); + +/* testReservedKeywordInclude1 */ +foobar(include: $value, /* testReservedKeywordInclude2 */ include: $value); + +/* testReservedKeywordInclude_once1 */ +foobar(include_once: $value, /* testReservedKeywordInclude_once2 */ include_once: $value); + +/* testReservedKeywordInstanceof1 */ +foobar(instanceof: $value, /* testReservedKeywordInstanceof2 */ instanceof: $value); + +/* testReservedKeywordInsteadof1 */ +foobar(insteadof: $value, /* testReservedKeywordInsteadof2 */ insteadof: $value); + +/* testReservedKeywordInterface1 */ +foobar(interface: $value, /* testReservedKeywordInterface2 */ interface: $value); + +/* testReservedKeywordIsset1 */ +foobar(isset: $value, /* testReservedKeywordIsset2 */ isset: $value); + +/* testReservedKeywordList1 */ +foobar(list: $value, /* testReservedKeywordList2 */ list: $value); + +/* testReservedKeywordMatch1 */ +foobar(match: $value, /* testReservedKeywordMatch2 */ match: $value); + +/* testReservedKeywordNamespace1 */ +foobar(namespace: $value, /* testReservedKeywordNamespace2 */ namespace: $value); + +/* testReservedKeywordNew1 */ +foobar(new: $value, /* testReservedKeywordNew2 */ new: $value); + +/* testReservedKeywordOr1 */ +foobar(or: $value, /* testReservedKeywordOr2 */ or: $value); + +/* testReservedKeywordPrint1 */ +foobar(print: $value, /* testReservedKeywordPrint2 */ print: $value); + +/* testReservedKeywordPrivate1 */ +foobar(private: $value, /* testReservedKeywordPrivate2 */ private: $value); + +/* testReservedKeywordProtected1 */ +foobar(protected: $value, /* testReservedKeywordProtected2 */ protected: $value); + +/* testReservedKeywordPublic1 */ +foobar(public: $value, /* testReservedKeywordPublic2 */ public: $value); + +/* testReservedKeywordReadonly1 */ +foobar(readonly: $value, /* testReservedKeywordReadonly2 */ readonly: $value); + +/* testReservedKeywordRequire1 */ +foobar(require: $value, /* testReservedKeywordRequire2 */ require: $value); + +/* testReservedKeywordRequire_once1 */ +foobar(require_once: $value, /* testReservedKeywordRequire_once2 */ require_once: $value); + +/* testReservedKeywordReturn1 */ +foobar(return: $value, /* testReservedKeywordReturn2 */ return: $value); + +/* testReservedKeywordStatic1 */ +foobar(static: $value, /* testReservedKeywordStatic2 */ static: $value); + +/* testReservedKeywordSwitch1 */ +foobar(switch: $value, /* testReservedKeywordSwitch2 */ switch: $value); + +/* testReservedKeywordThrow1 */ +foobar(throw: $value, /* testReservedKeywordThrow2 */ throw: $value); + +/* testReservedKeywordTrait1 */ +foobar(trait: $value, /* testReservedKeywordTrait2 */ trait: $value); + +/* testReservedKeywordTry1 */ +foobar(try: $value, /* testReservedKeywordTry2 */ try: $value); + +/* testReservedKeywordUnset1 */ +foobar(unset: $value, /* testReservedKeywordUnset2 */ unset: $value); + +/* testReservedKeywordUse1 */ +foobar(use: $value, /* testReservedKeywordUse2 */ use: $value); + +/* testReservedKeywordVar1 */ +foobar(var: $value, /* testReservedKeywordVar2 */ var: $value); + +/* testReservedKeywordWhile1 */ +foobar(while: $value, /* testReservedKeywordWhile2 */ while: $value); + +/* testReservedKeywordXor1 */ +foobar(xor: $value, /* testReservedKeywordXor2 */ xor: $value); + +/* testReservedKeywordYield1 */ +foobar(yield: $value, /* testReservedKeywordYield2 */ yield: $value); + +/* testReservedKeywordInt1 */ +foobar(int: $value, /* testReservedKeywordInt2 */ int: $value); + +/* testReservedKeywordFloat1 */ +foobar(float: $value, /* testReservedKeywordFloat2 */ float: $value); + +/* testReservedKeywordBool1 */ +foobar(bool: $value, /* testReservedKeywordBool2 */ bool: $value); + +/* testReservedKeywordString1 */ +foobar(string: $value, /* testReservedKeywordString2 */ string: $value); + +/* testReservedKeywordTrue1 */ +foobar(true: $value, /* testReservedKeywordTrue2 */ true: $value); + +/* testReservedKeywordFalse1 */ +foobar(false: $value, /* testReservedKeywordFalse2 */ false: $value); + +/* testReservedKeywordNull1 */ +foobar(null: $value, /* testReservedKeywordNull2 */ null: $value); + +/* testReservedKeywordVoid1 */ +foobar(void: $value, /* testReservedKeywordVoid2 */ void: $value); + +/* testReservedKeywordIterable1 */ +foobar(iterable: $value, /* testReservedKeywordIterable2 */ iterable: $value); + +/* testReservedKeywordObject1 */ +foobar(object: $value, /* testReservedKeywordObject2 */ object: $value); + +/* testReservedKeywordResource1 */ +foobar(resource: $value, /* testReservedKeywordResource2 */ resource: $value); + +/* testReservedKeywordMixed1 */ +foobar(mixed: $value, /* testReservedKeywordMixed2 */ mixed: $value); + +/* testReservedKeywordNumeric1 */ +foobar(numeric: $value, /* testReservedKeywordNumeric2 */ numeric: $value); + +/* testReservedKeywordParent1 */ +foobar(parent: $value, /* testReservedKeywordParent2 */ parent: $value); + +/* testReservedKeywordSelf1 */ +foobar(self: $value, /* testReservedKeywordSelf2 */ self: $value); + +/* testReservedKeywordNever1 */ +foobar(never: $value, /* testReservedKeywordNever2 */ never: $value); diff --git a/tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.php b/tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.php new file mode 100644 index 0000000000..cc57637c6f --- /dev/null +++ b/tests/Core/Tokenizer/NamedFunctionCallArgumentsTest.php @@ -0,0 +1,885 @@ + + * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; +use PHP_CodeSniffer\Util\Tokens; + +class NamedFunctionCallArgumentsTest extends AbstractMethodUnitTest +{ + + + /** + * Verify that parameter labels are tokenized as T_PARAM_NAME and that + * the colon after it is tokenized as a T_COLON. + * + * @param string $testMarker The comment prefacing the target token. + * @param array $parameters The token content for each parameter label to look for. + * + * @dataProvider dataNamedFunctionCallArguments + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testNamedFunctionCallArguments($testMarker, $parameters) + { + $tokens = self::$phpcsFile->getTokens(); + + foreach ($parameters as $content) { + $label = $this->getTargetToken($testMarker, [T_STRING, T_PARAM_NAME], $content); + + $this->assertSame( + T_PARAM_NAME, + $tokens[$label]['code'], + 'Token tokenized as '.$tokens[$label]['type'].', not T_PARAM_NAME (code)' + ); + $this->assertSame( + 'T_PARAM_NAME', + $tokens[$label]['type'], + 'Token tokenized as '.$tokens[$label]['type'].', not T_PARAM_NAME (type)' + ); + + // Get the next non-empty token. + $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($label + 1), null, true); + + $this->assertSame( + ':', + $tokens[$colon]['content'], + 'Next token after parameter name is not a colon. Found: '.$tokens[$colon]['content'] + ); + $this->assertSame( + T_COLON, + $tokens[$colon]['code'], + 'Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)' + ); + $this->assertSame( + 'T_COLON', + $tokens[$colon]['type'], + 'Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)' + ); + }//end foreach + + }//end testNamedFunctionCallArguments() + + + /** + * Data provider. + * + * @see testNamedFunctionCallArguments() + * + * @return array + */ + public function dataNamedFunctionCallArguments() + { + return [ + [ + '/* testNamedArgs */', + [ + 'start_index', + 'count', + 'value', + ], + ], + [ + '/* testNamedArgsMultiline */', + [ + 'start_index', + 'count', + 'value', + ], + ], + [ + '/* testNamedArgsWithWhitespaceAndComments */', + [ + 'start_index', + 'count', + 'value', + ], + ], + [ + '/* testMixedPositionalAndNamedArgs */', + ['double_encode'], + ], + [ + '/* testNestedFunctionCallOuter */', + [ + 'start_index', + 'count', + 'value', + ], + ], + [ + '/* testNestedFunctionCallInner1 */', + ['skip'], + ], + [ + '/* testNestedFunctionCallInner2 */', + ['array_or_countable'], + ], + [ + '/* testNamespaceOperatorFunction */', + [ + 'label', + 'more', + ], + ], + [ + '/* testNamespaceRelativeFunction */', + [ + 'label', + 'more', + ], + ], + [ + '/* testNamespacedFQNFunction */', + [ + 'label', + 'more', + ], + ], + [ + '/* testVariableFunction */', + [ + 'label', + 'more', + ], + ], + [ + '/* testVariableVariableFunction */', + [ + 'label', + 'more', + ], + ], + [ + '/* testMethodCall */', + [ + 'label', + 'more', + ], + ], + [ + '/* testVariableMethodCall */', + [ + 'label', + 'more', + ], + ], + [ + '/* testClassInstantiation */', + [ + 'label', + 'more', + ], + ], + [ + '/* testClassInstantiationSelf */', + [ + 'label', + 'more', + ], + ], + [ + '/* testClassInstantiationStatic */', + [ + 'label', + 'more', + ], + ], + [ + '/* testAnonClass */', + [ + 'label', + 'more', + ], + ], + [ + '/* testNonAsciiNames */', + [ + '💩💩💩', + 'Пасха', + '_valid', + ], + ], + + // Coding errors which should still be handled. + [ + '/* testCompileErrorNamedBeforePositional */', + ['param'], + ], + [ + '/* testDuplicateName1 */', + ['param'], + ], + [ + '/* testDuplicateName2 */', + ['param'], + ], + [ + '/* testIncorrectOrderWithVariadic */', + ['start_index'], + ], + [ + '/* testCompileErrorIncorrectOrderWithVariadic */', + ['param'], + ], + [ + '/* testParseErrorNoValue */', + [ + 'param1', + 'param2', + ], + ], + [ + '/* testParseErrorExit */', + ['status'], + ], + [ + '/* testParseErrorEmpty */', + ['variable'], + ], + [ + '/* testParseErrorEval */', + ['code'], + ], + [ + '/* testParseErrorArbitraryParentheses */', + ['something'], + ], + ]; + + }//end dataNamedFunctionCallArguments() + + + /** + * Verify that other T_STRING tokens within a function call are still tokenized as T_STRING. + * + * @param string $testMarker The comment prefacing the target token. + * @param string $content The token content to look for. + * + * @dataProvider dataOtherTstringInFunctionCall + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testOtherTstringInFunctionCall($testMarker, $content) + { + $tokens = self::$phpcsFile->getTokens(); + + $label = $this->getTargetToken($testMarker, [T_STRING, T_PARAM_NAME], $content); + + $this->assertSame( + T_STRING, + $tokens[$label]['code'], + 'Token tokenized as '.$tokens[$label]['type'].', not T_STRING (code)' + ); + $this->assertSame( + 'T_STRING', + $tokens[$label]['type'], + 'Token tokenized as '.$tokens[$label]['type'].', not T_STRING (type)' + ); + + }//end testOtherTstringInFunctionCall() + + + /** + * Data provider. + * + * @see testOtherTstringInFunctionCall() + * + * @return array + */ + public function dataOtherTstringInFunctionCall() + { + return [ + [ + '/* testPositionalArgs */', + 'START_INDEX', + ], + [ + '/* testPositionalArgs */', + 'COUNT', + ], + [ + '/* testPositionalArgs */', + 'VALUE', + ], + [ + '/* testNestedFunctionCallInner2 */', + 'count', + ], + ]; + + }//end dataOtherTstringInFunctionCall() + + + /** + * Verify whether the colons are tokenized correctly when a ternary is used in a mixed + * positional and named arguments function call. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testMixedPositionalAndNamedArgsWithTernary() + { + $tokens = self::$phpcsFile->getTokens(); + + $true = $this->getTargetToken('/* testMixedPositionalAndNamedArgsWithTernary */', T_TRUE); + + // Get the next non-empty token. + $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($true + 1), null, true); + + $this->assertSame( + T_INLINE_ELSE, + $tokens[$colon]['code'], + 'Token tokenized as '.$tokens[$colon]['type'].', not T_INLINE_ELSE (code)' + ); + $this->assertSame( + 'T_INLINE_ELSE', + $tokens[$colon]['type'], + 'Token tokenized as '.$tokens[$colon]['type'].', not T_INLINE_ELSE (type)' + ); + + $label = $this->getTargetToken('/* testMixedPositionalAndNamedArgsWithTernary */', T_PARAM_NAME, 'name'); + + // Get the next non-empty token. + $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($label + 1), null, true); + + $this->assertSame( + ':', + $tokens[$colon]['content'], + 'Next token after parameter name is not a colon. Found: '.$tokens[$colon]['content'] + ); + $this->assertSame( + T_COLON, + $tokens[$colon]['code'], + 'Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)' + ); + $this->assertSame( + 'T_COLON', + $tokens[$colon]['type'], + 'Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)' + ); + + }//end testMixedPositionalAndNamedArgsWithTernary() + + + /** + * Verify whether the colons are tokenized correctly when a ternary is used + * in a named arguments function call. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testNamedArgWithTernary() + { + $tokens = self::$phpcsFile->getTokens(); + + /* + * First argument. + */ + + $label = $this->getTargetToken('/* testNamedArgWithTernary */', T_PARAM_NAME, 'label'); + + // Get the next non-empty token. + $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($label + 1), null, true); + + $this->assertSame( + ':', + $tokens[$colon]['content'], + 'First arg: Next token after parameter name is not a colon. Found: '.$tokens[$colon]['content'] + ); + $this->assertSame( + T_COLON, + $tokens[$colon]['code'], + 'First arg: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)' + ); + $this->assertSame( + 'T_COLON', + $tokens[$colon]['type'], + 'First arg: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)' + ); + + $true = $this->getTargetToken('/* testNamedArgWithTernary */', T_TRUE); + + // Get the next non-empty token. + $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($true + 1), null, true); + + $this->assertSame( + T_INLINE_ELSE, + $tokens[$colon]['code'], + 'First arg ternary: Token tokenized as '.$tokens[$colon]['type'].', not T_INLINE_ELSE (code)' + ); + $this->assertSame( + 'T_INLINE_ELSE', + $tokens[$colon]['type'], + 'First arg ternary: Token tokenized as '.$tokens[$colon]['type'].', not T_INLINE_ELSE (type)' + ); + + /* + * Second argument. + */ + + $label = $this->getTargetToken('/* testNamedArgWithTernary */', T_PARAM_NAME, 'more'); + + // Get the next non-empty token. + $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($label + 1), null, true); + + $this->assertSame( + ':', + $tokens[$colon]['content'], + 'Second arg: Next token after parameter name is not a colon. Found: '.$tokens[$colon]['content'] + ); + $this->assertSame( + T_COLON, + $tokens[$colon]['code'], + 'Second arg: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)' + ); + $this->assertSame( + 'T_COLON', + $tokens[$colon]['type'], + 'Second arg: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)' + ); + + $true = $this->getTargetToken('/* testNamedArgWithTernary */', T_STRING, 'CONSTANT_A'); + + // Get the next non-empty token. + $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($true + 1), null, true); + + $this->assertSame( + T_INLINE_ELSE, + $tokens[$colon]['code'], + 'Second arg ternary: Token tokenized as '.$tokens[$colon]['type'].', not T_INLINE_ELSE (code)' + ); + $this->assertSame( + 'T_INLINE_ELSE', + $tokens[$colon]['type'], + 'Second arg ternary: Token tokenized as '.$tokens[$colon]['type'].', not T_INLINE_ELSE (type)' + ); + + }//end testNamedArgWithTernary() + + + /** + * Verify whether the colons are tokenized correctly when named arguments + * function calls are used in a ternary. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testTernaryWithFunctionCallsInThenElse() + { + $tokens = self::$phpcsFile->getTokens(); + + /* + * Then. + */ + + $label = $this->getTargetToken('/* testTernaryWithFunctionCallsInThenElse */', T_PARAM_NAME, 'label'); + + // Get the next non-empty token. + $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($label + 1), null, true); + + $this->assertSame( + ':', + $tokens[$colon]['content'], + 'Function in then: Next token after parameter name is not a colon. Found: '.$tokens[$colon]['content'] + ); + $this->assertSame( + T_COLON, + $tokens[$colon]['code'], + 'Function in then: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)' + ); + $this->assertSame( + 'T_COLON', + $tokens[$colon]['type'], + 'Function in then: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)' + ); + + $closeParens = $this->getTargetToken('/* testTernaryWithFunctionCallsInThenElse */', T_CLOSE_PARENTHESIS); + + // Get the next non-empty token. + $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($closeParens + 1), null, true); + + $this->assertSame( + T_INLINE_ELSE, + $tokens[$colon]['code'], + 'Token tokenized as '.$tokens[$colon]['type'].', not T_INLINE_ELSE (code)' + ); + $this->assertSame( + 'T_INLINE_ELSE', + $tokens[$colon]['type'], + 'Token tokenized as '.$tokens[$colon]['type'].', not T_INLINE_ELSE (type)' + ); + + /* + * Else. + */ + + $label = $this->getTargetToken('/* testTernaryWithFunctionCallsInThenElse */', T_PARAM_NAME, 'more'); + + // Get the next non-empty token. + $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($label + 1), null, true); + + $this->assertSame( + ':', + $tokens[$colon]['content'], + 'Function in else: Next token after parameter name is not a colon. Found: '.$tokens[$colon]['content'] + ); + $this->assertSame( + T_COLON, + $tokens[$colon]['code'], + 'Function in else: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)' + ); + $this->assertSame( + 'T_COLON', + $tokens[$colon]['type'], + 'Function in else: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)' + ); + + }//end testTernaryWithFunctionCallsInThenElse() + + + /** + * Verify whether the colons are tokenized correctly when constants are used in a ternary. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testTernaryWithConstantsInThenElse() + { + $tokens = self::$phpcsFile->getTokens(); + + $constant = $this->getTargetToken('/* testTernaryWithConstantsInThenElse */', T_STRING, 'CONSTANT_NAME'); + + // Get the next non-empty token. + $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($constant + 1), null, true); + + $this->assertSame( + T_INLINE_ELSE, + $tokens[$colon]['code'], + 'Token tokenized as '.$tokens[$colon]['type'].', not T_INLINE_ELSE (code)' + ); + $this->assertSame( + 'T_INLINE_ELSE', + $tokens[$colon]['type'], + 'Token tokenized as '.$tokens[$colon]['type'].', not T_INLINE_ELSE (type)' + ); + + }//end testTernaryWithConstantsInThenElse() + + + /** + * Verify whether the colons are tokenized correctly in a switch statement. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testSwitchStatement() + { + $tokens = self::$phpcsFile->getTokens(); + + $label = $this->getTargetToken('/* testSwitchCaseWithConstant */', T_STRING, 'MY_CONSTANT'); + + // Get the next non-empty token. + $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($label + 1), null, true); + + $this->assertSame( + T_COLON, + $tokens[$colon]['code'], + 'First case: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)' + ); + $this->assertSame( + 'T_COLON', + $tokens[$colon]['type'], + 'First case: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)' + ); + + $label = $this->getTargetToken('/* testSwitchCaseWithClassProperty */', T_STRING, 'property'); + + // Get the next non-empty token. + $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($label + 1), null, true); + + $this->assertSame( + T_COLON, + $tokens[$colon]['code'], + 'Second case: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)' + ); + $this->assertSame( + 'T_COLON', + $tokens[$colon]['type'], + 'Second case: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)' + ); + + $default = $this->getTargetToken('/* testSwitchDefault */', T_DEFAULT); + + // Get the next non-empty token. + $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($default + 1), null, true); + + $this->assertSame( + T_COLON, + $tokens[$colon]['code'], + 'Default case: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)' + ); + $this->assertSame( + 'T_COLON', + $tokens[$colon]['type'], + 'Default case: Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)' + ); + + }//end testSwitchStatement() + + + /** + * Verify that a variable parameter label (parse error) is still tokenized as T_VARIABLE. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testParseErrorVariableLabel() + { + $tokens = self::$phpcsFile->getTokens(); + + $label = $this->getTargetToken('/* testParseErrorDynamicName */', [T_VARIABLE, T_PARAM_NAME], '$variableStoringParamName'); + + $this->assertSame( + T_VARIABLE, + $tokens[$label]['code'], + 'Token tokenized as '.$tokens[$label]['type'].', not T_VARIABLE (code)' + ); + $this->assertSame( + 'T_VARIABLE', + $tokens[$label]['type'], + 'Token tokenized as '.$tokens[$label]['type'].', not T_VARIABLE (type)' + ); + + // Get the next non-empty token. + $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($label + 1), null, true); + + $this->assertSame( + ':', + $tokens[$colon]['content'], + 'Next token after parameter name is not a colon. Found: '.$tokens[$colon]['content'] + ); + $this->assertSame( + T_COLON, + $tokens[$colon]['code'], + 'Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)' + ); + $this->assertSame( + 'T_COLON', + $tokens[$colon]['type'], + 'Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)' + ); + + }//end testParseErrorVariableLabel() + + + /** + * Verify that reserved keywords used as a parameter label are tokenized as T_PARAM_NAME + * and that the colon after it is tokenized as a T_COLON. + * + * @param string $testMarker The comment prefacing the target token. + * @param array $tokenTypes The token codes to look for. + * @param string $tokenContent The token content to look for. + * + * @dataProvider dataReservedKeywordsAsName + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testReservedKeywordsAsName($testMarker, $tokenTypes, $tokenContent) + { + $tokens = self::$phpcsFile->getTokens(); + $label = $this->getTargetToken($testMarker, $tokenTypes, $tokenContent); + + $this->assertSame( + T_PARAM_NAME, + $tokens[$label]['code'], + 'Token tokenized as '.$tokens[$label]['type'].', not T_PARAM_NAME (code)' + ); + $this->assertSame( + 'T_PARAM_NAME', + $tokens[$label]['type'], + 'Token tokenized as '.$tokens[$label]['type'].', not T_PARAM_NAME (type)' + ); + + // Get the next non-empty token. + $colon = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($label + 1), null, true); + + $this->assertSame( + ':', + $tokens[$colon]['content'], + 'Next token after parameter name is not a colon. Found: '.$tokens[$colon]['content'] + ); + $this->assertSame( + T_COLON, + $tokens[$colon]['code'], + 'Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (code)' + ); + $this->assertSame( + 'T_COLON', + $tokens[$colon]['type'], + 'Token tokenized as '.$tokens[$colon]['type'].', not T_COLON (type)' + ); + + }//end testReservedKeywordsAsName() + + + /** + * Data provider. + * + * @see testReservedKeywordsAsName() + * + * @return array + */ + public function dataReservedKeywordsAsName() + { + $reservedKeywords = [ + // '__halt_compiler', NOT TESTABLE + 'abstract', + 'and', + 'array', + 'as', + 'break', + 'callable', + 'case', + 'catch', + 'class', + 'clone', + 'const', + 'continue', + 'declare', + 'default', + 'die', + 'do', + 'echo', + 'else', + 'elseif', + 'empty', + 'enddeclare', + 'endfor', + 'endforeach', + 'endif', + 'endswitch', + 'endwhile', + 'enum', + 'eval', + 'exit', + 'extends', + 'final', + 'finally', + 'fn', + 'for', + 'foreach', + 'function', + 'global', + 'goto', + 'if', + 'implements', + 'include', + 'include_once', + 'instanceof', + 'insteadof', + 'interface', + 'isset', + 'list', + 'match', + 'namespace', + 'new', + 'or', + 'print', + 'private', + 'protected', + 'public', + 'readonly', + 'require', + 'require_once', + 'return', + 'static', + 'switch', + 'throw', + 'trait', + 'try', + 'unset', + 'use', + 'var', + 'while', + 'xor', + 'yield', + 'int', + 'float', + 'bool', + 'string', + 'true', + 'false', + 'null', + 'void', + 'iterable', + 'object', + 'resource', + 'mixed', + 'numeric', + 'never', + + // Not reserved keyword, but do have their own token in PHPCS. + 'parent', + 'self', + ]; + + $data = []; + + foreach ($reservedKeywords as $keyword) { + $tokensTypes = [ + T_PARAM_NAME, + T_STRING, + T_GOTO_LABEL, + ]; + $tokenName = 'T_'.strtoupper($keyword); + + if ($keyword === 'and') { + $tokensTypes[] = T_LOGICAL_AND; + } else if ($keyword === 'die') { + $tokensTypes[] = T_EXIT; + } else if ($keyword === 'or') { + $tokensTypes[] = T_LOGICAL_OR; + } else if ($keyword === 'xor') { + $tokensTypes[] = T_LOGICAL_XOR; + } else if ($keyword === '__halt_compiler') { + $tokensTypes[] = T_HALT_COMPILER; + } else if (defined($tokenName) === true) { + $tokensTypes[] = constant($tokenName); + } + + $data[$keyword.'FirstParam'] = [ + '/* testReservedKeyword'.ucfirst($keyword).'1 */', + $tokensTypes, + $keyword, + ]; + + $data[$keyword.'SecondParam'] = [ + '/* testReservedKeyword'.ucfirst($keyword).'2 */', + $tokensTypes, + $keyword, + ]; + }//end foreach + + return $data; + + }//end dataReservedKeywordsAsName() + + +}//end class diff --git a/tests/Core/Tokenizer/NullsafeObjectOperatorTest.inc b/tests/Core/Tokenizer/NullsafeObjectOperatorTest.inc new file mode 100644 index 0000000000..982841eac4 --- /dev/null +++ b/tests/Core/Tokenizer/NullsafeObjectOperatorTest.inc @@ -0,0 +1,29 @@ +foo; + +/* testNullsafeObjectOperator */ +echo $obj?->foo; + +/* testNullsafeObjectOperatorWriteContext */ +// Intentional parse error, but not the concern of the tokenizer. +$foo?->bar->baz = 'baz'; + +/* testTernaryThen */ +echo $obj ? $obj->prop : $other->prop; + +/* testParseErrorWhitespaceNotAllowed */ +echo $obj ? + -> foo; + +/* testParseErrorCommentNotAllowed */ +echo $obj ?/*comment*/-> foo; + +/* testLiveCoding */ +// Intentional parse error. This has to be the last test in the file. +echo $obj? diff --git a/tests/Core/Tokenizer/NullsafeObjectOperatorTest.php b/tests/Core/Tokenizer/NullsafeObjectOperatorTest.php new file mode 100644 index 0000000000..8e465a3be1 --- /dev/null +++ b/tests/Core/Tokenizer/NullsafeObjectOperatorTest.php @@ -0,0 +1,140 @@ += 8.0 nullsafe object operator. + * + * @author Juliette Reinders Folmer + * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; +use PHP_CodeSniffer\Util\Tokens; + +class NullsafeObjectOperatorTest extends AbstractMethodUnitTest +{ + + /** + * Tokens to search for. + * + * @var array + */ + protected $find = [ + T_NULLSAFE_OBJECT_OPERATOR, + T_OBJECT_OPERATOR, + T_INLINE_THEN, + ]; + + + /** + * Test that a normal object operator is still tokenized as such. + * + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testObjectOperator() + { + $tokens = self::$phpcsFile->getTokens(); + + $operator = $this->getTargetToken('/* testObjectOperator */', $this->find); + $this->assertSame(T_OBJECT_OPERATOR, $tokens[$operator]['code'], 'Failed asserting code is object operator'); + $this->assertSame('T_OBJECT_OPERATOR', $tokens[$operator]['type'], 'Failed asserting type is object operator'); + + }//end testObjectOperator() + + + /** + * Test that a nullsafe object operator is tokenized as such. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @dataProvider dataNullsafeObjectOperator + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testNullsafeObjectOperator($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $operator = $this->getTargetToken($testMarker, $this->find); + $this->assertSame(T_NULLSAFE_OBJECT_OPERATOR, $tokens[$operator]['code'], 'Failed asserting code is nullsafe object operator'); + $this->assertSame('T_NULLSAFE_OBJECT_OPERATOR', $tokens[$operator]['type'], 'Failed asserting type is nullsafe object operator'); + + }//end testNullsafeObjectOperator() + + + /** + * Data provider. + * + * @see testNullsafeObjectOperator() + * + * @return array + */ + public function dataNullsafeObjectOperator() + { + return [ + ['/* testNullsafeObjectOperator */'], + ['/* testNullsafeObjectOperatorWriteContext */'], + ]; + + }//end dataNullsafeObjectOperator() + + + /** + * Test that a question mark not followed by an object operator is tokenized as T_TERNARY_THEN. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param bool $testObjectOperator Whether to test for the next non-empty token being tokenized + * as an object operator. + * + * @dataProvider dataTernaryThen + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testTernaryThen($testMarker, $testObjectOperator=false) + { + $tokens = self::$phpcsFile->getTokens(); + + $operator = $this->getTargetToken($testMarker, $this->find); + $this->assertSame(T_INLINE_THEN, $tokens[$operator]['code'], 'Failed asserting code is inline then'); + $this->assertSame('T_INLINE_THEN', $tokens[$operator]['type'], 'Failed asserting type is inline then'); + + if ($testObjectOperator === true) { + $next = self::$phpcsFile->findNext(Tokens::$emptyTokens, ($operator + 1), null, true); + $this->assertSame(T_OBJECT_OPERATOR, $tokens[$next]['code'], 'Failed asserting code is object operator'); + $this->assertSame('T_OBJECT_OPERATOR', $tokens[$next]['type'], 'Failed asserting type is object operator'); + } + + }//end testTernaryThen() + + + /** + * Data provider. + * + * @see testTernaryThen() + * + * @return array + */ + public function dataTernaryThen() + { + return [ + ['/* testTernaryThen */'], + [ + '/* testParseErrorWhitespaceNotAllowed */', + true, + ], + [ + '/* testParseErrorCommentNotAllowed */', + true, + ], + ['/* testLiveCoding */'], + ]; + + }//end dataTernaryThen() + + +}//end class diff --git a/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc b/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc new file mode 100644 index 0000000000..38e5a47d53 --- /dev/null +++ b/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.inc @@ -0,0 +1,19 @@ + new namespace\Baz; diff --git a/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php b/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php new file mode 100644 index 0000000000..23cbd9877c --- /dev/null +++ b/tests/Core/Tokenizer/ScopeSettingWithNamespaceOperatorTest.php @@ -0,0 +1,98 @@ + + * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class ScopeSettingWithNamespaceOperatorTest extends AbstractMethodUnitTest +{ + + + /** + * Test that the scope opener/closers are set correctly when the namespace keyword is encountered as an operator. + * + * @param string $testMarker The comment which prefaces the target tokens in the test file. + * @param int|string[] $tokenTypes The token type to search for. + * @param int|string[] $open Optional. The token type for the scope opener. + * @param int|string[] $close Optional. The token type for the scope closer. + * + * @dataProvider dataScopeSetting + * @covers PHP_CodeSniffer\Tokenizers\Tokenizer::recurseScopeMap + * + * @return void + */ + public function testScopeSetting($testMarker, $tokenTypes, $open=T_OPEN_CURLY_BRACKET, $close=T_CLOSE_CURLY_BRACKET) + { + $tokens = self::$phpcsFile->getTokens(); + + $target = $this->getTargetToken($testMarker, $tokenTypes); + $opener = $this->getTargetToken($testMarker, $open); + $closer = $this->getTargetToken($testMarker, $close); + + $this->assertArrayHasKey('scope_opener', $tokens[$target], 'Scope opener missing'); + $this->assertArrayHasKey('scope_closer', $tokens[$target], 'Scope closer missing'); + $this->assertSame($opener, $tokens[$target]['scope_opener'], 'Scope opener not same'); + $this->assertSame($closer, $tokens[$target]['scope_closer'], 'Scope closer not same'); + + $this->assertArrayHasKey('scope_opener', $tokens[$opener], 'Scope opener missing for open curly'); + $this->assertArrayHasKey('scope_closer', $tokens[$opener], 'Scope closer missing for open curly'); + $this->assertSame($opener, $tokens[$opener]['scope_opener'], 'Scope opener not same for open curly'); + $this->assertSame($closer, $tokens[$opener]['scope_closer'], 'Scope closer not same for open curly'); + + $this->assertArrayHasKey('scope_opener', $tokens[$closer], 'Scope opener missing for close curly'); + $this->assertArrayHasKey('scope_closer', $tokens[$closer], 'Scope closer missing for close curly'); + $this->assertSame($opener, $tokens[$closer]['scope_opener'], 'Scope opener not same for close curly'); + $this->assertSame($closer, $tokens[$closer]['scope_closer'], 'Scope closer not same for close curly'); + + }//end testScopeSetting() + + + /** + * Data provider. + * + * @see testScopeSetting() + * + * @return array + */ + public function dataScopeSetting() + { + return [ + [ + '/* testClassExtends */', + [T_CLASS], + ], + [ + '/* testClassImplements */', + [T_ANON_CLASS], + ], + [ + '/* testInterfaceExtends */', + [T_INTERFACE], + ], + [ + '/* testFunctionReturnType */', + [T_FUNCTION], + ], + [ + '/* testClosureReturnType */', + [T_CLOSURE], + ], + [ + '/* testArrowFunctionReturnType */', + [T_FN], + [T_FN_ARROW], + [T_SEMICOLON], + ], + ]; + + }//end dataScopeSetting() + + +}//end class diff --git a/tests/Core/Tokenizer/ShortArrayTest.inc b/tests/Core/Tokenizer/ShortArrayTest.inc new file mode 100644 index 0000000000..60b23a51cc --- /dev/null +++ b/tests/Core/Tokenizer/ShortArrayTest.inc @@ -0,0 +1,111 @@ +function_call()[$x]; + +/* testStaticMethodCallDereferencing */ +$var = ClassName::function_call()[$x]; + +/* testPropertyDereferencing */ +$var = $obj->property[2]; + +/* testPropertyDereferencingWithInaccessibleName */ +$var = $ref->{'ref-type'}[1]; + +/* testStaticPropertyDereferencing */ +$var ClassName::$property[2]; + +/* testStringDereferencing */ +$var = 'PHP'[1]; + +/* testStringDereferencingDoubleQuoted */ +$var = "PHP"[$y]; + +/* testConstantDereferencing */ +$var = MY_CONSTANT[1]; + +/* testClassConstantDereferencing */ +$var ClassName::CONSTANT_NAME[2]; + +/* testMagicConstantDereferencing */ +$var = __FILE__[0]; + +/* testArrayAccessCurlyBraces */ +$var = $array{'key'}['key']; + +/* testArrayLiteralDereferencing */ +echo array(1, 2, 3)[0]; + +echo [1, 2, 3]/* testShortArrayLiteralDereferencing */[0]; + +/* testClassMemberDereferencingOnInstantiation1 */ +(new foo)[0]; + +/* testClassMemberDereferencingOnInstantiation2 */ +$a = (new Foo( array(1, array(4, 5), 3) ))[1][0]; + +/* testClassMemberDereferencingOnClone */ +echo (clone $iterable)[20]; + +/* testNullsafeMethodCallDereferencing */ +$var = $obj?->function_call()[$x]; + +/* testInterpolatedStringDereferencing */ +$var = "PHP{$rocks}"[1]; + +/* + * Short array brackets. + */ + +/* testShortArrayDeclarationEmpty */ +$array = []; + +/* testShortArrayDeclarationWithOneValue */ +$array = [1]; + +/* testShortArrayDeclarationWithMultipleValues */ +$array = [1, 2, 3]; + +/* testShortArrayDeclarationWithDereferencing */ +echo [1, 2, 3][0]; + +/* testShortListDeclaration */ +[ $a, $b ] = $array; + +[ $a, $b, /* testNestedListDeclaration */, [$c, $d]] = $array; + +/* testArrayWithinFunctionCall */ +$var = functionCall([$x, $y]); + +if ( true ) { + /* testShortListDeclarationAfterBracedControlStructure */ + [ $a ] = [ 'hi' ]; +} + +if ( true ) + /* testShortListDeclarationAfterNonBracedControlStructure */ + [ $a ] = [ 'hi' ]; + +if ( true ) : + /* testShortListDeclarationAfterAlternativeControlStructure */ + [ $a ] = [ 'hi' ]; +endif; + +/* testLiveCoding */ +// Intentional parse error. This has to be the last test in the file. +$array = [ diff --git a/tests/Core/Tokenizer/ShortArrayTest.php b/tests/Core/Tokenizer/ShortArrayTest.php new file mode 100644 index 0000000000..1d97894f59 --- /dev/null +++ b/tests/Core/Tokenizer/ShortArrayTest.php @@ -0,0 +1,135 @@ + + * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class ShortArrayTest extends AbstractMethodUnitTest +{ + + + /** + * Test that real square brackets are still tokenized as square brackets. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @dataProvider dataSquareBrackets + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testSquareBrackets($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $opener = $this->getTargetToken($testMarker, [T_OPEN_SQUARE_BRACKET, T_OPEN_SHORT_ARRAY]); + $this->assertSame(T_OPEN_SQUARE_BRACKET, $tokens[$opener]['code']); + $this->assertSame('T_OPEN_SQUARE_BRACKET', $tokens[$opener]['type']); + + if (isset($tokens[$opener]['bracket_closer']) === true) { + $closer = $tokens[$opener]['bracket_closer']; + $this->assertSame(T_CLOSE_SQUARE_BRACKET, $tokens[$closer]['code']); + $this->assertSame('T_CLOSE_SQUARE_BRACKET', $tokens[$closer]['type']); + } + + }//end testSquareBrackets() + + + /** + * Data provider. + * + * @see testSquareBrackets() + * + * @return array + */ + public function dataSquareBrackets() + { + return [ + 'array access 1' => ['/* testArrayAccess1 */'], + 'array access 2' => ['/* testArrayAccess2 */'], + 'array assignment' => ['/* testArrayAssignment */'], + 'function call dereferencing' => ['/* testFunctionCallDereferencing */'], + 'method call dereferencing' => ['/* testMethodCallDereferencing */'], + 'static method call dereferencing' => ['/* testStaticMethodCallDereferencing */'], + 'property dereferencing' => ['/* testPropertyDereferencing */'], + 'property dereferencing with inaccessable name' => ['/* testPropertyDereferencingWithInaccessibleName */'], + 'static property dereferencing' => ['/* testStaticPropertyDereferencing */'], + 'string dereferencing single quotes' => ['/* testStringDereferencing */'], + 'string dereferencing double quotes' => ['/* testStringDereferencingDoubleQuoted */'], + 'global constant dereferencing' => ['/* testConstantDereferencing */'], + 'class constant dereferencing' => ['/* testClassConstantDereferencing */'], + 'magic constant dereferencing' => ['/* testMagicConstantDereferencing */'], + 'array access with curly braces' => ['/* testArrayAccessCurlyBraces */'], + 'array literal dereferencing' => ['/* testArrayLiteralDereferencing */'], + 'short array literal dereferencing' => ['/* testShortArrayLiteralDereferencing */'], + 'class member dereferencing on instantiation 1' => ['/* testClassMemberDereferencingOnInstantiation1 */'], + 'class member dereferencing on instantiation 2' => ['/* testClassMemberDereferencingOnInstantiation2 */'], + 'class member dereferencing on clone' => ['/* testClassMemberDereferencingOnClone */'], + 'nullsafe method call dereferencing' => ['/* testNullsafeMethodCallDereferencing */'], + 'interpolated string dereferencing' => ['/* testInterpolatedStringDereferencing */'], + 'live coding' => ['/* testLiveCoding */'], + ]; + + }//end dataSquareBrackets() + + + /** + * Test that short arrays and short lists are still tokenized as short arrays. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @dataProvider dataShortArrays + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testShortArrays($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $opener = $this->getTargetToken($testMarker, [T_OPEN_SQUARE_BRACKET, T_OPEN_SHORT_ARRAY]); + $this->assertSame(T_OPEN_SHORT_ARRAY, $tokens[$opener]['code']); + $this->assertSame('T_OPEN_SHORT_ARRAY', $tokens[$opener]['type']); + + if (isset($tokens[$opener]['bracket_closer']) === true) { + $closer = $tokens[$opener]['bracket_closer']; + $this->assertSame(T_CLOSE_SHORT_ARRAY, $tokens[$closer]['code']); + $this->assertSame('T_CLOSE_SHORT_ARRAY', $tokens[$closer]['type']); + } + + }//end testShortArrays() + + + /** + * Data provider. + * + * @see testShortArrays() + * + * @return array + */ + public function dataShortArrays() + { + return [ + 'short array empty' => ['/* testShortArrayDeclarationEmpty */'], + 'short array with value' => ['/* testShortArrayDeclarationWithOneValue */'], + 'short array with values' => ['/* testShortArrayDeclarationWithMultipleValues */'], + 'short array with dereferencing' => ['/* testShortArrayDeclarationWithDereferencing */'], + 'short list' => ['/* testShortListDeclaration */'], + 'short list nested' => ['/* testNestedListDeclaration */'], + 'short array within function call' => ['/* testArrayWithinFunctionCall */'], + 'short list after braced control structure' => ['/* testShortListDeclarationAfterBracedControlStructure */'], + 'short list after non-braced control structure' => ['/* testShortListDeclarationAfterNonBracedControlStructure */'], + 'short list after alternative control structure' => ['/* testShortListDeclarationAfterAlternativeControlStructure */'], + ]; + + }//end dataShortArrays() + + +}//end class diff --git a/tests/Core/Tokenizer/StableCommentWhitespaceTest.inc b/tests/Core/Tokenizer/StableCommentWhitespaceTest.inc new file mode 100644 index 0000000000..3bf013c66b --- /dev/null +++ b/tests/Core/Tokenizer/StableCommentWhitespaceTest.inc @@ -0,0 +1,139 @@ + + + + * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; +use PHP_CodeSniffer\Util\Tokens; + +class StableCommentWhitespaceTest extends AbstractMethodUnitTest +{ + + + /** + * Test that comment tokenization with new lines at the end of the comment is stable. + * + * @param string $testMarker The comment prefacing the test. + * @param array $expectedTokens The tokenization expected. + * + * @dataProvider dataCommentTokenization + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testCommentTokenization($testMarker, $expectedTokens) + { + $tokens = self::$phpcsFile->getTokens(); + $comment = $this->getTargetToken($testMarker, Tokens::$commentTokens); + + foreach ($expectedTokens as $key => $tokenInfo) { + $this->assertSame(constant($tokenInfo['type']), $tokens[$comment]['code']); + $this->assertSame($tokenInfo['type'], $tokens[$comment]['type']); + $this->assertSame($tokenInfo['content'], $tokens[$comment]['content']); + + ++$comment; + } + + }//end testCommentTokenization() + + + /** + * Data provider. + * + * @see testCommentTokenization() + * + * @return array + */ + public function dataCommentTokenization() + { + return [ + [ + '/* testSingleLineSlashComment */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testSingleLineSlashCommentTrailing */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testSingleLineSlashAnnotation */', + [ + [ + 'type' => 'T_PHPCS_DISABLE', + 'content' => '// phpcs:disable Stnd.Cat +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineSlashComment */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment1 +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment2 +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment3 +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineSlashCommentWithIndent */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment1 +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment2 +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment3 +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineSlashCommentWithAnnotationStart */', + [ + [ + 'type' => 'T_PHPCS_IGNORE', + 'content' => '// phpcs:ignore Stnd.Cat +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment2 +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment3 +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineSlashCommentWithAnnotationMiddle */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment1 +', + ], + [ + 'type' => 'T_PHPCS_IGNORE', + 'content' => '// @phpcs:ignore Stnd.Cat +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment3 +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineSlashCommentWithAnnotationEnd */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment1 +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment2 +', + ], + [ + 'type' => 'T_PHPCS_IGNORE', + 'content' => '// phpcs:ignore Stnd.Cat +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testSingleLineStarComment */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '/* Single line star comment */', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testSingleLineStarCommentTrailing */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '/* Comment */', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testSingleLineStarAnnotation */', + [ + [ + 'type' => 'T_PHPCS_IGNORE', + 'content' => '/* phpcs:ignore Stnd.Cat */', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineStarComment */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '/* Comment1 +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => ' * Comment2 +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => ' * Comment3 */', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineStarCommentWithIndent */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '/* Comment1 +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => ' * Comment2 +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => ' * Comment3 */', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineStarCommentWithAnnotationStart */', + [ + [ + 'type' => 'T_PHPCS_IGNORE', + 'content' => '/* @phpcs:ignore Stnd.Cat +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => ' * Comment2 +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => ' * Comment3 */', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineStarCommentWithAnnotationMiddle */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '/* Comment1 +', + ], + [ + 'type' => 'T_PHPCS_IGNORE', + 'content' => ' * phpcs:ignore Stnd.Cat +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => ' * Comment3 */', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineStarCommentWithAnnotationEnd */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '/* Comment1 +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => ' * Comment2 +', + ], + [ + 'type' => 'T_PHPCS_IGNORE', + 'content' => ' * phpcs:ignore Stnd.Cat */', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + + [ + '/* testSingleLineDocblockComment */', + [ + [ + 'type' => 'T_DOC_COMMENT_OPEN_TAG', + 'content' => '/**', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STRING', + 'content' => 'Comment ', + ], + [ + 'type' => 'T_DOC_COMMENT_CLOSE_TAG', + 'content' => '*/', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testSingleLineDocblockCommentTrailing */', + [ + [ + 'type' => 'T_DOC_COMMENT_OPEN_TAG', + 'content' => '/**', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STRING', + 'content' => 'Comment ', + ], + [ + 'type' => 'T_DOC_COMMENT_CLOSE_TAG', + 'content' => '*/', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testSingleLineDocblockAnnotation */', + [ + [ + 'type' => 'T_DOC_COMMENT_OPEN_TAG', + 'content' => '/**', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_PHPCS_IGNORE', + 'content' => 'phpcs:ignore Stnd.Cat.Sniff ', + ], + [ + 'type' => 'T_DOC_COMMENT_CLOSE_TAG', + 'content' => '*/', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + + [ + '/* testMultiLineDocblockComment */', + [ + [ + 'type' => 'T_DOC_COMMENT_OPEN_TAG', + 'content' => '/**', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STAR', + 'content' => '*', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STRING', + 'content' => 'Comment1', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STAR', + 'content' => '*', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STRING', + 'content' => 'Comment2', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STAR', + 'content' => '*', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STAR', + 'content' => '*', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_TAG', + 'content' => '@tag', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STRING', + 'content' => 'Comment', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_CLOSE_TAG', + 'content' => '*/', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineDocblockCommentWithIndent */', + [ + [ + 'type' => 'T_DOC_COMMENT_OPEN_TAG', + 'content' => '/**', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STAR', + 'content' => '*', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STRING', + 'content' => 'Comment1', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STAR', + 'content' => '*', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STRING', + 'content' => 'Comment2', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STAR', + 'content' => '*', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STAR', + 'content' => '*', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_TAG', + 'content' => '@tag', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STRING', + 'content' => 'Comment', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_CLOSE_TAG', + 'content' => '*/', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineDocblockCommentWithAnnotation */', + [ + [ + 'type' => 'T_DOC_COMMENT_OPEN_TAG', + 'content' => '/**', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STAR', + 'content' => '*', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STRING', + 'content' => 'Comment', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STAR', + 'content' => '*', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STAR', + 'content' => '*', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_PHPCS_IGNORE', + 'content' => 'phpcs:ignore Stnd.Cat', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STAR', + 'content' => '*', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_TAG', + 'content' => '@tag', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STRING', + 'content' => 'Comment', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_CLOSE_TAG', + 'content' => '*/', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineDocblockCommentWithTagAnnotation */', + [ + [ + 'type' => 'T_DOC_COMMENT_OPEN_TAG', + 'content' => '/**', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STAR', + 'content' => '*', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STRING', + 'content' => 'Comment', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STAR', + 'content' => '*', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STAR', + 'content' => '*', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_PHPCS_IGNORE', + 'content' => '@phpcs:ignore Stnd.Cat', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STAR', + 'content' => '*', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_TAG', + 'content' => '@tag', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_STRING', + 'content' => 'Comment', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_DOC_COMMENT_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_DOC_COMMENT_CLOSE_TAG', + 'content' => '*/', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testSingleLineHashComment */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '# Comment +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testSingleLineHashCommentTrailing */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '# Comment +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineHashComment */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '# Comment1 +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '# Comment2 +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '# Comment3 +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineHashCommentWithIndent */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '# Comment1 +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '# Comment2 +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '# Comment3 +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testSingleLineSlashCommentNoNewLineAtEnd */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '// Slash ', + ], + [ + 'type' => 'T_CLOSE_TAG', + 'content' => '?> +', + ], + ], + ], + [ + '/* testSingleLineHashCommentNoNewLineAtEnd */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '# Hash ', + ], + [ + 'type' => 'T_CLOSE_TAG', + 'content' => '?> +', + ], + ], + ], + [ + '/* testCommentAtEndOfFile */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '/* Comment', + ], + ], + ], + ]; + + }//end dataCommentTokenization() + + +}//end class diff --git a/tests/Core/Tokenizer/StableCommentWhitespaceWinTest.inc b/tests/Core/Tokenizer/StableCommentWhitespaceWinTest.inc new file mode 100644 index 0000000000..6ca7026d4b --- /dev/null +++ b/tests/Core/Tokenizer/StableCommentWhitespaceWinTest.inc @@ -0,0 +1,63 @@ + + + + * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; +use PHP_CodeSniffer\Util\Tokens; + +class StableCommentWhitespaceWinTest extends AbstractMethodUnitTest +{ + + + /** + * Test that comment tokenization with new lines at the end of the comment is stable. + * + * @param string $testMarker The comment prefacing the test. + * @param array $expectedTokens The tokenization expected. + * + * @dataProvider dataCommentTokenization + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testCommentTokenization($testMarker, $expectedTokens) + { + $tokens = self::$phpcsFile->getTokens(); + $comment = $this->getTargetToken($testMarker, Tokens::$commentTokens); + + foreach ($expectedTokens as $key => $tokenInfo) { + $this->assertSame(constant($tokenInfo['type']), $tokens[$comment]['code']); + $this->assertSame($tokenInfo['type'], $tokens[$comment]['type']); + $this->assertSame($tokenInfo['content'], $tokens[$comment]['content']); + + ++$comment; + } + + }//end testCommentTokenization() + + + /** + * Data provider. + * + * @see testCommentTokenization() + * + * @return array + */ + public function dataCommentTokenization() + { + return [ + [ + '/* testSingleLineSlashComment */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testSingleLineSlashCommentTrailing */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testSingleLineSlashAnnotation */', + [ + [ + 'type' => 'T_PHPCS_DISABLE', + 'content' => '// phpcs:disable Stnd.Cat +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineSlashComment */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment1 +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment2 +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment3 +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineSlashCommentWithIndent */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment1 +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment2 +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment3 +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineSlashCommentWithAnnotationStart */', + [ + [ + 'type' => 'T_PHPCS_IGNORE', + 'content' => '// phpcs:ignore Stnd.Cat +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment2 +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment3 +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineSlashCommentWithAnnotationMiddle */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment1 +', + ], + [ + 'type' => 'T_PHPCS_IGNORE', + 'content' => '// @phpcs:ignore Stnd.Cat +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment3 +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineSlashCommentWithAnnotationEnd */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment1 +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '// Comment2 +', + ], + [ + 'type' => 'T_PHPCS_IGNORE', + 'content' => '// phpcs:ignore Stnd.Cat +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testSingleLineSlashCommentNoNewLineAtEnd */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '// Slash ', + ], + [ + 'type' => 'T_CLOSE_TAG', + 'content' => '?> +', + ], + ], + ], + [ + '/* testSingleLineHashComment */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '# Comment +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testSingleLineHashCommentTrailing */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '# Comment +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineHashComment */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '# Comment1 +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '# Comment2 +', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '# Comment3 +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testMultiLineHashCommentWithIndent */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '# Comment1 +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '# Comment2 +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '# Comment3 +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testSingleLineHashCommentNoNewLineAtEnd */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '# Hash ', + ], + [ + 'type' => 'T_CLOSE_TAG', + 'content' => '?> +', + ], + ], + ], + [ + '/* testCommentAtEndOfFile */', + [ + [ + 'type' => 'T_COMMENT', + 'content' => '/* Comment', + ], + ], + ], + ]; + + }//end dataCommentTokenization() + + +}//end class diff --git a/tests/Core/Tokenizer/TypeIntersectionTest.inc b/tests/Core/Tokenizer/TypeIntersectionTest.inc new file mode 100644 index 0000000000..abf9b85ba8 --- /dev/null +++ b/tests/Core/Tokenizer/TypeIntersectionTest.inc @@ -0,0 +1,120 @@ + $param & $int; + +/* testTypeIntersectionArrowReturnType */ +$arrowWithReturnType = fn ($param) : Foo&Bar => $param * 10; + +/* testBitwiseAndInArrayKey */ +$array = array( + A & B => /* testBitwiseAndInArrayValue */ B & C +); + +/* testBitwiseAndInShortArrayKey */ +$array = [ + A & B => /* testBitwiseAndInShortArrayValue */ B & C +]; + +/* testBitwiseAndNonArrowFnFunctionCall */ +$obj->fn($something & $else); + +/* testBitwiseAnd6 */ +function &fn(/* testTypeIntersectionNonArrowFunctionDeclaration */ Foo&Bar $something) {} + +/* testTypeIntersectionWithInvalidTypes */ +function (int&string $var) {}; + +/* testLiveCoding */ +// Intentional parse error. This has to be the last test in the file. +return function( Foo& diff --git a/tests/Core/Tokenizer/TypeIntersectionTest.php b/tests/Core/Tokenizer/TypeIntersectionTest.php new file mode 100644 index 0000000000..2170021933 --- /dev/null +++ b/tests/Core/Tokenizer/TypeIntersectionTest.php @@ -0,0 +1,138 @@ + + * @author Jaroslav Hanslík + * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class TypeIntersectionTest extends AbstractMethodUnitTest +{ + + + /** + * Test that non-intersection type bitwise and tokens are still tokenized as bitwise and. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @dataProvider dataBitwiseAnd + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testBitwiseAnd($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $opener = $this->getTargetToken($testMarker, [T_BITWISE_AND, T_TYPE_INTERSECTION]); + $this->assertSame(T_BITWISE_AND, $tokens[$opener]['code']); + $this->assertSame('T_BITWISE_AND', $tokens[$opener]['type']); + + }//end testBitwiseAnd() + + + /** + * Data provider. + * + * @see testBitwiseAnd() + * + * @return array + */ + public function dataBitwiseAnd() + { + return [ + ['/* testBitwiseAnd1 */'], + ['/* testBitwiseAnd2 */'], + ['/* testBitwiseAndPropertyDefaultValue */'], + ['/* testBitwiseAndParamDefaultValue */'], + ['/* testBitwiseAnd3 */'], + ['/* testBitwiseAnd4 */'], + ['/* testBitwiseAnd5 */'], + ['/* testBitwiseAndClosureParamDefault */'], + ['/* testBitwiseAndArrowParamDefault */'], + ['/* testBitwiseAndArrowExpression */'], + ['/* testBitwiseAndInArrayKey */'], + ['/* testBitwiseAndInArrayValue */'], + ['/* testBitwiseAndInShortArrayKey */'], + ['/* testBitwiseAndInShortArrayValue */'], + ['/* testBitwiseAndNonArrowFnFunctionCall */'], + ['/* testBitwiseAnd6 */'], + ['/* testLiveCoding */'], + ]; + + }//end dataBitwiseAnd() + + + /** + * Test that bitwise and tokens when used as part of a intersection type are tokenized as `T_TYPE_INTERSECTION`. + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * + * @dataProvider dataTypeIntersection + * @covers PHP_CodeSniffer\Tokenizers\PHP::processAdditional + * + * @return void + */ + public function testTypeIntersection($testMarker) + { + $tokens = self::$phpcsFile->getTokens(); + + $opener = $this->getTargetToken($testMarker, [T_BITWISE_AND, T_TYPE_INTERSECTION]); + $this->assertSame(T_TYPE_INTERSECTION, $tokens[$opener]['code']); + $this->assertSame('T_TYPE_INTERSECTION', $tokens[$opener]['type']); + + }//end testTypeIntersection() + + + /** + * Data provider. + * + * @see testTypeIntersection() + * + * @return array + */ + public function dataTypeIntersection() + { + return [ + ['/* testTypeIntersectionPropertySimple */'], + ['/* testTypeIntersectionPropertyReverseModifierOrder */'], + ['/* testTypeIntersectionPropertyMulti1 */'], + ['/* testTypeIntersectionPropertyMulti2 */'], + ['/* testTypeIntersectionPropertyMulti3 */'], + ['/* testTypeIntersectionPropertyNamespaceRelative */'], + ['/* testTypeIntersectionPropertyPartiallyQualified */'], + ['/* testTypeIntersectionPropertyFullyQualified */'], + ['/* testTypeIntersectionPropertyWithReadOnlyKeyword */'], + ['/* testTypeIntersectionParam1 */'], + ['/* testTypeIntersectionParam2 */'], + ['/* testTypeIntersectionParam3 */'], + ['/* testTypeIntersectionParamNamespaceRelative */'], + ['/* testTypeIntersectionParamPartiallyQualified */'], + ['/* testTypeIntersectionParamFullyQualified */'], + ['/* testTypeIntersectionReturnType */'], + ['/* testTypeIntersectionConstructorPropertyPromotion */'], + ['/* testTypeIntersectionAbstractMethodReturnType1 */'], + ['/* testTypeIntersectionAbstractMethodReturnType2 */'], + ['/* testTypeIntersectionReturnTypeNamespaceRelative */'], + ['/* testTypeIntersectionReturnPartiallyQualified */'], + ['/* testTypeIntersectionReturnFullyQualified */'], + ['/* testTypeIntersectionClosureParamIllegalNullable */'], + ['/* testTypeIntersectionWithReference */'], + ['/* testTypeIntersectionWithSpreadOperator */'], + ['/* testTypeIntersectionClosureReturn */'], + ['/* testTypeIntersectionArrowParam */'], + ['/* testTypeIntersectionArrowReturnType */'], + ['/* testTypeIntersectionNonArrowFunctionDeclaration */'], + ['/* testTypeIntersectionWithInvalidTypes */'], + ]; + + }//end dataTypeIntersection() + + +}//end class diff --git a/tests/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.inc b/tests/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.inc new file mode 100644 index 0000000000..540f72c9dc --- /dev/null +++ b/tests/Core/Tokenizer/UndoNamespacedNameSingleTokenTest.inc @@ -0,0 +1,147 @@ + + * @copyright 2020 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Tests\Core\Tokenizer; + +use PHP_CodeSniffer\Tests\Core\AbstractMethodUnitTest; + +class UndoNamespacedNameSingleTokenTest extends AbstractMethodUnitTest +{ + + + /** + * Test that identifier names are tokenized the same across PHP versions, based on the PHP 5/7 tokenization. + * + * @param string $testMarker The comment prefacing the test. + * @param array $expectedTokens The tokenization expected. + * + * @dataProvider dataIdentifierTokenization + * @covers PHP_CodeSniffer\Tokenizers\PHP::tokenize + * + * @return void + */ + public function testIdentifierTokenization($testMarker, $expectedTokens) + { + $tokens = self::$phpcsFile->getTokens(); + $identifier = $this->getTargetToken($testMarker, constant($expectedTokens[0]['type'])); + + foreach ($expectedTokens as $key => $tokenInfo) { + $this->assertSame(constant($tokenInfo['type']), $tokens[$identifier]['code']); + $this->assertSame($tokenInfo['type'], $tokens[$identifier]['type']); + $this->assertSame($tokenInfo['content'], $tokens[$identifier]['content']); + + ++$identifier; + } + + }//end testIdentifierTokenization() + + + /** + * Data provider. + * + * @see testIdentifierTokenization() + * + * @return array + */ + public function dataIdentifierTokenization() + { + return [ + [ + '/* testNamespaceDeclaration */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'Package', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testNamespaceDeclarationWithLevels */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'Vendor', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'SubLevel', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Domain', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testUseStatement */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'ClassName', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testUseStatementWithLevels */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'Vendor', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Level', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Domain', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testFunctionUseStatement */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'function', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'function_name', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testFunctionUseStatementWithLevels */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'function', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Vendor', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Level', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'function_in_ns', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testConstantUseStatement */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'const', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'CONSTANT_NAME', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testConstantUseStatementWithLevels */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'const', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Vendor', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Level', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'OTHER_CONSTANT', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testMultiUseUnqualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'UnqualifiedClassName', + ], + [ + 'type' => 'T_COMMA', + 'content' => ',', + ], + ], + ], + [ + '/* testMultiUsePartiallyQualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'Sublevel', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'PartiallyClassName', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testGroupUseStatement */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'Vendor', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Level', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_OPEN_USE_GROUP', + 'content' => '{', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'AnotherDomain', + ], + [ + 'type' => 'T_COMMA', + 'content' => ',', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'function', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'function_grouped', + ], + [ + 'type' => 'T_COMMA', + 'content' => ',', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'const', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'CONSTANT_GROUPED', + ], + [ + 'type' => 'T_COMMA', + 'content' => ',', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Sub', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'YetAnotherDomain', + ], + [ + 'type' => 'T_COMMA', + 'content' => ',', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'function', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'SubLevelA', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'function_grouped_too', + ], + [ + 'type' => 'T_COMMA', + 'content' => ',', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'const', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'SubLevelB', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'CONSTANT_GROUPED_TOO', + ], + [ + 'type' => 'T_COMMA', + 'content' => ',', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_CLOSE_USE_GROUP', + 'content' => '}', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testClassName */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'MyClass', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testExtendedFQN */', + [ + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Vendor', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Level', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'FQN', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testImplementsRelative */', + [ + [ + 'type' => 'T_NAMESPACE', + 'content' => 'namespace', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Name', + ], + [ + 'type' => 'T_COMMA', + 'content' => ',', + ], + ], + ], + [ + '/* testImplementsFQN */', + [ + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Fully', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Qualified', + ], + [ + 'type' => 'T_COMMA', + 'content' => ',', + ], + ], + ], + [ + '/* testImplementsUnqualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'Unqualified', + ], + [ + 'type' => 'T_COMMA', + 'content' => ',', + ], + ], + ], + [ + '/* testImplementsPartiallyQualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'Sub', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Level', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Name', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + [ + '/* testFunctionName */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'function_name', + ], + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + ], + ], + [ + '/* testTypeDeclarationRelative */', + [ + [ + 'type' => 'T_NAMESPACE', + 'content' => 'namespace', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Name', + ], + [ + 'type' => 'T_TYPE_UNION', + 'content' => '|', + ], + [ + 'type' => 'T_STRING', + 'content' => 'object', + ], + ], + ], + [ + '/* testTypeDeclarationFQN */', + [ + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Fully', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Qualified', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Name', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + ], + ], + [ + '/* testTypeDeclarationUnqualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'Unqualified', + ], + [ + 'type' => 'T_TYPE_UNION', + 'content' => '|', + ], + [ + 'type' => 'T_FALSE', + 'content' => 'false', + ], + ], + ], + [ + '/* testTypeDeclarationPartiallyQualified */', + [ + [ + 'type' => 'T_NULLABLE', + 'content' => '?', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Sublevel', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Name', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + ], + ], + [ + '/* testReturnTypeFQN */', + [ + [ + 'type' => 'T_NULLABLE', + 'content' => '?', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Name', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + ], + ], + [ + '/* testFunctionCallRelative */', + [ + [ + 'type' => 'T_NAMESPACE', + 'content' => 'NameSpace', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'function_name', + ], + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + ], + ], + [ + '/* testFunctionCallFQN */', + [ + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Vendor', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Package', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'function_name', + ], + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + ], + ], + [ + '/* testFunctionCallUnqualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'function_name', + ], + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + ], + ], + [ + '/* testFunctionPartiallyQualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'Level', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'function_name', + ], + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + ], + ], + [ + '/* testCatchRelative */', + [ + [ + 'type' => 'T_NAMESPACE', + 'content' => 'namespace', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'SubLevel', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Exception', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + ], + ], + [ + '/* testCatchFQN */', + [ + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Exception', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + ], + ], + [ + '/* testCatchUnqualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'Exception', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + ], + ], + [ + '/* testCatchPartiallyQualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'Level', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Exception', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + ], + ], + [ + '/* testNewRelative */', + [ + [ + 'type' => 'T_NAMESPACE', + 'content' => 'namespace', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'ClassName', + ], + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + ], + ], + [ + '/* testNewFQN */', + [ + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Vendor', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'ClassName', + ], + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + ], + ], + [ + '/* testNewUnqualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'ClassName', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testNewPartiallyQualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'Level', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'ClassName', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testDoubleColonRelative */', + [ + [ + 'type' => 'T_NAMESPACE', + 'content' => 'namespace', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'ClassName', + ], + [ + 'type' => 'T_DOUBLE_COLON', + 'content' => '::', + ], + ], + ], + [ + '/* testDoubleColonFQN */', + [ + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'ClassName', + ], + [ + 'type' => 'T_DOUBLE_COLON', + 'content' => '::', + ], + ], + ], + [ + '/* testDoubleColonUnqualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'ClassName', + ], + [ + 'type' => 'T_DOUBLE_COLON', + 'content' => '::', + ], + ], + ], + [ + '/* testDoubleColonPartiallyQualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'Level', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'ClassName', + ], + [ + 'type' => 'T_DOUBLE_COLON', + 'content' => '::', + ], + ], + ], + [ + '/* testInstanceOfRelative */', + [ + [ + 'type' => 'T_NAMESPACE', + 'content' => 'namespace', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'ClassName', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testInstanceOfFQN */', + [ + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Full', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'ClassName', + ], + [ + 'type' => 'T_CLOSE_PARENTHESIS', + 'content' => ')', + ], + ], + ], + [ + '/* testInstanceOfUnqualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'ClassName', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + ], + ], + [ + '/* testInstanceOfPartiallyQualified */', + [ + [ + 'type' => 'T_STRING', + 'content' => 'Partially', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'ClassName', + ], + [ + 'type' => 'T_SEMICOLON', + 'content' => ';', + ], + ], + ], + [ + '/* testInvalidInPHP8Whitespace */', + [ + [ + 'type' => 'T_NAMESPACE', + 'content' => 'namespace', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Sublevel', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_STRING', + 'content' => 'function_name', + ], + [ + 'type' => 'T_OPEN_PARENTHESIS', + 'content' => '(', + ], + ], + ], + [ + '/* testInvalidInPHP8Comments */', + [ + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Fully', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_PHPCS_IGNORE', + 'content' => '// phpcs:ignore Stnd.Cat.Sniff -- for reasons +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Qualified', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_COMMENT', + 'content' => '/* comment */', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' ', + ], + [ + 'type' => 'T_NS_SEPARATOR', + 'content' => '\\', + ], + [ + 'type' => 'T_STRING', + 'content' => 'Name', + ], + [ + 'type' => 'T_WHITESPACE', + 'content' => ' +', + ], + ], + ], + ]; + + }//end dataIdentifierTokenization() + + +}//end class diff --git a/tests/Standards/AbstractSniffUnitTest.php b/tests/Standards/AbstractSniffUnitTest.php index dc7c14cbe8..c050a0c2af 100644 --- a/tests/Standards/AbstractSniffUnitTest.php +++ b/tests/Standards/AbstractSniffUnitTest.php @@ -320,6 +320,17 @@ public function generateFailureMessages(LocalFile $file) $warningsTemp = []; foreach ($warnings as $warning) { $warningsTemp[] = $warning['message'].' ('.$warning['source'].')'; + + $source = $warning['source']; + if (in_array($source, $GLOBALS['PHP_CODESNIFFER_SNIFF_CODES'], true) === false) { + $GLOBALS['PHP_CODESNIFFER_SNIFF_CODES'][] = $source; + } + + if ($warning['fixable'] === true + && in_array($source, $GLOBALS['PHP_CODESNIFFER_FIXABLE_CODES'], true) === false + ) { + $GLOBALS['PHP_CODESNIFFER_FIXABLE_CODES'][] = $source; + } } $allProblems[$line]['found_warnings'] = array_merge($foundWarningsTemp, $warningsTemp); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 560253c6a7..47084d1135 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -44,6 +44,13 @@ class_alias('PHPUnit_TextUI_TestRunner', 'PHPUnit'.'\TextUI\TestRunner'); class_alias('PHPUnit_Framework_TestResult', 'PHPUnit'.'\Framework\TestResult'); } +// Determine whether this is a PEAR install or not. +$GLOBALS['PHP_CODESNIFFER_PEAR'] = false; + +if (is_file(__DIR__.'/../autoload.php') === false) { + $GLOBALS['PHP_CODESNIFFER_PEAR'] = true; +} + /** * A global util function to help print unit test fixing data.