diff --git a/.coveralls.yml b/.coveralls.yml
index 1621caa9..30b618e3 100644
--- a/.coveralls.yml
+++ b/.coveralls.yml
@@ -1,3 +1,2 @@
coverage_clover: build/logs/clover.xml
json_path: build/logs/coveralls-upload.json
-service_name: travis-ci
diff --git a/.gitattributes b/.gitattributes
index b8f7709c..814f5484 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -8,10 +8,11 @@
/.coveralls.yml export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
-/.travis.yml export-ignore
+/.github/ export-ignore
/phpcs.xml.dist export-ignore
/phpunit.xml.dist export-ignore
/phpunit-bootstrap.php export-ignore
+/Modernize/Tests/ export-ignore
/NormalizedArrays/Tests/ export-ignore
/Universal/Tests/ export-ignore
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000..56043bed
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,76 @@
+---
+name: "\U0001F41B Bug report for sniffs"
+about: I got unexpected behavior and think it is a bug.
+
+---
+
+
+
+## Bug Description
+
+
+
+## Given the following reproduction Scenario
+
+
+The issue happens when running this command:
+
+```bash
+phpcs -ps file.php --standard=...
+```
+
+... over a file containing this code:
+```php
+// Place your code sample here.
+```
+
+
+... with this custom ruleset:
+```xml
+
+
+ ...
+
+```
+
+
+### I'd expect the following behaviour
+
+
+
+### Instead this happened
+
+
+
+## Environment
+
+
+| Environment | Answer
+| ------------------------ | -------
+| PHP version | x.y.z
+| PHP_CodeSniffer version | x.y.z
+| PHPCSExtra version | x.y.z
+| PHPCSUtils version | x.y.z
+| Install type | e.g. Composer global, Composer project local, git clone, other (please expand)
+
+
+## Additional Context (optional)
+
+
+
+## Tested Against `develop` branch?
+- [ ] I have verified the issue still exists in the `develop` branch of PHPCSExtra.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000..25a15a41
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: "\U0001F680 Feature request"
+about: I have a suggestion (and may want to implement it).
+---
+
+## Is your feature request related to a problem?
+
+
+## Describe the solution you'd like
+
+
+## Additional context (optional)
+
+
+- [ ] I intend to create a pull request to implement this feature.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..d7bb986a
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,27 @@
+# Dependabot configuration.
+#
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "composer"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 5 # Set to 0 to (temporarily) disable.
+ versioning-strategy: widen
+ commit-message:
+ prefix: "Composer:"
+ labels:
+ - "Type: chores/QA"
+
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 5
+ commit-message:
+ prefix: "GH Actions:"
+ labels:
+ - "Type: chores/QA"
diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml
new file mode 100644
index 00000000..9029b26a
--- /dev/null
+++ b/.github/workflows/basics.yml
@@ -0,0 +1,98 @@
+name: CS
+
+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:
+ # Allow manually triggering the workflow.
+ workflow_dispatch:
+
+# Cancels all previous workflow runs for the same branch that have not yet completed.
+concurrency:
+ # The concurrency group contains the workflow name and the branch name.
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ checkcs:
+ name: 'Basic CS and QA checks'
+ runs-on: ubuntu-latest
+
+ env:
+ XMLLINT_INDENT: ' '
+ # - COMPOSER_ROOT_VERSION is needed to get round the recursive dependency when using CI.
+ COMPOSER_ROOT_VERSION: '1.99.99'
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Install PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: 'latest'
+ coverage: none
+ tools: cs2pr
+
+ - name: 'Composer: adjust dependencies'
+ run: |
+ # The sniff stage doesn't run the unit tests, so no need for PHPUnit.
+ composer remove --no-update --dev phpunit/phpunit --no-scripts --no-interaction
+ # Using PHPCS `master` as an early detection system for bugs upstream.
+ composer require --no-update squizlabs/php_codesniffer:"dev-master" --no-interaction
+
+ # Install dependencies and handle caching in one go.
+ # @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-DD.
+ custom-cache-suffix: $(date -u -d "-0 month -$(($(date +%d)-1)) days" "+%F")
+
+ - name: Install xmllint
+ run: |
+ sudo apt-get update
+ sudo apt-get install --no-install-recommends -y libxml2-utils
+
+ # 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 Ruleset XML file.
+ # @link http://xmlsoft.org/xmllint.html
+ - name: Validate rulesets against schema
+ run: xmllint --noout --schema vendor/squizlabs/php_codesniffer/phpcs.xsd ./*/ruleset.xml
+
+ # Check the code-style consistency of the XML ruleset files.
+ - name: Check XML code style
+ run: |
+ diff -B ./Modernize/ruleset.xml <(xmllint --format "./Modernize/ruleset.xml")
+ diff -B ./NormalizedArrays/ruleset.xml <(xmllint --format "./NormalizedArrays/ruleset.xml")
+ diff -B ./Universal/ruleset.xml <(xmllint --format "./Universal/ruleset.xml")
+
+ # Validate the Documentation XML files.
+ - name: Validate documentation against schema
+ run: xmllint --noout --schema vendor/phpcsstandards/phpcsdevtools/DocsXsd/phpcsdocs.xsd ./*/Docs/*/*Standard.xml
+
+ # Check the code-style consistency of the PHP files.
+ - name: Check PHP code style
+ id: phpcs
+ run: vendor/bin/phpcs --report-full --report-checkstyle=./phpcs-report.xml
+
+ - name: Show PHPCS results in PR
+ if: ${{ always() && steps.phpcs.outcome == 'failure' }}
+ run: cs2pr ./phpcs-report.xml
+
+ # Check that the sniffs available are feature complete.
+ # For now, just check that all sniffs have unit tests.
+ # At a later stage the documentation check can be activated.
+ - name: Check sniff feature completeness
+ run: composer check-complete
+
+ # Validate the composer.json file.
+ # @link https://getcomposer.org/doc/03-cli.md#validate
+ - name: Validate Composer installation
+ run: composer validate --no-check-all --strict
diff --git a/.github/workflows/quicktest.yml b/.github/workflows/quicktest.yml
new file mode 100644
index 00000000..90c699e0
--- /dev/null
+++ b/.github/workflows/quicktest.yml
@@ -0,0 +1,88 @@
+name: Quicktest
+
+on:
+ # Run on pushes, including merges, to all branches except for `stable` and `develop`.
+ push:
+ branches-ignore:
+ - stable
+ - develop
+ paths-ignore:
+ - '**.md'
+ # Allow manually triggering the workflow.
+ workflow_dispatch:
+
+# Cancels all previous workflow runs for the same branch that have not yet completed.
+concurrency:
+ # The concurrency group contains the workflow name and the branch name.
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ #### QUICK TEST STAGE ####
+ # This is a much quicker test which only runs the unit tests and linting against the low/high
+ # supported PHP/PHPCS combinations.
+ # These are basically the same builds as in the Test->Coverage workflow, but then without doing
+ # the code-coverage.
+ quicktest:
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ php: ['5.4', 'latest']
+ phpcs_version: ['3.7.1', 'dev-master']
+
+ name: "QTest${{ matrix.phpcs_version == 'dev-master' && ' + Lint' || '' }}: PHP ${{ matrix.php }} - PHPCS ${{ matrix.phpcs_version }}"
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ # On stable PHPCS versions, allow for PHP deprecation notices.
+ # Unit tests don't need to fail on those for stable releases where those issues won't get fixed anymore.
+ - name: Setup ini config
+ id: set_ini
+ run: |
+ if [ "${{ matrix.phpcs_version }}" != "dev-master" ]; then
+ echo 'PHP_INI=error_reporting=E_ALL & ~E_DEPRECATED, display_errors=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
+
+ - name: 'Composer: set PHPCS version for tests'
+ run: composer require --no-update --no-scripts squizlabs/php_codesniffer:"${{ matrix.phpcs_version }}" --no-interaction
+
+ # 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 != 'latest'
+ uses: "ramsey/composer-install@v2"
+ with:
+ # Bust the cache at least once a month - output format: YYYY-MM-DD.
+ custom-cache-suffix: $(date -u -d "-0 month -$(($(date +%d)-1)) days" "+%F")
+
+ # For the PHP "latest", we need to install with ignore platform reqs as not all PHPUnit 7.x dependencies allow it.
+ - name: Install Composer dependencies - with ignore platform
+ if: matrix.php == 'latest'
+ uses: "ramsey/composer-install@v2"
+ with:
+ composer-options: --ignore-platform-reqs
+ custom-cache-suffix: $(date -u -d "-0 month -$(($(date +%d)-1)) days" "+%F")
+
+ - name: Lint against parse errors
+ if: matrix.phpcs_version == 'dev-master'
+ run: composer lint
+
+ - name: Run the unit tests - PHP 5.4 - 8.0
+ if: matrix.php != 'latest'
+ run: composer test
+
+ - name: Run the unit tests - PHP > 8.1
+ if: matrix.php == 'latest'
+ run: composer test -- --no-configuration --bootstrap=phpunit-bootstrap.php --dont-report-useless-tests
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 00000000..bc82d4aa
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,188 @@
+name: Test
+
+on:
+ # Run on pushes to `stable` and `develop` and on all pull requests.
+ push:
+ branches:
+ - stable
+ - develop
+ paths-ignore:
+ - '**.md'
+ pull_request:
+ # Allow manually triggering the workflow.
+ workflow_dispatch:
+
+# Cancels all previous workflow runs for the same branch that have not yet completed.
+concurrency:
+ # The concurrency group contains the workflow name and the branch name.
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ #### TEST STAGE ####
+ test:
+ if: ${{ github.ref != 'refs/heads/develop' }}
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ # The GHA matrix works different from Travis.
+ # You can define jobs here and then augment them with extra variables in `include`,
+ # as well as add extra jobs in `include`.
+ # @link https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix
+ #
+ # The matrix is set up so as not to duplicate the builds which are run for code coverage.
+ php: ['5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '8.0', '8.1', '8.2']
+ phpcs_version: ['3.7.1', 'dev-master']
+
+ name: "Test${{ matrix.phpcs_version == 'dev-master' && ' + Lint' || '' }}: PHP ${{ matrix.php }} - PHPCS ${{ matrix.phpcs_version }}"
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ # On stable PHPCS versions, allow for PHP deprecation notices.
+ # Unit tests don't need to fail on those for stable releases where those issues won't get fixed anymore.
+ - name: Setup ini config
+ id: set_ini
+ run: |
+ if [[ "${{ matrix.phpcs_version }}" != "dev-master" ]]; then
+ echo 'PHP_INI=error_reporting=E_ALL & ~E_DEPRECATED, display_errors=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
+
+ - name: 'Composer: set PHPCS version for tests'
+ run: composer require --no-update squizlabs/php_codesniffer:"${{ matrix.phpcs_version }}" --no-interaction
+
+ # Install dependencies and handle caching in one go.
+ # @link https://github.com/marketplace/actions/install-composer-dependencies
+ - name: Install Composer dependencies - normal
+ if: ${{ startsWith( matrix.php, '8' ) == false }}
+ uses: "ramsey/composer-install@v2"
+ with:
+ # Bust the cache at least once a month - output format: YYYY-MM-DD.
+ custom-cache-suffix: $(date -u -d "-0 month -$(($(date +%d)-1)) days" "+%F")
+
+ # For the PHP 8/"nightly", we need to install with ignore platform reqs as we're still using PHPUnit 7.
+ - name: Install Composer dependencies - with ignore platform
+ if: ${{ startsWith( matrix.php, '8' ) }}
+ uses: "ramsey/composer-install@v2"
+ with:
+ composer-options: --ignore-platform-reqs
+ custom-cache-suffix: $(date -u -d "-0 month -$(($(date +%d)-1)) days" "+%F")
+
+ - name: Lint against parse errors
+ if: matrix.phpcs_version == 'dev-master'
+ run: composer lint
+
+ - name: Run the unit tests - PHP 5.4 - 8.0
+ if: ${{ matrix.php < '8.1' }}
+ run: composer test
+
+ - name: Run the unit tests - PHP > 8.1
+ if: ${{ matrix.php >= '8.1' }}
+ run: composer test -- --no-configuration --bootstrap=phpunit-bootstrap.php --dont-report-useless-tests
+
+ #### CODE COVERAGE STAGE ####
+ # N.B.: Coverage is only checked on the lowest and highest stable PHP versions
+ # and a low/high of each major for PHPCS.
+ # These builds are left out off the "test" stage so as not to duplicate test runs.
+ coverage:
+ # No use running the coverage builds if there are failing test builds.
+ needs: test
+ # The default condition is success(), but this is false when one of the previous jobs is skipped
+ if: always() && (needs.test.result == 'success' || needs.test.result == 'skipped')
+
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ # 7.4 should be updated to 8.2 when higher PHPUnit versions can be supported.
+ php: ['5.4', '7.4']
+ phpcs_version: ['3.7.1', 'dev-master']
+
+ name: "Coverage${{ matrix.phpcs_version == 'dev-master' && ' + Lint' || '' }}: PHP ${{ matrix.php }} - PHPCS ${{ matrix.phpcs_version }}"
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ # On stable PHPCS versions, allow for PHP deprecation notices.
+ # Unit tests don't need to fail on those for stable releases where those issues won't get fixed anymore.
+ - name: Setup ini config
+ id: set_ini
+ run: |
+ if [ "${{ matrix.phpcs_version }}" != "dev-master" ]; then
+ echo 'PHP_INI=error_reporting=E_ALL & ~E_DEPRECATED, display_errors=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: xdebug
+ tools: cs2pr
+
+ - name: 'Composer: adjust dependencies'
+ run: |
+ # Set a specific PHPCS version.
+ composer require --no-update squizlabs/php_codesniffer:"${{ matrix.phpcs_version }}" --no-scripts --no-interaction
+
+ # Install dependencies and handle caching in one go.
+ # @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-DD.
+ custom-cache-suffix: $(date -u -d "-0 month -$(($(date +%d)-1)) days" "+%F")
+
+ - name: Lint against parse errors
+ if: matrix.phpcs_version == 'dev-master'
+ run: composer lint -- --checkstyle | cs2pr
+
+ - name: Run the unit tests with code coverage
+ run: composer coverage
+
+ # Uploading the results with PHP Coveralls v1 won't work from GH Actions, so switch the PHP version.
+ - name: Switch to PHP 7.4
+ if: ${{ success() && matrix.php != '7.4' }}
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: 7.4
+ coverage: none
+
+ - name: Install Coveralls
+ if: ${{ success() }}
+ run: composer require php-coveralls/php-coveralls:"^2.5.3" --no-interaction
+
+ - name: Upload coverage results to Coveralls
+ if: ${{ success() }}
+ env:
+ COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ COVERALLS_PARALLEL: true
+ COVERALLS_FLAG_NAME: php-${{ matrix.php }}-phpcs-${{ matrix.phpcs_version }}
+ run: vendor/bin/php-coveralls -v -x build/logs/clover.xml
+
+ coveralls-finish:
+ needs: coverage
+ if: always() && needs.coverage.result == 'success'
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Coveralls Finished
+ uses: coverallsapp/github-action@master
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ parallel-finished: true
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index e20d7c2b..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,198 +0,0 @@
-dist: trusty
-
-language: php
-
-## Cache composer and apt downloads.
-cache:
- apt: true
- directories:
- # Cache directory for older Composer versions.
- - $HOME/.composer/cache/files
- # Cache directory for more recent Composer versions.
- - $HOME/.cache/composer/files
-
-php:
- - 5.5
- - 5.6
- - 7.0
- - 7.1
- - 7.2
-
-env:
- jobs:
- # PHPCS `master`.
- - PHPCS_VERSION="dev-master" LINT=1
- # Lowest supported PHPCS version.
- - PHPCS_VERSION="3.3.1"
-
-# Define the stages used.
-# For non-PRs, only the sniff and quicktest stages are run.
-# For pull requests and merges, the full script is run (skipping quicktest).
-# Note: for pull requests, "develop" is the base branch name.
-# See: https://docs.travis-ci.com/user/conditions-v1
-stages:
- - name: sniff
- - name: validate
- - name: quicktest
- if: type = push AND branch NOT IN (stable, develop)
- - name: test
- if: branch IN (stable, develop)
- - name: coverage
- if: branch IN (stable, develop)
-
-jobs:
- fast_finish: true
-
- include:
- #### SNIFF STAGE ####
- - stage: sniff
- php: 7.4
- install: skip
- before_script: skip
- script:
- # Check the code style of the code base.
- - composer travis-checkcs
-
- - stage: validate
- php: 7.4
- env: PHPCS_VERSION="dev-master"
- addons:
- apt:
- packages:
- - libxml2-utils
- script:
- # Validate the composer.json file.
- # @link https://getcomposer.org/doc/03-cli.md#validate
- - composer validate --no-check-all --strict
-
- # Validate the xml files.
- # @link http://xmlsoft.org/xmllint.html
- - xmllint --noout --schema ./vendor/squizlabs/php_codesniffer/phpcs.xsd ./NormalizedArrays/ruleset.xml
- - xmllint --noout --schema ./vendor/squizlabs/php_codesniffer/phpcs.xsd ./Universal/ruleset.xml
-
- # Check the code-style consistency of the xml files.
- - diff -B ./NormalizedArrays/ruleset.xml <(xmllint --format "./NormalizedArrays/ruleset.xml")
- - diff -B ./Universal/ruleset.xml <(xmllint --format "./Universal/ruleset.xml")
-
- # Check that the sniffs available are feature complete.
- - composer check-complete
-
- #### QUICK TEST STAGE ####
- # This is a much quicker test which only runs the unit tests and linting against the low/high
- # supported PHP/PHPCS combinations.
- # These are basically the same builds as in the Coverage stage, but then without doing
- # the code-coverage.
- - stage: quicktest
- php: 7.4
- env: PHPCS_VERSION="dev-master" LINT=1
- - php: 7.3
- env: PHPCS_VERSION="3.3.1"
-
- - php: 5.4
- env: PHPCS_VERSION="dev-master" LINT=1
- - php: 5.4
- env: PHPCS_VERSION="3.3.1"
-
- #### TEST STAGE ####
- # Additional builds to prevent issues with PHPCS versions incompatible with certain PHP versions.
- - stage: test
- # PHPCS is only compatible with PHP 7.4 as of version 3.5.0.
- php: 7.4
- env: PHPCS_VERSION="3.5.0"
- - php: 7.3
- env: PHPCS_VERSION="dev-master" LINT=1
-
- - php: 7.4
- env: PHPCS_VERSION="4.0.x-dev@dev"
-
- - php: "nightly"
- env: PHPCS_VERSION="dev-master" LINT=1
-
- #### CODE COVERAGE STAGE ####
- # N.B.: Coverage is only checked on the lowest and highest stable PHP versions for all PHPCS versions.
- # These builds are left out off the "test" stage so as not to duplicate test runs.
- # The script used is the default script below, the same as for the `test` stage.
- - stage: coverage
- php: 7.4
- env: PHPCS_VERSION="dev-master" LINT=1 COVERALLS_VERSION="^2.0"
- - php: 7.3
- env: PHPCS_VERSION="3.3.1" COVERALLS_VERSION="^2.0"
-
- - php: 5.4
- env: PHPCS_VERSION="dev-master" LINT=1 COVERALLS_VERSION="^1.0"
- - php: 5.4
- env: PHPCS_VERSION="3.3.1" COVERALLS_VERSION="^1.0"
-
- allow_failures:
- # Allow failures for unstable builds.
- - php: "nightly"
- - env: PHPCS_VERSION="4.0.x-dev@dev"
-
-
-before_install:
- # Speed up build time by disabling Xdebug when its not needed.
- - |
- if [[ "${TRAVIS_BUILD_STAGE_NAME^}" != "Coverage" ]]; then
- phpenv config-rm xdebug.ini || echo 'No xdebug config.'
- fi
-
- - export XMLLINT_INDENT=" "
-
- # On stable PHPCS versions, allow for PHP deprecation notices.
- # Unit tests don't need to fail on those for stable releases where those issues won't get fixed anymore.
- - |
- if [[ "${TRAVIS_BUILD_STAGE_NAME^}" != "Sniff" && $PHPCS_BRANCH != "dev-master" ]]; then
- echo 'error_reporting = E_ALL & ~E_DEPRECATED' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini
- fi
-
-
-install:
- # Set up test environment using Composer.
- - composer require --no-update --no-scripts squizlabs/php_codesniffer:${PHPCS_VERSION}
- - |
- if [[ "${TRAVIS_BUILD_STAGE_NAME^}" == "Coverage" ]]; then
- composer require --no-update --no-suggest --no-scripts php-coveralls/php-coveralls:${COVERALLS_VERSION}
- fi
-
- - |
- if [[ "${PHPCS_VERSION:0:3}" == "4.0" ]]; then
- # Remove devtools as it will not (yet) install on PHPCS 4.x.
- composer remove --dev phpcsstandards/phpcsdevtools --no-update
- # --prefer-source ensures that the PHPCS native unit test framework will be available in PHPCS 4.x.
- composer install --prefer-source --no-suggest
- elif [[ $TRAVIS_PHP_VERSION == "nightly" ]]; then
- # Ignore PHPUnit platform requirements for installing on nightly.
- composer install --prefer-dist --no-suggest --ignore-platform-reqs
- else
- # --prefer-dist will allow for optimal use of the travis caching ability.
- composer install --prefer-dist --no-suggest
- fi
- # The Composer PHPCS plugin takes care of setting the installed_paths for PHPCS.
-
-
-before_script:
- - if [[ "${TRAVIS_BUILD_STAGE_NAME^}" == "Coverage" ]]; then mkdir -p build/logs; fi
- - phpenv rehash
-
-
-script:
- # Lint PHP files against parse errors.
- - if [[ "$LINT" == "1" ]]; then composer lint; fi
-
- # Run the unit tests.
- - |
- if [[ "${TRAVIS_BUILD_STAGE_NAME^}" != "Coverage" ]]; then
- composer test
- elif [[ "${TRAVIS_BUILD_STAGE_NAME^}" == "Coverage" ]]; then
- composer coverage
- fi
-
-after_success:
- - |
- if [[ "${TRAVIS_BUILD_STAGE_NAME^}" == "Coverage" && $COVERALLS_VERSION == "^1.0" ]]; then
- php vendor/bin/coveralls -v -x build/logs/clover.xml
- fi
- - |
- if [[ "${TRAVIS_BUILD_STAGE_NAME^}" == "Coverage" && $COVERALLS_VERSION == "^2.0" ]]; then
- php vendor/bin/php-coveralls -v -x build/logs/clover.xml
- fi
diff --git a/CHANGELOG.md b/CHANGELOG.md
index db5f02ee..8e97109d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-# Change Log for the PHPCSExtra standard for PHP Codesniffer
+# Change Log for the PHPCSExtra standard for PHP CodeSniffer
All notable changes to this project will be documented in this file.
@@ -14,24 +14,191 @@ This projects adheres to [Keep a CHANGELOG](http://keepachangelog.com/) and uses
_Nothing yet._
+
+## [1.0.0-RC1] - 2022-12-07
+
+:warning: Important: this package now requires [PHPCSUtils 1.0.0-alpha4]. Please make sure you use `--with-[all-]dependencies` when running `composer update`. :exclamation:
+
+### Added
+
+#### Modernize
+
+* This is a new standard with one sniff to start with.
+* :wrench: :books: New `Modernize.FunctionCalls.Dirname` sniff to detect and auto-fix two typical code modernizations which can be made related to the [`dirname()`][php-manual-dirname] function. [#172]
+
+#### Universal
+
+* :wrench: :bar_chart: :books: New `Universal.Classes.DisallowAnonClassParentheses` sniff to disallow the use of parentheses when declaring an anonymous class without passing parameters. [#76], [#162]
+* :wrench: :bar_chart: :books: New `Universal.Classes.RequireAnonClassParentheses` sniff to require the use of parentheses when declaring an anonymous class, whether parameters are passed or not. [#76], [#166]
+* :wrench: :bar_chart: :books: New `Universal.Classes.DisallowFinalClass` sniff to disallow classes being declared `final`. [#108], [#114], [#148], [#163]
+* :wrench: :bar_chart: :books: New `Universal.Classes.RequireFinalClass` sniff to require all non-`abstract` classes to be declared `final`. [#109], [#148], [#164]
+ Warning: the auto-fixer for this sniff _may_ have unintended side-effects for applications and should be used with care! This is considered a _risky_ fixer.
+* :wrench: :bar_chart: :books: New `Universal.Classes.ModifierKeywordOrder` sniff to standardize the modifier keyword order for class declarations. [#142]
+ The sniff offers an `order` property to specify the preferred order.
+* :wrench: :books: New `Universal.CodeAnalysis.ConstructorDestructorReturn` sniff to verify that class constructor/destructor methods 1) do not have a return type declaration and 2) do not return a value. [#137], [#140], [#146] Inspired by [@derickr].
+* :wrench: :books: New `Universal.CodeAnalysis.ForeachUniqueAssignment` sniff to detect `foreach` control structures which use the same variable for both the key as well as the value assignment as this will lead to unexpected - and most likely unintended - behaviour. [#110], [#175]
+ The fixer will maintain the existing behaviour of the code. Mind: this may not be the _intended_ behaviour.
+* :wrench: :books: New `Universal.CodeAnalysis.StaticInFinalClass` sniff to detect using `static` instead of `self` in OO constructs which are `final`. [#116], [#180]
+ The sniff has modular error codes to allow for making exceptions based on the type of use for `static`.
+* :wrench: :bar_chart: :books: New `Universal.Constants.LowercaseClassResolutionKeyword` sniff to enforce that the `class` keyword when used for class name resolution, i.e. `::class`, is in lowercase. [#72]
+* :wrench: :bar_chart: :books: New `Universal.Constants.ModifierKeywordOrder` sniff to standardize the modifier keyword order for OO constant declarations. [#143]
+ The sniff offers an `order` property to specify the preferred order.
+* :wrench: :books: New `Universal.ControlStructures.DisallowLonelyIf` sniff to disallow `if` statements as the only statement in an `else` block. [#85], [#168], [#169]
+ Inspired by the [ESLint "no lonely if"] rule.
+ Note: This sniff will not fix the indentation of the "inner" code. It is strongly recommended to run this sniff together with the `Generic.WhiteSpace.ScopeIndent` sniff to get the correct indentation.
+* :bar_chart: :books: New `Universal.Files.SeparateFunctionsFromOO` sniff to enforce that a file should either declare (global/namespaced) functions or declare OO structures, but not both. [#95], [#170], [#171]
+ Nested function declarations, i.e. functions declared within a function/method will be disregarded for the purposes of this sniff.
+ The same goes for anonymous classes, closures and arrow functions.
+* :books: New `Universal.NamingConventions.NoReservedKeywordParameterNames` sniff to verify that function parameters do not use reserved keywords as names, as this can quickly become confusing when people use them in function calls using named parameters. [#80], [#81], [#106], [#107], [#173]
+ The sniff has modular error codes to allow for making exceptions for specific keywords.
+* :wrench: :bar_chart: :books: New `Universal.Operators.TypeSeparatorSpacing` sniff to enforce no spaces around union type and intersection type separators. [#117]
+* :wrench: :books: New `Universal.PHP.OneStatementInShortEchoTag` sniff to disallow short open echo tags `=` containing more than one PHP statement. [#89], [#147], [#165]
+* :wrench: :bar_chart: :books: New `Universal.WhiteSpace.AnonClassKeywordSpacing` sniff to standardize the amount of spacing between the `class` keyword and the open parenthesis (if any) for anonymous class declarations. [#120]
+ The sniff offers a `spacing` property to set the amount of spaces the sniff should check for.
+* :wrench: :books: New `Universal.WhiteSpace.PrecisionAlignment` sniff to enforce indentation to always be a multiple of a tabstop, i.e. disallow precision alignment. [#119], [#122], [#123], [#124]
+ Note:
+ - This sniff does not concern itself with tabs versus spaces.
+ It is recommended to use the sniff in combination with the PHPCS native `Generic.WhiteSpace.DisallowTabIndent` or the `Generic.WhiteSpace.DisallowSpaceIndent` sniff.
+ - When using this sniff with tab-based standards, please ensure that the `tab-width` is set and either don't set the `$indent` property or set it to the tab-width (or a multiple thereof).
+ - The fixer works based on "best guess" and may not always result in the desired indentation. Combine this sniff with the `Generic.WhiteSpace.ScopeIndent` sniff for more precise indentation fixes.
+ - The behaviour of the sniff is customizable via the following properties:
+ - `indent`: the indent used for the codebase.
+ - `ignoreAlignmentBefore`: allows for providing a list of token names for which (preceding) precision alignment should be ignored.
+ - `ignoreBlankLines`: whether or not potential trailing whitespace on otherwise blank lines should be examined or ignored.
+
+### Changed
+
+#### Universal
+* `Universal.Arrays.DisallowShortArraySyntax`: the sniff will now record metrics about long vs short array usage. [#154]
+* `Universal.Arrays.DuplicateArrayKey`: where relevant, the sniff will now make a distinction between keys which will be duplicate in all PHP version and (numeric) keys which will only be a duplicate key in [PHP < 8.0 or PHP >= 8.0][php-rfc-negative_array_index]. [#177], [#178]
+ If a [`php_version` configuration option][php_version-config] has been passed to PHPCS, it will be respected by the sniff and only report duplicate keys for the configured PHP version.
+* `Universal.ControlStructures.DisallowAlternativeSyntax`: the sniff will now also record a metric when single-line (no body) control structures are encountered. [#158]
+* `Universal.ControlStructures.DisallowAlternativeSyntax`: the error message thrown by the sniff is now more descriptive. [#159]
+* `Universal.ControlStructures.DisallowAlternativeSyntax`: metrics will no longer be recorded for `elseif` and `else` keywords, but only on the `if` keyword as the type of syntax used has to be the same for the whole "chain". [#161]
+* `Universal.Lists.DisallowLongListSyntax`: the sniff will no longer record (incomplete) metrics about long vs short list usage. [#155]
+* `Universal.Lists.DisallowShortListSyntax`: the sniff will now record (complete) metrics about long vs short list usage. [#155]
+* `Universal.OOStructures.AlphabeticExtendsImplements`: documented support for `enum ... implements`. [#150]
+* `Universal.UseStatements.DisallowUseClass`: updated error message and metric name to take PHP 8.1 `enum`s into account. [#149]
+* `Universal.UseStatements.NoLeadingBackslash`: the sniff will now also flag and auto-fix leading backslashes in group use statements. [#167]
+
+#### Other
+* Updated the sniffs for compatibility with PHPCSUtils 1.0.0-alpha4. [#134]
+* Updated the sniffs to correctly handle PHP 8.0/8.1/8.2 features whenever relevant.
+* Readme: Updated installation instructions for compatibility with Composer 2.2+. [#101]
+* Composer: The minimum `PHP_CodeSniffer` requirement has been updated to `^3.7.1` (was ^3.3.1). [#115], [#130]
+* Composer: The package will now identify itself as a static analysis tool. Thanks [@GaryJones]! [#126]
+* All non-`abstract` classes in this package are now `final`. [#121]
+* All XML documentation now has a schema annotation. [#128]
+* Various housekeeping.
+
+### Fixed
+
+The upgrade to PHPCSUtils 1.0.0-alpha4 took care of a number of bugs, which potentially could have affected sniffs in this package.
+
+#### NormalizedArrays
+* `NormalizedArrays.Arrays.ArrayBraceSpacing`: the sniff now allows for trailing comments after the array opener in multi-line arrays. [#118]
+* `NormalizedArrays.Arrays.ArrayBraceSpacing`: trailing comments at the end of an array, but before the closer, in multi-line arrays will no longer confuse the sniff. [#135]
+* `NormalizedArrays.Arrays.CommaAfterLast`: the fixer will now recognize PHP 7.3+ flexible heredoc/nowdocs and in that case, will add the comma on the same line as the heredoc/nowdoc closer. [#144]
+
+#### Universal
+* `Universal.Arrays.DisallowShortArraySyntax`: nested short arrays in short lists will now be detected and fixed correctly. [#153]
+* `Universal.ControlStructures.DisallowAlternativeSyntax`: the sniff will no longer bow out indiscriminately when the `allowWithInlineHTML` property is set to `true`. [#90], [#161]
+* `Universal.ControlStructures.DisallowAlternativeSyntax`: when alternative control structure syntax is allowed in combination with inline HTML (`allowWithInlineHTML` property set to `true`), inline HTML in functions declared within the control structure body will no longer be taken into account for determining whether or not the control structure contains inline HTML. [#160]
+* `Universal.Lists.DisallowShortListSyntax`: the sniff will work around a tokenizer bug in PHPCS 3.7.1, which previously could lead to false negatives. [#151].
+* `Universal.Lists.DisallowShortListSyntax`: nested short lists in short arrays will now be detected and fixed correctly. [#152]
+* `Universal.Operators.DisallowStandalonePostIncrementDecrement`: the sniff will now correctly recognize stand-alone statements which end on a PHP close tag. [#176]
+
+[#72]: https://github.com/PHPCSStandards/PHPCSExtra/pull/72
+[#76]: https://github.com/PHPCSStandards/PHPCSExtra/pull/76
+[#80]: https://github.com/PHPCSStandards/PHPCSExtra/pull/80
+[#81]: https://github.com/PHPCSStandards/PHPCSExtra/pull/81
+[#85]: https://github.com/PHPCSStandards/PHPCSExtra/pull/85
+[#89]: https://github.com/PHPCSStandards/PHPCSExtra/pull/89
+[#90]: https://github.com/PHPCSStandards/PHPCSExtra/pull/90
+[#95]: https://github.com/PHPCSStandards/PHPCSExtra/pull/95
+[#101]: https://github.com/PHPCSStandards/PHPCSExtra/pull/101
+[#106]: https://github.com/PHPCSStandards/PHPCSExtra/pull/106
+[#107]: https://github.com/PHPCSStandards/PHPCSExtra/pull/107
+[#108]: https://github.com/PHPCSStandards/PHPCSExtra/pull/108
+[#109]: https://github.com/PHPCSStandards/PHPCSExtra/pull/109
+[#110]: https://github.com/PHPCSStandards/PHPCSExtra/pull/110
+[#114]: https://github.com/PHPCSStandards/PHPCSExtra/pull/114
+[#115]: https://github.com/PHPCSStandards/PHPCSExtra/pull/115
+[#116]: https://github.com/PHPCSStandards/PHPCSExtra/pull/116
+[#117]: https://github.com/PHPCSStandards/PHPCSExtra/pull/117
+[#118]: https://github.com/PHPCSStandards/PHPCSExtra/pull/118
+[#119]: https://github.com/PHPCSStandards/PHPCSExtra/pull/119
+[#120]: https://github.com/PHPCSStandards/PHPCSExtra/pull/120
+[#121]: https://github.com/PHPCSStandards/PHPCSExtra/pull/121
+[#122]: https://github.com/PHPCSStandards/PHPCSExtra/pull/122
+[#123]: https://github.com/PHPCSStandards/PHPCSExtra/pull/123
+[#124]: https://github.com/PHPCSStandards/PHPCSExtra/pull/124
+[#126]: https://github.com/PHPCSStandards/PHPCSExtra/pull/126
+[#128]: https://github.com/PHPCSStandards/PHPCSExtra/pull/128
+[#130]: https://github.com/PHPCSStandards/PHPCSExtra/pull/130
+[#134]: https://github.com/PHPCSStandards/PHPCSExtra/pull/134
+[#137]: https://github.com/PHPCSStandards/PHPCSExtra/pull/137
+[#140]: https://github.com/PHPCSStandards/PHPCSExtra/pull/140
+[#142]: https://github.com/PHPCSStandards/PHPCSExtra/pull/142
+[#143]: https://github.com/PHPCSStandards/PHPCSExtra/pull/143
+[#144]: https://github.com/PHPCSStandards/PHPCSExtra/pull/144
+[#146]: https://github.com/PHPCSStandards/PHPCSExtra/pull/146
+[#147]: https://github.com/PHPCSStandards/PHPCSExtra/pull/147
+[#148]: https://github.com/PHPCSStandards/PHPCSExtra/pull/148
+[#149]: https://github.com/PHPCSStandards/PHPCSExtra/pull/149
+[#150]: https://github.com/PHPCSStandards/PHPCSExtra/pull/150
+[#151]: https://github.com/PHPCSStandards/PHPCSExtra/pull/151
+[#152]: https://github.com/PHPCSStandards/PHPCSExtra/pull/152
+[#153]: https://github.com/PHPCSStandards/PHPCSExtra/pull/153
+[#154]: https://github.com/PHPCSStandards/PHPCSExtra/pull/154
+[#155]: https://github.com/PHPCSStandards/PHPCSExtra/pull/155
+[#158]: https://github.com/PHPCSStandards/PHPCSExtra/pull/158
+[#159]: https://github.com/PHPCSStandards/PHPCSExtra/pull/159
+[#160]: https://github.com/PHPCSStandards/PHPCSExtra/pull/160
+[#161]: https://github.com/PHPCSStandards/PHPCSExtra/pull/161
+[#162]: https://github.com/PHPCSStandards/PHPCSExtra/pull/162
+[#163]: https://github.com/PHPCSStandards/PHPCSExtra/pull/163
+[#164]: https://github.com/PHPCSStandards/PHPCSExtra/pull/164
+[#165]: https://github.com/PHPCSStandards/PHPCSExtra/pull/165
+[#166]: https://github.com/PHPCSStandards/PHPCSExtra/pull/166
+[#167]: https://github.com/PHPCSStandards/PHPCSExtra/pull/167
+[#168]: https://github.com/PHPCSStandards/PHPCSExtra/pull/168
+[#169]: https://github.com/PHPCSStandards/PHPCSExtra/pull/169
+[#170]: https://github.com/PHPCSStandards/PHPCSExtra/pull/170
+[#171]: https://github.com/PHPCSStandards/PHPCSExtra/pull/171
+[#172]: https://github.com/PHPCSStandards/PHPCSExtra/pull/172
+[#173]: https://github.com/PHPCSStandards/PHPCSExtra/pull/173
+[#175]: https://github.com/PHPCSStandards/PHPCSExtra/pull/175
+[#176]: https://github.com/PHPCSStandards/PHPCSExtra/pull/176
+[#177]: https://github.com/PHPCSStandards/PHPCSExtra/pull/177
+[#178]: https://github.com/PHPCSStandards/PHPCSExtra/pull/178
+[#180]: https://github.com/PHPCSStandards/PHPCSExtra/pull/180
+
+[php-manual-dirname]: https://www.php.net/function.dirname
+[php-rfc-negative_array_index]: https://wiki.php.net/rfc/negative_array_index
+[php_version-config]: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Configuration-Options#setting-the-php-version
+[ESLint "no lonely if"]: https://eslint.org/docs/rules/no-lonely-if
+[PHPCSUtils 1.0.0-alpha4]: https://github.com/PHPCSStandards/PHPCSUtils/releases/tag/1.0.0-alpha4
+
+
## [1.0.0-alpha3] - 2020-06-29
### Added
#### Universal
-* :wrench: :books: New `Universal.Arrays.DisallowShortArraySyntax` sniff to disallow short array syntax. [#40](https://github.com/PHPCSStandards/PHPCSExtra/pull/40)
+* :wrench: :books: New `Universal.Arrays.DisallowShortArraySyntax` sniff to disallow short array syntax. [#40]
In contrast to the PHPCS native `Generic.Arrays.DisallowShortArraySyntax` sniff, this sniff will ignore short list syntax and not cause parse errors when the fixer is used.
-* :wrench: :bar_chart: :books: New `Universal.Constants.UppercaseMagicConstants` sniff to enforce that PHP native magic constants are in uppercase. [#64](https://github.com/PHPCSStandards/PHPCSExtra/pull/64)
-* :bar_chart: :books: New `Universal.Namespaces.DisallowDeclarationWithoutName` sniff to disallow namespace declarations without a namespace name. [#50](https://github.com/PHPCSStandards/PHPCSExtra/pull/50)
-* :bar_chart: :books: New `Universal.Operators.DisallowLogicalAndOr` sniff to enforce the use of the boolean `&&` and `||` operators instead of the logical `and`/`or` operators. [#52](https://github.com/PHPCSStandards/PHPCSExtra/pull/52)
- Note: as the [operator precedence](https://www.php.net/manual/en/language.operators.precedence.php) of the logical operators is significantly lower than the operator precedence of boolean operators, this sniff does not contain an auto-fixer.
-* :bar_chart: :books: New `Universal.Operators.DisallowShortTernary` sniff to disallow the use of short ternaries `?:`. [#42](https://github.com/PHPCSStandards/PHPCSExtra/pull/42)
+* :wrench: :bar_chart: :books: New `Universal.Constants.UppercaseMagicConstants` sniff to enforce that PHP native magic constants are in uppercase. [#64]
+* :bar_chart: :books: New `Universal.Namespaces.DisallowDeclarationWithoutName` sniff to disallow namespace declarations without a namespace name. [#50]
+* :bar_chart: :books: New `Universal.Operators.DisallowLogicalAndOr` sniff to enforce the use of the boolean `&&` and `||` operators instead of the logical `and`/`or` operators. [#52]
+ Note: as the [operator precedence] of the logical operators is significantly lower than the operator precedence of boolean operators, this sniff does not contain an auto-fixer.
+* :bar_chart: :books: New `Universal.Operators.DisallowShortTernary` sniff to disallow the use of short ternaries `?:`. [#42]
While short ternaries are useful when used correctly, the principle of them is often misunderstood and they are more often than not used incorrectly, leading to hard to debug issues and/or PHP warnings/notices.
-* :wrench: :bar_chart: :books: New `Universal.Operators.DisallowStandalonePostIncrementDecrement` sniff disallow the use of post-in/decrements in stand-alone statements and discourage the use of multiple increment/decrement operators in a stand-alone statement. [#65](https://github.com/PHPCSStandards/PHPCSExtra/pull/65)
-* :wrench: :bar_chart: :books: New `Universal.Operators.StrictComparisons` sniff to enforce the use of strict comparisons. [#48](https://github.com/PHPCSStandards/PHPCSExtra/pull/48)
+* :wrench: :bar_chart: :books: New `Universal.Operators.DisallowStandalonePostIncrementDecrement` sniff disallow the use of post-in/decrements in stand-alone statements and discourage the use of multiple increment/decrement operators in a stand-alone statement. [#65]
+* :wrench: :bar_chart: :books: New `Universal.Operators.StrictComparisons` sniff to enforce the use of strict comparisons. [#48]
Warning: the auto-fixer for this sniff _may_ cause bugs in applications and should be used with care! This is considered a _risky_ fixer.
-* :wrench: :bar_chart: :books: New `Universal.OOStructures.AlphabeticExtendsImplements` sniff to verify that the names used in a class "implements" statement or an interface "extends" statement are listed in alphabetic order. [#55](https://github.com/PHPCSStandards/PHPCSExtra/pull/55)
+* :wrench: :bar_chart: :books: New `Universal.OOStructures.AlphabeticExtendsImplements` sniff to verify that the names used in a class "implements" statement or an interface "extends" statement are listed in alphabetic order. [#55]
* This sniff contains a public `orderby` property to determine the sort order to use for the statement.
If all names used are unqualified, the sort order won't make a difference.
However, if one or more of the names are partially or fully qualified, the chosen sort order will determine how the sorting between unqualified, partially and fully qualified names is handled.
@@ -47,38 +214,53 @@ _Nothing yet._
* When fixing, the existing spacing between the names in an `implements`/`extends` statement will not be maintained.
The fixer will separate each name with a comma and one space.
If alternative formatting is desired, a sniff which will check and fix the formatting should be added to the ruleset.
-* :wrench: :bar_chart: :books: New `Universal.UseStatements.LowercaseFunctionConst` sniff to enforce that `function` and `const` keywords when used in an import `use` statement are always lowercase. [#58](https://github.com/PHPCSStandards/PHPCSExtra/pull/58)
-* :wrench: :bar_chart: :books: New `Universal.UseStatements.NoLeadingBackslash` sniff to verify that a name being imported in an import `use` statement does not start with a leading backslash. [#46](https://github.com/PHPCSStandards/PHPCSExtra/pull/46)
+* :wrench: :bar_chart: :books: New `Universal.UseStatements.LowercaseFunctionConst` sniff to enforce that `function` and `const` keywords when used in an import `use` statement are always lowercase. [#58]
+* :wrench: :bar_chart: :books: New `Universal.UseStatements.NoLeadingBackslash` sniff to verify that a name being imported in an import `use` statement does not start with a leading backslash. [#46]
Names in import `use` statements should always be fully qualified, so a leading backslash is not needed and it is strongly recommended not to use one.
This sniff handles all types of import use statements supported by PHP, in contrast to other sniffs for the same in, for instance, the PSR12 or the Slevomat standard, which are incomplete.
-* :wrench: :books: New `Universal.WhiteSpace.DisallowInlineTabs` sniff to enforce using spaces for mid-line alignment. [#43](https://github.com/PHPCSStandards/PHPCSExtra/pull/43)
+* :wrench: :books: New `Universal.WhiteSpace.DisallowInlineTabs` sniff to enforce using spaces for mid-line alignment. [#43]
### Changed
#### Other
* The `master` branch has been renamed to `stable`.
-* Composer: The version requirements for the [DealerDirect Composer PHPCS plugin] have been widened to allow for version 0.7.0 which supports Composer 2.0.0. [#62](https://github.com/PHPCSStandards/PHPCSExtra/pull/62)
+* Composer: The version requirements for the [Composer PHPCS plugin] have been widened to allow for version 0.7.0 which supports Composer 2.0.0. [#62]
* Various housekeeping.
+[#40]: https://github.com/PHPCSStandards/PHPCSExtra/pull/40
+[#42]: https://github.com/PHPCSStandards/PHPCSExtra/pull/42
+[#43]: https://github.com/PHPCSStandards/PHPCSExtra/pull/43
+[#46]: https://github.com/PHPCSStandards/PHPCSExtra/pull/46
+[#48]: https://github.com/PHPCSStandards/PHPCSExtra/pull/48
+[#50]: https://github.com/PHPCSStandards/PHPCSExtra/pull/50
+[#52]: https://github.com/PHPCSStandards/PHPCSExtra/pull/52
+[#55]: https://github.com/PHPCSStandards/PHPCSExtra/pull/55
+[#58]: https://github.com/PHPCSStandards/PHPCSExtra/pull/58
+[#62]: https://github.com/PHPCSStandards/PHPCSExtra/pull/62
+[#64]: https://github.com/PHPCSStandards/PHPCSExtra/pull/64
+[#65]: https://github.com/PHPCSStandards/PHPCSExtra/pull/65
+
+[operator precedence]: https://www.php.net/manual/en/language.operators.precedence.php
+
## [1.0.0-alpha2] - 2020-02-18
### Added
#### Universal
-* :wrench: :bar_chart: :books: New `Universal.ControlStructures.DisallowAlternativeSyntax` sniff to disallow using the alternative syntax for control structures. [#23](https://github.com/PHPCSStandards/PHPCSExtra/pull/23)
+* :wrench: :bar_chart: :books: New `Universal.ControlStructures.DisallowAlternativeSyntax` sniff to disallow using the alternative syntax for control structures. [#23]
- This sniff contains a `allowWithInlineHTML` property to allow alternative syntax when inline HTML is used within the control structure. In all other cases, the use of the alternative syntax will still be disallowed.
- The sniff has modular error codes to allow for making exceptions based on specific control structures and/or specific control structures in combination with inline HTML.
-* :bar_chart: `Universal.UseStatements.DisallowUseClass/Function/Const`: new, additional metrics about the import source will be shown in the `info` report. [#25](https://github.com/PHPCSStandards/PHPCSExtra/pull/25)
+* :bar_chart: `Universal.UseStatements.DisallowUseClass/Function/Const`: new, additional metrics about the import source will be shown in the `info` report. [#25]
#### Other
-* Readme: installation instructions and sniff list. [#26](https://github.com/PHPCSStandards/PHPCSExtra/pull/26)
+* Readme: installation instructions and sniff list. [#26]
### Changed
#### Universal
-* `Universal.Arrays.DuplicateArrayKey`: wording of the error message. [#18](https://github.com/PHPCSStandards/PHPCSExtra/pull/18)
-* `Universal.UseStatements.DisallowUseClass/Function/Const`: the error codes have been made more modular. [#25](https://github.com/PHPCSStandards/PHPCSExtra/pull/25)
+* `Universal.Arrays.DuplicateArrayKey`: wording of the error message. [#18]
+* `Universal.UseStatements.DisallowUseClass/Function/Const`: the error codes have been made more modular. [#25]
Each of these sniffs now has four additional error codes:
FoundSameNamespace
, FoundSameNamespaceWithAlias
for use
statements importing from the same namespace;
@@ -87,17 +269,25 @@ _Nothing yet._
In all other circumstances, the existing error codes FoundWithAlias
and FoundWithoutAlias
will continue to be used.
#### Other
-* Improved formatting of the CLI documentation which can be viewed using `--generator=text`. [#17](https://github.com/PHPCSStandards/PHPCSExtra/pull/17)
+* Improved formatting of the CLI documentation which can be viewed using `--generator=text`. [#17]
* Various housekeeping.
### Fixed
#### Universal
-* `Universal.Arrays.DuplicateArrayKey`: improved handling of parse errors. [#34](https://github.com/PHPCSStandards/PHPCSExtra/pull/34)
-* `Universal.ControlStructures.IfElseDeclaration`: the fixer will now respect tab indentation. [#19](https://github.com/PHPCSStandards/PHPCSExtra/pull/19)
-* `Universal.UseStatements.DisallowUseClass/Function/Const`: the determination of whether a import is aliased in now done in a case-insensitive manner. [#25](https://github.com/PHPCSStandards/PHPCSExtra/pull/25)
-* `Universal.UseStatements.DisallowUseClass/Function/Const`: an import from the global namespace would previously always be seen as non-aliased, even when it was aliased. [#25](https://github.com/PHPCSStandards/PHPCSExtra/pull/25)
-* `Universal.UseStatements.DisallowUseClass/Function/Const`: improved tolerance for `use` import statements with leading backslashes. [#25](https://github.com/PHPCSStandards/PHPCSExtra/pull/25)
+* `Universal.Arrays.DuplicateArrayKey`: improved handling of parse errors. [#34]
+* `Universal.ControlStructures.IfElseDeclaration`: the fixer will now respect tab indentation. [#19]
+* `Universal.UseStatements.DisallowUseClass/Function/Const`: the determination of whether a import is aliased in now done in a case-insensitive manner. [#25]
+* `Universal.UseStatements.DisallowUseClass/Function/Const`: an import from the global namespace would previously always be seen as non-aliased, even when it was aliased. [#25]
+* `Universal.UseStatements.DisallowUseClass/Function/Const`: improved tolerance for `use` import statements with leading backslashes. [#25]
+
+[#17]: https://github.com/PHPCSStandards/PHPCSExtra/pull/17
+[#18]: https://github.com/PHPCSStandards/PHPCSExtra/pull/18
+[#19]: https://github.com/PHPCSStandards/PHPCSExtra/pull/19
+[#23]: https://github.com/PHPCSStandards/PHPCSExtra/pull/23
+[#25]: https://github.com/PHPCSStandards/PHPCSExtra/pull/25
+[#26]: https://github.com/PHPCSStandards/PHPCSExtra/pull/26
+[#34]: https://github.com/PHPCSStandards/PHPCSExtra/pull/34
## 1.0.0-alpha1 - 2020-01-23
@@ -149,7 +339,12 @@ This initial alpha release contains the following sniffs:
* :bar_chart: :books: `Universal.UseStatements.DisallowUseFunction`: forbid using import use statements for functions.
Individual sub-types can be allowed by excluding specific error codes.
+[Composer PHPCS plugin]: https://github.com/PHPCSStandards/composer-installer
[Unreleased]: https://github.com/PHPCSStandards/PHPCSExtra/compare/stable...HEAD
+[1.0.0-RC1]: https://github.com/PHPCSStandards/PHPCSExtra/compare/1.0.0-alpha3...1.0.0-rc1
[1.0.0-alpha3]: https://github.com/PHPCSStandards/PHPCSExtra/compare/1.0.0-alpha2...1.0.0-alpha3
[1.0.0-alpha2]: https://github.com/PHPCSStandards/PHPCSExtra/compare/1.0.0-alpha1...1.0.0-alpha2
+
+[@derickr]: https://github.com/derickr
+[@GaryJones]: https://github.com/GaryJones
diff --git a/Modernize/Docs/FunctionCalls/DirnameStandard.xml b/Modernize/Docs/FunctionCalls/DirnameStandard.xml
new file mode 100644
index 00000000..5f486646
--- /dev/null
+++ b/Modernize/Docs/FunctionCalls/DirnameStandard.xml
@@ -0,0 +1,40 @@
+
+
+
+ = 5.3: Usage of dirname(__FILE__) can be replaced with __DIR__.
+ ]]>
+
+
+
+ __DIR__;
+ ]]>
+
+
+ dirname(__FILE__);
+ ]]>
+
+
+
+ = 7.0: Nested calls to dirname() can be replaced by using dirname() with the $levels parameter.
+ ]]>
+
+
+
+ dirname($file, 3);
+ ]]>
+
+
+ dirname(dirname(dirname($file)));
+ ]]>
+
+
+
diff --git a/Modernize/Sniffs/FunctionCalls/DirnameSniff.php b/Modernize/Sniffs/FunctionCalls/DirnameSniff.php
new file mode 100644
index 00000000..ab944a60
--- /dev/null
+++ b/Modernize/Sniffs/FunctionCalls/DirnameSniff.php
@@ -0,0 +1,339 @@
+getTokens();
+
+ if (\strtolower($tokens[$stackPtr]['content']) !== 'dirname') {
+ // Not our target.
+ return;
+ }
+
+ $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
+ if ($nextNonEmpty === false
+ || $tokens[$nextNonEmpty]['code'] !== \T_OPEN_PARENTHESIS
+ || isset($tokens[$nextNonEmpty]['parenthesis_owner']) === true
+ ) {
+ // Not our target.
+ return;
+ }
+
+ if (isset($tokens[$nextNonEmpty]['parenthesis_closer']) === false) {
+ // Live coding or parse error, ignore.
+ return;
+ }
+
+ // Check if it is really a function call to the global function.
+ $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
+
+ if (isset(Collections::objectOperators()[$tokens[$prevNonEmpty]['code']]) === true
+ || $tokens[$prevNonEmpty]['code'] === \T_NEW
+ ) {
+ // Method call, class instantiation or other "not our target".
+ return;
+ }
+
+ if ($tokens[$prevNonEmpty]['code'] === \T_NS_SEPARATOR) {
+ $prevPrevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevNonEmpty - 1), null, true);
+ if ($tokens[$prevPrevToken]['code'] === \T_STRING
+ || $tokens[$prevPrevToken]['code'] === \T_NAMESPACE
+ ) {
+ // Namespaced function.
+ return;
+ }
+ }
+
+ /*
+ * As of here, we can be pretty sure this is a function call to the global function.
+ */
+ $opener = $nextNonEmpty;
+ $closer = $tokens[$nextNonEmpty]['parenthesis_closer'];
+
+ $parameters = PassedParameters::getParameters($phpcsFile, $stackPtr);
+ $paramCount = \count($parameters);
+ if (empty($parameters) || $paramCount > 2) {
+ // No parameters or too many parameter.
+ return;
+ }
+
+ $pathParam = PassedParameters::getParameterFromStack($parameters, 1, 'path');
+ if ($pathParam === false) {
+ // If the path parameter doesn't exist, there's nothing to do.
+ return;
+ }
+
+ $levelsParam = PassedParameters::getParameterFromStack($parameters, 2, 'levels');
+ if ($levelsParam === false && $paramCount === 2) {
+ // There must be a typo in the param name or an otherwise stray parameter. Ignore.
+ return;
+ }
+
+ /*
+ * PHP 5.3+: Detect use of dirname(__FILE__).
+ */
+ if ($pathParam['clean'] === '__FILE__') {
+ // Determine if the issue is auto-fixable.
+ $hasComment = $phpcsFile->findNext(Tokens::$commentTokens, ($opener + 1), $closer);
+ $fixable = ($hasComment === false);
+
+ if ($fixable === true) {
+ $levelsValue = $this->getLevelsValue($phpcsFile, $levelsParam);
+ if ($levelsParam !== false && $levelsValue === false) {
+ // Can't autofix if we don't know the value of the $levels parameter.
+ $fixable = false;
+ }
+ }
+
+ $error = 'Use the __DIR__ constant instead of calling dirname(__FILE__) (PHP >= 5.3)';
+ $code = 'FileConstant';
+
+ // Throw the error.
+ if ($fixable === false) {
+ $phpcsFile->addError($error, $stackPtr, $code);
+ return;
+ }
+
+ $fix = $phpcsFile->addFixableError($error, $stackPtr, $code);
+ if ($fix === true) {
+ if ($levelsParam === false || $levelsValue === 1) {
+ // No $levels or $levels set to 1: we can replace the complete function call.
+ $phpcsFile->fixer->beginChangeset();
+
+ $phpcsFile->fixer->replaceToken($stackPtr, '__DIR__');
+
+ for ($i = ($stackPtr + 1); $i <= $closer; $i++) {
+ $phpcsFile->fixer->replaceToken($i, '');
+ }
+
+ // Remove potential leading \.
+ if ($tokens[$prevNonEmpty]['code'] === \T_NS_SEPARATOR) {
+ $phpcsFile->fixer->replaceToken($prevNonEmpty, '');
+ }
+
+ $phpcsFile->fixer->endChangeset();
+ } else {
+ // We can replace the $path parameter and will need to adjust the $levels parameter.
+ $filePtr = $phpcsFile->findNext(\T_FILE, $pathParam['start'], ($pathParam['end'] + 1));
+ $levelsPtr = $phpcsFile->findNext(\T_LNUMBER, $levelsParam['start'], ($levelsParam['end'] + 1));
+
+ $phpcsFile->fixer->beginChangeset();
+ $phpcsFile->fixer->replaceToken($filePtr, '__DIR__');
+ $phpcsFile->fixer->replaceToken($levelsPtr, ($levelsValue - 1));
+ $phpcsFile->fixer->endChangeset();
+ }
+ }
+
+ return;
+ }
+
+ /*
+ * PHP 7.0+: Detect use of nested calls to dirname().
+ */
+ if (\preg_match('`^\s*\\\\?dirname\s*\(`i', $pathParam['clean']) !== 1) {
+ return;
+ }
+
+ /*
+ * Check if there is something _behind_ the nested dirname() call within the same parameter.
+ *
+ * Note: the findNext() calls are safe and will always match the dirname() function call
+ * as otherwise the above regex wouldn't have matched.
+ */
+ $innerDirnamePtr = $phpcsFile->findNext(\T_STRING, $pathParam['start'], ($pathParam['end'] + 1));
+ $innerOpener = $phpcsFile->findNext(\T_OPEN_PARENTHESIS, ($innerDirnamePtr + 1), ($pathParam['end'] + 1));
+ if (isset($tokens[$innerOpener]['parenthesis_closer']) === false) {
+ // Shouldn't be possible.
+ return; // @codeCoverageIgnore
+ }
+
+ $innerCloser = $tokens[$innerOpener]['parenthesis_closer'];
+ if ($innerCloser !== $pathParam['end']) {
+ $hasContentAfter = $phpcsFile->findNext(
+ Tokens::$emptyTokens,
+ ($innerCloser + 1),
+ ($pathParam['end'] + 1),
+ true
+ );
+ if ($hasContentAfter !== false) {
+ // Matched code like: `dirname(dirname($file) . 'something')`. Ignore.
+ return;
+ }
+ }
+
+ /*
+ * Determine if this is an auto-fixable error.
+ */
+
+ // Step 1: Are there comments ? If so, not auto-fixable as we don't want to remove comments.
+ $fixable = true;
+ for ($i = ($opener + 1); $i < $closer; $i++) {
+ if (isset(Tokens::$commentTokens[$tokens[$i]['code']])) {
+ $fixable = false;
+ break;
+ }
+
+ if ($tokens[$i]['code'] === \T_OPEN_PARENTHESIS
+ && isset($tokens[$i]['parenthesis_closer'])
+ ) {
+ // Skip over everything within the nested dirname() function call.
+ $i = $tokens[$i]['parenthesis_closer'];
+ }
+ }
+
+ // Step 2: Does the `$levels` parameter exist for the outer dirname() call and if so, is it usable ?
+ if ($fixable === true) {
+ $outerLevelsValue = $this->getLevelsValue($phpcsFile, $levelsParam);
+ if ($levelsParam !== false && $outerLevelsValue === false) {
+ // Can't autofix if we don't know the value of the $levels parameter.
+ $fixable = false;
+ }
+ }
+
+ // Step 3: Does the `$levels` parameter exist for the inner dirname() call and if so, is it usable ?
+ if ($fixable === true) {
+ $innerParameters = PassedParameters::getParameters($phpcsFile, $innerDirnamePtr);
+ $innerLevelsParam = PassedParameters::getParameterFromStack($innerParameters, 2, 'levels');
+ $innerLevelsValue = $this->getLevelsValue($phpcsFile, $innerLevelsParam);
+ if ($innerLevelsParam !== false && $innerLevelsValue === false) {
+ // Can't autofix if we don't know the value of the $levels parameter.
+ $fixable = false;
+ }
+ }
+
+ /*
+ * Throw the error.
+ */
+ $error = 'Pass the $levels parameter to the dirname() call instead of using nested dirname() calls';
+ $error .= ' (PHP >= 7.0)';
+ $code = 'Nested';
+
+ if ($fixable === false) {
+ $phpcsFile->addError($error, $stackPtr, $code);
+ return;
+ }
+
+ $fix = $phpcsFile->addFixableError($error, $stackPtr, $code);
+ if ($fix === false) {
+ return;
+ }
+
+ /*
+ * Fix the error.
+ */
+ $phpcsFile->fixer->beginChangeset();
+
+ // Remove the info in the _outer_ param call.
+ for ($i = $opener; $i < $innerOpener; $i++) {
+ $phpcsFile->fixer->replaceToken($i, '');
+ }
+
+ for ($i = ($innerCloser + 1); $i <= $closer; $i++) {
+ $phpcsFile->fixer->replaceToken($i, '');
+ }
+
+ if ($innerLevelsParam !== false) {
+ // Inner $levels parameter already exists, just adjust the value.
+ $innerLevelsPtr = $phpcsFile->findNext(
+ \T_LNUMBER,
+ $innerLevelsParam['start'],
+ ($innerLevelsParam['end'] + 1)
+ );
+ $phpcsFile->fixer->replaceToken($innerLevelsPtr, ($innerLevelsValue + $outerLevelsValue));
+ } else {
+ // Inner $levels parameter does not exist yet. We need to add it.
+ $content = ', ';
+
+ $prevBeforeCloser = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($innerCloser - 1), null, true);
+ if ($tokens[$prevBeforeCloser]['code'] === \T_COMMA) {
+ // Trailing comma found, no need to add the comma.
+ $content = ' ';
+ }
+
+ $innerPathParam = PassedParameters::getParameterFromStack($innerParameters, 1, 'path');
+ if (isset($innerPathParam['name_token']) === true) {
+ // Non-named param cannot follow named param, so add param name.
+ $content .= 'levels: ';
+ }
+
+ $content .= ($innerLevelsValue + $outerLevelsValue);
+ $phpcsFile->fixer->addContentBefore($innerCloser, $content);
+ }
+
+ $phpcsFile->fixer->endChangeset();
+ }
+
+ /**
+ * Determine the value of the $levels parameter passed to dirname().
+ *
+ * @since 1.0.0
+ *
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
+ * @param array|false $levelsParam The information about the parameter as retrieved
+ * via PassedParameters::getParameterFromStack().
+ *
+ * @return int|false Integer levels value or FALSE if the levels value couldn't be determined.
+ */
+ private function getLevelsValue($phpcsFile, $levelsParam)
+ {
+ if ($levelsParam === false) {
+ return 1;
+ }
+
+ $ignore = Tokens::$emptyTokens;
+ $ignore[] = \T_LNUMBER;
+
+ $hasNonNumber = $phpcsFile->findNext($ignore, $levelsParam['start'], ($levelsParam['end'] + 1), true);
+ if ($hasNonNumber !== false) {
+ return false;
+ }
+
+ return (int) $levelsParam['clean'];
+ }
+}
diff --git a/Modernize/Tests/FunctionCalls/DirnameUnitTest.inc b/Modernize/Tests/FunctionCalls/DirnameUnitTest.inc
new file mode 100644
index 00000000..30e67bb1
--- /dev/null
+++ b/Modernize/Tests/FunctionCalls/DirnameUnitTest.inc
@@ -0,0 +1,142 @@
+dirname(__FILE__);
+$path = $obj?->dirname(__FILE__);
+$path = Package\dirname(__FILE__);
+$path = \My\Package\dirname(__FILE__);
+echo Foo::DIRNAME;
+$obj = new dirname(__FILE__);
+
+echo DIRNAME . '/file.php';
+
+
+/*
+ * These should not be flagged.
+ */
+echo foo(__FILE__);
+$path = dirname();
+$path = dirname( $file );
+$path = dirname( __DIR__ );
+$path = dirname( '.' );
+$path = dirname( get_path(__FILE__) );
+$path = dirname(__FILE__ . '/..');
+
+$path = dirname( ABSPATH . '/path/to/file.php', 3 );
+$path = dirname( __DIR__, 3 );
+
+$path = dirname(levels: 2); // ArgumentCountError, require param dirname missing. Ignore as not the concern of this sniff.
+$path = dirname( __FILE__, 3, $extra ); // ArgumentCountError, too many args. Ignore as not the concern of this sniff.
+
+$path = dirname( paths: __FILE__ ); // Error: unknown named parameter. Ignore as not the concern of this sniff.
+$path = dirname( __FILE__, level: 2); // Error: unknown named parameter. Ignore as not the concern of this sniff.
+
+$path = dirname(\dirname(__DIR__) . '/file.php'); // Nested dirname() call not stand-alone. Silly code, but should not be flagged.
+
+
+/*
+ * These should be flagged for use of dirname(__FILE__).
+ */
+$path = dirname(__FILE__);
+$path = \dirname ( __FILE__ , ) ; // Includes trailing comma.
+
+$path = DirName(__FILE__ /* todo: replace with __DIR__ */); // Not auto-fixable.
+
+// Handling of multi-line function calls.
+$path = dirname(
+ __FILE__
+);
+
+// Uses __FILE__, but also has $levels parameter.
+$path = dirname(__FILE__, 1);
+$path = dirname(__FILE__, 2);
+$path = dirname(__FILE__, 03); // Octal 3.
+
+$path = dirname(__FILE__, $levels); // Not auto-fixable.
+$path = dirname(__FILE__, get_levels(2)); // Not auto-fixable.
+
+// With named parameters.
+$path = dirname(path: __FILE__, levels: 1);
+$path = dirname(levels: 3, path: __FILE__);
+
+// phpcs:ignore Modernize.FunctionCalls.Dirname.Nested -- Only apply __DIR__ fix, not $levels.
+$path = dirname(dirname(dirname(dirname(__FILE__))));
+
+
+/*
+ * These should be flagged for use of nested dirname().
+ */
+$path = dirname(dirname(dirname(dirname(__DIR__))));
+$path = dirname(dirname(DIRNAME(dirname(__DIR__, 2,)))); // Includes trailing comma.
+
+$path = dirname(\dirname($file,), 2); // Includes trailing comma in inner dirname() call.
+$path = dirname(dirname(dirname($file), 2), 2);
+
+// Handling of multi-line function calls.
+$path = dirname(
+ dirname(
+ __DIR__,
+ 2
+ ),
+ 2
+);
+
+$path = dirname(
+ \dirname(__DIR__, 1), // Comment within the outer dirname() scope.
+ 2
+); // Not auto-fixable.
+
+$path = dirname(
+ dirname(
+ __DIR__, // Comment within the inner dirname() scope.
+ 3
+ ),
+ 2
+); // Auto-fixable.
+
+$path = dirname(dirname(__DIR__, $levels), 2); // Not auto-fixable.
+$path = dirname(dirname(__DIR__, 2), get_levels()); // Not auto-fixable.
+
+$path = dirname(dirname(dirname($file, 2), 2), FOO::LEVELS); // Partially auto-fixable.
+
+// With named parameters.
+$path = dirname(levels: 2, path: \dirname(levels: 2, path: DIRNAME(path: dirname(path: __DIR__), levels: 2)));
+$path = dirname(levels: 1, path: dirname(levels: 3, path: \DIRNAME(levels: 1, path: dirname(levels: 2, path: __DIR__,)))); // Includes trailing comma.
+
+
+/*
+ * These should be flagged for both.
+ */
+$path = dirname(dirname(dirname(dirname(__FILE__))));
+$path = dirname(DirName(dirname(dirname(__FILE__, 2))));
+
+// With named parameters.
+$path = dirname(path: DirName(path: dirname(path: dirname(path: __FILE__))));
+
+// With named parameters, multi-line function call, trailing comma, non-lowercase function call.
+$path = dirname(
+ path: DirName(
+ levels: 2,
+ path: dirname(
+ path: dirname(
+ path: __FILE__
+ ),
+ levels: 3,
+ )
+ )
+);
+
+
+// Parse error.
+// This must be the last test in the file.
+echo dirname(__FILE__
diff --git a/Modernize/Tests/FunctionCalls/DirnameUnitTest.inc.fixed b/Modernize/Tests/FunctionCalls/DirnameUnitTest.inc.fixed
new file mode 100644
index 00000000..f2511799
--- /dev/null
+++ b/Modernize/Tests/FunctionCalls/DirnameUnitTest.inc.fixed
@@ -0,0 +1,126 @@
+dirname(__FILE__);
+$path = $obj?->dirname(__FILE__);
+$path = Package\dirname(__FILE__);
+$path = \My\Package\dirname(__FILE__);
+echo Foo::DIRNAME;
+$obj = new dirname(__FILE__);
+
+echo DIRNAME . '/file.php';
+
+
+/*
+ * These should not be flagged.
+ */
+echo foo(__FILE__);
+$path = dirname();
+$path = dirname( $file );
+$path = dirname( __DIR__ );
+$path = dirname( '.' );
+$path = dirname( get_path(__FILE__) );
+$path = dirname(__FILE__ . '/..');
+
+$path = dirname( ABSPATH . '/path/to/file.php', 3 );
+$path = dirname( __DIR__, 3 );
+
+$path = dirname(levels: 2); // ArgumentCountError, require param dirname missing. Ignore as not the concern of this sniff.
+$path = dirname( __FILE__, 3, $extra ); // ArgumentCountError, too many args. Ignore as not the concern of this sniff.
+
+$path = dirname( paths: __FILE__ ); // Error: unknown named parameter. Ignore as not the concern of this sniff.
+$path = dirname( __FILE__, level: 2); // Error: unknown named parameter. Ignore as not the concern of this sniff.
+
+$path = dirname(\dirname(__DIR__) . '/file.php'); // Nested dirname() call not stand-alone. Silly code, but should not be flagged.
+
+
+/*
+ * These should be flagged for use of dirname(__FILE__).
+ */
+$path = __DIR__;
+$path = __DIR__ ; // Includes trailing comma.
+
+$path = DirName(__FILE__ /* todo: replace with __DIR__ */); // Not auto-fixable.
+
+// Handling of multi-line function calls.
+$path = __DIR__;
+
+// Uses __FILE__, but also has $levels parameter.
+$path = __DIR__;
+$path = dirname(__DIR__, 1);
+$path = dirname(__DIR__, 2); // Octal 3.
+
+$path = dirname(__FILE__, $levels); // Not auto-fixable.
+$path = dirname(__FILE__, get_levels(2)); // Not auto-fixable.
+
+// With named parameters.
+$path = __DIR__;
+$path = dirname(levels: 2, path: __DIR__);
+
+// phpcs:ignore Modernize.FunctionCalls.Dirname.Nested -- Only apply __DIR__ fix, not $levels.
+$path = dirname(dirname(dirname(__DIR__)));
+
+
+/*
+ * These should be flagged for use of nested dirname().
+ */
+$path = dirname(__DIR__, 4);
+$path = dirname(__DIR__, 5,); // Includes trailing comma.
+
+$path = dirname($file, 3); // Includes trailing comma in inner dirname() call.
+$path = dirname($file, 5);
+
+// Handling of multi-line function calls.
+$path = dirname(
+ __DIR__,
+ 4
+ );
+
+$path = dirname(
+ \dirname(__DIR__, 1), // Comment within the outer dirname() scope.
+ 2
+); // Not auto-fixable.
+
+$path = dirname(
+ __DIR__, // Comment within the inner dirname() scope.
+ 5
+ ); // Auto-fixable.
+
+$path = dirname(dirname(__DIR__, $levels), 2); // Not auto-fixable.
+$path = dirname(dirname(__DIR__, 2), get_levels()); // Not auto-fixable.
+
+$path = dirname(dirname($file, 4), FOO::LEVELS); // Partially auto-fixable.
+
+// With named parameters.
+$path = dirname(path: __DIR__, levels: 7);
+$path = dirname(levels: 7, path: __DIR__,); // Includes trailing comma.
+
+
+/*
+ * These should be flagged for both.
+ */
+$path = dirname(__DIR__, 3);
+$path = dirname(__DIR__, 4);
+
+// With named parameters.
+$path = dirname(path: __DIR__, levels: 3);
+
+// With named parameters, multi-line function call, trailing comma, non-lowercase function call.
+$path = dirname(
+ path: __DIR__
+ , levels: 6);
+
+
+// Parse error.
+// This must be the last test in the file.
+echo dirname(__FILE__
diff --git a/Modernize/Tests/FunctionCalls/DirnameUnitTest.php b/Modernize/Tests/FunctionCalls/DirnameUnitTest.php
new file mode 100644
index 00000000..eaa73dba
--- /dev/null
+++ b/Modernize/Tests/FunctionCalls/DirnameUnitTest.php
@@ -0,0 +1,82 @@
+
+ */
+ public function getErrorList()
+ {
+ return [
+ 50 => 1,
+ 51 => 1,
+ 53 => 1,
+ 56 => 1,
+ 61 => 1,
+ 62 => 1,
+ 63 => 1,
+ 65 => 1,
+ 66 => 1,
+ 69 => 1,
+ 70 => 1,
+ 73 => 1,
+ 79 => 3,
+ 80 => 3,
+ 82 => 1,
+ 83 => 2,
+ 86 => 1,
+ 94 => 1,
+ 99 => 1,
+ 107 => 1,
+ 108 => 1,
+ 110 => 2,
+ 113 => 3,
+ 114 => 3,
+ 120 => 4,
+ 121 => 4,
+ 124 => 4,
+ 127 => 1,
+ 128 => 1,
+ 130 => 1,
+ 131 => 1,
+ ];
+ }
+
+ /**
+ * 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 [];
+ }
+}
diff --git a/Modernize/ruleset.xml b/Modernize/ruleset.xml
new file mode 100644
index 00000000..bbfc2ea8
--- /dev/null
+++ b/Modernize/ruleset.xml
@@ -0,0 +1,5 @@
+
+
+
+ A collection of sniffs to detect code modernization opportunties.
+
diff --git a/NormalizedArrays/Docs/Arrays/ArrayBraceSpacingStandard.xml b/NormalizedArrays/Docs/Arrays/ArrayBraceSpacingStandard.xml
index 28ae1bad..e8a006f0 100644
--- a/NormalizedArrays/Docs/Arrays/ArrayBraceSpacingStandard.xml
+++ b/NormalizedArrays/Docs/Arrays/ArrayBraceSpacingStandard.xml
@@ -1,4 +1,8 @@
-
+
+
+
+
no comma after the last array item.
diff --git a/NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php b/NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php
index bc0f0c5e..d09a336a 100644
--- a/NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php
+++ b/NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php
@@ -12,7 +12,9 @@
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
+use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\Fixers\SpacesFixer;
+use PHPCSUtils\Tokens\Collections;
use PHPCSUtils\Utils\Arrays;
/**
@@ -30,7 +32,7 @@
*
* @since 1.0.0
*/
-class ArrayBraceSpacingSniff implements Sniff
+final class ArrayBraceSpacingSniff implements Sniff
{
/**
@@ -104,11 +106,7 @@ class ArrayBraceSpacingSniff implements Sniff
*/
public function register()
{
- return [
- \T_ARRAY,
- \T_OPEN_SHORT_ARRAY,
- \T_OPEN_SQUARE_BRACKET,
- ];
+ return Collections::arrayOpenTokensBC();
}
/**
@@ -156,7 +154,7 @@ public function process(File $phpcsFile, $stackPtr)
$openClose = Arrays::getOpenClose($phpcsFile, $stackPtr);
if ($openClose === false) {
- // Short list or real square brackets.
+ // Live coding, short list or real square brackets.
return;
}
@@ -264,10 +262,23 @@ public function process(File $phpcsFile, $stackPtr)
$error = 'Expected %s after the array opener in a multi line array. Found: %s';
$code = 'SpaceAfterArrayOpenerMultiLine';
+ $nextNonWhitespace = $phpcsFile->findNext(\T_WHITESPACE, ($opener + 1), null, true);
+ if ($this->spacesMultiLine === 'newline') {
+ // Check for a trailing comment after the array opener and allow for it.
+ if (($tokens[$nextNonWhitespace]['code'] === \T_COMMENT
+ || isset(Tokens::$phpcsCommentTokens[$tokens[$nextNonWhitespace]['code']]) === true)
+ && $tokens[$nextNonWhitespace]['line'] === $tokens[$opener]['line']
+ ) {
+ // We found a trailing comment after array opener. Treat that as the opener instead.
+ $opener = $nextNonWhitespace;
+ $nextNonWhitespace = $phpcsFile->findNext(\T_WHITESPACE, ($opener + 1), null, true);
+ }
+ }
+
SpacesFixer::checkAndFix(
$phpcsFile,
$opener,
- $phpcsFile->findNext(\T_WHITESPACE, ($opener + 1), null, true),
+ $nextNonWhitespace,
$this->spacesMultiLine,
$error,
$code,
diff --git a/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php b/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php
index 2adf4a1f..9697ce8a 100644
--- a/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php
+++ b/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php
@@ -13,6 +13,7 @@
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
+use PHPCSUtils\Tokens\Collections;
use PHPCSUtils\Utils\Arrays;
/**
@@ -22,9 +23,18 @@
*
* @since 1.0.0
*/
-class CommaAfterLastSniff implements Sniff
+final class CommaAfterLastSniff implements Sniff
{
+ /**
+ * Name of the metric.
+ *
+ * @since 1.0.0
+ *
+ * @var string
+ */
+ const METRIC_NAME = '%s array - comma after last item';
+
/**
* Whether or not to enforce a comma after the last array item in a single-line array.
*
@@ -81,11 +91,7 @@ class CommaAfterLastSniff implements Sniff
*/
public function register()
{
- return [
- \T_ARRAY,
- \T_OPEN_SHORT_ARRAY,
- \T_OPEN_SQUARE_BRACKET,
- ];
+ return Collections::arrayOpenTokensBC();
}
/**
@@ -143,7 +149,7 @@ public function process(File $phpcsFile, $stackPtr)
$phpcsFile->recordMetric(
$stackPtr,
- \ucfirst($phrase) . ' array - comma after last item',
+ \sprintf(self::METRIC_NAME, \ucfirst($phrase)),
($isComma === true ? 'yes' : 'no')
);
@@ -160,8 +166,10 @@ public function process(File $phpcsFile, $stackPtr)
if ($fix === true) {
$extraContent = ',';
- if ($tokens[$lastNonEmpty]['code'] === \T_END_HEREDOC
- || $tokens[$lastNonEmpty]['code'] === \T_END_NOWDOC
+ if (($tokens[$lastNonEmpty]['code'] === \T_END_HEREDOC
+ || $tokens[$lastNonEmpty]['code'] === \T_END_NOWDOC)
+ // Check for indentation, if indented, it's a PHP 7.3+ heredoc/nowdoc.
+ && $tokens[$lastNonEmpty]['content'] === \ltrim($tokens[$lastNonEmpty]['content'])
) {
// Prevent parse errors in PHP < 7.3 which doesn't support flexible heredoc/nowdoc.
$extraContent = $phpcsFile->eolChar . $extraContent;
@@ -197,6 +205,7 @@ public function process(File $phpcsFile, $stackPtr)
}
$phpcsFile->fixer->beginChangeset();
+
for ($i = $start; $i <= $end; $i++) {
$phpcsFile->fixer->replaceToken($i, '');
}
diff --git a/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc b/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc
index 61a3dd1b..60cb8e93 100644
--- a/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc
+++ b/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc
@@ -134,12 +134,58 @@ $array = [
$a,
- $b]; // Error x 2.
+ $b]; // Error x 1.
$array = array($a,
$b
); // Error x 1.
+$array = array(
+ // Comment at the first line is always fine.
+ $a,
+ $b
+);
+
+$array = array( // Allow trailing comment after opener.
+ $a,
+ $b
+);
+
+$array = array( /* Allow trailing comment after opener. */
+ $a,
+ $b
+);
+
+$array = array( // phpcs:ignore Stdn.Cat.Sniff -- Allow trailing annotation after opener.
+ $a,
+ $b
+);
+
+$array = array( /* Allow trailing comment after opener. */ /* But don't allow a second one */
+ $a,
+ $b
+); // Error x 1.
+
+$array = array( /* This is still an error */ $a,
+ $b
+); // Error x 1.
+
+$array = [ // Allow trailing comment after opener.
+
+
+
+ $a,
+ $b
+];
+
+$array = [ /* Allow trailing comment after opener */
+
+
+
+ $a,
+ $b
+];
+
// phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesMultiLine 0
$array = [$a,
@@ -163,6 +209,30 @@ $array = array($a,
$b
); // Error x 1.
+$array = array( // Comment. Error x 1.
+ $a,
+ $b);
+
+$array = array( /* Comment. Error x 1. */
+ $a,
+ $b);
+
+$array = array( // phpcs:ignore Stdn.Cat.Sniff -- Error x 1.
+ $a,
+ $b);
+
+$array = array(// Comment. No space, this is fine.
+ $a,
+ $b);
+
+$array = array(/* Comment. No space, this is fine. */
+ $a,
+ $b);
+
+$array = array(// phpcs:ignore Stdn.Cat.Sniff -- No space, this is fine.
+ $a,
+ $b);
+
// phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesMultiLine 1
$array = [ $a,
@@ -186,6 +256,30 @@ $array = array($a,
$b
); // Error x 2.
+$array = array( // Comment. One space, this is fine.
+ $a,
+ $b );
+
+$array = array( /* Comment. One space, this is fine. */
+ $a,
+ $b );
+
+$array = array( // phpcs:ignore Stdn.Cat.Sniff -- One space, this is fine.
+ $a,
+ $b );
+
+$array = array(// Comment. No space, error x 1.
+ $a,
+ $b );
+
+$array = array(/* Comment. No space, error x 1. */
+ $a,
+ $b );
+
+$array = array(// phpcs:ignore Stdn.Cat.Sniff -- No space, error x 1.
+ $a,
+ $b );
+
// phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesMultiLine false
$array = array(
@@ -222,3 +316,8 @@ $array = array($a,
// phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesWhenEmpty 0
// phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesSingleLine 0
// phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesMultiLine newline
+
+$foo = array(
+ 'key' => 'value', // Comment.
+ // Comment.
+);
diff --git a/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc.fixed b/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc.fixed
index 583ac9f1..54a2d7fd 100644
--- a/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc.fixed
+++ b/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc.fixed
@@ -135,13 +135,61 @@ $array = [
$a,
$b
-]; // Error x 2.
+]; // Error x 1.
$array = array(
$a,
$b
); // Error x 1.
+$array = array(
+ // Comment at the first line is always fine.
+ $a,
+ $b
+);
+
+$array = array( // Allow trailing comment after opener.
+ $a,
+ $b
+);
+
+$array = array( /* Allow trailing comment after opener. */
+ $a,
+ $b
+);
+
+$array = array( // phpcs:ignore Stdn.Cat.Sniff -- Allow trailing annotation after opener.
+ $a,
+ $b
+);
+
+$array = array( /* Allow trailing comment after opener. */
+/* But don't allow a second one */
+ $a,
+ $b
+); // Error x 1.
+
+$array = array( /* This is still an error */
+$a,
+ $b
+); // Error x 1.
+
+$array = [ // Allow trailing comment after opener.
+
+
+
+ $a,
+ $b
+];
+
+$array = [ /* Allow trailing comment after opener */
+
+
+
+ $a,
+ $b
+];
+
// phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesMultiLine 0
$array = [$a,
@@ -158,6 +206,30 @@ $array = [$a,
$array = array($a,
$b); // Error x 1.
+$array = array(// Comment. Error x 1.
+ $a,
+ $b);
+
+$array = array(/* Comment. Error x 1. */
+ $a,
+ $b);
+
+$array = array(// phpcs:ignore Stdn.Cat.Sniff -- Error x 1.
+ $a,
+ $b);
+
+$array = array(// Comment. No space, this is fine.
+ $a,
+ $b);
+
+$array = array(/* Comment. No space, this is fine. */
+ $a,
+ $b);
+
+$array = array(// phpcs:ignore Stdn.Cat.Sniff -- No space, this is fine.
+ $a,
+ $b);
+
// phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesMultiLine 1
$array = [ $a,
@@ -174,6 +246,30 @@ $array = [ $a,
$array = array( $a,
$b ); // Error x 2.
+$array = array( // Comment. One space, this is fine.
+ $a,
+ $b );
+
+$array = array( /* Comment. One space, this is fine. */
+ $a,
+ $b );
+
+$array = array( // phpcs:ignore Stdn.Cat.Sniff -- One space, this is fine.
+ $a,
+ $b );
+
+$array = array( // Comment. No space, error x 1.
+ $a,
+ $b );
+
+$array = array( /* Comment. No space, error x 1. */
+ $a,
+ $b );
+
+$array = array( // phpcs:ignore Stdn.Cat.Sniff -- No space, error x 1.
+ $a,
+ $b );
+
// phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesMultiLine false
$array = array(
@@ -210,3 +306,8 @@ $array = array($a,
// phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesWhenEmpty 0
// phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesSingleLine 0
// phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesMultiLine newline
+
+$foo = array(
+ 'key' => 'value', // Comment.
+ // Comment.
+);
diff --git a/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.php b/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.php
index 5f325807..14d3713d 100644
--- a/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.php
+++ b/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.php
@@ -19,7 +19,7 @@
*
* @since 1.0.0
*/
-class ArrayBraceSpacingUnitTest extends AbstractSniffUnitTest
+final class ArrayBraceSpacingUnitTest extends AbstractSniffUnitTest
{
/**
@@ -58,16 +58,24 @@ public function getErrorList()
130 => 1,
137 => 1,
139 => 1,
- 150 => 1,
- 153 => 1,
- 155 => 1,
164 => 1,
- 173 => 1,
- 176 => 1,
- 178 => 1,
- 183 => 1,
- 185 => 1,
- 187 => 1,
+ 169 => 1,
+ 196 => 1,
+ 199 => 1,
+ 201 => 1,
+ 210 => 1,
+ 212 => 1,
+ 216 => 1,
+ 220 => 1,
+ 243 => 1,
+ 246 => 1,
+ 248 => 1,
+ 253 => 1,
+ 255 => 1,
+ 257 => 1,
+ 271 => 1,
+ 275 => 1,
+ 279 => 1,
];
}
diff --git a/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.inc b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.1.inc
similarity index 100%
rename from NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.inc
rename to NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.1.inc
diff --git a/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.inc.fixed b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.1.inc.fixed
similarity index 100%
rename from NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.inc.fixed
rename to NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.1.inc.fixed
diff --git a/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.2.inc b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.2.inc
new file mode 100644
index 00000000..76439f5e
--- /dev/null
+++ b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.2.inc
@@ -0,0 +1,44 @@
+ << <<<"EOD"
+ Some content
+ EOD,
+];
+
+$array = [
+ 'nowdoc' => <<<'EOD'
+ Some content
+ EOD,
+];
+
+// Missing comma.
+$array = [
+ 'heredoc' => << <<<"EOD"
+ Some content
+ EOD
+];
+
+$array = [
+ 'nowdoc' => <<<'EOD'
+ Some content
+ EOD
+];
diff --git a/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.2.inc.fixed b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.2.inc.fixed
new file mode 100644
index 00000000..dd7af9a3
--- /dev/null
+++ b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.2.inc.fixed
@@ -0,0 +1,44 @@
+ << <<<"EOD"
+ Some content
+ EOD,
+];
+
+$array = [
+ 'nowdoc' => <<<'EOD'
+ Some content
+ EOD,
+];
+
+// Missing comma.
+$array = [
+ 'heredoc' => << <<<"EOD"
+ Some content
+ EOD,
+];
+
+$array = [
+ 'nowdoc' => <<<'EOD'
+ Some content
+ EOD,
+];
diff --git a/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.php b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.php
index 31d34634..b528729f 100644
--- a/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.php
+++ b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.php
@@ -19,36 +19,56 @@
*
* @since 1.0.0
*/
-class CommaAfterLastUnitTest extends AbstractSniffUnitTest
+final class CommaAfterLastUnitTest extends AbstractSniffUnitTest
{
/**
* Returns the lines where errors should occur.
*
+ * @param string $testFile The name of the file being tested.
+ *
* @return array =>
*/
- public function getErrorList()
+ public function getErrorList($testFile = '')
{
- return [
- 52 => 1,
- 53 => 1,
- 75 => 1,
- 79 => 1,
- 87 => 1,
- 89 => 1,
- 91 => 1,
- 96 => 1,
- 103 => 1,
- 114 => 1,
- 115 => 1,
- 136 => 1,
- 140 => 1,
- 148 => 1,
- 150 => 1,
- 152 => 1,
- 159 => 1,
- 166 => 1,
- ];
+ switch ($testFile) {
+ case 'CommaAfterLastUnitTest.1.inc':
+ return [
+ 52 => 1,
+ 53 => 1,
+ 75 => 1,
+ 79 => 1,
+ 87 => 1,
+ 89 => 1,
+ 91 => 1,
+ 96 => 1,
+ 103 => 1,
+ 114 => 1,
+ 115 => 1,
+ 136 => 1,
+ 140 => 1,
+ 148 => 1,
+ 150 => 1,
+ 152 => 1,
+ 159 => 1,
+ 166 => 1,
+ ];
+
+ case 'CommaAfterLastUnitTest.2.inc':
+ // This test will only work for PHP 7.3+.
+ if (\PHP_VERSION_ID < 70300) {
+ return [];
+ }
+
+ return [
+ 31 => 1,
+ 37 => 1,
+ 43 => 1,
+ ];
+
+ default:
+ return [];
+ }
}
/**
diff --git a/README.md b/README.md
index e3f5a395..34f5a2b8 100644
--- a/README.md
+++ b/README.md
@@ -3,16 +3,16 @@ PHPCSExtra
-[![Latest Stable Version](https://poser.pugx.org/phpcsstandards/phpcsextra/v/stable)](https://packagist.org/packages/phpcsstandards/phpcsextra)
-[![Travis Build Status](https://travis-ci.com/PHPCSStandards/PHPCSExtra.svg?branch=stable)](https://travis-ci.com/PHPCSStandards/PHPCSExtra/branches)
+[![Latest Stable Version](https://poser.pugx.org/phpcsstandards/phpcsextra/v/stable)][phpcsextra-packagist]
[![Release Date of the Latest Version](https://img.shields.io/github/release-date/PHPCSStandards/PHPCSExtra.svg?maxAge=1800)](https://github.com/PHPCSStandards/PHPCSExtra/releases)
:construction:
[![Latest Unstable Version](https://img.shields.io/badge/unstable-dev--develop-e68718.svg?maxAge=2419200)](https://packagist.org/packages/phpcsstandards/phpcsextra#dev-develop)
-[![Travis Build Status](https://travis-ci.com/PHPCSStandards/PHPCSExtra.svg?branch=develop)](https://travis-ci.com/PHPCSStandards/PHPCSExtra/branches)
[![Last Commit to Unstable](https://img.shields.io/github/last-commit/PHPCSStandards/PHPCSExtra/develop.svg)](https://github.com/PHPCSStandards/PHPCSExtra/commits/develop)
-[![Minimum PHP Version](https://img.shields.io/packagist/php-v/phpcsstandards/phpcsextra.svg?maxAge=3600)](https://packagist.org/packages/phpcsstandards/phpcsextra)
-[![Tested on PHP 5.4 to 7.4](https://img.shields.io/badge/tested%20on-PHP%205.4%20|%205.5%20|%205.6%20|%207.0%20|%207.1%20|%207.2%20|%207.3%20|%207.4-brightgreen.svg?maxAge=2419200)](https://travis-ci.com/PHPCSStandards/PHPCSExtra)
+[![Minimum PHP Version](https://img.shields.io/packagist/php-v/phpcsstandards/phpcsextra.svg?maxAge=3600)][phpcsextra-packagist]
+[![CS Build Status](https://github.com/PHPCSStandards/PHPCSExtra/actions/workflows/basics.yml/badge.svg?branch=develop)][gha-qa-results]
+[![Test Build Status](https://github.com/PHPCSStandards/PHPCSExtra/actions/workflows/test.yml/badge.svg?branch=develop)][gha-test-results]
+[![Tested on PHP 5.4 to 8.2](https://img.shields.io/badge/tested%20on-PHP%205.4%20|%205.5%20|%205.6%20|%207.0%20|%207.1%20|%207.2%20|%207.3%20|%207.4%20|%208.0%20|%208.1%20|%208.2-brightgreen.svg?maxAge=2419200)][gha-test-results]
[![Coverage Status](https://coveralls.io/repos/github/PHPCSStandards/PHPCSExtra/badge.svg)](https://coveralls.io/github/PHPCSStandards/PHPCSExtra)
[![License: LGPLv3](https://poser.pugx.org/phpcsstandards/phpcsextra/license)](https://github.com/PHPCSStandards/PHPCSExtra/blob/stable/LICENSE)
@@ -25,8 +25,10 @@ PHPCSExtra
* [Installation](#installation)
+ [Composer Project-based Installation](#composer-project-based-installation)
+ [Composer Global Installation](#composer-global-installation)
+ + [Updating to a newer version](#updating-to-a-newer-version)
* [Features](#features)
* [Sniffs](#sniffs)
+ + [Modernize](#modernize)
+ [NormalizedArrays](#normalizedarrays)
+ [Universal](#universal)
* [Contributing](#contributing)
@@ -36,15 +38,15 @@ PHPCSExtra
Introduction
-------------------------------------------
-PHPCSExtra is a collection of sniffs and standards for use with [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer).
+PHPCSExtra is a collection of sniffs and standards for use with [PHP_CodeSniffer][phpcs-gh].
Minimum Requirements
-------------------------------------------
* PHP 5.4 or higher.
-* [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) version **3.3.1** or higher.
-* [PHPCSUtils](https://github.com/PHPCSStandards/PHPCSUtils) version **1.0.0** or higher.
+* [PHP_CodeSniffer][phpcs-gh] version **3.7.1** or higher.
+* [PHPCSUtils][phpcsutils-gh] version **1.0.0** or higher.
Installation
@@ -52,12 +54,13 @@ Installation
Installing via Composer is highly recommended.
-[Composer](http://getcomposer.org/) will automatically install the project dependencies and register the rulesets from PHPCSExtra and other external standards with PHP_CodeSniffer using the [DealerDirect Composer PHPCS plugin](https://github.com/Dealerdirect/phpcodesniffer-composer-installer/).
+[Composer](http://getcomposer.org/) will automatically install the project dependencies and register the rulesets from PHPCSExtra and other external standards with PHP_CodeSniffer using the [Composer PHPCS plugin][composer-installer-gh].
### Composer Project-based Installation
Run the following from the root of your project:
```bash
+composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer require --dev phpcsstandards/phpcsextra:"^1.0"
```
@@ -65,15 +68,36 @@ composer require --dev phpcsstandards/phpcsextra:"^1.0"
Alternatively, you may want to install this standard globally:
```bash
+composer global config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer global require --dev phpcsstandards/phpcsextra:"^1.0"
```
+### Updating to a newer version
+
+If you installed PHPCSExtra using either of the above commands, you can update to a newer version as follows:
+```bash
+# Project local install
+composer update phpcsstandards/phpcsextra --with-dependencies
+
+# Global install
+composer global update phpcsstandards/phpcsextra --with-dependencies
+```
+
+> If your project includes `require[-dev]`s for the `squizlabs/php_codesniffer`, `phpcsstandards/phpcsutils` or
+> `dealerdirect/phpcodesniffer-composer-installer` packages in its `composer.json` file, you may need to use
+> `--with-all-dependencies` instead of `--with-dependencies`.
+>
+> :bulb: **Pro-tip**: Unless your project is a PHPCS standard which actually uses any of these packages directly,
+> it is recommended to remove these packages from your own `composer.json` file, in favour of letting PHPCSExtra
+> (and potential other external PHPCS standards you use), manage the version requirements for these packages.
+
Features
-------------------------------------------
-Once this project is installed, you will see two new rulesets in the list of installed standards when you run `phpcs -i`: `NormalizedArrays` and `Universal`.
+Once this project is installed, you will see three new rulesets in the list of installed standards when you run `vendor/bin/phpcs -i`: `Modernize`, `NormalizedArrays` and `Universal`.
+* The `Modernize` ruleset is a standard which checks code for modernization opportunaties.
* The `NormalizedArrays` ruleset is a standard to check the formatting of array declarations.
* The `Universal` ruleset is **NOT** a standard, but a sniff collection.
It should **NOT** be included in custom rulesets as a standard as it contains contradictory rules.
@@ -85,8 +109,22 @@ Sniffs
**Legend**:
* :wrench: = Includes auto-fixer.
+ _Use the `phpcbf` command to run the fixers._
* :bar_chart: = Includes metrics.
+ _Use `phpcs` with `--report=info` to see the metrics._
* :books: = Includes CLI documentation.
+ _Use `phpcs` with `--generator=Text` to see the documentation._
+
+
+### Modernize
+
+#### `Modernize.FunctionCalls.Dirname` :wrench: :books:
+
+This sniff will detect and auto-fix two typical code modernizations which can be made related to the `dirname()` function:
+1. Since PHP 5.3, calls to `dirname(__FILE__)` can be replaced by `__DIR__`.
+ Errorcode: `Modernize.FunctionCalls.Dirname.FileConstant`.
+2. Since PHP 7.0, nested function calls to `dirname()` can be changed to use the `$levels` parameter.
+ Errorcode: `Modernize.FunctionCalls.Dirname.Nested`.
### NormalizedArrays
@@ -121,9 +159,10 @@ Use any of the following values to change the properties: `enforce`, `forbid` or
The default for the `singleLine` property is `forbid`. The default for the `multiLine` property is `enforce`.
+
### Universal
-#### `Universal.Arrays.DisallowShortArraySyntax` :wrench: :books:
+#### `Universal.Arrays.DisallowShortArraySyntax` :wrench: :bar_chart: :books:
Disallow short array syntax.
@@ -133,14 +172,76 @@ In contrast to the PHPCS native `Generic.Arrays.DisallowShortArraySyntax` sniff,
Detects duplicate array keys in array declarations.
+The sniff will make a distinction between keys which will be duplicate in all PHP version and (numeric) keys which will only be a duplicate key in [PHP < 8.0 or PHP >= 8.0][php-rfc-negative_array_index].
+
+If a [`php_version` configuration option][php_version-config] has been passed to PHPCS using either `--config-set` or `--runtime-set`, it will be respected by the sniff and only report duplicate keys for the configured PHP version.
+
+[php-rfc-negative_array_index]: https://wiki.php.net/rfc/negative_array_index
+[php_version-config]: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Configuration-Options#setting-the-php-version
+
#### `Universal.Arrays.MixedArrayKeyTypes` :books:
-Best practice sniff: don't use a mix of integer and numeric keys for array items.
+Best practice sniff: don't use a mix of integer and string keys for array items.
#### `Universal.Arrays.MixedKeyedUnkeyedArray` :books:
Best practice sniff: don't use a mix of keyed and unkeyed array items.
+#### `Universal.Classes.DisallowAnonClassParentheses` :wrench: :bar_chart: :books:
+
+Disallow the use of parentheses when declaring an anonymous class without passing parameters.
+
+#### `Universal.Classes.RequireAnonClassParentheses` :wrench: :bar_chart: :books:
+
+Require the use of parentheses when declaring an anonymous class, whether parameters are passed or not.
+
+#### `Universal.Classes.DisallowFinalClass` :wrench: :bar_chart: :books:
+
+Disallow classes being declared `final`.
+
+#### `Universal.Classes.RequireFinalClass` :wrench: :bar_chart: :books:
+
+Require all non-`abstract` classes to be declared `final`.
+
+:warning: **Warning**: the auto-fixer for this sniff _may_ have unintended side-effects for applications and should be used with care!
+This is considered a **_risky_ fixer**.
+
+#### `Universal.Classes.ModifierKeywordOrder` :wrench: :bar_chart: :books:
+
+Require a consistent modifier keyword order for class declarations.
+
+* This sniff contains an `order` property to specify the preferred order.
+ Accepted values: (string) `'extendability readonly'`|`'readonly extendability'`. Defaults to `'extendability readonly'`.
+
+#### `Universal.CodeAnalysis.ConstructorDestructorReturn` :wrench: :books:
+
+* Disallows return type declarations on constructor/destructor methods - error code: `ReturnTypeFound`, auto-fixable.
+* Discourages constructor/destructor methods returning a value - error code: `ReturnValueFound`.
+
+#### `Universal.CodeAnalysis.ForeachUniqueAssignment` :wrench: :books:
+
+Detects `foreach` control structures which use the same variable for both the key as well as the value assignment as this will lead to unexpected - and most likely unintended - behaviour.
+
+Note: The fixer will maintain the existing behaviour of the code. This may not be the _intended_ behaviour.
+
+#### `Universal.CodeAnalysis.StaticInFinalClass` :wrench: :books:
+
+Detects using `static` instead of `self` in OO constructs which are `final`.
+
+* The sniff has modular error codes to allow for making exceptions based on the type of use for `static`.
+ The available error codes are: `ReturnType`, `InstanceOf`, `NewInstance`, `ScopeResolution`.
+
+#### `Universal.Constants.LowercaseClassResolutionKeyword` :wrench: :bar_chart: :books:
+
+Enforce that the `class` keyword when used for class name resolution, i.e. `::class`, is in lowercase.
+
+#### `Universal.Constants.ModifierKeywordOrder` :wrench: :bar_chart: :books:
+
+Require a consistent modifier keyword order for OO constant declarations.
+
+* This sniff contains an `order` property to specify the preferred order.
+ Accepted values: (string) `'final visibility'`|`'visibility final'`. Defaults to `'final visibility'`.
+
#### `Universal.Constants.UppercaseMagicConstants` :wrench: :bar_chart: :books:
Enforce uppercase when using PHP native magic constants, like `__FILE__` et al.
@@ -150,18 +251,37 @@ Enforce uppercase when using PHP native magic constants, like `__FILE__` et al.
Disallow using the alternative syntax for control structures.
* This sniff contains an `allowWithInlineHTML` property to allow alternative syntax when inline HTML is used within the control structure. In all other cases, the use of the alternative syntax will still be disallowed.
- Acceped values: (bool) `true`|`false`. Defaults to `false`.
+ Accepted values: (bool) `true`|`false`. Defaults to `false`.
* The sniff has modular error codes to allow for making exceptions based on specific control structures and/or specific control structures in combination with inline HTML.
The error codes follow the following pattern: `Found[ControlStructure][WithInlineHTML]`. Examples: `FoundIf`, `FoundSwitchWithInlineHTML`.
+#### `Universal.ControlStructures.DisallowLonelyIf` :wrench: :books:
+
+Disallow `if` statements as the only statement in an `else` block.
+
+Note: This sniff will not fix the indentation of the "inner" code.
+It is strongly recommended to run this sniff together with the `Generic.WhiteSpace.ScopeIndent` sniff to get the correct indentation.
+
#### `Universal.ControlStructures.IfElseDeclaration` :wrench: :bar_chart: :books:
Verify that else(if) statements with braces are on a new line.
-#### `Universal.Lists.DisallowLongListSyntax` :wrench: :bar_chart: :books:
+#### `Universal.Files.SeparateFunctionsFromOO` :bar_chart: :books:
+
+Enforce for a file to either declare (global/namespaced) functions or declare OO structures, but not both.
+
+* Nested function declarations, i.e. functions declared within a function/method will be disregarded for the purposes of this sniff.
+ The same goes for anonymous classes, closures and arrow functions.
+* Note: This sniff has no opinion on side effects. If you want to sniff for those, use the PHPCS native `PSR1.Files.SideEffects` sniff.
+* Also note: This sniff has no opinion on multiple OO structures being declared in one file.
+ If you want to sniff for that, use the PHPCS native `Generic.Files.OneObjectStructurePerFile` sniff.
+
+#### `Universal.Lists.DisallowLongListSyntax` :wrench: :books:
Disallow the use of long `list`s.
+> For metrics about the use of long lists vs short lists, please use the `Universal.Lists.DisallowShortListSyntax` sniff.
+
#### `Universal.Lists.DisallowShortListSyntax` :wrench: :bar_chart: :books:
Disallow the use of short lists.
@@ -184,9 +304,16 @@ Enforce the use of the alternative namespace syntax using curly braces.
Disallow the use of multiple namespaces within a file.
+#### `Universal.NamingConventions.NoReservedKeywordParameterNames` :books:
+
+Disallow function parameters using reserved keywords as names, as this can quickly become confusing when people use them in function calls using named parameters
+
+* The sniff has modular error codes to allow for making exceptions for specific keywords.
+ The error codes follow the following pattern: `[keyword]Found`.
+
#### `Universal.OOStructures.AlphabeticExtendsImplements` :wrench: :bar_chart: :books:
-Enforce that the names used in a class "implements" statement or an interface "extends" statement are listed in alphabetic order.
+Enforce that the names used in a class/enum "implements" statement or an interface "extends" statement are listed in alphabetic order.
* This sniff contains a `orderby` property to determine the sort order to use for the statement.
If all names used are unqualified, the sort order won't make a difference.
@@ -229,9 +356,19 @@ Enforce the use of strict comparisons.
:warning: **Warning**: the auto-fixer for this sniff _may_ cause bugs in applications and should be used with care!
This is considered a **_risky_ fixer**.
+#### `Universal.Operators.TypeSeparatorSpacing` :wrench: :bar_chart: :books:
+
+Enforce no spaces around the union type and intersection type operators.
+
+The available error codes are: `UnionTypeSpacesBefore`, `UnionTypeSpacesAfter`, `IntersectionTypeSpacesBefore`, `IntersectionTypeSpacesAfter`.
+
+#### `Universal.PHP.OneStatementInShortEchoTag` :wrench: :books:
+
+Disallow short open echo tags `=` containing more than one PHP statement.
+
#### `Universal.UseStatements.DisallowUseClass` :bar_chart: :books:
-Forbid using import `use` statements for classes/traits/interfaces.
+Forbid using import `use` statements for classes/traits/interfaces/enums.
Individual sub-types - with/without alias, global imports, imports from the same namespace - can be forbidden by including that specific error code and/or allowed including the whole sniff and excluding specific error codes.
@@ -263,6 +400,13 @@ Names in import `use` statements should always be fully qualified, so a leading
This sniff handles all types of import use statements supported by PHP, in contrast to other sniffs for the same in, for instance, the PHPCS native `PSR12` or the Slevomat standard, which are incomplete.
+#### `Universal.WhiteSpace.AnonClassKeywordSpacing` :wrench: :bar_chart: :books:
+
+Standardize the amount of spacing between the `class` keyword and the open parenthesis (if any) for anonymous class declarations.
+
+* This sniff contains an `spacing` property to set the amount of spaces the sniff should check for.
+ Accepted values: (int) number of spaces. Defaults to `0` (spaces).
+
#### `Universal.WhiteSpace.DisallowInlineTabs` :wrench: :books:
Enforce using spaces for mid-line alignment.
@@ -280,6 +424,41 @@ While tab versus space based indentation is a question of preference, for mid-li
> However, the sister-sniff `Generic.Whitespace.DisallowSpaceIndent` leaves mid-line tabs/spaces alone.
> This sniff fills that gap.
+#### `Universal.WhiteSpace.PrecisionAlignment` :wrench: :books:
+
+Enforce code indentation to always be a multiple of a tabstop, i.e. disallow precision alignment.
+
+Note:
+* This sniff does not concern itself with tabs versus spaces.
+ It is recommended to use the sniff in combination with the PHPCS native `Generic.WhiteSpace.DisallowTabIndent` or the `Generic.WhiteSpace.DisallowSpaceIndent` sniff.
+* When using this sniff with tab-based standards, please ensure that the `tab-width` is set and either don't set the `$indent` property or set it to the tab-width (or a multiple thereof).
+* The fixer works based on "best guess" and may not always result in the desired indentation. Combine this sniff with the `Generic.WhiteSpace.ScopeIndent` sniff for more precise indentation fixes.
+
+The behaviour of the sniff is customizable via the following properties:
+* `indent`: the indent used for the codebase.
+ Accepted values: (int|null) number of spaces. Defaults to `null`.
+ If this property is not set, the sniff will look to the `--tab-width` CLI value.
+ If that also isn't set, the default tab-width of `4` will be used.
+* `ignoreAlignmentBefore`: allows for providing a list of token names for which (preceding) precision alignment should be ignored.
+ Accepted values: (array
) token constant names. Defaults to an empty array.
+ Usage example:
+ ```xml
+
+
+
+
+
+
+
+
+
+
+
+ ```
+* `ignoreBlankLines`: whether or not potential trailing whitespace on otherwise blank lines should be examined or ignored.
+ It is recommended to only set this to `false` if the standard including this sniff does not include the `Squiz.WhiteSpace.SuperfluousWhitespace` sniff (which is included in most standards).
+ Accepted values: (bool)`true`|`false`. Defaults to `true`.
+
Contributing
-------
@@ -289,4 +468,13 @@ If unsure whether the changes you are proposing would be welcome, open an issue
License
-------
-This code is released under the GNU Lesser General Public License (LGPLv3). For more information, visit http://www.gnu.org/copyleft/lesser.html
+This code is released under the GNU Lesser General Public License (LGPLv3). For more information, visit
+
+
+[phpcsextra-packagist]: https://packagist.org/packages/phpcsstandards/phpcsextra
+[gha-qa-results]: https://github.com/PHPCSStandards/PHPCSExtra/actions/workflows/basics.yml
+[gha-test-results]: https://github.com/PHPCSStandards/PHPCSExtra/actions/workflows/test.yml
+
+[phpcs-gh]: https://github.com/squizlabs/PHP_CodeSniffer
+[phpcsutils-gh]: https://github.com/PHPCSStandards/PHPCSUtils
+[composer-installer-gh]: https://github.com/PHPCSStandards/composer-installer
diff --git a/Universal/Docs/Arrays/DisallowShortArraySyntaxStandard.xml b/Universal/Docs/Arrays/DisallowShortArraySyntaxStandard.xml
index e37839e8..4a84b536 100644
--- a/Universal/Docs/Arrays/DisallowShortArraySyntaxStandard.xml
+++ b/Universal/Docs/Arrays/DisallowShortArraySyntaxStandard.xml
@@ -1,4 +1,8 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {};
+$anon = new class($param) {};
+ ]]>
+
+
+ () {};
+ ]]>
+
+
+
diff --git a/Universal/Docs/Classes/DisallowFinalClassStandard.xml b/Universal/Docs/Classes/DisallowFinalClassStandard.xml
new file mode 100644
index 00000000..fd7ddb67
--- /dev/null
+++ b/Universal/Docs/Classes/DisallowFinalClassStandard.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ class Foo {}
+abstract class Bar implements MyInterface {}
+ ]]>
+
+
+ final class Foo {}
+final class Bar extends MyAbstract {}
+ ]]>
+
+
+
diff --git a/Universal/Docs/Classes/ModifierKeywordOrderStandard.xml b/Universal/Docs/Classes/ModifierKeywordOrderStandard.xml
new file mode 100644
index 00000000..a80ae0da
--- /dev/null
+++ b/Universal/Docs/Classes/ModifierKeywordOrderStandard.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+ final readonly class Foo {}
+abstract readonly class Bar {}
+ ]]>
+
+
+ readonly final class Foo {}
+readonly abstract class Bar {}
+ ]]>
+
+
+
diff --git a/Universal/Docs/Classes/RequireAnonClassParenthesesStandard.xml b/Universal/Docs/Classes/RequireAnonClassParenthesesStandard.xml
new file mode 100644
index 00000000..48fa148e
--- /dev/null
+++ b/Universal/Docs/Classes/RequireAnonClassParenthesesStandard.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+ () {};
+ ]]>
+
+
+ {};
+ ]]>
+
+
+
diff --git a/Universal/Docs/Classes/RequireFinalClassStandard.xml b/Universal/Docs/Classes/RequireFinalClassStandard.xml
new file mode 100644
index 00000000..107b8f41
--- /dev/null
+++ b/Universal/Docs/Classes/RequireFinalClassStandard.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ final class Foo {}
+final class Bar extends MyAbstract {}
+ ]]>
+
+
+ class Foo {}
+abstract class Bar implements MyInterface {}
+ ]]>
+
+
+
diff --git a/Universal/Docs/CodeAnalysis/ConstructorDestructorReturnStandard.xml b/Universal/Docs/CodeAnalysis/ConstructorDestructorReturnStandard.xml
new file mode 100644
index 00000000..b44b4777
--- /dev/null
+++ b/Universal/Docs/CodeAnalysis/ConstructorDestructorReturnStandard.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+ : int {}
+}
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+ $this;
+ }
+
+ public function __destruct() {
+ // Do something.
+ return false;
+ }
+}
+ ]]>
+
+
+
diff --git a/Universal/Docs/CodeAnalysis/ForeachUniqueAssignmentStandard.xml b/Universal/Docs/CodeAnalysis/ForeachUniqueAssignmentStandard.xml
new file mode 100644
index 00000000..6f9676ee
--- /dev/null
+++ b/Universal/Docs/CodeAnalysis/ForeachUniqueAssignmentStandard.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+ $v ) {}
+ ]]>
+
+
+ $k ) {}
+ ]]>
+
+
+
diff --git a/Universal/Docs/CodeAnalysis/StaticInFinalClassStandard.xml b/Universal/Docs/CodeAnalysis/StaticInFinalClassStandard.xml
new file mode 100644
index 00000000..8c5f3bd8
--- /dev/null
+++ b/Universal/Docs/CodeAnalysis/StaticInFinalClassStandard.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+ self
+ {
+ $var = self::functionCall();
+ $var = $obj instanceof self;
+ $var = new self;
+ }
+}
+ ]]>
+
+
+ static|false {
+ $var = static::$prop;
+ $var = $obj instanceof static;
+ $var = new static();
+ }
+};
+ ]]>
+
+
+
diff --git a/Universal/Docs/Constants/LowercaseClassResolutionKeywordStandard.xml b/Universal/Docs/Constants/LowercaseClassResolutionKeywordStandard.xml
new file mode 100644
index 00000000..7b61c8fa
--- /dev/null
+++ b/Universal/Docs/Constants/LowercaseClassResolutionKeywordStandard.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+ MyClass::CLASS;
+ ]]>
+
+
+
diff --git a/Universal/Docs/Constants/ModifierKeywordOrderStandard.xml b/Universal/Docs/Constants/ModifierKeywordOrderStandard.xml
new file mode 100644
index 00000000..f42483a0
--- /dev/null
+++ b/Universal/Docs/Constants/ModifierKeywordOrderStandard.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ final public const FOO = 'foo';
+}
+ ]]>
+
+
+ protected final const BAR = 'foo';
+}
+ ]]>
+
+
+
diff --git a/Universal/Docs/Constants/UppercaseMagicConstantsStandard.xml b/Universal/Docs/Constants/UppercaseMagicConstantsStandard.xml
index 9e737d4c..2a9583fa 100644
--- a/Universal/Docs/Constants/UppercaseMagicConstantsStandard.xml
+++ b/Universal/Docs/Constants/UppercaseMagicConstantsStandard.xml
@@ -1,5 +1,8 @@
-
+
-
+
+
+
+
+
+
+
+ elseif ($bar) {
+ // ...
+}
+
+if ($foo) {
+ // ...
+} else {
+ if ($bar) {
+ // ...
+ }
+
+ doSomethingElse();
+
+}
+
+ ]]>
+
+
+ if ($bar) {
+ // ...
+ } else {
+ // ...
+ }
+}
+ ]]>
+
+
+
diff --git a/Universal/Docs/ControlStructures/IfElseDeclarationStandard.xml b/Universal/Docs/ControlStructures/IfElseDeclarationStandard.xml
index fe3cb9a6..b1506f64 100644
--- a/Universal/Docs/ControlStructures/IfElseDeclarationStandard.xml
+++ b/Universal/Docs/ControlStructures/IfElseDeclarationStandard.xml
@@ -1,5 +1,8 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Universal/Docs/Lists/DisallowLongListSyntaxStandard.xml b/Universal/Docs/Lists/DisallowLongListSyntaxStandard.xml
index d1b0ea61..35475eab 100644
--- a/Universal/Docs/Lists/DisallowLongListSyntaxStandard.xml
+++ b/Universal/Docs/Lists/DisallowLongListSyntaxStandard.xml
@@ -1,4 +1,8 @@
-
+
+
+
+
-
+
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Universal/Docs/OOStructures/AlphabeticExtendsImplementsStandard.xml b/Universal/Docs/OOStructures/AlphabeticExtendsImplementsStandard.xml
index 35b83f3b..f22a7193 100644
--- a/Universal/Docs/OOStructures/AlphabeticExtendsImplementsStandard.xml
+++ b/Universal/Docs/OOStructures/AlphabeticExtendsImplementsStandard.xml
@@ -1,4 +1,8 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |string $paramA,
+ TypeA&TypeB $paramB
+): int|false {}
+ ]]>
+
+
+ | string $paramA,
+ TypeA & TypeB $paramB
+): int
+ |
+ false {}
+ ]]>
+
+
+
diff --git a/Universal/Docs/PHP/OneStatementInShortEchoTagStandard.xml b/Universal/Docs/PHP/OneStatementInShortEchoTagStandard.xml
new file mode 100644
index 00000000..103ca13e
--- /dev/null
+++ b/Universal/Docs/PHP/OneStatementInShortEchoTagStandard.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+ = $text ?>
+ ]]>
+
+
+ = $text; echo $moreText; ?>
+ ]]>
+
+
+
+
+
+echo $text;
+echo $moreText;
+?>
+ ]]>
+
+
+ = $text;
+echo $moreText;
+?>
+ ]]>
+
+
+
diff --git a/Universal/Docs/UseStatements/DisallowUseClassStandard.xml b/Universal/Docs/UseStatements/DisallowUseClassStandard.xml
index 00b69ba0..1699d9db 100644
--- a/Universal/Docs/UseStatements/DisallowUseClassStandard.xml
+++ b/Universal/Docs/UseStatements/DisallowUseClassStandard.xml
@@ -1,5 +1,8 @@
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+ class($param)
+{
+ public function __construct($p) {}
+};
+ ]]>
+
+
+ class ($param)
+{
+ public function __construct($p) {}
+};
+ ]]>
+
+
+
diff --git a/Universal/Docs/WhiteSpace/DisallowInlineTabsStandard.xml b/Universal/Docs/WhiteSpace/DisallowInlineTabsStandard.xml
index ac4e560c..b259ab40 100644
--- a/Universal/Docs/WhiteSpace/DisallowInlineTabsStandard.xml
+++ b/Universal/Docs/WhiteSpace/DisallowInlineTabsStandard.xml
@@ -1,4 +1,8 @@
-
+
+
+
+
+
+
+
+
+ [space][space][space][space]$foo = 'bar';
+
+[tab]$foo = 'bar';
+ ]]>
+
+
+ [space][space]$foo = 'bar';
+
+[tab][space]$foo = 'bar';
+ ]]>
+
+
+
diff --git a/Universal/Helpers/DummyTokenizer.php b/Universal/Helpers/DummyTokenizer.php
index a3710c65..29a58fe6 100644
--- a/Universal/Helpers/DummyTokenizer.php
+++ b/Universal/Helpers/DummyTokenizer.php
@@ -19,7 +19,7 @@
*
* @since 1.0.0
*/
-class DummyTokenizer extends Tokenizer
+final class DummyTokenizer extends Tokenizer
{
/**
diff --git a/Universal/Sniffs/Arrays/DisallowShortArraySyntaxSniff.php b/Universal/Sniffs/Arrays/DisallowShortArraySyntaxSniff.php
index da16cce7..c4e8987c 100644
--- a/Universal/Sniffs/Arrays/DisallowShortArraySyntaxSniff.php
+++ b/Universal/Sniffs/Arrays/DisallowShortArraySyntaxSniff.php
@@ -12,6 +12,7 @@
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
+use PHPCSUtils\Tokens\Collections;
use PHPCSUtils\Utils\Arrays;
/**
@@ -26,9 +27,16 @@
* @since 1.0.0 This sniff is loosely based on and inspired by the upstream
* `Generic.Arrays.DisallowShortArraySyntax` sniff.
*/
-class DisallowShortArraySyntaxSniff implements Sniff
+final class DisallowShortArraySyntaxSniff implements Sniff
{
+ /**
+ * The phrase to use for the metric recorded by this sniff.
+ *
+ * @var string
+ */
+ const METRIC_NAME = 'Short array syntax used';
+
/**
* Registers the tokens that this sniff wants to listen for.
*
@@ -38,7 +46,7 @@ class DisallowShortArraySyntaxSniff implements Sniff
*/
public function register()
{
- return [\T_OPEN_SHORT_ARRAY];
+ return Collections::arrayOpenTokensBC();
}
/**
@@ -56,11 +64,18 @@ public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
+ if ($tokens[$stackPtr]['code'] === \T_ARRAY) {
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'no');
+ return;
+ }
+
if (Arrays::isShortArray($phpcsFile, $stackPtr) === false) {
- // No need to examine nested subs of this short list.
- return $tokens[$stackPtr]['bracket_closer'];
+ // Square brackets, but not a short array. Probably short list or real square brackets.
+ return;
}
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes');
+
$error = 'Short array syntax is not allowed';
$fix = $phpcsFile->addFixableError($error, $stackPtr, 'Found');
diff --git a/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php b/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php
index 3c1c7d1d..92532e5c 100644
--- a/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php
+++ b/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php
@@ -13,6 +13,7 @@
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\AbstractSniffs\AbstractArrayDeclarationSniff;
+use PHPCSUtils\BackCompat\Helper;
/**
* Detect duplicate array keys in array declarations.
@@ -20,29 +21,61 @@
* This sniff will detect duplicate keys with high precision, though any array key
* set via a variable/constant/function call is excluded from the examination.
*
+ * The sniff will handle the change in how numeric array keys are set
+ * since PHP 8.0 and will flag keys which would be duplicates cross-version.
+ * {@link https://wiki.php.net/rfc/negative_array_index}
+ *
* @since 1.0.0
*/
-class DuplicateArrayKeySniff extends AbstractArrayDeclarationSniff
+final class DuplicateArrayKeySniff extends AbstractArrayDeclarationSniff
{
/**
- * Keep track of which array keys have been seen already.
+ * Keep track of which array keys have been seen already on PHP < 8.0.
+ *
+ * @since 1.0.0
+ *
+ * @var array
+ */
+ private $keysSeenLt8 = [];
+
+ /**
+ * Keep track of which array keys have been seen already on PHP >= 8.0.
*
* @since 1.0.0
*
* @var array
*/
- private $keysSeen = [];
+ private $keysSeenGt8 = [];
/**
* Keep track of the maximum seen integer key to know what the next value will be for
- * array items without a key.
+ * array items without a key on PHP < 8.0.
*
* @since 1.0.0
*
* @var int
*/
- private $currentMaxIntKey = -1;
+ private $currentMaxIntKeyLt8;
+
+ /**
+ * Keep track of the maximum seen integer key to know what the next value will be for
+ * array items without a key on PHP >= 8.0.
+ *
+ * @since 1.0.0
+ *
+ * @var int
+ */
+ private $currentMaxIntKeyGt8;
+
+ /**
+ * The current PHP version.
+ *
+ * @since 1.0.0
+ *
+ * @var int
+ */
+ private $phpVersion;
/**
* Process every part of the array declaration.
@@ -60,8 +93,17 @@ class DuplicateArrayKeySniff extends AbstractArrayDeclarationSniff
public function processArray(File $phpcsFile)
{
// Reset properties before processing this array.
- $this->keysSeen = [];
- $this->currentMaxIntKey = -1;
+ $this->keysSeenLt8 = [];
+ $this->keysSeenGt8 = [];
+
+ if (isset($this->phpVersion) === false) {
+ $phpVersion = Helper::getConfigData('php_version');
+ if ($phpVersion !== null) {
+ $this->phpVersion = (int) $phpVersion;
+ }
+ }
+
+ unset($this->currentMaxIntKeyLt8, $this->currentMaxIntKeyGt8);
parent::processArray($phpcsFile);
}
@@ -92,41 +134,122 @@ public function processKey(File $phpcsFile, $startPtr, $endPtr, $itemNr)
$integerKey = \is_int($key);
+ $errorMsg = 'Duplicate array key found. The value will be overwritten%s.'
+ . ' The %s array key "%s" was first seen on line %d';
+ $errorCode = 'Found';
+ $errors = [];
+ $baseData = [
+ ($integerKey === true) ? 'integer' : 'string',
+ $key,
+ ];
+
/*
- * Check if we've seen it before.
+ * Check if we've seen the key before.
*/
- if (isset($this->keysSeen[$key]) === true) {
- $firstSeen = $this->keysSeen[$key];
+ if ((isset($this->phpVersion) === false || $this->phpVersion < 80000)
+ && isset($this->keysSeenLt8[$key]) === true
+ ) {
+ $errors['phplt8'] = [
+ 'data_subset' => $baseData,
+ 'error_suffix' => '',
+ 'code_suffix' => '',
+ ];
+
+ if ($integerKey === true) {
+ $errors['phplt8']['error_suffix'] = ' when using PHP < 8.0';
+ $errors['phplt8']['code_suffix'] = 'ForPHPlt80';
+ }
+
+ $firstSeen = $this->keysSeenLt8[$key];
$firstNonEmptyFirstSeen = $phpcsFile->findNext(Tokens::$emptyTokens, $firstSeen['ptr'], null, true);
- $firstNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $startPtr, null, true);
- $data = [
- ($integerKey === true) ? 'integer' : 'string',
- $key,
- $this->tokens[$firstNonEmptyFirstSeen]['line'],
+ $errors['phplt8']['data_subset'][] = $this->tokens[$firstNonEmptyFirstSeen]['line'];
+ }
+
+ if ((isset($this->phpVersion) === false || $this->phpVersion >= 80000)
+ && isset($this->keysSeenGt8[$key]) === true
+ ) {
+ $errors['phpgt8'] = [
+ 'data_subset' => $baseData,
+ 'error_suffix' => '',
+ 'code_suffix' => '',
];
- $phpcsFile->addError(
- 'Duplicate array key found. The value will be overwritten.'
- . ' The %s array key "%s" was first seen on line %d',
- $firstNonEmpty,
- 'Found',
- $data
- );
+ if ($integerKey === true) {
+ $errors['phpgt8']['error_suffix'] = ' when using PHP >= 8.0';
+ $errors['phpgt8']['code_suffix'] = 'ForPHPgte80';
+ }
+
+ $firstSeen = $this->keysSeenGt8[$key];
+ $firstNonEmptyFirstSeen = $phpcsFile->findNext(Tokens::$emptyTokens, $firstSeen['ptr'], null, true);
+
+ $errors['phpgt8']['data_subset'][] = $this->tokens[$firstNonEmptyFirstSeen]['line'];
+ }
+
+ /*
+ * Throw the error(s).
+ *
+ * If no PHP version was passed, throw errors both for PHP < 8.0 and PHP >= 8.0.
+ * If a PHP version was set, only throw the error appropriate for the selected PHP version.
+ * If both errors would effectively be the same, only throw one.
+ */
+ if ($errors !== []) {
+ $firstNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $startPtr, null, true);
+
+ if (isset($errors['phplt8'], $errors['phpgt8'])
+ && $errors['phplt8']['data_subset'] === $errors['phpgt8']['data_subset']
+ ) {
+ // Only throw the error once if it would be the same for PHP < 8.0 and PHP >= 8.0.
+ $data = $errors['phplt8']['data_subset'];
+ \array_unshift($data, '');
+
+ $phpcsFile->addError($errorMsg, $firstNonEmpty, $errorCode, $data);
+ return;
+ }
+
+ if (isset($errors['phplt8'])) {
+ $code = $errorCode . $errors['phplt8']['code_suffix'];
+ $data = $errors['phplt8']['data_subset'];
+ \array_unshift($data, $errors['phplt8']['error_suffix']);
+
+ $phpcsFile->addError($errorMsg, $firstNonEmpty, $code, $data);
+ }
+
+ if (isset($errors['phpgt8'])) {
+ $code = $errorCode . $errors['phpgt8']['code_suffix'];
+ $data = $errors['phpgt8']['data_subset'];
+ \array_unshift($data, $errors['phpgt8']['error_suffix']);
+
+ $phpcsFile->addError($errorMsg, $firstNonEmpty, $code, $data);
+ }
return;
}
/*
- * Key not seen before. Add to array.
+ * Key not seen before. Add to arrays.
*/
- $this->keysSeen[$key] = [
+ $this->keysSeenLt8[$key] = [
'item' => $itemNr,
'ptr' => $startPtr,
];
+ $this->keysSeenGt8[$key] = [
+ 'item' => $itemNr,
+ 'ptr' => $startPtr,
+ ];
+
+ if ($integerKey === true) {
+ if ((isset($this->currentMaxIntKeyLt8) === false && $key > -1)
+ || (isset($this->currentMaxIntKeyLt8) === true && $key > $this->currentMaxIntKeyLt8)
+ ) {
+ $this->currentMaxIntKeyLt8 = $key;
+ }
- if ($integerKey === true && $key > $this->currentMaxIntKey) {
- $this->currentMaxIntKey = $key;
+ if (isset($this->currentMaxIntKeyGt8) === false
+ || $key > $this->currentMaxIntKeyGt8
+ ) {
+ $this->currentMaxIntKeyGt8 = $key;
+ }
}
}
@@ -146,8 +269,24 @@ public function processKey(File $phpcsFile, $startPtr, $endPtr, $itemNr)
*/
public function processNoKey(File $phpcsFile, $startPtr, $itemNr)
{
- ++$this->currentMaxIntKey;
- $this->keysSeen[$this->currentMaxIntKey] = [
+ // Track the key for PHP < 8.0.
+ if (isset($this->currentMaxIntKeyLt8) === false) {
+ $this->currentMaxIntKeyLt8 = -1;
+ }
+
+ ++$this->currentMaxIntKeyLt8;
+ $this->keysSeenLt8[$this->currentMaxIntKeyLt8] = [
+ 'item' => $itemNr,
+ 'ptr' => $startPtr,
+ ];
+
+ // Track the key for PHP 8.0+.
+ if (isset($this->currentMaxIntKeyGt8) === false) {
+ $this->currentMaxIntKeyGt8 = -1;
+ }
+
+ ++$this->currentMaxIntKeyGt8;
+ $this->keysSeenGt8[$this->currentMaxIntKeyGt8] = [
'item' => $itemNr,
'ptr' => $startPtr,
];
diff --git a/Universal/Sniffs/Arrays/MixedArrayKeyTypesSniff.php b/Universal/Sniffs/Arrays/MixedArrayKeyTypesSniff.php
index 516472ef..391844e4 100644
--- a/Universal/Sniffs/Arrays/MixedArrayKeyTypesSniff.php
+++ b/Universal/Sniffs/Arrays/MixedArrayKeyTypesSniff.php
@@ -19,7 +19,7 @@
*
* @since 1.0.0
*/
-class MixedArrayKeyTypesSniff extends AbstractArrayDeclarationSniff
+final class MixedArrayKeyTypesSniff extends AbstractArrayDeclarationSniff
{
/**
diff --git a/Universal/Sniffs/Arrays/MixedKeyedUnkeyedArraySniff.php b/Universal/Sniffs/Arrays/MixedKeyedUnkeyedArraySniff.php
index 236bb686..f8236583 100644
--- a/Universal/Sniffs/Arrays/MixedKeyedUnkeyedArraySniff.php
+++ b/Universal/Sniffs/Arrays/MixedKeyedUnkeyedArraySniff.php
@@ -19,7 +19,7 @@
*
* @since 1.0.0
*/
-class MixedKeyedUnkeyedArraySniff extends AbstractArrayDeclarationSniff
+final class MixedKeyedUnkeyedArraySniff extends AbstractArrayDeclarationSniff
{
/**
diff --git a/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php b/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php
new file mode 100644
index 00000000..7a3c01cf
--- /dev/null
+++ b/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php
@@ -0,0 +1,112 @@
+getTokens();
+ $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
+
+ // Note: no need to check for `false` as PHPCS won't retokenize `class` to `T_ANON_CLASS` in that case.
+ if ($tokens[$nextNonEmpty]['code'] !== \T_OPEN_PARENTHESIS) {
+ // No parentheses found.
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'no');
+ return;
+ }
+
+ if (isset($tokens[$nextNonEmpty]['parenthesis_closer']) === false) {
+ /*
+ * Incomplete set of parentheses. Ignore.
+ * Shouldn't be possible as PHPCS won't retokenize `class` to `T_ANON_CLASS` in that case.
+ */
+ // @codeCoverageIgnoreStart
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes');
+ return;
+ // @codeCoverageIgnoreEnd
+ }
+
+ $opener = $nextNonEmpty;
+ $closer = $tokens[$opener]['parenthesis_closer'];
+ $hasParams = $phpcsFile->findNext(Tokens::$emptyTokens, ($opener + 1), $closer, true);
+ if ($hasParams !== false) {
+ // There is something between the parentheses. Ignore.
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes, with parameter(s)');
+ return;
+ }
+
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes');
+
+ $fix = $phpcsFile->addFixableError(
+ 'Parenthesis not allowed when creating a new anonymous class without passing parameters',
+ $stackPtr,
+ 'Found'
+ );
+
+ if ($fix === true) {
+ $phpcsFile->fixer->beginChangeset();
+
+ for ($i = $opener; $i <= $closer; $i++) {
+ if (isset(Tokens::$commentTokens[$tokens[$i]['code']]) === true) {
+ continue;
+ }
+
+ $phpcsFile->fixer->replaceToken($i, '');
+ }
+
+ $phpcsFile->fixer->endChangeset();
+ }
+ }
+}
diff --git a/Universal/Sniffs/Classes/DisallowFinalClassSniff.php b/Universal/Sniffs/Classes/DisallowFinalClassSniff.php
new file mode 100644
index 00000000..8920958a
--- /dev/null
+++ b/Universal/Sniffs/Classes/DisallowFinalClassSniff.php
@@ -0,0 +1,116 @@
+recordMetric($stackPtr, self::METRIC_NAME, 'abstract');
+ return;
+ }
+
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'not abstract, not final');
+ return;
+ }
+
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'final');
+
+ $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
+ if ($nextNonEmpty === false) {
+ // Live coding or parse error.
+ return;
+ }
+
+ // No extra safeguards needed, we know the keyword will exist based on the check above.
+ $finalKeyword = $phpcsFile->findPrevious(\T_FINAL, ($stackPtr - 1));
+ $snippetEnd = $nextNonEmpty;
+ $classCloser = '';
+
+ $tokens = $phpcsFile->getTokens();
+ if (isset($tokens[$stackPtr]['scope_opener']) === true) {
+ $snippetEnd = $tokens[$stackPtr]['scope_opener'];
+ $classCloser = '}';
+ }
+
+ $snippet = GetTokensAsString::compact($phpcsFile, $finalKeyword, $snippetEnd, true);
+ $fix = $phpcsFile->addFixableError(
+ 'Declaring a class as final is not allowed. Found: %s%s',
+ $finalKeyword,
+ 'FinalClassFound',
+ [$snippet, $classCloser]
+ );
+
+ if ($fix === true) {
+ $phpcsFile->fixer->beginChangeset();
+ $phpcsFile->fixer->replaceToken($finalKeyword, '');
+
+ // Remove redundant whitespace.
+ for ($i = ($finalKeyword + 1); $i < $stackPtr; $i++) {
+ if ($tokens[$i]['code'] === \T_WHITESPACE) {
+ $phpcsFile->fixer->replaceToken($i, '');
+ continue;
+ }
+
+ break;
+ }
+
+ $phpcsFile->fixer->endChangeset();
+ }
+ }
+}
diff --git a/Universal/Sniffs/Classes/ModifierKeywordOrderSniff.php b/Universal/Sniffs/Classes/ModifierKeywordOrderSniff.php
new file mode 100644
index 00000000..f0d1f21c
--- /dev/null
+++ b/Universal/Sniffs/Classes/ModifierKeywordOrderSniff.php
@@ -0,0 +1,219 @@
+getTokens();
+ $valid = Collections::classModifierKeywords() + Tokens::$emptyTokens;
+ $classProp = [
+ 'abstract_token' => false,
+ 'final_token' => false,
+ 'readonly_token' => false,
+ ];
+
+ for ($i = ($stackPtr - 1); $i > 0; $i--) {
+ if (isset($valid[$tokens[$i]['code']]) === false) {
+ break;
+ }
+
+ switch ($tokens[$i]['code']) {
+ case \T_ABSTRACT:
+ $classProp['abstract_token'] = $i;
+ break;
+
+ case \T_FINAL:
+ $classProp['final_token'] = $i;
+ break;
+
+ case \T_READONLY:
+ $classProp['readonly_token'] = $i;
+ break;
+ }
+ }
+
+ if ($classProp['readonly_token'] === false
+ || ($classProp['final_token'] === false && $classProp['abstract_token'] === false)
+ ) {
+ /*
+ * Either no modifier keywords found at all; or only one type of modifier
+ * keyword (abstract/final or readonly) declared, but not both. No ordering needed.
+ */
+ return;
+ }
+
+ if ($classProp['final_token'] !== false && $classProp['abstract_token'] !== false) {
+ // Parse error. Ignore.
+ return;
+ }
+
+ $readonly = $classProp['readonly_token'];
+
+ if ($classProp['final_token'] !== false) {
+ $extendability = $classProp['final_token'];
+ } else {
+ $extendability = $classProp['abstract_token'];
+ }
+
+ if ($readonly < $extendability) {
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, self::READONLY_EXTEND);
+ } else {
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, self::EXTEND_READONLY);
+ }
+
+ $message = 'Class modifier keywords are not in the correct order. Expected: "%s", found: "%s"';
+
+ switch ($this->order) {
+ case self::READONLY_EXTEND:
+ if ($readonly < $extendability) {
+ // Order is correct. Nothing to do.
+ return;
+ }
+
+ $this->handleError($phpcsFile, $extendability, $readonly);
+ break;
+
+ case self::EXTEND_READONLY:
+ default:
+ if ($extendability < $readonly) {
+ // Order is correct. Nothing to do.
+ return;
+ }
+
+ $this->handleError($phpcsFile, $readonly, $extendability);
+ break;
+ }
+ }
+
+ /**
+ * Throw the error and potentially fix it.
+ *
+ * @since 1.0.0
+ *
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
+ * @param int $firstKeyword The position of the first keyword found.
+ * @param int $secondKeyword The position of the second keyword token.
+ *
+ * @return void
+ */
+ private function handleError(File $phpcsFile, $firstKeyword, $secondKeyword)
+ {
+ $tokens = $phpcsFile->getTokens();
+
+ $message = 'Class modifier keywords are not in the correct order. Expected: "%s", found: "%s"';
+ $data = [
+ $tokens[$secondKeyword]['content'] . ' ' . $tokens[$firstKeyword]['content'],
+ $tokens[$firstKeyword]['content'] . ' ' . $tokens[$secondKeyword]['content'],
+ ];
+
+ $fix = $phpcsFile->addFixableError($message, $firstKeyword, 'Incorrect', $data);
+
+ if ($fix === true) {
+ $phpcsFile->fixer->beginChangeset();
+
+ $phpcsFile->fixer->replaceToken($secondKeyword, '');
+
+ // Prevent leaving behind trailing whitespace.
+ $i = ($secondKeyword + 1);
+ while ($tokens[$i]['code'] === \T_WHITESPACE) {
+ $phpcsFile->fixer->replaceToken($i, '');
+ $i++;
+ }
+
+ // Use the original token content as the case used for keywords is not the concern of this sniff.
+ $phpcsFile->fixer->addContentBefore($firstKeyword, $tokens[$secondKeyword]['content'] . ' ');
+
+ $phpcsFile->fixer->endChangeset();
+ }
+ }
+}
diff --git a/Universal/Sniffs/Classes/RequireAnonClassParenthesesSniff.php b/Universal/Sniffs/Classes/RequireAnonClassParenthesesSniff.php
new file mode 100644
index 00000000..c35f9f99
--- /dev/null
+++ b/Universal/Sniffs/Classes/RequireAnonClassParenthesesSniff.php
@@ -0,0 +1,81 @@
+getTokens();
+ $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
+
+ // Note: no need to check for `false` as PHPCS won't retokenize `class` to `T_ANON_CLASS` in that case.
+ if ($tokens[$nextNonEmpty]['code'] === \T_OPEN_PARENTHESIS) {
+ // Parentheses found.
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes');
+ return;
+ }
+
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'no');
+
+ $fix = $phpcsFile->addFixableError(
+ 'Parenthesis required when creating a new anonymous class.',
+ $stackPtr,
+ 'Missing'
+ );
+
+ if ($fix === true) {
+ $phpcsFile->fixer->addContent($stackPtr, '()');
+ }
+ }
+}
diff --git a/Universal/Sniffs/Classes/RequireFinalClassSniff.php b/Universal/Sniffs/Classes/RequireFinalClassSniff.php
new file mode 100644
index 00000000..2c46c601
--- /dev/null
+++ b/Universal/Sniffs/Classes/RequireFinalClassSniff.php
@@ -0,0 +1,102 @@
+recordMetric($stackPtr, self::METRIC_NAME, 'final');
+ return;
+ }
+
+ if ($classProp['is_abstract'] === true) {
+ // Abstract classes can't be final.
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'abstract');
+ return;
+ }
+
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'not abstract, not final');
+
+ $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
+ if ($nextNonEmpty === false) {
+ // Live coding or parse error.
+ return;
+ }
+
+ $snippetEnd = $nextNonEmpty;
+ $classCloser = '';
+
+ $tokens = $phpcsFile->getTokens();
+ if (isset($tokens[$stackPtr]['scope_opener']) === true) {
+ $snippetEnd = $tokens[$stackPtr]['scope_opener'];
+ $classCloser = '}';
+ }
+
+ $snippet = GetTokensAsString::compact($phpcsFile, $stackPtr, $snippetEnd, true);
+ $fix = $phpcsFile->addFixableError(
+ 'A non-abstract class should be declared as final. Found: %s%s',
+ $stackPtr,
+ 'NonFinalClassFound',
+ [$snippet, $classCloser]
+ );
+
+ if ($fix === true) {
+ $phpcsFile->fixer->addContentBefore($stackPtr, 'final ');
+ }
+ }
+}
diff --git a/Universal/Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php b/Universal/Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php
new file mode 100644
index 00000000..afbf59a9
--- /dev/null
+++ b/Universal/Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php
@@ -0,0 +1,161 @@
+getTokens();
+ $properties = FunctionDeclarations::getProperties($phpcsFile, $stackPtr);
+ if ($properties['return_type'] !== '' && $properties['return_type_token'] !== false) {
+ $data = [
+ $functionType,
+ $properties['return_type'],
+ ];
+
+ $fix = $phpcsFile->addFixableError(
+ '%s can not declare a return type. Found: %s',
+ $properties['return_type_token'],
+ 'ReturnTypeFound',
+ $data
+ );
+
+ if ($fix === true) {
+ $phpcsFile->fixer->beginChangeset();
+
+ $parensCloser = $tokens[$stackPtr]['parenthesis_closer'];
+ for ($i = ($parensCloser + 1); $i <= $properties['return_type_end_token']; $i++) {
+ if (isset(Tokens::$commentTokens[$tokens[$i]['code']])) {
+ // Ignore comments and leave them be.
+ continue;
+ }
+
+ $phpcsFile->fixer->replaceToken($i, '');
+ }
+
+ $phpcsFile->fixer->endChangeset();
+ }
+ }
+
+ if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) {
+ // Abstract/interface method, live coding or parse error.
+ return;
+ }
+
+ // Check for a value being returned.
+ $current = $tokens[$stackPtr]['scope_opener'];
+ $end = $tokens[$stackPtr]['scope_closer'];
+
+ do {
+ $current = $phpcsFile->findNext(\T_RETURN, ($current + 1), $end);
+ if ($current === false) {
+ break;
+ }
+
+ $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($current + 1), $end, true);
+ if ($next === false
+ || $tokens[$next]['code'] === \T_SEMICOLON
+ || $tokens[$next]['code'] === \T_CLOSE_TAG
+ ) {
+ // Return statement without value.
+ continue;
+ }
+
+ $endOfStatement = BCFile::findEndOfStatement($phpcsFile, $next);
+
+ $data = [
+ $functionType,
+ GetTokensAsString::compact($phpcsFile, $current, $endOfStatement, true),
+ ];
+
+ $phpcsFile->addWarning(
+ '%s can not return a value. Found: "%s"',
+ $current,
+ 'ReturnValueFound',
+ $data
+ );
+ } while ($current < $end);
+ }
+}
diff --git a/Universal/Sniffs/CodeAnalysis/ForeachUniqueAssignmentSniff.php b/Universal/Sniffs/CodeAnalysis/ForeachUniqueAssignmentSniff.php
new file mode 100644
index 00000000..5cb5b5ee
--- /dev/null
+++ b/Universal/Sniffs/CodeAnalysis/ForeachUniqueAssignmentSniff.php
@@ -0,0 +1,153 @@
+getTokens();
+
+ if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_closer']) === false) {
+ // Parse error or live coding, not our concern.
+ return;
+ }
+
+ $opener = $tokens[$stackPtr]['parenthesis_opener'];
+ $closer = $tokens[$stackPtr]['parenthesis_closer'];
+
+ $asPtr = $phpcsFile->findNext(\T_AS, ($opener + 1), $closer);
+ if ($asPtr === false) {
+ // Parse error or live coding, not our concern.
+ return;
+ }
+
+ // Real target.
+ $find = [\T_DOUBLE_ARROW];
+ // Prevent matching on double arrows within a list assignment.
+ $find += Collections::listTokens();
+
+ $doubleArrowPtr = $phpcsFile->findNext($find, ($asPtr + 1), $closer);
+ if ($doubleArrowPtr === false
+ || $tokens[$doubleArrowPtr]['code'] !== \T_DOUBLE_ARROW
+ ) {
+ // No key assignment.
+ return;
+ }
+
+ $isListAssignment = $phpcsFile->findNext(Tokens::$emptyTokens, ($doubleArrowPtr + 1), $closer, true);
+ if ($isListAssignment === false) {
+ // Parse error or live coding, not our concern.
+ }
+
+ $keyAsString = \ltrim(GetTokensAsString::noEmpties($phpcsFile, ($asPtr + 1), ($doubleArrowPtr - 1)), '&');
+ $valueAssignments = [];
+ if (isset(Collections::listTokens()[$tokens[$isListAssignment]['code']]) === false) {
+ // Single value assignment.
+ $valueAssignments[] = GetTokensAsString::noEmpties($phpcsFile, ($doubleArrowPtr + 1), ($closer - 1));
+ } else {
+ // List assignment.
+ $assignments = Lists::getAssignments($phpcsFile, $isListAssignment);
+ foreach ($assignments as $listItem) {
+ if ($listItem['assignment'] === '') {
+ // Ignore empty list assignments.
+ continue;
+ }
+
+ // Note: this doesn't take nested lists into account (yet).
+ $valueAssignments[] = $listItem['assignment'];
+ }
+ }
+
+ if (empty($valueAssignments)) {
+ // No assignments found.
+ return;
+ }
+
+ foreach ($valueAssignments as $valueAsString) {
+ $valueAsString = \ltrim($valueAsString, '&');
+
+ if ($keyAsString !== $valueAsString) {
+ // Key and value not the same.
+ continue;
+ }
+
+ $error = 'The variables used for the key and the value in a foreach assignment should be unique.';
+ $error .= 'Both the key and the value will currently be assigned to: "%s"';
+
+ $fix = $phpcsFile->addFixableError($error, $doubleArrowPtr, 'NotUnique', [$valueAsString]);
+ if ($fix === true) {
+ $phpcsFile->fixer->beginChangeset();
+
+ // Remove the key.
+ for ($i = ($asPtr + 1); $i < ($doubleArrowPtr + 1); $i++) {
+ if ($tokens[$i]['code'] === \T_WHITESPACE
+ && isset(Tokens::$commentTokens[$tokens[($i + 1)]['code']])
+ ) {
+ // Don't remove whitespace when followed directly by a comment.
+ continue;
+ }
+
+ if (isset(Tokens::$commentTokens[$tokens[$i]['code']])) {
+ // Don't remove comments.
+ continue;
+ }
+
+ // Remove everything else.
+ $phpcsFile->fixer->replaceToken($i, '');
+ }
+
+ $phpcsFile->fixer->endChangeset();
+ }
+
+ break;
+ }
+ }
+}
diff --git a/Universal/Sniffs/CodeAnalysis/StaticInFinalClassSniff.php b/Universal/Sniffs/CodeAnalysis/StaticInFinalClassSniff.php
new file mode 100644
index 00000000..1e1a3857
--- /dev/null
+++ b/Universal/Sniffs/CodeAnalysis/StaticInFinalClassSniff.php
@@ -0,0 +1,216 @@
+getTokens();
+
+ if ($tokens[$stackPtr]['code'] === \T_STRING
+ && \strtolower($tokens[$stackPtr]['content']) !== 'static'
+ ) {
+ return;
+ }
+
+ if ($tokens[$stackPtr]['code'] === \T_FUNCTION
+ || $tokens[$stackPtr]['code'] === \T_FN
+ ) {
+ /*
+ * Check return types for methods in final classes, anon classes and enums.
+ *
+ * Will return the scope opener of the function to prevent potential duplicate notifications.
+ */
+ $scopeOpener = $stackPtr;
+ if (isset($tokens[$stackPtr]['scope_opener']) === true) {
+ $scopeOpener = $tokens[$stackPtr]['scope_opener'];
+ }
+
+ if ($tokens[$stackPtr]['code'] === \T_FUNCTION) {
+ $ooPtr = Scopes::validDirectScope($phpcsFile, $stackPtr, $this->validOOScopes);
+ if ($ooPtr === false) {
+ // Method in a trait (not known where it is used), interface (never final) or not in an OO scope.
+ return $scopeOpener;
+ }
+ } else {
+ $ooPtr = Conditions::getLastCondition($phpcsFile, $stackPtr, $this->validOOScopes);
+ if ($ooPtr === false) {
+ // Arrow function outside of OO.
+ return $scopeOpener;
+ }
+ }
+
+ if ($tokens[$ooPtr]['code'] === \T_CLASS) {
+ $classProps = ObjectDeclarations::getClassProperties($phpcsFile, $ooPtr);
+ if ($classProps['is_final'] === false) {
+ // Method in a non-final class.
+ return $scopeOpener;
+ }
+ }
+
+ $functionProps = FunctionDeclarations::getProperties($phpcsFile, $stackPtr);
+ if ($functionProps['return_type'] === '') {
+ return $scopeOpener;
+ }
+
+ $staticPtr = $phpcsFile->findNext(
+ \T_STATIC,
+ $functionProps['return_type_token'],
+ ($functionProps['return_type_end_token'] + 1)
+ );
+
+ if ($staticPtr === false) {
+ return $scopeOpener;
+ }
+
+ // Found a return type containing the `static` type.
+ $this->handleError($phpcsFile, $staticPtr, 'ReturnType', '"static" return type');
+
+ return $scopeOpener;
+ }
+
+ /*
+ * Check other uses of static.
+ */
+ $functionPtr = Conditions::getLastCondition($phpcsFile, $stackPtr, [\T_FUNCTION, \T_CLOSURE]);
+ if ($functionPtr === false || $tokens[$functionPtr]['code'] === \T_CLOSURE) {
+ /*
+ * When `false`, this code is absolutely invalid, but not something to be addressed via this sniff.
+ * When a closure, we're not interested in it. The closure class is final, but closures
+ * can be bound to other classes. This needs further research and should maybe get its own sniff.
+ */
+ return;
+ }
+
+ $ooPtr = Scopes::validDirectScope($phpcsFile, $functionPtr, $this->validOOScopes);
+ if ($ooPtr === false) {
+ // Not in an OO context.
+ return;
+ }
+
+ if ($tokens[$ooPtr]['code'] === \T_CLASS) {
+ $classProps = ObjectDeclarations::getClassProperties($phpcsFile, $ooPtr);
+ if ($classProps['is_final'] === false) {
+ // Token in a non-final class.
+ return;
+ }
+ }
+
+ $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
+ if ($prevNonEmpty !== false) {
+ if ($tokens[$prevNonEmpty]['code'] === \T_INSTANCEOF) {
+ $prevPrevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevNonEmpty - 1), null, true);
+ $extraMsg = GetTokensAsString::compact($phpcsFile, $prevPrevNonEmpty, $stackPtr, true);
+ $this->handleError($phpcsFile, $stackPtr, 'InstanceOf', '"' . $extraMsg . '"');
+ return;
+ }
+
+ if ($tokens[$prevNonEmpty]['code'] === \T_NEW) {
+ $this->handleError($phpcsFile, $stackPtr, 'NewInstance', '"new static"');
+ return;
+ }
+ }
+
+ $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
+ if ($nextNonEmpty !== false && $tokens[$nextNonEmpty]['code'] === \T_DOUBLE_COLON) {
+ $nextNextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), null, true);
+ $extraMsg = GetTokensAsString::compact($phpcsFile, $stackPtr, $nextNextNonEmpty, true);
+ $this->handleError($phpcsFile, $stackPtr, 'ScopeResolution', '"' . $extraMsg . '"');
+ return;
+ }
+ }
+
+ /**
+ * Throw and potentially fix the error.
+ *
+ * @since 1.0.0
+ *
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
+ * @param int $stackPtr The position of erroneous `T_STATIC` token.
+ * @param string $errorCode The error code for the message.
+ * @param string $extraMsg Addition to the error message.
+ *
+ * @return void
+ */
+ private function handleError($phpcsFile, $stackPtr, $errorCode, $extraMsg)
+ {
+ $fix = $phpcsFile->addFixableError(
+ 'Use "self" instead of "static" when using late static binding in a final OO construct. Found: %s',
+ $stackPtr,
+ $errorCode,
+ [$extraMsg]
+ );
+
+ if ($fix === true) {
+ $phpcsFile->fixer->replaceToken($stackPtr, 'self');
+ }
+ }
+}
diff --git a/Universal/Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php b/Universal/Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php
new file mode 100644
index 00000000..d461dd2e
--- /dev/null
+++ b/Universal/Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php
@@ -0,0 +1,100 @@
+getTokens();
+ $content = $tokens[$stackPtr]['content'];
+ $contentLC = \strtolower($content);
+
+ if ($contentLC !== 'class') {
+ return;
+ }
+
+ $prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
+ if ($prevToken === false || $tokens[$prevToken]['code'] !== \T_DOUBLE_COLON) {
+ return;
+ }
+
+ if ($contentLC === $content) {
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'lowercase');
+ return;
+ }
+
+ $error = "The ::class keyword for class name resolution must be in lowercase. Expected: '::%s'; found: '::%s'";
+ $data = [
+ $contentLC,
+ $content,
+ ];
+
+ $errorCode = '';
+ if (\strtoupper($content) === $content) {
+ $errorCode = 'Uppercase';
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'uppercase');
+ } else {
+ $errorCode = 'Mixedcase';
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'mixed case');
+ }
+
+ $fix = $phpcsFile->addFixableError($error, $stackPtr, $errorCode, $data);
+ if ($fix === true) {
+ $phpcsFile->fixer->replaceToken($stackPtr, $contentLC);
+ }
+ }
+}
diff --git a/Universal/Sniffs/Constants/ModifierKeywordOrderSniff.php b/Universal/Sniffs/Constants/ModifierKeywordOrderSniff.php
new file mode 100644
index 00000000..8299dc0d
--- /dev/null
+++ b/Universal/Sniffs/Constants/ModifierKeywordOrderSniff.php
@@ -0,0 +1,216 @@
+ \T_FINAL,
+ ];
+
+ /**
+ * Returns an array of tokens this test wants to listen for.
+ *
+ * @since 1.0.0
+ *
+ * @return array
+ */
+ public function register()
+ {
+ // Add the visibility keywords to the constant modifier keywords.
+ $this->constantModifierKeywords += Tokens::$scopeModifiers;
+
+ return [\T_CONST];
+ }
+
+ /**
+ * Processes this test, when one of its tokens is encountered.
+ *
+ * @since 1.0.0
+ *
+ * @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 (Scopes::isOOConstant($phpcsFile, $stackPtr) === false) {
+ return;
+ }
+
+ /*
+ * Note to self: This can be switched to use the `Collections::constantModifierKeywords()`
+ * method as of the next version of PHPCSUtils.
+ */
+ $tokens = $phpcsFile->getTokens();
+ $valid = $this->constantModifierKeywords + Tokens::$emptyTokens;
+
+ $finalPtr = false;
+ $visibilityPtr = false;
+
+ for ($i = ($stackPtr - 1); $i > 0; $i--) {
+ if (isset($valid[$tokens[$i]['code']]) === false) {
+ break;
+ }
+
+ if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) {
+ continue;
+ }
+
+ if ($tokens[$i]['code'] === \T_FINAL) {
+ $finalPtr = $i;
+ } else {
+ $visibilityPtr = $i;
+ }
+ }
+
+ if ($finalPtr === false || $visibilityPtr === false) {
+ /*
+ * Either no modifier keywords found at all; or only one type of modifier
+ * keyword (final or visibility) declared, but not both. No ordering needed.
+ */
+ return;
+ }
+
+ if ($visibilityPtr < $finalPtr) {
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, self::VISIBILITY_FINAL);
+ } else {
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, self::FINAL_VISIBILITY);
+ }
+
+ $message = 'OO constant modifier keywords are not in the correct order. Expected: "%s", found: "%s"';
+
+ switch ($this->order) {
+ case self::VISIBILITY_FINAL:
+ if ($visibilityPtr < $finalPtr) {
+ // Order is correct. Nothing to do.
+ return;
+ }
+
+ $this->handleError($phpcsFile, $finalPtr, $visibilityPtr);
+ break;
+
+ case self::FINAL_VISIBILITY:
+ default:
+ if ($finalPtr < $visibilityPtr) {
+ // Order is correct. Nothing to do.
+ return;
+ }
+
+ $this->handleError($phpcsFile, $visibilityPtr, $finalPtr);
+ break;
+ }
+ }
+
+ /**
+ * Throw the error and potentially fix it.
+ *
+ * @since 1.0.0
+ *
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
+ * @param int $firstKeyword The position of the first keyword found.
+ * @param int $secondKeyword The position of the second keyword token.
+ *
+ * @return void
+ */
+ private function handleError(File $phpcsFile, $firstKeyword, $secondKeyword)
+ {
+ $tokens = $phpcsFile->getTokens();
+
+ $message = 'Constant modifier keywords are not in the correct order. Expected: "%s", found: "%s"';
+ $data = [
+ $tokens[$secondKeyword]['content'] . ' ' . $tokens[$firstKeyword]['content'],
+ $tokens[$firstKeyword]['content'] . ' ' . $tokens[$secondKeyword]['content'],
+ ];
+
+ $fix = $phpcsFile->addFixableError($message, $firstKeyword, 'Incorrect', $data);
+
+ if ($fix === true) {
+ $phpcsFile->fixer->beginChangeset();
+
+ $phpcsFile->fixer->replaceToken($secondKeyword, '');
+
+ // Prevent leaving behind trailing whitespace.
+ $i = ($secondKeyword + 1);
+ while ($tokens[$i]['code'] === \T_WHITESPACE) {
+ $phpcsFile->fixer->replaceToken($i, '');
+ $i++;
+ }
+
+ // Use the original token content as the case used for keywords is not the concern of this sniff.
+ $phpcsFile->fixer->addContentBefore($firstKeyword, $tokens[$secondKeyword]['content'] . ' ');
+
+ $phpcsFile->fixer->endChangeset();
+ }
+ }
+}
diff --git a/Universal/Sniffs/Constants/UppercaseMagicConstantsSniff.php b/Universal/Sniffs/Constants/UppercaseMagicConstantsSniff.php
index ee19e3d0..1b234f92 100644
--- a/Universal/Sniffs/Constants/UppercaseMagicConstantsSniff.php
+++ b/Universal/Sniffs/Constants/UppercaseMagicConstantsSniff.php
@@ -12,7 +12,7 @@
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
-use PHPCSUtils\Tokens\Collections;
+use PHP_CodeSniffer\Util\Tokens;
/**
* Verifies that PHP native `__...__` magic constants are in uppercase when used.
@@ -21,9 +21,18 @@
*
* @since 1.0.0
*/
-class UppercaseMagicConstantsSniff implements Sniff
+final class UppercaseMagicConstantsSniff implements Sniff
{
+ /**
+ * Name of the metric.
+ *
+ * @since 1.0.0
+ *
+ * @var string
+ */
+ const METRIC_NAME = 'Magic constant case';
+
/**
* Returns an array of tokens this test wants to listen for.
*
@@ -33,7 +42,7 @@ class UppercaseMagicConstantsSniff implements Sniff
*/
public function register()
{
- return Collections::$magicConstants;
+ return Tokens::$magicConstants;
}
/**
@@ -53,7 +62,7 @@ public function process(File $phpcsFile, $stackPtr)
$content = $tokens[$stackPtr]['content'];
$contentUC = \strtoupper($content);
if ($contentUC === $content) {
- $phpcsFile->recordMetric($stackPtr, 'Magic constant case', 'uppercase');
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'uppercase');
return;
}
@@ -66,10 +75,10 @@ public function process(File $phpcsFile, $stackPtr)
if (\strtolower($content) === $content) {
$errorCode = 'Lowercase';
- $phpcsFile->recordMetric($stackPtr, 'Magic constant case', 'lowercase');
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'lowercase');
} else {
$errorCode = 'Mixedcase';
- $phpcsFile->recordMetric($stackPtr, 'Magic constant case', 'mixed case');
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'mixed case');
}
$fix = $phpcsFile->addFixableError($error, $stackPtr, $errorCode, $data);
diff --git a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php
index 77cec877..86f06a6a 100644
--- a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php
+++ b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php
@@ -21,13 +21,27 @@
*
* @since 1.0.0
*/
-class DisallowAlternativeSyntaxSniff implements Sniff
+final class DisallowAlternativeSyntaxSniff implements Sniff
{
+ /**
+ * Name of the metric.
+ *
+ * @since 1.0.0
+ *
+ * @var string
+ */
+ const METRIC_NAME = 'Control Structure Style';
+
/**
* Whether to allow the alternative syntax when it is wrapped around
* inline HTML, as is often seen in views.
*
+ * Note: inline HTML within "closed scopes" - like function declarations -,
+ * within the control structure body will not be taken into account.
+ *
+ * @since 1.0.0
+ *
* @var bool
*/
public $allowWithInlineHTML = false;
@@ -41,16 +55,12 @@ class DisallowAlternativeSyntaxSniff implements Sniff
*/
public function register()
{
- return [
- \T_IF,
- \T_ELSE,
- \T_ELSEIF,
- \T_FOR,
- \T_FOREACH,
- \T_SWITCH,
- \T_WHILE,
- \T_DECLARE,
- ];
+ $targets = Collections::alternativeControlStructureSyntaxes();
+
+ // Don't look for elseif/else as they need to be dealt with in one go with the if.
+ unset($targets[\T_ELSEIF], $targets[\T_ELSE]);
+
+ return $targets;
}
/**
@@ -66,47 +76,23 @@ public function register()
*/
public function process(File $phpcsFile, $stackPtr)
{
- $tokens = $phpcsFile->getTokens();
-
/*
- * Deal with `else if`.
+ * Ignore control structures without body (i.e. single line control structures).
+ * This doesn't ignore _empty_ bodies.
*/
- if ($tokens[$stackPtr]['code'] === \T_ELSE
- && isset($tokens[$stackPtr]['scope_opener']) === false
- && ControlStructures::isElseIf($phpcsFile, $stackPtr) === true
- ) {
- // This is an `else if` - this will be dealt with on the `if` token.
+ if (ControlStructures::hasBody($phpcsFile, $stackPtr, true) === false) {
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'single line (without body)');
return;
}
- /*
- * Ignore control structures without body.
- */
- if (ControlStructures::hasBody($phpcsFile, $stackPtr) === false) {
- return;
- }
-
- /*
- * Deal with declare. Declare with alternative syntax does not get the scope opener/closer
- * assigned in the tokens array prior to PHPCS 3.5.4, so let's set those ourselves.
- *
- * @link https://github.com/squizlabs/PHP_CodeSniffer/pull/2843
- */
- if ($tokens[$stackPtr]['code'] === \T_DECLARE && isset($tokens[$stackPtr]['scope_opener']) === false) {
- $declareOpenClose = ControlStructures::getDeclareScopeOpenClose($phpcsFile, $stackPtr);
- if ($declareOpenClose !== false) {
- // Overrule the scope indexes in our local copy of the $tokens array.
- $tokens[$stackPtr]['scope_opener'] = $declareOpenClose['opener'];
- $tokens[$stackPtr]['scope_closer'] = $declareOpenClose['closer'];
- }
- }
+ $tokens = $phpcsFile->getTokens();
/*
- * Now check if the control structure uses alternative syntax.
+ * Check if the control structure uses alternative syntax.
*/
if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) {
// No scope opener found: inline control structure or parse error.
- $phpcsFile->recordMetric($stackPtr, 'Control structure style', 'inline');
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'inline');
return;
}
@@ -115,58 +101,114 @@ public function process(File $phpcsFile, $stackPtr)
if ($tokens[$opener]['code'] !== \T_COLON) {
// Curly brace syntax (not our concern).
- $phpcsFile->recordMetric($stackPtr, 'Control structure style', 'curly braces');
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'curly braces');
return;
}
- $hasInlineHTML = $phpcsFile->findNext(
- \T_INLINE_HTML,
- $opener,
- $closer
- );
+ /*
+ * As of here, we *know* the control structure must be using alternative syntax and
+ * must have all scope openers/closers set as, in case of parse errors, PHPCS wouldn't
+ * have set the scope opener, even for the first `if`.
+ *
+ * Also note that alternative syntax cannot be used with `else if`, so we don't need to take that
+ * into account.
+ */
+
+ /*
+ * Determine whether there is inline HTML.
+ *
+ * For "chained" control structures (if - elseif - else), the complete control structure
+ * needs to be examined in one go as these cannot be changed individually, only as a complete group.
+ */
+ $closedScopes = Collections::closedScopes();
+ $find = $closedScopes;
+ $find[\T_INLINE_HTML] = \T_INLINE_HTML;
+
+ $chainedIssues = [];
+ $hasInlineHTML = false;
+ $currentPtr = $stackPtr;
+
+ do {
+ $opener = $tokens[$currentPtr]['scope_opener'];
+ $closer = $tokens[$currentPtr]['scope_closer'];
+ $chainedIssues[$opener] = $closer;
+
+ if ($hasInlineHTML === true) {
+ // No need to search the contents, we already know there is inline HTML.
+ $currentPtr = $closer;
+ continue;
+ }
+
+ $inlineHTMLPtr = $opener;
- if ($hasInlineHTML !== false) {
- $phpcsFile->recordMetric($stackPtr, 'Control structure style', 'alternative syntax with inline HTML');
+ do {
+ $inlineHTMLPtr = $phpcsFile->findNext($find, ($inlineHTMLPtr + 1), $closer);
+ if ($tokens[$inlineHTMLPtr]['code'] === \T_INLINE_HTML) {
+ $hasInlineHTML = true;
+ break;
+ }
+
+ if (isset($closedScopes[$tokens[$inlineHTMLPtr]['code']], $tokens[$inlineHTMLPtr]['scope_closer'])) {
+ $inlineHTMLPtr = $tokens[$inlineHTMLPtr]['scope_closer'];
+ }
+ } while ($inlineHTMLPtr !== false && $inlineHTMLPtr < $closer);
+
+ $currentPtr = $closer;
+ } while (isset(Collections::alternativeControlStructureSyntaxes()[$tokens[$closer]['code']]) === true);
+
+ if ($hasInlineHTML === true) {
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'alternative syntax with inline HTML');
} else {
- $phpcsFile->recordMetric($stackPtr, 'Control structure style', 'alternative syntax');
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'alternative syntax');
}
- if ($this->allowWithInlineHTML === true) {
+ if ($hasInlineHTML === true && $this->allowWithInlineHTML === true) {
return;
}
- $error = 'Using control structures with the alternative syntax - %1$s(): ... end%1$s; - is not allowed.';
- $code = 'Found' . \ucfirst($tokens[$stackPtr]['content']);
- $data = [$tokens[$stackPtr]['content']];
- if ($tokens[$stackPtr]['code'] === \T_ELSEIF || $tokens[$stackPtr]['code'] === \T_ELSE) {
- $data = ['if'];
+ $error = 'Using control structures with the alternative syntax is not allowed';
+ if ($this->allowWithInlineHTML === true) {
+ $error .= ' unless the control structure contains inline HTML';
}
+ $error .= '. Found: %1$s(): ... end%1$s;';
- if ($hasInlineHTML !== false) {
+ $code = 'Found' . \ucfirst($tokens[$stackPtr]['content']);
+ if ($hasInlineHTML === true) {
$code .= 'WithInlineHTML';
}
- $fix = $phpcsFile->addFixableError($error, $tokens[$stackPtr]['scope_opener'], $code, $data);
+ $data = [$tokens[$stackPtr]['content']];
+
+ foreach ($chainedIssues as $opener => $closer) {
+ $fix = $phpcsFile->addFixableError($error, $opener, $code, $data);
+ }
+
if ($fix === false) {
return;
}
/*
- * Fix it.
+ * Fix all issues for this chain in one go to diminish the chance of conflicts.
*/
$phpcsFile->fixer->beginChangeset();
- $phpcsFile->fixer->replaceToken($opener, '{');
- if (isset(Collections::$alternativeControlStructureSyntaxCloserTokens[$tokens[$closer]['code']]) === true) {
- $phpcsFile->fixer->replaceToken($closer, '}');
-
- $semicolon = $phpcsFile->findNext(Tokens::$emptyTokens, ($closer + 1), null, true);
- if ($semicolon !== false && $tokens[$semicolon]['code'] === \T_SEMICOLON) {
- $phpcsFile->fixer->replaceToken($semicolon, '');
+ foreach ($chainedIssues as $opener => $closer) {
+ $phpcsFile->fixer->replaceToken($opener, '{');
+
+ if (isset(Collections::alternativeControlStructureSyntaxClosers()[$tokens[$closer]['code']]) === true) {
+ $phpcsFile->fixer->replaceToken($closer, '}');
+
+ $semicolon = $phpcsFile->findNext(Tokens::$emptyTokens, ($closer + 1), null, true);
+ if ($semicolon !== false && $tokens[$semicolon]['code'] === \T_SEMICOLON) {
+ $phpcsFile->fixer->replaceToken($semicolon, '');
+ }
+ } else {
+ /*
+ * This must be an if/else using alternative syntax.
+ * The closer will be the next control structure keyword.
+ */
+ $phpcsFile->fixer->addContentBefore($closer, '} ');
}
- } else {
- // This must be an if/else using alternative syntax. The closer will be the next control structure keyword.
- $phpcsFile->fixer->addContentBefore($closer, '} ');
}
$phpcsFile->fixer->endChangeset();
diff --git a/Universal/Sniffs/ControlStructures/DisallowLonelyIfSniff.php b/Universal/Sniffs/ControlStructures/DisallowLonelyIfSniff.php
new file mode 100644
index 00000000..61825425
--- /dev/null
+++ b/Universal/Sniffs/ControlStructures/DisallowLonelyIfSniff.php
@@ -0,0 +1,348 @@
+getTokens();
+
+ /*
+ * Deal with `else if`.
+ */
+ if (ControlStructures::isElseIf($phpcsFile, $stackPtr) === true) {
+ // Ignore, not our real target.
+ return;
+ }
+
+ if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) {
+ // Either an else without curly braces or a parse error. Ignore.
+ return;
+ }
+
+ $outerScopeOpener = $tokens[$stackPtr]['scope_opener'];
+ $outerScopeCloser = $tokens[$stackPtr]['scope_closer'];
+
+ $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($outerScopeOpener + 1), $outerScopeCloser, true);
+ if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== \T_IF) {
+ // Definitely not a lonely if statement.
+ return;
+ }
+
+ if (isset($tokens[$nextNonEmpty]['scope_closer']) === false) {
+ // Either a control structure without curly braces or a parse error. Ignore.
+ return;
+ }
+
+ /*
+ * Find the end of an if - else chain.
+ */
+
+ $innerIfPtr = $nextNonEmpty;
+ $innerIfToken = $tokens[$innerIfPtr];
+ $autoFixable = true;
+ $innerScopeCloser = $innerIfToken['scope_closer'];
+
+ // For alternative syntax fixer only.
+ // Remember the individual inner scope opener and closers so the fixer doesn't need
+ // to do the same walking over the if/else chain again.
+ $innerScopes = [
+ $innerIfToken['scope_opener'] => $innerScopeCloser,
+ ];
+
+ do {
+ /*
+ * Handle control structures using alternative syntax.
+ */
+ if ($tokens[$innerScopeCloser]['code'] !== \T_CLOSE_CURLY_BRACKET) {
+ if ($tokens[$innerScopeCloser]['code'] === \T_ENDIF) {
+ $nextAfter = $phpcsFile->findNext(
+ Tokens::$emptyTokens,
+ ($innerScopeCloser + 1),
+ $outerScopeCloser,
+ true
+ );
+
+ if ($tokens[$nextAfter]['code'] === \T_CLOSE_TAG) {
+ // Not "lonely" as at the very least there must be a PHP open tag before the outer closer.
+ return;
+ }
+
+ if ($tokens[$nextAfter]['code'] === \T_SEMICOLON) {
+ $innerScopeCloser = $nextAfter;
+ } else {
+ // Missing semi-colon. Report, but don't auto-fix.
+ $autoFixable = false;
+ }
+ } else {
+ // This must be an else[if].
+ --$innerScopeCloser;
+ }
+ }
+
+ $innerNextNonEmpty = $phpcsFile->findNext(
+ Tokens::$emptyTokens,
+ ($innerScopeCloser + 1),
+ $outerScopeCloser,
+ true
+ );
+ if ($innerNextNonEmpty === false) {
+ // This was the last closer.
+ break;
+ }
+
+ if ($tokens[$innerNextNonEmpty]['code'] !== \T_ELSE
+ && $tokens[$innerNextNonEmpty]['code'] !== \T_ELSEIF
+ ) {
+ // Found another statement after the control structure. The "if" is not lonely.
+ return;
+ }
+
+ if (isset($tokens[$innerNextNonEmpty]['scope_closer']) === false) {
+ // This may still be an "else if"...
+ $nextAfter = $phpcsFile->findNext(
+ Tokens::$emptyTokens,
+ ($innerNextNonEmpty + 1),
+ $outerScopeCloser,
+ true
+ );
+
+ if ($nextAfter === false
+ || $tokens[$nextAfter]['code'] !== \T_IF
+ || isset($tokens[$nextAfter]['scope_closer']) === false
+ ) {
+ // Defense in depth. Either a control structure without curly braces or a parse error. Ignore.
+ return;
+ }
+
+ $innerNextNonEmpty = $nextAfter;
+ }
+
+ $innerScopeCloser = $tokens[$innerNextNonEmpty]['scope_closer'];
+ $innerScopes[$tokens[$innerNextNonEmpty]['scope_opener']] = $innerScopeCloser;
+ } while (true);
+
+ /*
+ * As of now, we know we have an error. Check if it can be auto-fixed.
+ */
+ if ($phpcsFile->findNext(\T_WHITESPACE, ($innerScopeCloser + 1), $outerScopeCloser, true) !== false) {
+ // Comment between the inner and outer closers.
+ $autoFixable = false;
+ }
+
+ if ($tokens[$innerScopeCloser]['code'] === \T_SEMICOLON) {
+ $hasComment = $phpcsFile->findPrevious(\T_WHITESPACE, ($innerScopeCloser - 1), null, true);
+ if ($tokens[$hasComment]['code'] !== \T_ENDIF) {
+ // Comment between the "endif" and the semi-colon.
+ $autoFixable = false;
+ }
+ }
+
+ if ($tokens[$outerScopeOpener]['line'] !== $innerIfToken['line']) {
+ for ($startOfNextLine = ($outerScopeOpener + 1); $startOfNextLine < $innerIfPtr; $startOfNextLine++) {
+ if ($tokens[$outerScopeOpener]['line'] !== $tokens[$startOfNextLine]['line']) {
+ break;
+ }
+ }
+
+ if ($phpcsFile->findNext(\T_WHITESPACE, $startOfNextLine, $innerIfPtr, true) !== false) {
+ // Comment between the inner and outer openers.
+ $autoFixable = false;
+ }
+ }
+
+ if (isset($innerIfToken['parenthesis_opener'], $innerIfToken['parenthesis_closer']) === false) {
+ // Start/end of the condition of the if unclear. Most likely a parse error.
+ $autoFixable = false;
+ }
+
+ /*
+ * Throw the error and potentially fix it.
+ */
+ $error = 'If control structure block found as the only statement within an "else" block. Use elseif instead.';
+ $code = 'Found';
+
+ if ($autoFixable === false) {
+ $phpcsFile->addError($error, $stackPtr, $code);
+ return;
+ }
+
+ $fix = $phpcsFile->addFixableError($error, $stackPtr, $code);
+ if ($fix === false) {
+ return;
+ }
+
+ /*
+ * Fix it.
+ */
+ $outerInnerSameType = false;
+ if (($tokens[$outerScopeCloser]['code'] === \T_CLOSE_CURLY_BRACKET
+ && $tokens[$innerScopeCloser]['code'] === \T_CLOSE_CURLY_BRACKET)
+ || ($tokens[$outerScopeCloser]['code'] === \T_ENDIF
+ && $tokens[$innerScopeCloser]['code'] === \T_SEMICOLON)
+ ) {
+ $outerInnerSameType = true;
+ }
+
+ $targetIsCurly = ($tokens[$outerScopeCloser]['code'] === \T_CLOSE_CURLY_BRACKET);
+
+ $innerScopeCount = \count($innerScopes);
+
+ $condition = GetTokensAsString::origContent($phpcsFile, ($innerIfPtr + 1), ($innerIfToken['scope_opener'] - 1));
+ if ($targetIsCurly === true) {
+ $condition = \rtrim($condition) . ' ';
+ }
+
+ $phpcsFile->fixer->beginChangeset();
+
+ // Remove the inner if + condition up to and including the scope opener.
+ for ($i = $innerIfPtr; $i <= $innerIfToken['scope_opener']; $i++) {
+ $phpcsFile->fixer->replaceToken($i, '');
+ }
+
+ // Potentially remove trailing whitespace/new line if there is no comment after the inner condition.
+ while ($tokens[$i]['line'] === $innerIfToken['line']
+ && $tokens[$i]['code'] === \T_WHITESPACE
+ ) {
+ $phpcsFile->fixer->replaceToken($i, '');
+ ++$i;
+ }
+
+ // Remove any potential indentation whitespace for the inner if.
+ if ($tokens[$outerScopeOpener]['line'] !== $innerIfToken['line']
+ && $tokens[$i]['line'] !== $innerIfToken['line']
+ ) {
+ $i = ($nextNonEmpty - 1);
+ while ($tokens[$i]['line'] === $innerIfToken['line']
+ && $tokens[$i]['code'] === \T_WHITESPACE
+ ) {
+ $phpcsFile->fixer->replaceToken($i, '');
+ --$i;
+ }
+ }
+
+ // Remove the inner scope closer.
+ $phpcsFile->fixer->replaceToken($innerScopeCloser, '');
+ $i = ($innerScopeCloser - 1);
+
+ // Handle alternative syntax for the closer.
+ if ($tokens[$innerScopeCloser]['code'] === \T_SEMICOLON) {
+ // Remove potential whitespace between the "endif" and the semicolon.
+ while ($tokens[$i]['code'] === \T_WHITESPACE) {
+ $phpcsFile->fixer->replaceToken($i, '');
+ --$i;
+ }
+
+ // Remove the "endif".
+ $phpcsFile->fixer->replaceToken($i, '');
+ --$i;
+ }
+
+ // Remove superfluous whitespace before the inner scope closer.
+ while ($tokens[$i]['code'] === \T_WHITESPACE) {
+ $phpcsFile->fixer->replaceToken($i, '');
+ --$i;
+ }
+
+ // Replace the else.
+ $phpcsFile->fixer->replaceToken($stackPtr, 'elseif' . $condition);
+
+ // Remove potential superfluous whitespace between the new condition and the scope opener.
+ $i = ($stackPtr + 1);
+ while ($tokens[$i]['line'] === $tokens[$stackPtr]['line']
+ && $tokens[$i]['code'] === \T_WHITESPACE
+ ) {
+ $phpcsFile->fixer->replaceToken($i, '');
+ ++$i;
+ }
+
+ if ($outerInnerSameType === false
+ && $innerScopeCount > 1
+ ) {
+ $loop = 1;
+ foreach ($innerScopes as $opener => $closer) {
+ if ($targetIsCurly === true) {
+ if ($loop !== 1) {
+ // Only handle the opener when it's not the first of the chain as that's already handled above.
+ $phpcsFile->fixer->replaceToken($opener, ' {');
+ }
+
+ if ($loop !== $innerScopeCount) {
+ // Only handle the closer when it's not the last of the chain as that's already handled above.
+ $phpcsFile->fixer->addContentBefore($closer, '} ');
+ }
+ } else {
+ if ($loop !== 1) {
+ // Only handle the opener when it's not the first of the chain as that's already handled above.
+ $phpcsFile->fixer->replaceToken($opener, ':');
+ }
+
+ if ($loop !== $innerScopeCount) {
+ // Only handle the closer when it's not the last of the chain as that's already handled above.
+ $phpcsFile->fixer->replaceToken($closer, '');
+
+ $j = ($closer + 1);
+ while ($tokens[$j]['code'] === \T_WHITESPACE) {
+ $phpcsFile->fixer->replaceToken($j, '');
+ ++$j;
+ }
+ }
+ }
+
+ ++$loop;
+ }
+ }
+
+ $phpcsFile->fixer->endChangeset();
+ }
+}
diff --git a/Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php b/Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php
index 85780f7d..01984c2c 100644
--- a/Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php
+++ b/Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php
@@ -28,9 +28,18 @@
*
* @since 1.0.0
*/
-class IfElseDeclarationSniff implements Sniff
+final class IfElseDeclarationSniff implements Sniff
{
+ /**
+ * Name of the metric.
+ *
+ * @since 1.0.0
+ *
+ * @var string
+ */
+ const METRIC_NAME = 'Else(if) on a new line';
+
/**
* Returns an array of tokens this test wants to listen for.
*
@@ -85,16 +94,16 @@ public function process(File $phpcsFile, $stackPtr)
*/
$prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
if ($prevNonEmpty === false || $tokens[$prevNonEmpty]['code'] !== \T_CLOSE_CURLY_BRACKET) {
- // Parse error. Not our concern.
+ // Parse error or mixing braced and non-braced. Not our concern.
return;
}
if ($tokens[$prevNonEmpty]['line'] !== $tokens[$stackPtr]['line']) {
- $phpcsFile->recordMetric($stackPtr, 'Else(if) on a new line', 'yes');
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes');
return;
}
- $phpcsFile->recordMetric($stackPtr, 'Else(if) on a new line', 'no');
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'no');
$errorBase = \strtoupper($tokens[$stackPtr]['content']);
$error = $errorBase . ' statement must be on a new line.';
diff --git a/Universal/Sniffs/Files/SeparateFunctionsFromOOSniff.php b/Universal/Sniffs/Files/SeparateFunctionsFromOOSniff.php
new file mode 100644
index 00000000..c2c1be52
--- /dev/null
+++ b/Universal/Sniffs/Files/SeparateFunctionsFromOOSniff.php
@@ -0,0 +1,189 @@
+ \T_START_HEREDOC,
+ \T_START_NOWDOC => \T_START_NOWDOC,
+ ];
+
+ /**
+ * Returns an array of tokens this test wants to listen for.
+ *
+ * @since 1.0.0
+ *
+ * @return array
+ */
+ public function register()
+ {
+ $this->search += Tokens::$ooScopeTokens;
+ $this->search += Collections::functionDeclarationTokens();
+
+ return Collections::phpOpenTags();
+ }
+
+ /**
+ * Processes this test, when one of its tokens is encountered.
+ *
+ * @since 1.0.0
+ *
+ * @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();
+
+ $firstOO = null;
+ $firstFunction = null;
+ $functionCount = 0;
+ $OOCount = 0;
+
+ for ($i = 0; $i < $phpcsFile->numTokens; $i++) {
+ // Ignore anything within square brackets.
+ if ($tokens[$i]['code'] !== \T_OPEN_CURLY_BRACKET
+ && isset($tokens[$i]['bracket_opener'], $tokens[$i]['bracket_closer'])
+ && $i === $tokens[$i]['bracket_opener']
+ ) {
+ $i = $tokens[$i]['bracket_closer'];
+ continue;
+ }
+
+ // Skip past nested arrays, function calls and arbitrary groupings.
+ if ($tokens[$i]['code'] === \T_OPEN_PARENTHESIS
+ && isset($tokens[$i]['parenthesis_closer'])
+ ) {
+ $i = $tokens[$i]['parenthesis_closer'];
+ continue;
+ }
+
+ // Skip over potentially large docblocks.
+ if ($tokens[$i]['code'] === \T_DOC_COMMENT_OPEN_TAG
+ && isset($tokens[$i]['comment_closer'])
+ ) {
+ $i = $tokens[$i]['comment_closer'];
+ continue;
+ }
+
+ // Ignore everything else we're not interested in.
+ if (isset($this->search[$tokens[$i]['code']]) === false) {
+ continue;
+ }
+
+ // Skip over structures which won't contain anything we're interested in.
+ if (($tokens[$i]['code'] === \T_START_HEREDOC
+ || $tokens[$i]['code'] === \T_START_NOWDOC
+ || $tokens[$i]['code'] === \T_ANON_CLASS
+ || $tokens[$i]['code'] === \T_CLOSURE
+ || $tokens[$i]['code'] === \T_FN)
+ && isset($tokens[$i]['scope_condition'], $tokens[$i]['scope_closer'])
+ && $tokens[$i]['scope_condition'] === $i
+ ) {
+ $i = $tokens[$i]['scope_closer'];
+ continue;
+ }
+
+ // This will be either a function declaration or an OO declaration token.
+ if ($tokens[$i]['code'] === \T_FUNCTION) {
+ if (isset($firstFunction) === false) {
+ $firstFunction = $i;
+ }
+
+ ++$functionCount;
+ } else {
+ if (isset($firstOO) === false) {
+ $firstOO = $i;
+ }
+
+ ++$OOCount;
+ }
+
+ if (isset($tokens[$i]['scope_closer']) === true) {
+ $i = $tokens[$i]['scope_closer'];
+ }
+ }
+
+ if ($functionCount > 0 && $OOCount > 0) {
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'Both function and OO declarations');
+
+ $reportToken = \max($firstFunction, $firstOO);
+
+ $phpcsFile->addError(
+ 'A file should either contain function declarations or OO structure declarations, but not both.'
+ . ' Found %d function declaration(s) and %d OO structure declaration(s).'
+ . ' The first function declaration was found on line %d;'
+ . ' the first OO declaration was found on line %d',
+ $reportToken,
+ 'Mixed',
+ [
+ $functionCount,
+ $OOCount,
+ $tokens[$firstFunction]['line'],
+ $tokens[$firstOO]['line'],
+ ]
+ );
+ } elseif ($functionCount > 0) {
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'Only function(s)');
+ } elseif ($OOCount > 0) {
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'Only OO structure(s)');
+ } else {
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'Neither');
+ }
+
+ // Ignore the rest of the file.
+ return ($phpcsFile->numTokens + 1);
+ }
+}
diff --git a/Universal/Sniffs/Lists/DisallowLongListSyntaxSniff.php b/Universal/Sniffs/Lists/DisallowLongListSyntaxSniff.php
index 19499268..1b57510f 100644
--- a/Universal/Sniffs/Lists/DisallowLongListSyntaxSniff.php
+++ b/Universal/Sniffs/Lists/DisallowLongListSyntaxSniff.php
@@ -19,7 +19,7 @@
*
* @since 1.0.0
*/
-class DisallowLongListSyntaxSniff implements Sniff
+final class DisallowLongListSyntaxSniff implements Sniff
{
/**
@@ -53,8 +53,6 @@ public function process(File $phpcsFile, $stackPtr)
return;
}
- $phpcsFile->recordMetric($stackPtr, 'Short list syntax used', 'no');
-
$fix = $phpcsFile->addFixableError('Long list syntax is not allowed', $stackPtr, 'Found');
if ($fix === true) {
diff --git a/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php b/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php
index bcba3ba6..56087409 100644
--- a/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php
+++ b/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php
@@ -12,6 +12,7 @@
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
+use PHPCSUtils\Tokens\Collections;
use PHPCSUtils\Utils\Lists;
/**
@@ -19,9 +20,16 @@
*
* @since 1.0.0
*/
-class DisallowShortListSyntaxSniff implements Sniff
+final class DisallowShortListSyntaxSniff implements Sniff
{
+ /**
+ * The phrase to use for the metric recorded by this sniff.
+ *
+ * @var string
+ */
+ const METRIC_NAME = 'Short list syntax used';
+
/**
* Registers the tokens that this sniff wants to listen for.
*
@@ -31,7 +39,10 @@ class DisallowShortListSyntaxSniff implements Sniff
*/
public function register()
{
- return [\T_OPEN_SHORT_ARRAY];
+ $targets = Collections::shortArrayListOpenTokensBC();
+ $targets[\T_LIST] = \T_LIST; // Only for recording metrics.
+
+ return $targets;
}
/**
@@ -47,20 +58,21 @@ public function register()
*/
public function process(File $phpcsFile, $stackPtr)
{
- $tokens = $phpcsFile->getTokens();
+ $tokens = $phpcsFile->getTokens();
+
+ if ($tokens[$stackPtr]['code'] === \T_LIST) {
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'no');
+ return;
+ }
+
$openClose = Lists::getOpenClose($phpcsFile, $stackPtr);
if ($openClose === false) {
// Not a short list, live coding or parse error.
- if (isset($tokens[$stackPtr]['bracket_closer']) === true) {
- // No need to examine nested subs of this short array/array access.
- return $tokens[$stackPtr]['bracket_closer'];
- }
-
return;
}
- $phpcsFile->recordMetric($stackPtr, 'Short list syntax used', 'yes');
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes');
$fix = $phpcsFile->addFixableError('Short list syntax is not allowed', $stackPtr, 'Found');
diff --git a/Universal/Sniffs/Namespaces/DisallowCurlyBraceSyntaxSniff.php b/Universal/Sniffs/Namespaces/DisallowCurlyBraceSyntaxSniff.php
index b0eed5f3..00384ac6 100644
--- a/Universal/Sniffs/Namespaces/DisallowCurlyBraceSyntaxSniff.php
+++ b/Universal/Sniffs/Namespaces/DisallowCurlyBraceSyntaxSniff.php
@@ -19,9 +19,18 @@
*
* @since 1.0.0
*/
-class DisallowCurlyBraceSyntaxSniff implements Sniff
+final class DisallowCurlyBraceSyntaxSniff implements Sniff
{
+ /**
+ * Name of the metric.
+ *
+ * @since 1.0.0
+ *
+ * @var string
+ */
+ const METRIC_NAME = 'Namespace declaration using curly brace syntax';
+
/**
* Returns an array of tokens this test wants to listen for.
*
@@ -57,11 +66,11 @@ public function process(File $phpcsFile, $stackPtr)
if (isset($tokens[$stackPtr]['scope_condition']) === false
|| $tokens[$stackPtr]['scope_condition'] !== $stackPtr
) {
- $phpcsFile->recordMetric($stackPtr, 'Namespace declaration using curly brace syntax', 'no');
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'no');
return;
}
- $phpcsFile->recordMetric($stackPtr, 'Namespace declaration using curly brace syntax', 'yes');
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes');
$phpcsFile->addError(
'Namespace declarations using the curly brace syntax are not allowed.',
diff --git a/Universal/Sniffs/Namespaces/DisallowDeclarationWithoutNameSniff.php b/Universal/Sniffs/Namespaces/DisallowDeclarationWithoutNameSniff.php
index bb14eeaa..b3200278 100644
--- a/Universal/Sniffs/Namespaces/DisallowDeclarationWithoutNameSniff.php
+++ b/Universal/Sniffs/Namespaces/DisallowDeclarationWithoutNameSniff.php
@@ -19,7 +19,7 @@
*
* @since 1.0.0
*/
-class DisallowDeclarationWithoutNameSniff implements Sniff
+final class DisallowDeclarationWithoutNameSniff implements Sniff
{
/**
@@ -65,7 +65,6 @@ public function process(File $phpcsFile, $stackPtr)
if ($name !== '') {
// Named namespace.
$phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes');
-
return;
}
diff --git a/Universal/Sniffs/Namespaces/EnforceCurlyBraceSyntaxSniff.php b/Universal/Sniffs/Namespaces/EnforceCurlyBraceSyntaxSniff.php
index a5a9825d..b6676c26 100644
--- a/Universal/Sniffs/Namespaces/EnforceCurlyBraceSyntaxSniff.php
+++ b/Universal/Sniffs/Namespaces/EnforceCurlyBraceSyntaxSniff.php
@@ -19,9 +19,18 @@
*
* @since 1.0.0
*/
-class EnforceCurlyBraceSyntaxSniff implements Sniff
+final class EnforceCurlyBraceSyntaxSniff implements Sniff
{
+ /**
+ * Name of the metric.
+ *
+ * @since 1.0.0
+ *
+ * @var string
+ */
+ const METRIC_NAME = 'Namespace declaration using curly brace syntax';
+
/**
* Returns an array of tokens this test wants to listen for.
*
@@ -57,11 +66,11 @@ public function process(File $phpcsFile, $stackPtr)
if (isset($tokens[$stackPtr]['scope_condition']) === true
&& $tokens[$stackPtr]['scope_condition'] === $stackPtr
) {
- $phpcsFile->recordMetric($stackPtr, 'Namespace declaration using curly brace syntax', 'yes');
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes');
return;
}
- $phpcsFile->recordMetric($stackPtr, 'Namespace declaration using curly brace syntax', 'no');
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'no');
$phpcsFile->addError(
'Namespace declarations without curly braces are not allowed.',
diff --git a/Universal/Sniffs/Namespaces/OneDeclarationPerFileSniff.php b/Universal/Sniffs/Namespaces/OneDeclarationPerFileSniff.php
index d18c59a0..c53cdf0b 100644
--- a/Universal/Sniffs/Namespaces/OneDeclarationPerFileSniff.php
+++ b/Universal/Sniffs/Namespaces/OneDeclarationPerFileSniff.php
@@ -19,7 +19,7 @@
*
* @since 1.0.0
*/
-class OneDeclarationPerFileSniff implements Sniff
+final class OneDeclarationPerFileSniff implements Sniff
{
/**
diff --git a/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php b/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php
new file mode 100644
index 00000000..15da4aea
--- /dev/null
+++ b/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php
@@ -0,0 +1,190 @@
+ string> Key is the lowercased keyword, value the "proper" cased keyword.
+ */
+ private $reservedNames = [
+ 'abstract' => 'abstract',
+ 'and' => 'and',
+ 'array' => 'array',
+ 'as' => 'as',
+ 'break' => 'break',
+ 'callable' => 'callable',
+ 'case' => 'case',
+ 'catch' => 'catch',
+ 'class' => 'class',
+ 'clone' => 'clone',
+ 'const' => 'const',
+ 'continue' => 'continue',
+ 'declare' => 'declare',
+ 'default' => 'default',
+ 'die' => 'die',
+ 'do' => 'do',
+ 'echo' => 'echo',
+ 'else' => 'else',
+ 'elseif' => 'elseif',
+ 'empty' => 'empty',
+ 'enddeclare' => 'enddeclare',
+ 'endfor' => 'endfor',
+ 'endforeach' => 'endforeach',
+ 'endif' => 'endif',
+ 'endswitch' => 'endswitch',
+ 'endwhile' => 'endwhile',
+ 'enum' => 'enum',
+ 'eval' => 'eval',
+ 'exit' => 'exit',
+ 'extends' => 'extends',
+ 'final' => 'final',
+ 'finally' => 'finally',
+ 'fn' => 'fn',
+ 'for' => 'for',
+ 'foreach' => 'foreach',
+ 'function' => 'function',
+ 'global' => 'global',
+ 'goto' => 'goto',
+ 'if' => 'if',
+ 'implements' => 'implements',
+ 'include' => 'include',
+ 'include_once' => 'include_once',
+ 'instanceof' => 'instanceof',
+ 'insteadof' => 'insteadof',
+ 'interface' => 'interface',
+ 'isset' => 'isset',
+ 'list' => 'list',
+ 'match' => 'match',
+ 'namespace' => 'namespace',
+ 'new' => 'new',
+ 'or' => 'or',
+ 'print' => 'print',
+ 'private' => 'private',
+ 'protected' => 'protected',
+ 'public' => 'public',
+ 'readonly' => 'readonly',
+ 'require' => 'require',
+ 'require_once' => 'require_once',
+ 'return' => 'return',
+ 'static' => 'static',
+ 'switch' => 'switch',
+ 'throw' => 'throw',
+ 'trait' => 'trait',
+ 'try' => 'try',
+ 'unset' => 'unset',
+ 'use' => 'use',
+ 'var' => 'var',
+ 'while' => 'while',
+ 'xor' => 'xor',
+ 'yield' => 'yield',
+ '__class__' => '__CLASS__',
+ '__dir__' => '__DIR__',
+ '__file__' => '__FILE__',
+ '__function__' => '__FUNCTION__',
+ '__line__' => '__LINE__',
+ '__method__' => '__METHOD__',
+ '__namespace__' => '__NAMESPACE__',
+ '__trait__' => '__TRAIT__',
+ 'int' => 'int',
+ 'float' => 'float',
+ 'bool' => 'bool',
+ 'string' => 'string',
+ 'true' => 'true',
+ 'false' => 'false',
+ 'null' => 'null',
+ 'void' => 'void',
+ 'iterable' => 'iterable',
+ 'object' => 'object',
+ 'resource' => 'resource',
+ 'mixed' => 'mixed',
+ 'numeric' => 'numeric',
+ 'never' => 'never',
+
+ /*
+ * Not reserved keywords, but equally confusing when used in the context of function calls
+ * with named parameters.
+ */
+ 'parent' => 'parent',
+ 'self' => 'self',
+ ];
+
+ /**
+ * Returns an array of tokens this test wants to listen for.
+ *
+ * @since 1.0.0
+ *
+ * @return array
+ */
+ public function register()
+ {
+ return Collections::functionDeclarationTokens();
+ }
+
+ /**
+ * Processes this test, when one of its tokens is encountered.
+ *
+ * @since 1.0.0
+ *
+ * @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)
+ {
+ // Get all parameters from method signature.
+ $parameters = FunctionDeclarations::getParameters($phpcsFile, $stackPtr);
+ if (empty($parameters)) {
+ return;
+ }
+
+ $message = 'It is recommended not to use reserved keyword "%s" as function parameter name. Found: %s';
+
+ foreach ($parameters as $param) {
+ $name = \ltrim($param['name'], '$');
+ $nameLC = \strtolower($name);
+ if (isset($this->reservedNames[$nameLC]) === true) {
+ $errorCode = $nameLC . 'Found';
+ $data = [
+ $this->reservedNames[$nameLC],
+ $param['name'],
+ ];
+
+ $phpcsFile->addWarning($message, $param['token'], $errorCode, $data);
+ }
+ }
+ }
+}
diff --git a/Universal/Sniffs/OOStructures/AlphabeticExtendsImplementsSniff.php b/Universal/Sniffs/OOStructures/AlphabeticExtendsImplementsSniff.php
index 5d99519e..33c9609c 100644
--- a/Universal/Sniffs/OOStructures/AlphabeticExtendsImplementsSniff.php
+++ b/Universal/Sniffs/OOStructures/AlphabeticExtendsImplementsSniff.php
@@ -17,12 +17,12 @@
use PHPCSUtils\Utils\ObjectDeclarations;
/**
- * Verifies that the names used in a class "implements" statement or an interface "extends" statement
+ * Verifies that the interface names used in a class/enum "implements" statement or an interface "extends" statement,
* are listed in alphabetic order.
*
* @since 1.0.0
*/
-class AlphabeticExtendsImplementsSniff implements Sniff
+final class AlphabeticExtendsImplementsSniff implements Sniff
{
/**
@@ -32,7 +32,7 @@ class AlphabeticExtendsImplementsSniff implements Sniff
*
* @var string
*/
- const METRIC_NAME_ALPHA = 'Interface names in implements/extends order alphabetically (%s)';
+ const METRIC_NAME_ALPHA = 'Interface names in implements/extends ordered alphabetically (%s)';
/**
* Name of the "interface count" metric.
@@ -86,7 +86,7 @@ class AlphabeticExtendsImplementsSniff implements Sniff
*/
public function register()
{
- return (Collections::$OOCanExtend + Collections::$OOCanImplement);
+ return (Collections::ooCanExtend() + Collections::ooCanImplement());
}
/**
@@ -122,14 +122,14 @@ public function process(File $phpcsFile, $stackPtr)
/*
* Get the names.
*/
- if (isset(Collections::$OOCanImplement[$tokens[$stackPtr]['code']]) === true) {
+ if (isset(Collections::ooCanImplement()[$tokens[$stackPtr]['code']]) === true) {
$names = ObjectDeclarations::findImplementedInterfaceNames($phpcsFile, $stackPtr);
} else {
$names = ObjectDeclarations::findExtendedInterfaceNames($phpcsFile, $stackPtr);
}
if (\is_array($names) === false) {
- // Class/interface doesn't extend or implement.
+ // Class/interface/enum doesn't extend or implement.
$phpcsFile->recordMetric($stackPtr, self::METRIC_NAME_COUNT, 0);
$phpcsFile->recordMetric($stackPtr, $metricNameAlpha, 'n/a');
return;
@@ -165,7 +165,7 @@ public function process(File $phpcsFile, $stackPtr)
* Throw the error.
*/
$keyword = \T_IMPLEMENTS;
- if (isset(Collections::$OOCanImplement[$tokens[$stackPtr]['code']]) === false) {
+ if (isset(Collections::ooCanImplement()[$tokens[$stackPtr]['code']]) === false) {
$keyword = \T_EXTENDS;
}
diff --git a/Universal/Sniffs/Operators/DisallowLogicalAndOrSniff.php b/Universal/Sniffs/Operators/DisallowLogicalAndOrSniff.php
index be8b2df1..b654c280 100644
--- a/Universal/Sniffs/Operators/DisallowLogicalAndOrSniff.php
+++ b/Universal/Sniffs/Operators/DisallowLogicalAndOrSniff.php
@@ -22,9 +22,18 @@
*
* @since 1.0.0
*/
-class DisallowLogicalAndOrSniff implements Sniff
+final class DisallowLogicalAndOrSniff implements Sniff
{
+ /**
+ * Name of the metric.
+ *
+ * @since 1.0.0
+ *
+ * @var string
+ */
+ const METRIC_NAME = 'Type of and/or operator used';
+
/**
* The tokens this sniff records metrics for.
*
@@ -85,7 +94,7 @@ public function process(File $phpcsFile, $stackPtr)
$tokens = $phpcsFile->getTokens();
$tokenCode = $tokens[$stackPtr]['code'];
- $phpcsFile->recordMetric($stackPtr, 'Type of and/or operator used', $this->metricType[$tokenCode]);
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, $this->metricType[$tokenCode]);
if (isset($this->targetTokenInfo[$tokenCode]) === false) {
// Already using boolean operator.
diff --git a/Universal/Sniffs/Operators/DisallowShortTernarySniff.php b/Universal/Sniffs/Operators/DisallowShortTernarySniff.php
index 64280be6..078e70bb 100644
--- a/Universal/Sniffs/Operators/DisallowShortTernarySniff.php
+++ b/Universal/Sniffs/Operators/DisallowShortTernarySniff.php
@@ -23,9 +23,18 @@
*
* @since 1.0.0
*/
-class DisallowShortTernarySniff implements Sniff
+final class DisallowShortTernarySniff implements Sniff
{
+ /**
+ * Name of the metric.
+ *
+ * @since 1.0.0
+ *
+ * @var string
+ */
+ const METRIC_NAME = 'Ternary usage';
+
/**
* Registers the tokens that this sniff wants to listen for.
*
@@ -52,11 +61,11 @@ public function register()
public function process(File $phpcsFile, $stackPtr)
{
if (Operators::isShortTernary($phpcsFile, $stackPtr) === false) {
- $phpcsFile->recordMetric($stackPtr, 'Ternary usage', 'long');
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'long');
return;
}
- $phpcsFile->recordMetric($stackPtr, 'Ternary usage', 'short');
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'short');
$phpcsFile->addError(
'Using short ternaries is not allowed as they are rarely used correctly',
diff --git a/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php b/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php
index 094c75e9..905dd1ec 100644
--- a/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php
+++ b/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php
@@ -28,9 +28,18 @@
*
* @since 1.0.0
*/
-class DisallowStandalonePostIncrementDecrementSniff implements Sniff
+final class DisallowStandalonePostIncrementDecrementSniff implements Sniff
{
+ /**
+ * Name of the metric.
+ *
+ * @since 1.0.0
+ *
+ * @var string
+ */
+ const METRIC_NAME = 'In/decrement usage in stand-alone statements';
+
/**
* Tokens which can be expected in a stand-alone in/decrement statement.
*
@@ -53,11 +62,17 @@ class DisallowStandalonePostIncrementDecrementSniff implements Sniff
*/
public function register()
{
- $this->allowedTokens += Collections::$OOHierarchyKeywords;
- $this->allowedTokens += Collections::$objectOperators;
- $this->allowedTokens += Collections::$OONameTokens;
+ $this->allowedTokens += Collections::ooHierarchyKeywords();
+ $this->allowedTokens += Collections::objectOperators();
+ $this->allowedTokens += Collections::namespacedNameTokens();
- return Collections::$incrementDecrementOperators;
+ /*
+ * Remove nullsafe object operator. In/decrement not allowed in write context,
+ * so ignore.
+ */
+ unset($this->allowedTokens[\T_NULLSAFE_OBJECT_OPERATOR]);
+
+ return Collections::incrementDecrementOperators();
}
/**
@@ -83,19 +98,27 @@ public function process(File $phpcsFile, $stackPtr)
$start = BCFile::findStartOfStatement($phpcsFile, $stackPtr);
$end = BCFile::findEndOfStatement($phpcsFile, $stackPtr);
- if ($tokens[$end]['code'] !== \T_SEMICOLON) {
+ if (isset(Collections::incrementDecrementOperators()[$tokens[$end]['code']])) {
+ // Statement ends on a PHP close tag, set the end pointer to the close tag.
+ $end = $phpcsFile->findNext(Tokens::$emptyTokens, ($end + 1), null, true);
+ }
+
+ if ($tokens[$end]['code'] !== \T_SEMICOLON
+ && $tokens[$end]['code'] !== \T_CLOSE_TAG
+ ) {
// Not a stand-alone statement.
return $end;
}
- $counter = 0;
- $lastCode = null;
+ $counter = 0;
+ $lastCode = null;
+ $operators = Collections::incrementDecrementOperators();
for ($i = $start; $i < $end; $i++) {
if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) {
continue;
}
- if (isset(Collections::$incrementDecrementOperators[$tokens[$i]['code']]) === true) {
+ if (isset($operators[$tokens[$i]['code']]) === true) {
$lastCode = $tokens[$i]['code'];
++$counter;
continue;
@@ -139,7 +162,7 @@ public function process(File $phpcsFile, $stackPtr)
$lastNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($end - 1), $start, true);
if ($start === $stackPtr && $lastNonEmpty !== $stackPtr) {
// This is already pre-in/decrement.
- $phpcsFile->recordMetric($stackPtr, 'In/decrement usage in stand-alone statements', 'pre-' . $type);
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'pre-' . $type);
return $end;
}
@@ -148,7 +171,7 @@ public function process(File $phpcsFile, $stackPtr)
return $end;
}
- $phpcsFile->recordMetric($stackPtr, 'In/decrement usage in stand-alone statements', 'post-' . $type);
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'post-' . $type);
$error = 'Stand-alone post-%1$s statement found. Use pre-%1$s instead: %2$s.';
$errorCode = 'Post' . \ucfirst($type) . 'Found';
@@ -161,15 +184,13 @@ public function process(File $phpcsFile, $stackPtr)
$fix = $phpcsFile->addFixableError($error, $stackPtr, $errorCode, $data);
- if ($fix === false) {
- return $end;
+ if ($fix === true) {
+ $phpcsFile->fixer->beginChangeset();
+ $phpcsFile->fixer->replaceToken($stackPtr, '');
+ $phpcsFile->fixer->addContentBefore($start, $tokens[$stackPtr]['content']);
+ $phpcsFile->fixer->endChangeset();
}
- $phpcsFile->fixer->beginChangeset();
- $phpcsFile->fixer->replaceToken($stackPtr, '');
- $phpcsFile->fixer->addContentBefore($start, $tokens[$stackPtr]['content']);
- $phpcsFile->fixer->endChangeset();
-
return $end;
}
}
diff --git a/Universal/Sniffs/Operators/StrictComparisonsSniff.php b/Universal/Sniffs/Operators/StrictComparisonsSniff.php
index a2145324..c1fead35 100644
--- a/Universal/Sniffs/Operators/StrictComparisonsSniff.php
+++ b/Universal/Sniffs/Operators/StrictComparisonsSniff.php
@@ -21,9 +21,18 @@
*
* @since 1.0.0
*/
-class StrictComparisonsSniff implements Sniff
+final class StrictComparisonsSniff implements Sniff
{
+ /**
+ * Name of the metric.
+ *
+ * @since 1.0.0
+ *
+ * @var string
+ */
+ const METRIC_NAME = 'Type of comparison used';
+
/**
* The tokens this sniff records metrics for.
*
@@ -84,7 +93,7 @@ public function process(File $phpcsFile, $stackPtr)
$tokens = $phpcsFile->getTokens();
$tokenCode = $tokens[$stackPtr]['code'];
- $phpcsFile->recordMetric($stackPtr, 'Type of comparison used', $this->metricType[$tokenCode]);
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, $this->metricType[$tokenCode]);
if (isset($this->targetTokenInfo[$tokenCode]) === false) {
// Already using strict comparison operator.
diff --git a/Universal/Sniffs/Operators/TypeSeparatorSpacingSniff.php b/Universal/Sniffs/Operators/TypeSeparatorSpacingSniff.php
new file mode 100644
index 00000000..cfed01dc
--- /dev/null
+++ b/Universal/Sniffs/Operators/TypeSeparatorSpacingSniff.php
@@ -0,0 +1,85 @@
+getTokens();
+
+ $type = ($tokens[$stackPtr]['code'] === \T_TYPE_UNION) ? 'union' : 'intersection';
+ $code = \ucfirst($type) . 'Type';
+
+ $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
+ SpacesFixer::checkAndFix(
+ $phpcsFile,
+ $stackPtr,
+ $prevNonEmpty,
+ 0, // Expected spaces.
+ 'Expected %s before the ' . $type . ' type separator. Found: %s',
+ $code . 'SpacesBefore',
+ 'error',
+ 0, // Severity.
+ 'Space before ' . $type . ' type separator'
+ );
+
+ $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
+ SpacesFixer::checkAndFix(
+ $phpcsFile,
+ $stackPtr,
+ $nextNonEmpty,
+ 0, // Expected spaces.
+ 'Expected %s after the ' . $type . ' type separator. Found: %s',
+ $code . 'SpacesAfter',
+ 'error',
+ 0, // Severity.
+ 'Space after ' . $type . ' type separator'
+ );
+ }
+}
diff --git a/Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php b/Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php
new file mode 100644
index 00000000..a8fc725e
--- /dev/null
+++ b/Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php
@@ -0,0 +1,101 @@
+getTokens();
+
+ for ($endOfStatement = ($stackPtr + 1); $endOfStatement < $phpcsFile->numTokens; $endOfStatement++) {
+ if ($tokens[$endOfStatement]['code'] === \T_CLOSE_TAG
+ || $tokens[$endOfStatement]['code'] === \T_SEMICOLON
+ ) {
+ break;
+ }
+
+ // Skip over anything within parenthesis.
+ if ($tokens[$endOfStatement]['code'] === \T_OPEN_PARENTHESIS
+ && isset($tokens[$endOfStatement]['parenthesis_closer'])
+ ) {
+ $endOfStatement = $tokens[$endOfStatement]['parenthesis_closer'];
+ }
+ }
+
+ if ($endOfStatement === $phpcsFile->numTokens
+ || $tokens[$endOfStatement]['code'] === \T_CLOSE_TAG
+ ) {
+ return;
+ }
+
+ // Semi-colon, so check for any code between it and the close tag.
+ $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($endOfStatement + 1), null, true);
+ if ($nextNonEmpty === false
+ || $tokens[$nextNonEmpty]['code'] === \T_CLOSE_TAG
+ ) {
+ return;
+ }
+
+ $fix = $phpcsFile->addFixableError(
+ 'Only one statement is allowed when using short open echo PHP tags.'
+ . ' Use the "fixer->replaceToken($stackPtr, 'fixer->replaceToken($stackPtr, 'findNext(\T_STRING, ($reportPtr + 1), $endOfStatement, false, $alias);
if ($reportPtr === false) {
// Shouldn't be possible.
- continue 2;
+ continue 2; // @codeCoverageIgnore
}
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($reportPtr + 1), $endOfStatement, true);
@@ -158,7 +158,7 @@ public function process(File $phpcsFile, $stackPtr)
* in case this is a non-namespaced file.
*/
- $error = 'Use import statements for class/interface/trait%s are not allowed.';
+ $error = 'Use import statements for class/interface/trait/enum%s are not allowed.';
$error .= ' Found import statement for: "%s"';
$errorCode = 'Found';
$data = [
diff --git a/Universal/Sniffs/UseStatements/DisallowUseConstSniff.php b/Universal/Sniffs/UseStatements/DisallowUseConstSniff.php
index 8584b305..b0a69755 100644
--- a/Universal/Sniffs/UseStatements/DisallowUseConstSniff.php
+++ b/Universal/Sniffs/UseStatements/DisallowUseConstSniff.php
@@ -26,7 +26,7 @@
*
* @since 1.0.0
*/
-class DisallowUseConstSniff implements Sniff
+final class DisallowUseConstSniff implements Sniff
{
/**
@@ -133,7 +133,7 @@ public function process(File $phpcsFile, $stackPtr)
$reportPtr = $phpcsFile->findNext(\T_STRING, ($reportPtr + 1), $endOfStatement, false, $alias);
if ($reportPtr === false) {
// Shouldn't be possible.
- continue 2;
+ continue 2; // @codeCoverageIgnore
}
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($reportPtr + 1), $endOfStatement, true);
diff --git a/Universal/Sniffs/UseStatements/DisallowUseFunctionSniff.php b/Universal/Sniffs/UseStatements/DisallowUseFunctionSniff.php
index dc270966..64aaa130 100644
--- a/Universal/Sniffs/UseStatements/DisallowUseFunctionSniff.php
+++ b/Universal/Sniffs/UseStatements/DisallowUseFunctionSniff.php
@@ -26,7 +26,7 @@
*
* @since 1.0.0
*/
-class DisallowUseFunctionSniff implements Sniff
+final class DisallowUseFunctionSniff implements Sniff
{
/**
@@ -133,7 +133,7 @@ public function process(File $phpcsFile, $stackPtr)
$reportPtr = $phpcsFile->findNext(\T_STRING, ($reportPtr + 1), $endOfStatement, false, $alias);
if ($reportPtr === false) {
// Shouldn't be possible.
- continue 2;
+ continue 2; // @codeCoverageIgnore
}
$next = $phpcsFile->findNext(Tokens::$emptyTokens, ($reportPtr + 1), $endOfStatement, true);
diff --git a/Universal/Sniffs/UseStatements/LowercaseFunctionConstSniff.php b/Universal/Sniffs/UseStatements/LowercaseFunctionConstSniff.php
index 0780a689..6cfb8313 100644
--- a/Universal/Sniffs/UseStatements/LowercaseFunctionConstSniff.php
+++ b/Universal/Sniffs/UseStatements/LowercaseFunctionConstSniff.php
@@ -23,7 +23,7 @@
*
* @since 1.0.0
*/
-class LowercaseFunctionConstSniff implements Sniff
+final class LowercaseFunctionConstSniff implements Sniff
{
/**
diff --git a/Universal/Sniffs/UseStatements/NoLeadingBackslashSniff.php b/Universal/Sniffs/UseStatements/NoLeadingBackslashSniff.php
index 1dfdeea3..811210b8 100644
--- a/Universal/Sniffs/UseStatements/NoLeadingBackslashSniff.php
+++ b/Universal/Sniffs/UseStatements/NoLeadingBackslashSniff.php
@@ -24,7 +24,7 @@
*
* @since 1.0.0
*/
-class NoLeadingBackslashSniff implements Sniff
+final class NoLeadingBackslashSniff implements Sniff
{
/**
@@ -76,43 +76,95 @@ public function process(File $phpcsFile, $stackPtr)
$current = $stackPtr;
do {
- $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($current + 1), $endOfStatement, true);
- if ($nextNonEmpty === false) {
- // Reached the end of the statement.
- return;
+ $continue = $this->processImport($phpcsFile, $current, $endOfStatement);
+ if ($continue === false) {
+ break;
}
- // Skip past 'function'/'const' keyword.
- $contentLC = \strtolower($tokens[$nextNonEmpty]['content']);
- if ($tokens[$nextNonEmpty]['code'] === \T_STRING
- && ($contentLC === 'function' || $contentLC === 'const')
- ) {
- $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), $endOfStatement, true);
- if ($nextNonEmpty === false) {
- // Reached the end of the statement.
- return;
- }
- }
+ // Move the stackPtr forward to the next part of the use statement, if any.
+ $current = $phpcsFile->findNext(\T_COMMA, ($current + 1), $endOfStatement);
+ } while ($current !== false);
- if ($tokens[$nextNonEmpty]['code'] === \T_NS_SEPARATOR) {
- $phpcsFile->recordMetric($nextNonEmpty, self::METRIC_NAME, 'yes');
+ if ($tokens[$endOfStatement]['code'] !== \T_OPEN_USE_GROUP) {
+ // Finished the statement.
+ return;
+ }
- $error = 'An import use statement should never start with a leading backslash';
- $fix = $phpcsFile->addFixableError($error, $nextNonEmpty, 'LeadingBackslashFound');
+ $current = $endOfStatement; // Group open brace.
+ $endOfStatement = $phpcsFile->findNext([\T_CLOSE_USE_GROUP], ($endOfStatement + 1));
+ if ($endOfStatement === false) {
+ // Live coding or parse error.
+ return;
+ }
- if ($fix === true) {
- if ($tokens[$nextNonEmpty - 1]['code'] !== \T_WHITESPACE) {
- $phpcsFile->fixer->replaceToken($nextNonEmpty, ' ');
- } else {
- $phpcsFile->fixer->replaceToken($nextNonEmpty, '');
- }
- }
- } else {
- $phpcsFile->recordMetric($nextNonEmpty, self::METRIC_NAME, 'no');
+ do {
+ $continue = $this->processImport($phpcsFile, $current, $endOfStatement, true);
+ if ($continue === false) {
+ break;
}
// Move the stackPtr forward to the next part of the use statement, if any.
$current = $phpcsFile->findNext(\T_COMMA, ($current + 1), $endOfStatement);
} while ($current !== false);
}
+
+ /**
+ * Examine an individual import statement.
+ *
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
+ * @param int $stackPtr The position of the current token.
+ * @param int $endOfStatement End token for the current import statement.
+ * @param bool $groupUse Whether the current statement is a partial one
+ * within a group use statement.
+ *
+ * @return bool Whether or not to continue examining this import use statement.
+ */
+ private function processImport(File $phpcsFile, $stackPtr, $endOfStatement, $groupUse = false)
+ {
+ $tokens = $phpcsFile->getTokens();
+
+ $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), $endOfStatement, true);
+ if ($nextNonEmpty === false) {
+ // Reached the end of the statement.
+ return false;
+ }
+
+ // Skip past 'function'/'const' keyword.
+ $contentLC = \strtolower($tokens[$nextNonEmpty]['content']);
+ if ($tokens[$nextNonEmpty]['code'] === \T_STRING
+ && ($contentLC === 'function' || $contentLC === 'const')
+ ) {
+ $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), $endOfStatement, true);
+ if ($nextNonEmpty === false) {
+ // Reached the end of the statement.
+ return false;
+ }
+ }
+
+ if ($tokens[$nextNonEmpty]['code'] === \T_NS_SEPARATOR) {
+ $phpcsFile->recordMetric($nextNonEmpty, self::METRIC_NAME, 'yes');
+
+ $error = 'An import use statement should never start with a leading backslash';
+ $code = 'LeadingBackslashFound';
+
+ if ($groupUse === true) {
+ $error = 'Parse error: partial import use statement in a use group starting with a leading backslash';
+ $code = 'LeadingBackslashFoundInGroup';
+ }
+
+ $fix = $phpcsFile->addFixableError($error, $nextNonEmpty, $code);
+
+ if ($fix === true) {
+ if ($tokens[$nextNonEmpty - 1]['code'] !== \T_WHITESPACE) {
+ $phpcsFile->fixer->replaceToken($nextNonEmpty, ' ');
+ } else {
+ $phpcsFile->fixer->replaceToken($nextNonEmpty, '');
+ }
+ }
+ } else {
+ $phpcsFile->recordMetric($nextNonEmpty, self::METRIC_NAME, 'no');
+ }
+
+ return true;
+ }
}
diff --git a/Universal/Sniffs/WhiteSpace/AnonClassKeywordSpacingSniff.php b/Universal/Sniffs/WhiteSpace/AnonClassKeywordSpacingSniff.php
new file mode 100644
index 00000000..4cde4b26
--- /dev/null
+++ b/Universal/Sniffs/WhiteSpace/AnonClassKeywordSpacingSniff.php
@@ -0,0 +1,79 @@
+getTokens();
+ $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
+ if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== \T_OPEN_PARENTHESIS) {
+ // No parentheses, nothing to do.
+ return;
+ }
+
+ SpacesFixer::checkAndFix(
+ $phpcsFile,
+ $stackPtr,
+ $nextNonEmpty,
+ (int) $this->spacing,
+ 'There must be %1$s between the class keyword and the open parenthesis for an anonymous class. Found: %2$s',
+ 'Incorrect',
+ 'error',
+ 0,
+ 'Anon class: space between keyword and open parenthesis'
+ );
+ }
+}
diff --git a/Universal/Sniffs/WhiteSpace/DisallowInlineTabsSniff.php b/Universal/Sniffs/WhiteSpace/DisallowInlineTabsSniff.php
index e0a2b8bd..6eb2b0d2 100644
--- a/Universal/Sniffs/WhiteSpace/DisallowInlineTabsSniff.php
+++ b/Universal/Sniffs/WhiteSpace/DisallowInlineTabsSniff.php
@@ -15,6 +15,7 @@
use PHP_CodeSniffer\Util\Tokens;
use PHPCSExtra\Universal\Helpers\DummyTokenizer;
use PHPCSUtils\BackCompat\Helper;
+use PHPCSUtils\Tokens\Collections;
/**
* Enforces using spaces for mid-line alignment.
@@ -38,7 +39,7 @@
*
* @since 1.0.0
*/
-class DisallowInlineTabsSniff implements Sniff
+final class DisallowInlineTabsSniff implements Sniff
{
/**
@@ -73,10 +74,7 @@ class DisallowInlineTabsSniff implements Sniff
*/
public function register()
{
- return [
- \T_OPEN_TAG,
- \T_OPEN_TAG_WITH_ECHO,
- ];
+ return Collections::phpOpenTags();
}
/**
diff --git a/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php b/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php
new file mode 100644
index 00000000..c4f970f9
--- /dev/null
+++ b/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php
@@ -0,0 +1,441 @@
+
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * @since 1.0.0
+ *
+ * @var array
+ */
+ public $ignoreAlignmentBefore = [];
+
+ /**
+ * Whether or not potential trailing whitespace on otherwise blank lines should be examined or ignored.
+ *
+ * Defaults to `true`, i.e. ignore blank lines.
+ *
+ * It is recommended to only set this to `false` if the standard including this sniff does not
+ * include the `Squiz.WhiteSpace.SuperfluousWhitespace` sniff (which is included in most standards).
+ *
+ * @since 1.0.0
+ *
+ * @var bool
+ */
+ public $ignoreBlankLines = true;
+
+ /**
+ * The --tab-width CLI value that is being used.
+ *
+ * @since 1.0.0
+ *
+ * @var int
+ */
+ private $tabWidth;
+
+ /**
+ * Whitespace tokens and tokens which can contain leading whitespace.
+ *
+ * A few additional tokens will be added to this list in the register() method.
+ *
+ * @since 1.0.0
+ *
+ * @var array
+ */
+ private $tokensToCheck = [
+ \T_WHITESPACE => \T_WHITESPACE,
+ \T_INLINE_HTML => \T_INLINE_HTML,
+ \T_DOC_COMMENT_WHITESPACE => \T_DOC_COMMENT_WHITESPACE,
+ \T_COMMENT => \T_COMMENT,
+ \T_END_HEREDOC => \T_END_HEREDOC,
+ \T_END_NOWDOC => \T_END_NOWDOC,
+ ];
+
+ /**
+ * Returns an array of tokens this test wants to listen for.
+ *
+ * @since 1.0.0
+ *
+ * @return array
+ */
+ public function register()
+ {
+ // Add the ignore annotation tokens to the list of tokens to check.
+ $this->tokensToCheck += Tokens::$phpcsCommentTokens;
+
+ return Collections::phpOpenTags();
+ }
+
+ /**
+ * Processes this test, when one of its tokens is encountered.
+ *
+ * @since 1.0.0
+ *
+ * @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 int Integer stack pointer to skip the rest of the file.
+ */
+ public function process(File $phpcsFile, $stackPtr)
+ {
+ /*
+ * Handle the properties.
+ */
+ if (isset($this->tabWidth) === false || \defined('PHP_CODESNIFFER_IN_TESTS') === true) {
+ $this->tabWidth = Helper::getTabWidth($phpcsFile);
+ }
+
+ if (isset($this->indent) === true) {
+ $indent = (int) $this->indent;
+ } else {
+ $indent = $this->tabWidth;
+ }
+
+ $ignoreTokens = (array) $this->ignoreAlignmentBefore;
+ if (empty($ignoreTokens) === false) {
+ $ignoreTokens = \array_flip($ignoreTokens);
+ }
+
+ /*
+ * Check the whole file in one go.
+ */
+ $tokens = $phpcsFile->getTokens();
+
+ for ($i = 0; $i < $phpcsFile->numTokens; $i++) {
+ if ($tokens[$i]['column'] !== 1) {
+ // Only interested in the first token on each line.
+ continue;
+ }
+
+ if (isset($this->tokensToCheck[$tokens[$i]['code']]) === false) {
+ // Not one of the target tokens.
+ continue;
+ }
+
+ if ($tokens[$i]['content'] === $phpcsFile->eolChar) {
+ // Skip completely blank lines.
+ continue;
+ }
+
+ if (isset($ignoreTokens[$tokens[$i]['type']]) === true
+ || (isset($tokens[($i + 1)]) && isset($ignoreTokens[$tokens[($i + 1)]['type']]))
+ ) {
+ // This is one of the tokens being ignored.
+ continue;
+ }
+
+ $origContent = null;
+ if (isset($tokens[$i]['orig_content']) === true) {
+ $origContent = $tokens[$i]['orig_content'];
+ }
+
+ $spaces = 0;
+ switch ($tokens[$i]['code']) {
+ case \T_WHITESPACE:
+ if ($this->ignoreBlankLines === true
+ && isset($tokens[($i + 1)])
+ && $tokens[$i]['line'] !== $tokens[($i + 1)]['line']
+ ) {
+ // Skip blank lines which only contain trailing whitespace.
+ continue 2;
+ }
+
+ $spaces = ($tokens[$i]['length'] % $indent);
+ break;
+
+ case \T_DOC_COMMENT_WHITESPACE:
+ /*
+ * Blank lines with trailing whitespace in docblocks are tokenized as
+ * two T_DOC_COMMENT_WHITESPACE tokens: one for the trailing whitespace,
+ * one for the new line character.
+ */
+ if ($this->ignoreBlankLines === true
+ && isset($tokens[($i + 1)])
+ && $tokens[($i + 1)]['content'] === $phpcsFile->eolChar
+ && isset($tokens[($i + 2)])
+ && $tokens[$i]['line'] !== $tokens[($i + 2)]['line']
+ ) {
+ // Skip blank lines which only contain trailing whitespace.
+ continue 2;
+ }
+
+ $spaces = ($tokens[$i]['length'] % $indent);
+
+ if (isset($tokens[($i + 1)]) === true
+ && ($tokens[($i + 1)]['code'] === \T_DOC_COMMENT_STAR
+ || $tokens[($i + 1)]['code'] === \T_DOC_COMMENT_CLOSE_TAG)
+ && $spaces !== 0
+ ) {
+ // One alignment space expected before the *.
+ --$spaces;
+ }
+ break;
+
+ case \T_COMMENT:
+ case \T_INLINE_HTML:
+ if ($this->ignoreBlankLines === true
+ && \trim($tokens[$i]['content']) === ''
+ && isset($tokens[($i + 1)])
+ && $tokens[$i]['line'] !== $tokens[($i + 1)]['line']
+ ) {
+ // Skip blank lines which only contain trailing whitespace.
+ continue 2;
+ }
+
+ // Deliberate fall-through.
+
+ case \T_PHPCS_ENABLE:
+ case \T_PHPCS_DISABLE:
+ case \T_PHPCS_SET:
+ case \T_PHPCS_IGNORE:
+ case \T_PHPCS_IGNORE_FILE:
+ /*
+ * Indentation is included in the contents of the token for:
+ * - inline HTML
+ * - PHP 7.3+ flexible heredoc/nowdoc closer identifiers (see below);
+ * - subsequent lines of multi-line comments;
+ * - PHPCS native annotations when part of a multi-line comment.
+ */
+ $content = \ltrim($tokens[$i]['content']);
+ $whitespace = \str_replace($content, '', $tokens[$i]['content']);
+
+ /*
+ * If there is no content, this is a blank line in a comment or in inline HTML.
+ * In that case, use the predetermined length as otherwise the new line character
+ * at the end of the whitespace will throw the count off.
+ */
+ $length = ($content === '') ? $tokens[$i]['length'] : \strlen($whitespace);
+ $spaces = ($length % $indent);
+
+ /*
+ * For multi-line star-comments, which use (aligned) stars on subsequent
+ * lines, we don't want to trigger on the one extra space before the star.
+ *
+ * While not 100% correct, don't exclude inline HTML from this check as
+ * otherwise the sniff would trigger on multi-line /*-style inline javascript comments.
+ * This may cause false negatives as there is no check for being in a
+ *
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Warning: [4 spaces][4 spaces][extra space][extra space][extra space].
+ -> anothermethod(); // Warning: [4 spaces][4 spaces][extra space][extra space][extra space].
+
+ ?>
+
+
+ Warning: Some inline HTML with precision alignment.
+
+
+
+
+
+
+
+
diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.10.inc.fixed b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.10.inc.fixed
new file mode 100644
index 00000000..02a68088
--- /dev/null
+++ b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.10.inc.fixed
@@ -0,0 +1,6 @@
+
+
diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.11.inc b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.11.inc
new file mode 100644
index 00000000..1be28bf7
--- /dev/null
+++ b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.11.inc
@@ -0,0 +1,128 @@
+= 7.3: flexible heredoc/nowdocs.
+ *
+ * In this case, the indentation before the closing identifier will be checked.
+ * As before, the text lines will not be checked.
+ *
+ * Notes:
+ * - These tests also safeguard that for nowdoc/heredoc closers, the indent is always rounded DOWN
+ * as rounding up could cause a parse error (invalid body indentation level)!
+ * - These tests also verify correct handling of spaces vs tabs in tokens in which PHPCS
+ * may not have done the replacement natively.
+ */
+
+// Space indent.
+$heredoc = <<= 7.3: flexible heredoc/nowdocs.
+ *
+ * In this case, the indentation before the closing identifier will be checked.
+ * As before, the text lines will not be checked.
+ *
+ * Notes:
+ * - These tests also safeguard that for nowdoc/heredoc closers, the indent is always rounded DOWN
+ * as rounding up could cause a parse error (invalid body indentation level)!
+ * - These tests also verify correct handling of spaces vs tabs in tokens in which PHPCS
+ * may not have done the replacement natively.
+ */
+
+// Space indent.
+$heredoc = <<
+The line below only contains a new line char. OK.
+
+The line below has 4 spaces trailing whitespace! Ignore.
+
+The line below has 5 spaces trailing whitespace! Ignore, even though not matching tab width.
+
+
+ The line below only contains a new line char. OK.
+
+ The line below has 4 spaces trailing whitespace! Ignore.
+
+ The line below has 5 spaces trailing whitespace! Ignore, even though not matching tab width.
+
+
+The line below only contains a new line char. OK.
+
+The line below has 4 spaces trailing whitespace! Ignore.
+
+The line below has 5 spaces trailing whitespace! Ignore, even though not matching tab width.
+
+
+ The line below only contains a new line char. OK.
+
+ The line below has 4 spaces trailing whitespace! Ignore.
+
+ The line below has 5 spaces trailing whitespace! Ignore, even though not matching tab width.
+
+
+The line below only contains a new line char. OK.
+
+The line below has 4 spaces trailing whitespace! Ignore.
+
+The line below has 5 spaces trailing whitespace! Ignore, even though not matching tab width.
+
+
+ The line below only contains a new line char. OK.
+
+ The line below has 4 spaces trailing whitespace! Ignore.
+
+ The line below has 5 spaces trailing whitespace! Ignore, even though not matching tab width.
+
+ functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Ignored.
+ -> anothermethod(); // Ignored.
+
+ ?>
+
+
+ Warning: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Ignored.
+ -> anothermethod(); // Ignored.
+
+ ?>
+
+
+ Warning: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Ignored.
+ -> anothermethod(); // Ignored.
+
+ ?>
+
+
+ Warning: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Ignored.
+ -> anothermethod(); // Ignored.
+
+ ?>
+
+
+ Warning: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Warning: [4 spaces][4 spaces][extra space][extra space][extra space].
+ -> anothermethod(); // Warning: [4 spaces][4 spaces][extra space][extra space][extra space].
+
+ ?>
+
+
+ Ignored: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Warning: [4 spaces][4 spaces][extra space][extra space][extra space].
+ -> anothermethod(); // Warning: [4 spaces][4 spaces][extra space][extra space][extra space].
+
+ ?>
+
+
+ Ignored: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Warning: [tab][tab][space][space][space].
+ -> anothermethod(); // Warning: [tab][tab][space][space][space].
+
+ ?>
+
+
+ Ignored: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Warning: [tab][tab][space][space][space].
+ -> anothermethod(); // Warning: [tab][tab][space][space][space].
+
+ ?>
+
+
+ Ignored: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Warning: [4 spaces][4 spaces][extra space][extra space][extra space].
+ -> anothermethod(); // Warning: [4 spaces][4 spaces][extra space][extra space][extra space].
+
+ ?>
+
+
+ Warning: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Warning: [4 spaces][4 spaces][extra space][extra space][extra space].
+ -> anothermethod(); // Warning: [4 spaces][4 spaces][extra space][extra space][extra space].
+
+ ?>
+
+
+ Warning: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Warning: [3 spaces][3 spaces][extra space].
+ -> anothermethod(); // Warning: [3 spaces][3 spaces][extra space].
+
+ ?>
+
+
+ Warning: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Warning: [3 spaces][3 spaces][extra space].
+ -> anothermethod(); // Warning: [3 spaces][3 spaces][extra space].
+
+ ?>
+
+
+ Warning: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Warning: [2 spaces][2 spaces][2 spaces][2 spaces][extra space].
+ -> anothermethod(); // Warning: [2 spaces][2 spaces][2 spaces][2 spaces][extra space].
+
+ ?>
+
+
+ Warning: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Warning: [2 spaces][2 spaces][2 spaces][2 spaces][extra space].
+ -> anothermethod(); // Warning: [2 spaces][2 spaces][2 spaces][2 spaces][extra space].
+
+ ?>
+
+
+ Warning: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Warning: [tab][tab][space][space][space].
+ -> anothermethod(); // Warning: [tab][tab][space][space][space].
+
+ ?>
+
+
+ Warning: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Warning: [tab][tab][space][space][space].
+ -> anothermethod(); // Warning: [tab][tab][space][space][space].
+
+ ?>
+
+
+ Warning: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Warning: [tab][tab][space][space][space].
+ -> anothermethod(); // Warning: [tab][tab][space][space][space].
+
+ ?>
+
+
+ Warning: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Warning: [tab][tab][space][space][space].
+ -> anothermethod(); // Warning: [tab][tab][space][space][space].
+
+ ?>
+
+
+ Warning: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Warning: [tab][tab][space].
+ -> anothermethod(); // Warning: [tab][tab][space][space].
+
+ ?>
+
+
+ Warning: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Warning: [tab][tab][space].
+ -> anothermethod(); // Warning: [tab][tab][space][space].
+
+ ?>
+
+
+ Warning: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Warning: [tab][tab][space][space][space].
+ -> anothermethod(); // Warning: [tab][tab][space][space][space].
+
+ ?>
+
+
+ Warning: Some inline HTML with precision alignment.
+
+
+
+
+
+
+functioncall()
+ -> chained()
+ -> anothermethod();
+
+ $object->functioncall()
+ -> chained() // Warning: [tab][tab][space][space][space].
+ -> anothermethod(); // Warning: [tab][tab][space][space][space].
+
+ ?>
+
+
+ Warning: Some inline HTML with precision alignment.
+
+
+
+
+
+
+
+
diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.9.inc.fixed b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.9.inc.fixed
new file mode 100644
index 00000000..79df0922
--- /dev/null
+++ b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.9.inc.fixed
@@ -0,0 +1,6 @@
+
+
diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.php b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.php
new file mode 100644
index 00000000..a0cf0d58
--- /dev/null
+++ b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.php
@@ -0,0 +1,221 @@
+ File name => tab width to set.
+ */
+ private $tabBasedFiles = [
+ 'PrecisionAlignmentUnitTest.5.inc' => 4,
+ 'PrecisionAlignmentUnitTest.6.inc' => 4,
+ 'PrecisionAlignmentUnitTest.7.inc' => 3,
+ 'PrecisionAlignmentUnitTest.8.inc' => 2,
+ 'PrecisionAlignmentUnitTest.10.inc' => 4,
+ 'PrecisionAlignmentUnitTest.11.inc' => 4,
+ 'PrecisionAlignmentUnitTest.15.inc' => 4,
+ 'PrecisionAlignmentUnitTest.17.inc' => 4,
+ 'PrecisionAlignmentUnitTest.2.css' => 4,
+ 'PrecisionAlignmentUnitTest.2.js' => 4,
+ ];
+
+ /**
+ * Set CLI values before the file is tested.
+ *
+ * @param string $testFile The name of the file being tested.
+ * @param \PHP_CodeSniffer\Config $config The config data for the test run.
+ *
+ * @return void
+ */
+ public function setCliValues($testFile, $config)
+ {
+ if (isset($this->tabBasedFiles[$testFile]) === true) {
+ $config->tabWidth = $this->tabBasedFiles[$testFile];
+ } else {
+ $config->tabWidth = 0;
+ }
+
+ // Testing a file with "--ignore-annotations".
+ if ($testFile === 'PrecisionAlignmentUnitTest.18.inc') {
+ $config->annotations = false;
+ } else {
+ $config->annotations = true;
+ }
+ }
+
+ /**
+ * Returns the lines where errors should occur.
+ *
+ * @return array
+ */
+ public function getErrorList()
+ {
+ return [];
+ }
+
+ /**
+ * Returns the lines where warnings should occur.
+ *
+ * @param string $testFile The name of the file being tested.
+ *
+ * @return array
+ */
+ public function getWarningList($testFile = '')
+ {
+ $phpcsVersion = Helper::getVersion();
+ $isPhpcs4 = \version_compare($phpcsVersion, '3.99.99', '>');
+
+ switch ($testFile) {
+ case 'PrecisionAlignmentUnitTest.1.inc': // Space-based, default indent.
+ case 'PrecisionAlignmentUnitTest.2.inc': // Space-based, custom indent 4.
+ case 'PrecisionAlignmentUnitTest.3.inc': // Space-based, custom indent 3.
+ case 'PrecisionAlignmentUnitTest.5.inc': // Tab-based, default indent, tab-width 4.
+ case 'PrecisionAlignmentUnitTest.6.inc': // Tab-based, custom indent 4, tab-width 4.
+ case 'PrecisionAlignmentUnitTest.7.inc': // Tab-based, custom indent 3, tab-width 3.
+ return [
+ 23 => 1,
+ 29 => 1,
+ 36 => 1,
+ 39 => 1,
+ 40 => 1,
+ 41 => 1,
+ 48 => 1,
+ 49 => 1,
+ 51 => 1,
+ 54 => 1,
+ 57 => 1,
+ 81 => 1,
+ ];
+
+ case 'PrecisionAlignmentUnitTest.4.inc': // Space-based, custom indent 2.
+ case 'PrecisionAlignmentUnitTest.8.inc': // Tab-based, custom indent 2, tab-width 2.
+ return [
+ 29 => 1,
+ 39 => 1,
+ 40 => 1,
+ 41 => 1,
+ 48 => 1,
+ 49 => 1,
+ 51 => 1,
+ 54 => 1,
+ 57 => 1,
+ 81 => 1,
+ ];
+
+ // Verify handling of files only containing short open echo tags.
+ case 'PrecisionAlignmentUnitTest.9.inc': // Space-based.
+ case 'PrecisionAlignmentUnitTest.10.inc': // Tab-based.
+ return [
+ 2 => 1,
+ 3 => 1,
+ 4 => 1,
+ 5 => 1,
+ 6 => 1,
+ ];
+
+ // Verify handling of hereodc/nowdocs closing identifiers.
+ case 'PrecisionAlignmentUnitTest.11.inc':
+ if (\PHP_VERSION_ID >= 70300) {
+ return [
+ 77 => 1,
+ 83 => 1,
+ 88 => 1,
+ 93 => 1,
+ 112 => 1,
+ 118 => 1,
+ 123 => 1,
+ 128 => 1,
+ ];
+ }
+
+ // PHP 7.2 or lower: PHP version which doesn't support flexible heredocs/nowdocs yet.
+ return [];
+
+ case 'PrecisionAlignmentUnitTest.12.inc':
+ // Testing that precision alignment on blank lines is ignored.
+ return [];
+
+ case 'PrecisionAlignmentUnitTest.13.inc':
+ // Testing that precision alignment on blank lines is NOT ignored.
+ return [
+ 19 => 1,
+ 26 => 1,
+ 35 => 1,
+ 44 => 1,
+ 57 => 1,
+ 64 => 1,
+ 73 => 1,
+ 82 => 1,
+ ];
+
+ // Testing ignoring the indentation before certain tokens.
+ case 'PrecisionAlignmentUnitTest.14.inc': // Space-based.
+ case 'PrecisionAlignmentUnitTest.15.inc': // Tab-based.
+ return [
+ 23 => 1,
+ 29 => 1,
+ 36 => 1,
+ 51 => 1,
+ 54 => 1,
+ 81 => 1,
+ ];
+
+ // Testing ignoring the indentation before certain tokens.
+ case 'PrecisionAlignmentUnitTest.16.inc': // Space-based.
+ case 'PrecisionAlignmentUnitTest.17.inc': // Tab-based.
+ return [
+ 39 => 1,
+ 40 => 1,
+ 41 => 1,
+ 48 => 1,
+ 49 => 1,
+ 51 => 1,
+ ];
+
+ // Verify detection of precision alignment for ignore annotation lines.
+ case 'PrecisionAlignmentUnitTest.18.inc':
+ return [
+ 4 => 1,
+ ];
+
+ case 'PrecisionAlignmentUnitTest.1.css': // Space-based.
+ case 'PrecisionAlignmentUnitTest.2.css': // Tab-based.
+ return [
+ 5 => ($isPhpcs4 === true ? 0 : 1),
+ ];
+
+ case 'PrecisionAlignmentUnitTest.1.js': // Space-based.
+ case 'PrecisionAlignmentUnitTest.2.js': // Tab-based.
+ return [
+ 5 => ($isPhpcs4 === true ? 0 : 1),
+ 6 => ($isPhpcs4 === true ? 0 : 1),
+ 7 => ($isPhpcs4 === true ? 0 : 1),
+ ];
+
+ default:
+ return [];
+ }
+ }
+}
diff --git a/composer.json b/composer.json
index 6685590d..8d5d0f56 100644
--- a/composer.json
+++ b/composer.json
@@ -2,7 +2,7 @@
"name" : "phpcsstandards/phpcsextra",
"description" : "A collection of sniffs and standards for use with PHP_CodeSniffer.",
"type" : "phpcodesniffer-standard",
- "keywords" : [ "phpcs", "phpcbf", "standards", "php_codesniffer", "phpcodesniffer-standard" ],
+ "keywords" : [ "phpcs", "phpcbf", "standards", "static analysis", "php_codesniffer", "phpcodesniffer-standard" ],
"license" : "LGPL-3.0-or-later",
"authors" : [
{
@@ -21,47 +21,37 @@
},
"require" : {
"php" : ">=5.4",
- "squizlabs/php_codesniffer" : "^3.3.1",
+ "squizlabs/php_codesniffer" : "^3.7.1",
"dealerdirect/phpcodesniffer-composer-installer" : "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7",
"phpcsstandards/phpcsutils" : "^1.0 || dev-develop"
},
"require-dev" : {
- "php-parallel-lint/php-parallel-lint": "^1.0",
- "php-parallel-lint/php-console-highlighter": "^0.5",
- "phpcsstandards/phpcsdevtools": "^1.0",
+ "php-parallel-lint/php-parallel-lint": "^1.3.2",
+ "php-parallel-lint/php-console-highlighter": "^1.0",
+ "phpcsstandards/phpcsdevcs": "^1.1.5",
+ "phpcsstandards/phpcsdevtools": "^1.2.0",
"phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0"
},
+ "extra": {
+ "branch-alias": {
+ "dev-stable": "1.x-dev",
+ "dev-develop": "1.x-dev"
+ }
+ },
"minimum-stability": "dev",
"prefer-stable": true,
"scripts" : {
- "install-standards": [
- "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin::run"
- ],
"lint": [
- "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --exclude vendor --exclude .git"
- ],
- "install-devcs": [
- "composer global require --dev phpcsstandards/phpcsdevcs:\"^1.0\" --no-suggest"
- ],
- "remove-devcs": [
- "composer global remove --dev phpcsstandards/phpcsdevcs"
+ "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . --show-deprecated -e php --exclude vendor --exclude .git"
],
"checkcs": [
- "@install-devcs",
- "phpcs",
- "@remove-devcs"
+ "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs"
],
"fixcs": [
- "@install-devcs",
- "phpcbf",
- "@remove-devcs"
- ],
- "travis-checkcs": [
- "@install-devcs",
- "/home/travis/.composer/vendor/bin/phpcs"
+ "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf"
],
"check-complete": [
- "@php ./vendor/phpcsstandards/phpcsdevtools/bin/phpcs-check-feature-completeness ./NormalizedArrays ./Universal"
+ "@php ./vendor/phpcsstandards/phpcsdevtools/bin/phpcs-check-feature-completeness ./Modernize ./NormalizedArrays ./Universal"
],
"test": [
"@php ./vendor/phpunit/phpunit/phpunit --filter PHPCSExtra --no-coverage ./vendor/squizlabs/php_codesniffer/tests/AllTests.php"
@@ -72,5 +62,10 @@
"coverage-local": [
"@php ./vendor/phpunit/phpunit/phpunit --filter PHPCSExtra ./vendor/squizlabs/php_codesniffer/tests/AllTests.php --coverage-html ./build/coverage-html"
]
+ },
+ "config": {
+ "allow-plugins": {
+ "dealerdirect/phpcodesniffer-composer-installer": true
+ }
}
}
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index a410e719..fd117cfe 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -44,9 +44,6 @@
-
-
-