diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000000..edc525d281 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,21 @@ +codecov: + notify: + after_n_builds: 4 + +coverage: + round: nearest + # Status will be green when coverage is between 90 and 100%. + range: "90...100" + status: + project: + default: + target: auto + threshold: 1% + paths: + - "WordPress" + patch: off + +ignore: + - "WordPress/Tests" + +comment: false diff --git a/.gitattributes b/.gitattributes index 5e6e2b7903..3472eccbfa 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,20 +1,23 @@ # # Exclude these files from release archives. # This will also make them unavailable when using Composer with `--prefer-dist`. -# If you develop for WPCS using Composer, use `--prefer-source`. +# If you develop for WordPressCS using Composer, use `--prefer-source`. # https://blog.madewithlove.be/post/gitattributes/ # -/.travis.yml export-ignore -/.phpcs.xml.dist export-ignore -/phpunit.xml.dist export-ignore -/.github export-ignore -/bin export-ignore -/Test export-ignore -/WordPress/Tests export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.codecov.yml export-ignore +/.phpcs.xml.dist export-ignore +/CODE_OF_CONDUCT.md export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/.github export-ignore +/Tests export-ignore +/WordPress/Tests export-ignore # # Auto detect text files and perform LF normalization -# http://davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/ +# https://pablorsk.medium.com/be-a-git-ninja-the-gitattributes-file-e58c07c9e915 # * text=auto diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index a497a78486..e65dae451a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -11,17 +11,20 @@ Bug reports containing a minimal code sample which can be used to reproduce the ## Upstream Issues -Since WPCS employs many sniffs that are part of PHPCS, sometimes an issue will be caused by a bug in PHPCS and not in WPCS itself. If the error message in question doesn't come from a sniff whose name starts with `WordPress`, the issue is probably a bug in PHPCS itself, and should be [reported there](https://github.com/squizlabs/PHP_CodeSniffer/issues). +Since WordPressCS employs many sniffs that are part of PHP_CodeSniffer itself or PHPCSExtra, sometimes an issue will be caused by a bug in PHPCS or PHPCSExtra and not in WordPressCS itself. +If the error message in question doesn't come from a sniff whose name starts with `WordPress`, the issue is probably a bug in PHPCS or PHPCSExtra. + +* Bugs for sniffs starting with `Generic`, `PEAR`, `PSR1`, `PSR2`, `PSR12`, `Squiz` or `Zend` should be [reported to PHPCS](https://github.com/squizlabs/PHP_CodeSniffer/issues). +* Bugs for sniffs starting with `Modernize`, `NormalizedArrays` or `Universal` should be [reported to PHPCSExtra](https://github.com/PHPCSStandards/PHPCSExtra/issues). # Contributing patches and new features ## Branches -Ongoing development will be done in the `develop` branch with merges done into `master` once considered stable. - -To contribute an improvement to this project, fork the repo and open a pull request to the `develop` branch. Alternatively, if you have push access to this repo, create a feature branch prefixed by `feature/` and then open an intra-repo PR from that branch to `develop`. +Ongoing development will be done in the `develop` branch with merges to `main` once considered stable. -Once a commit is made to `develop`, a PR should be opened from `develop` into `master` and named "Next release". This PR will provide collaborators with a forum to discuss the upcoming stable release. +To contribute an improvement to this project, fork the repo, run `composer install`, make your changes to the code, run the unit tests and code style checks by running `composer check-all`, and if all is good, open a pull request to the `develop` branch. +Alternatively, if you have push access to this repo, create a feature branch prefixed by `feature/` and then open an intra-repo PR from that branch to `develop`. # Considerations when writing sniffs @@ -30,122 +33,72 @@ Once a commit is made to `develop`, a PR should be opened from `develop` into `m When writing sniffs, always remember that any `public` sniff property can be overruled via a custom ruleset by the end-user. Only make a property `public` if that is the intended behaviour. -When you introduce new `public` sniff properties, or your sniff extends a class from which you inherit a `public` property, please don't forget to update the [public properties wiki page](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties) with the relevant details once your PR has been merged into the `develop` branch. - -## Whitelist comments - -> **Important**: -> PHPCS 3.2.0 introduced new selective ignore annotations, which can be considered an improved version of the whitelist mechanism which WPCS contains. -> -> There is a [tentative intention to drop support for the WPCS native whitelist comments](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/issues/1048#issuecomment-340698249) in WPCS 2.0.0. -> -> Considering that, the introduction of new whitelist comments is discouraged. -> -> The below information remains as guidance for exceptional cases and to aid in understanding the previous implementation. - -Sometimes, a sniff will flag code which upon further inspection by a human turns out to be OK. - -If the sniff you are writing is susceptible to this, please consider adding the ability to [whitelist lines of code](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Whitelisting-code-which-flags-errors). - -To this end, the `WordPress\Sniff::has_whitelist_comment()` method was introduced. - -Example usage: -```php -namespace WordPress\Sniffs\Security; - -use WordPress\Sniff; - -class NonceVerificationSniff extends Sniff { - - public function process_token( $stackPtr ) { - - // Check something. - - if ( $this->has_whitelist_comment( 'CSRF', $stackPtr ) ) { - return; - } - - $this->phpcsFile->addError( ... ); - } -} -``` - -When you introduce a new whitelist comment, please don't forget to update the [whitelisting code wiki page](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Whitelisting-code-which-flags-errors) with the relevant details once your PR has been merged into the `develop` branch. - +When you introduce new `public` sniff properties, or your sniff extends a class from which you inherit a `public` property, please don't forget to update the [public properties wiki page](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties) with the relevant details once your PR has been merged into the `develop` branch. # Unit Testing ## Pre-requisites * WordPress-Coding-Standards -* PHP_CodeSniffer 2.9.x or 3.x +* PHP_CodeSniffer 3.7.2 or higher +* PHPCSUtils 1.0.8 or higher +* PHPCSExtra 1.1.0 or higher * PHPUnit 4.x, 5.x, 6.x or 7.x -The WordPress Coding Standards use the PHP_CodeSniffer native unit test suite for unit testing the sniffs. - -Presuming you have installed PHP_CodeSniffer and the WordPress-Coding-Standards as [noted in the README](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards#how-to-use-this), all you need now is `PHPUnit`. - -N.B.: If you installed WPCS using Composer, make sure you used `--prefer-source` or run `composer install --prefer-source` now to make sure the unit tests are available. +The WordPress Coding Standards use the `PHP_CodeSniffer` native unit test framework for unit testing the sniffs. -If you already have PHPUnit installed on your system: Congrats, you're all set. +## Getting ready to test -If not, you can navigate to the directory where the `PHP_CodeSniffer` repo is checked out and do `composer install` to install the `dev` dependencies. -Alternatively, you can [install PHPUnit](https://phpunit.de/manual/5.7/en/installation.html) as a PHAR file. +Presuming you have cloned WordPressCS for development, to run the unit tests you need to make sure you have run `composer install` from the root directory of your WordPressCS git clone. -## Before running the unit tests +## Custom develop setups -N.B.: _If you used Composer to install the WordPress Coding Standards, you can skip this step._ - -For the unit tests to work, you need to make sure PHPUnit can find your `PHP_CodeSniffer` install. - -The easiest way to do this is to add a `phpunit.xml` file to the root of your WPCS installation and set a `PHPCS_DIR` environment variable from within this file. Make sure to adjust the path to reflect your local setup. -```xml - - - - - -``` +If you are developing with a stand-alone PHP_CodeSniffer (git clone) installation and want to use that git clone to test WordPressCS, there are three extra things you need to do: +1. Install [PHPCSUtils](https://github.com/PHPCSStandards/PHPCSUtils). + If you are using a git clone of PHPCS, you may want to `git clone` PHPCSUtils as well. +2. Register PHPCSUtils with your stand-alone PHP_CodeSniffer installation by running the following commands: + ```bash + phpcs --config-show + ``` + Copy the value from "installed_paths" and add the path to your local install of PHPCSUtils to it (and the path to WordPressCS if it's not registered with PHPCS yet). + Now use the adjusted value to run: + ```bash + phpcs --config-set installed_paths /path/1,/path/2,/path/3 + ``` +3. Make sure PHPUnit can find your `PHP_CodeSniffer` install. + The most straight-forward way to do this is to add a `phpunit.xml` file to the root of your WordPressCS installation and set a `PHPCS_DIR` environment variable from within this file. + Copy the existing `phpunit.xml.dist` file and add the below `` directive within the `` section. Make sure to adjust the path to reflect your local setup. + ```xml + + + + ``` ## Running the unit tests -The WordPress Coding Standards are compatible with both PHPCS 2.x as well as 3.x. This has some implications for running the unit tests. +From the root of your WordPressCS install, run the unit tests like so: +```bash +composer run-tests -* Make sure you have registered the directory in which you installed WPCS with PHPCS using; - ```sh - phpcs --config-set installed_paths path/to/WPCS - ``` -* Navigate to the directory in which you installed WPCS. -* To run the unit tests with PHPCS 3.x: - ```sh - phpunit --bootstrap="./Test/phpcs3-bootstrap.php" --filter WordPress /path/to/PHP_CodeSniffer/tests/AllTests.php - ``` -* To run the unit tests with PHPCS 2.x: - ```sh - phpunit --bootstrap="./Test/phpcs2-bootstrap.php" --filter WordPress ./Test/AllTests.php - ``` +# Or if you want to use a globally installed version of PHPUnit: +phpunit --filter WordPress /path/to/PHP_CodeSniffer/tests/AllTests.php +``` Expected output: ``` -PHPUnit 6.5.8 by Sebastian Bergmann and contributors. +PHPUnit 7.5.20 by Sebastian Bergmann and contributors. -Runtime: PHP 7.2.7 with Xdebug 2.6.0 -Configuration: /WordPressCS/phpunit.xml +Runtime: PHP 7.4.33 +Configuration: /WordPressCS/phpunit.xml.dist -................................................................. 65 / 77 ( 84%) -............ 77 / 77 (100%) +......................................................... 57 / 57 (100%) -Tests generated 576 unique error codes; 51 were fixable (8.85%) +201 sniff test files generated 744 unique error codes; 50 were fixable (6%) -Time: 22.93 seconds, Memory: 40.00MB +Time: 10.19 seconds, Memory: 40.00 MB -OK (77 tests, 0 assertions) +OK (57 tests, 0 assertions) ``` -[![asciicast](https://asciinema.org/a/98078.png)](https://asciinema.org/a/98078) - ## Unit Testing conventions If you look inside the `WordPress/Tests` subdirectory, you'll see the structure mimics the `WordPress/Sniffs` subdirectory structure. For example, the `WordPress/Sniffs/PHP/POSIXFunctionsSniff.php` sniff has its unit test class defined in `WordPress/Tests/PHP/POSIXFunctionsUnitTest.php` which checks the `WordPress/Tests/PHP/POSIXFunctionsUnitTest.inc` test case file. See the file naming convention? @@ -153,17 +106,16 @@ If you look inside the `WordPress/Tests` subdirectory, you'll see the structure Lets take a look at what's inside `POSIXFunctionsUnitTest.php`: ```php -... -namespace WordPress\Tests\PHP; +namespace WordPressCS\WordPress\Tests\PHP; use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; -class POSIXFunctionsUnitTest extends AbstractSniffUnitTest { +final class POSIXFunctionsUnitTest extends AbstractSniffUnitTest { /** * Returns the lines where errors should occur. * - * @return array => + * @return array Key is the line number, value is the number of expected errors. */ public function getErrorList() { return array( @@ -175,17 +127,18 @@ class POSIXFunctionsUnitTest extends AbstractSniffUnitTest { 24 => 1, 26 => 1, ); - } -... + + ... +} ``` -Also note the class name convention. The method `getErrorList()` MUST return an array of line numbers indicating errors (when running `phpcs`) found in `WordPress/Tests/PHP/POSIXFunctionsUnitTest.inc`. -If you run: +Also note the class name convention. The method `getErrorList()` MUST return an array of line numbers indicating errors (when running `phpcs`) found in `WordPress/Tests/PHP/POSIXFunctionsUnitTest.inc`. Similarly, the `getWarningList()` method must return an array of line numbers with the number of expected warnings. + +If you run the following from the root directory of your WordPressCS clone: ```sh -$ cd /path-to-cloned/phpcs -$ ./bin/phpcs --standard=Wordpress -s /path/to/WordPress/Tests/PHP/POSIXFunctionsUnitTest.inc --sniffs=WordPress.PHP.POSIXFunctions +$ "vendor/bin/phpcs" --standard=Wordpress -s ./Tests/PHP/POSIXFunctionsUnitTest.inc --sniffs=WordPress.PHP.POSIXFunctions ... -------------------------------------------------------------------------------- FOUND 7 ERRORS AFFECTING 7 LINES @@ -196,23 +149,23 @@ FOUND 7 ERRORS AFFECTING 7 LINES 16 | ERROR | eregi() has been deprecated since PHP 5.3 and removed in PHP 7.0, | | please use preg_match() instead. | | (WordPress.PHP.POSIXFunctions.ereg_eregi) - 18 | ERROR | ereg_replace() has been deprecated since PHP 5.3 and removed in PHP - | | 7.0, please use preg_replace() instead. + 18 | ERROR | ereg_replace() has been deprecated since PHP 5.3 and removed in + | | PHP 7.0, please use preg_replace() instead. | | (WordPress.PHP.POSIXFunctions.ereg_replace_ereg_replace) - 20 | ERROR | eregi_replace() has been deprecated since PHP 5.3 and removed in PHP - | | 7.0, please use preg_replace() instead. + 20 | ERROR | eregi_replace() has been deprecated since PHP 5.3 and removed in + | | PHP 7.0, please use preg_replace() instead. | | (WordPress.PHP.POSIXFunctions.ereg_replace_eregi_replace) 22 | ERROR | split() has been deprecated since PHP 5.3 and removed in PHP 7.0, | | please use explode(), str_split() or preg_split() instead. | | (WordPress.PHP.POSIXFunctions.split_split) - 24 | ERROR | spliti() has been deprecated since PHP 5.3 and removed in PHP 7.0, - | | please use explode(), str_split() or preg_split() instead. - | | (WordPress.PHP.POSIXFunctions.split_spliti) - 26 | ERROR | sql_regcase() has been deprecated since PHP 5.3 and removed in PHP - | | 7.0, please use preg_match() instead. + 24 | ERROR | spliti() has been deprecated since PHP 5.3 and removed in PHP + | | 7.0, please use explode(), str_split() or preg_split() + | | instead. (WordPress.PHP.POSIXFunctions.split_spliti) + 26 | ERROR | sql_regcase() has been deprecated since PHP 5.3 and removed in + | | PHP 7.0, please use preg_match() instead. | | (WordPress.PHP.POSIXFunctions.ereg_sql_regcase) -------------------------------------------------------------------------------- -.... +... ``` You'll see the line number and number of ERRORs we need to return in the `getErrorList()` method. @@ -220,4 +173,4 @@ The `--sniffs=...` directive limits the output to the sniff you are testing. ## Code Standards for this project -The sniffs and test files - not test _case_ files! - for WPCS should be written such that they pass the `WordPress-Extra` and the `WordPress-Docs` code standards using the custom ruleset as found in `/.phpcs.xml.dist`. +The sniffs and test files - not test _case_ files! - for WordPressCS should be written such that they pass the `WordPress-Extra` and the `WordPress-Docs` code standards using the custom ruleset as found in `/.phpcs.xml.dist`. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 5fec9e2c30..7e7884e138 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,6 +4,11 @@ about: Create a report to help us improve --- + + ## Bug Description ## Minimal Code Snippet - + +The issue happens when running this command: +```bash +phpcs ... +``` + +... over a file containing this code: ```php // Place your code sample here. ``` -For bugs with fixers: How was the code fixed? How did you expect the code to be fixed? + + +The file was auto-fixed via `phpcbf` to: +```php +// Place your code sample here. +``` + +... while I expected the code to be fixed to: +```php +// Place your code sample here. +``` ## Error Code +## Custom Ruleset + + +```xml + + + ... + +``` + ## Environment + -| Question | Answer -| ------------------------| ------- -| PHP version | x.y.z -| PHP_CodeSniffer version | x.y.z -| WPCS version | x.y.z -| WPCS install type | e.g. Composer global, Composer project local, git clone, other (please expand) -| IDE (if relevant) | Name and version e.g. PhpStorm 2018.2.2 +| Question | Answer +| ------------------------ | ------- +| PHP version | x.y.z +| PHP_CodeSniffer version | x.y.z +| WordPressCS version | x.y.z +| PHPCSUtils version | x.y.z +| PHPCSExtra version | x.y.z +| WordPressCS install type | e.g. Composer global, Composer project local, other (please expand) +| IDE (if relevant) | Name and version e.g. PhpStorm 2018.2.2 ## Additional Context (optional) -## Tested Against `develop` branch? -- [ ] I have verified the issue still exists in the `develop` branch of WPCS. +## Tested Against `develop` Branch? +- [ ] I have verified the issue still exists in the `develop` branch of WordPressCS. diff --git a/.github/ISSUE_TEMPLATE/dependency-change.md b/.github/ISSUE_TEMPLATE/dependency-change.md index 740abf51ad..0dfbf51ed3 100644 --- a/.github/ISSUE_TEMPLATE/dependency-change.md +++ b/.github/ISSUE_TEMPLATE/dependency-change.md @@ -1,14 +1,14 @@ --- name: Dependency Change -about: A reminder to take action when a WPCS dependency changes +about: A reminder to take action when a WordPressCS dependency changes --- - + ## Rationale - + ## References diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 34b43e7bf8..d3a6bf3d42 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -17,3 +17,5 @@ And preferably also code samples of code which shouldn't be flagged. ## Additional context (optional) + +- [ ] I intend to create a pull request to implement this feature. diff --git a/.github/issue_template.md b/.github/issue_template.md deleted file mode 100644 index f6e4f49872..0000000000 --- a/.github/issue_template.md +++ /dev/null @@ -1,11 +0,0 @@ - \ No newline at end of file diff --git a/.github/release-checklist.md b/.github/release-checklist.md new file mode 100644 index 0000000000..3c39e21ed0 --- /dev/null +++ b/.github/release-checklist.md @@ -0,0 +1,70 @@ +# Template to use for release PRs from `develop` to `main` + +:warning: **DO NOT MERGE (YET)** :warning: + +**Please **do** add approvals if you agree as otherwise we won't be able to release.** + +PR for tracking changes for the x.x.x release. Target release date: **DOW MONTH DAY YEAR**. + +## Release checklist + +### General + +- [ ] Verify, and if necessary, update the allowed version ranges for various dependencies in the `composer.json` - PR #xxx +- [ ] PHPCS: check if there have been [releases][phpcs-releases] since the last WordPressCS release and check through the changelog to see if there is anything WordPressCS could take advantage of - PR #xxx +- [ ] PHPCSUtils: check if there have been [releases][phpcsutils-releases] since the last WordPressCS release and update WordPressCS code to take advantage of any new utilities - PR #xxx +- [ ] PHPCSExtra: check if there have been [releases][phpcsextra-releases] since the last WordPressCS release and check through the changelog to see if there is anything WordPressCS could take advantage of - PR #xxx +- [ ] Check if the minimum WP version property needs updating in `MinimumWPVersionTrait::$default_minimum_wp_version` and if so, action it - PR #xxx +- [ ] Check if any of the list based sniffs need updating and if so, action it. + :pencil2: Make sure the "last updated" annotation in the docblocks for these lists has also been updated! + List based sniffs: + - [ ] `WordPress.WP.ClassNameCase` - PR #xxx + - [ ] `WordPress.WP.DeprecatedClasses` - PR #xxx + - [ ] `WordPress.WP.DeprecatedFunctions` - PR #xxx + - [ ] `WordPress.WP.DeprecatedParameters` - PR #xxx + - [ ] `WordPress.WP.DeprecatedParameterValues` - PR #xxx +- [ ] Check if any of the other lists containing information about WP Core need updating and if so, action it. + - [ ] `$allowed_core_constants` in `WordPress.NamingConventions.PrefixAllGlobals` - PR #xxx + - [ ] `$pluggable_functions` in `WordPress.NamingConventions.PrefixAllGlobals` - PR #xxx + - [ ] `$pluggable_classes` in `WordPress.NamingConventions.PrefixAllGlobals` - PR #xxx + - [ ] `$target_functions` in `WordPress.Security.PluginMenuSlug` - PR #xxx + - [ ] `$reserved_names` in `WordPress.NamingConventions.ValidPostTypeSlug` - PR #xxx + - [ ] `$wp_time_constants` in `WordPress.WP.CronInterval` - PR #xxx + - [ ] `$known_test_classes` in `IsUnitTestTrait` - PR #xxx + - [ ] ...etc... + +### Release prep + +- [ ] Add changelog for the release - PR #xxx + :pencil2: Remember to add a release link at the bottom! +- [ ] Update `README` (if applicable) - PR #xxx +- [ ] Update wiki (new customizable properties etc.) (if applicable) + +### Release + +- [ ] Merge this PR. +- [ ] Make sure all CI builds are green. +- [ ] Tag and create a release against `main` (careful, GH defaults to `develop`!) & copy & paste the changelog to it. + :pencil2: Check if anything from the link collection at the bottom of the changelog needs to be copied in! +- [ ] Make sure all CI builds are green. +- [ ] Close the milestone. +- [ ] Open a new milestone for the next release. +- [ ] If any open PRs/issues which were milestoned for this release did not make it into the release, update their milestone. +- [ ] Fast-forward `develop` to be equal to `main`. + +### After release + +- [ ] Open a Trac ticket for WordPress Core to update. + +### Publicize + +- [ ] [Major releases only] Publish post about the release on Make WordPress. +- [ ] Tweet, toot, etc about the release. +- [ ] Post about it in Slack. +- [ ] Submit for ["Month in WordPress"][month-in-wp]. + + +[phpcs-releases]: https://github.com/squizlabs/PHP_CodeSniffer/releases +[phpcsutils-releases]: https://github.com/PHPCSStandards/PHPCSUtils/releases +[phpcsextra-releases]: https://github.com/PHPCSStandards/PHPCSExtra/releases +[month-in-wp]: https://make.wordpress.org/community/month-in-wordpress-submissions/ diff --git a/.github/workflows/basic-qa.yml b/.github/workflows/basic-qa.yml new file mode 100644 index 0000000000..859cc0566d --- /dev/null +++ b/.github/workflows/basic-qa.yml @@ -0,0 +1,190 @@ +name: Basic QA checks + +on: + push: + 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: + # Check code style of sniffs, rulesets and XML documentation. + # Check that all sniffs are feature complete. + sniffs: + name: Run code sniffs + runs-on: ubuntu-latest + + env: + XMLLINT_INDENT: ' ' + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 'latest' + coverage: none + tools: cs2pr + + # @link https://getcomposer.org/doc/03-cli.md#validate + - name: Validate the composer.json file + run: composer validate --no-check-all --strict + + # Using PHPCS `master` as an early detection system for bugs upstream. + - name: Set PHPCS version + run: composer require squizlabs/php_codesniffer:"dev-master" --no-update --no-scripts --no-interaction + + - name: Install Composer dependencies + uses: ramsey/composer-install@v2 + with: + # Bust the cache at least once a month - output format: YYYY-MM. + custom-cache-suffix: $(date -u "+%Y-%m") + + - name: 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 + + - name: Check the code style of the PHP files + 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 + + # Validate the Ruleset XML files. + # @link http://xmlsoft.org/xmllint.html + - name: Validate the WordPress rulesets + run: xmllint --noout --schema vendor/squizlabs/php_codesniffer/phpcs.xsd ./*/ruleset.xml + + - name: Validate the sample ruleset + run: xmllint --noout --schema vendor/squizlabs/php_codesniffer/phpcs.xsd ./phpcs.xml.dist.sample + + # Validate the Documentation XML files. + - name: Validate documentation against schema + run: xmllint --noout --schema vendor/phpcsstandards/phpcsdevtools/DocsXsd/phpcsdocs.xsd ./WordPress/Docs/*/*Standard.xml + + - name: Check the code-style consistency of the xml files + run: | + diff -B --tabsize=4 ./WordPress/ruleset.xml <(xmllint --format "./WordPress/ruleset.xml") + diff -B --tabsize=4 ./WordPress-Core/ruleset.xml <(xmllint --format "./WordPress-Core/ruleset.xml") + diff -B --tabsize=4 ./WordPress-Docs/ruleset.xml <(xmllint --format "./WordPress-Docs/ruleset.xml") + diff -B --tabsize=4 ./WordPress-Extra/ruleset.xml <(xmllint --format "./WordPress-Extra/ruleset.xml") + diff -B --tabsize=4 ./phpcs.xml.dist.sample <(xmllint --format "./phpcs.xml.dist.sample") + + # 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 + + # Makes sure the rulesets don't throw unexpected errors or warnings. + # This workflow needs to be run against a high PHP version to prevent triggering the syntax error check. + # It also needs to be run against all PHPCS versions WPCS is tested against. + ruleset-tests: + runs-on: ubuntu-latest + strategy: + matrix: + php: [ 'latest' ] + phpcs_version: [ 'lowest', 'dev-master' ] + + name: "Ruleset test: PHP ${{ matrix.php }} on PHPCS ${{ matrix.phpcs_version }}" + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + # Allow for PHP deprecation notices. + ini-values: error_reporting = E_ALL & ~E_DEPRECATED + coverage: none + + - name: "Set PHPCS version (master)" + if: ${{ matrix.phpcs_version != 'lowest' }} + run: composer require squizlabs/php_codesniffer:"${{ matrix.phpcs_version }}" --no-update --no-scripts --no-interaction + + - name: Install Composer dependencies + uses: ramsey/composer-install@v2 + with: + composer-options: --no-dev + # Bust the cache at least once a month - output format: YYYY-MM. + custom-cache-suffix: $(date -u "+%Y-%m") + + - name: "Set PHPCS version (lowest)" + if: ${{ matrix.phpcs_version == 'lowest' }} + run: composer update squizlabs/php_codesniffer --prefer-lowest --ignore-platform-req=php+ --no-scripts --no-interaction + + - name: Test the WordPress-Core ruleset + run: $(pwd)/vendor/bin/phpcs -ps ./Tests/RulesetCheck/class-ruleset-test.inc --standard=WordPress-Core + + - name: Test the WordPress-Docs ruleset + run: $(pwd)/vendor/bin/phpcs -ps ./Tests/RulesetCheck/class-ruleset-test.inc --standard=WordPress-Docs + + - name: Test the WordPress-Extra ruleset + run: $(pwd)/vendor/bin/phpcs -ps ./Tests/RulesetCheck/class-ruleset-test.inc --standard=WordPress-Extra + + - name: Test the WordPress ruleset + run: $(pwd)/vendor/bin/phpcs -ps ./Tests/RulesetCheck/class-ruleset-test.inc --standard=WordPress + + # Test for fixer conflicts by running the auto-fixers of the complete WPCS over the test case files. + # This is not an exhaustive test, but should give an early indication for typical fixer conflicts. + # If only fixable errors are found, the exit code will be 1, which can be interpreted as success. + # + # Note: the ValidVariableNameUnitTest.inc file is temporarily ignored until upstream PHPCS PR 3833 has been merged. + - name: Test for fixer conflicts (fixes expected) + if: ${{ matrix.phpcs_version == 'dev-master' }} + id: phpcbf + continue-on-error: true + run: | + set +e + $(pwd)/vendor/bin/phpcbf -pq ./WordPress/Tests/ --standard=WordPress --extensions=inc --exclude=Generic.PHP.Syntax --report=summary --ignore=/WordPress/Tests/NamingConventions/ValidVariableNameUnitTest.inc,/WordPress/Tests/WP/GlobalVariablesOverrideUnitTest.7.inc + exitcode="$?" + echo "EXITCODE=$exitcode" >> $GITHUB_OUTPUT + exit "$exitcode" + + - name: Fail the build on fixer conflicts and other errors + if: ${{ steps.phpcbf.outputs.EXITCODE != 0 && steps.phpcbf.outputs.EXITCODE != 1 }} + run: exit ${{ steps.phpcbf.outputs.EXITCODE }} + + phpstan: + name: "PHPStan" + + runs-on: "ubuntu-latest" + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + coverage: none + tools: phpstan + + # Install dependencies and handle caching in one go. + # Dependencies need to be installed to make sure the PHPCS and PHPUnit classes are recognized. + # @link https://github.com/marketplace/actions/install-composer-dependencies + - name: Install Composer dependencies + uses: "ramsey/composer-install@v2" + with: + # Bust the cache at least once a month - output format: YYYY-MM. + custom-cache-suffix: $(date -u "+%Y-%m") + + - name: Run PHPStan + run: phpstan analyse diff --git a/.github/workflows/manage-labels.yml b/.github/workflows/manage-labels.yml new file mode 100644 index 0000000000..6d27b145c1 --- /dev/null +++ b/.github/workflows/manage-labels.yml @@ -0,0 +1,56 @@ +name: Remove outdated labels + +on: + # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target + pull_request_target: + types: + - closed + issues: + types: + - closed + +jobs: + on-pr-merge: + runs-on: ubuntu-latest + if: github.repository_owner == 'WordPress' && github.event.pull_request.merged == true + + name: Clean up labels on PR merge + + steps: + - uses: mondeja/remove-labels-gh-action@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + labels: | + Status: Awaiting feedback + Status: Review ready + + on-pr-close: + runs-on: ubuntu-latest + if: github.repository_owner == 'WordPress' && github.event_name == 'pull_request_target' && github.event.pull_request.merged == false + + name: Clean up labels on PR close + + steps: + - uses: mondeja/remove-labels-gh-action@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + labels: | + Status: Awaiting feedback + Status: Close candidate + Status: Review ready + + on-issue-close: + runs-on: ubuntu-latest + if: github.repository_owner == 'WordPress' && github.event.issue.state == 'closed' + + name: Clean up labels on issue close + + steps: + - uses: mondeja/remove-labels-gh-action@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + labels: | + Status: Awaiting feedback + Status: Close candidate + Status: Good first issue + Status: Help wanted diff --git a/.github/workflows/quicktest.yml b/.github/workflows/quicktest.yml new file mode 100644 index 0000000000..52f0d3bb8d --- /dev/null +++ b/.github/workflows/quicktest.yml @@ -0,0 +1,98 @@ +name: Quick Tests + +on: + push: + branches-ignore: + - main + 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: + # Performs some quick tests. + # This is a much quicker test suite which only runs the unit tests and linting + # against the low/high supported PHP/PHPCS combinations. + quick-tests: + runs-on: ubuntu-latest + strategy: + matrix: + php: [ '5.4', '7.4', 'latest' ] + phpcs_version: [ 'lowest', 'dev-master' ] + + name: QTest - PHP ${{ matrix.php }} on PHPCS ${{ matrix.phpcs_version }} + + steps: + - name: Checkout repository + 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: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + ini-values: ${{ steps.set_ini.outputs.PHP_INI }} + coverage: ${{ github.ref_name == 'develop' && 'xdebug' || 'none' }} + + - name: "Set PHPCS version (master)" + if: ${{ matrix.phpcs_version != 'lowest' }} + run: composer require squizlabs/php_codesniffer:"${{ matrix.phpcs_version }}" --no-update --no-scripts --no-interaction + + - name: Install Composer dependencies (PHP < 8.0 ) + if: ${{ matrix.php < 8.0 && matrix.php != 'latest' }} + uses: ramsey/composer-install@v2 + with: + # Bust the cache at least once a month - output format: YYYY-MM. + custom-cache-suffix: $(date -u "+%Y-%m") + + - name: Install Composer dependencies (PHP >= 8.0) + if: ${{ matrix.php >= 8.0 || matrix.php == 'latest' }} + uses: ramsey/composer-install@v2 + with: + composer-options: --ignore-platform-req=php+ + custom-cache-suffix: $(date -u "+%Y-%m") + + - name: "Set PHPCS version (lowest)" + if: ${{ matrix.phpcs_version == 'lowest' }} + run: composer update squizlabs/php_codesniffer --prefer-lowest --ignore-platform-req=php+ --no-scripts --no-interaction + + - name: Lint PHP files against parse errors + if: ${{ matrix.phpcs_version == 'dev-master' }} + run: composer lint -- --checkstyle + + - name: Run the unit tests without code coverage - PHP 5.4 - 8.0 + if: ${{ matrix.php == '5.4' && github.ref_name != 'develop' }} + run: composer run-tests + + # Until PHPCS supports PHPUnit 9, we cannot run code coverage on PHP 8.0+, so run it on PHP 5.4 and 7.4. + - name: Run the unit tests with code coverage - PHP 5.4 - 8.0 + if: ${{ matrix.php != 'latest' && github.ref_name == 'develop' }} + run: composer coverage + + - name: Run the unit tests without code coverage - PHP >= 8.1 + if: ${{ matrix.php == 'latest' }} + run: composer run-tests -- --no-configuration --bootstrap=./Tests/bootstrap.php --dont-report-useless-tests + + - name: Send coverage report to Codecov + if: ${{ success() && github.ref_name == 'develop' && matrix.php != 'latest' }} + uses: codecov/codecov-action@v3 + with: + files: ./build/logs/clover.xml + fail_ci_if_error: true + verbose: true diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000000..384bfa226e --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,130 @@ +name: Unit Tests + +on: + push: + branches: + - main + 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: + # Runs the test suite against all supported branches and combinations. + # Linting is performed on all jobs run against PHPCS `dev-master`. + test: + runs-on: ubuntu-latest + strategy: + matrix: + php: [ '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '8.0', '8.1', '8.2', '8.3' ] + phpcs_version: [ 'lowest', 'dev-master' ] + extensions: [ '' ] + coverage: [false] + + include: + - php: '7.4' + phpcs_version: 'dev-master' + extensions: ':mbstring' # = Disable Mbstring. + coverage: true # Make sure coverage is recorded for this too. + + # Run code coverage builds against high/low PHP and high/low PHPCS. + # Note: Until PHPCS supports PHPUnit 9, we cannot run code coverage on PHP 8.0+. + - php: '5.4' + phpcs_version: 'dev-master' + extensions: '' + coverage: true + - php: '5.4' + phpcs_version: 'lowest' + extensions: '' + coverage: true + - php: '7.4' + phpcs_version: 'dev-master' + extensions: '' + coverage: true + - php: '7.4' + phpcs_version: 'lowest' + extensions: '' + coverage: true + + # Add extra build to test against PHPCS 4. + #- php: '7.4' + # phpcs_version: '4.0.x-dev as 3.9.99' + + name: PHP ${{ matrix.php }} on PHPCS ${{ matrix.phpcs_version }} + + continue-on-error: ${{ matrix.php == '8.3' }} + + steps: + - name: Checkout repository + 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: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + ini-values: ${{ steps.set_ini.outputs.PHP_INI }} + coverage: ${{ matrix.coverage && 'xdebug' || 'none' }} + tools: cs2pr + + - name: "Set PHPCS version (master)" + if: ${{ matrix.phpcs_version != 'lowest' }} + run: composer require squizlabs/php_codesniffer:"${{ matrix.phpcs_version }}" --no-update --no-scripts --no-interaction + + - name: Install Composer dependencies (PHP < 8.0 ) + if: ${{ matrix.php < 8.0 }} + uses: ramsey/composer-install@v2 + with: + # Bust the cache at least once a month - output format: YYYY-MM. + custom-cache-suffix: $(date -u "+%Y-%m") + + - name: Install Composer dependencies (PHP >= 8.0) + if: ${{ matrix.php >= 8.0 }} + uses: ramsey/composer-install@v2 + with: + composer-options: --ignore-platform-req=php+ + custom-cache-suffix: $(date -u "+%Y-%m") + + - name: "Set PHPCS version (lowest)" + if: ${{ matrix.phpcs_version == 'lowest' }} + run: composer update squizlabs/php_codesniffer --prefer-lowest --ignore-platform-req=php+ --no-scripts --no-interaction + + - name: Lint PHP files against parse errors + if: ${{ matrix.phpcs_version == 'dev-master' }} + run: composer lint -- --checkstyle | cs2pr + + - name: Run the unit tests without code coverage - PHP 5.4 - 8.0 + if: ${{ matrix.php < '8.1' && matrix.coverage == false }} + run: composer run-tests + + - name: Run the unit tests with code coverage - PHP 5.4 - 8.0 + if: ${{ matrix.php < '8.1' && matrix.coverage == true }} + run: composer coverage + + # Until PHPCS supports PHPUnit 9, we cannot run code coverage on PHP 8.0+. + - name: Run the unit tests without code coverage - PHP >= 8.1 + if: ${{ matrix.php >= '8.1' && matrix.coverage == false }} + run: composer run-tests -- --no-configuration --bootstrap=./Tests/bootstrap.php --dont-report-useless-tests + + - name: Send coverage report to Codecov + if: ${{ success() && matrix.coverage == true }} + uses: codecov/codecov-action@v3 + with: + files: ./build/logs/clover.xml + fail_ci_if_error: true + verbose: true diff --git a/.gitignore b/.gitignore index bfec4c3c30..f4a8c2e298 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ -vendor +vendor/ composer.lock +build/ phpunit.xml phpcs.xml .phpcs.xml +phpstan.neon diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index ec29a1708c..af658a72a5 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -1,41 +1,62 @@ - + + The Coding standard for the WordPress Coding Standards itself. + + . - + + */vendor/* + + - - /Test/AllTests.php - /Test/Standards/*.php - /bin/class-ruleset-test.php + + - - */vendor/* + + + + + - + + + + + - - - + + - - + + + + + - - - - - + + - + + + - + @@ -47,9 +68,33 @@ + + + + + + + + + /WordPress/Sniffs/NamingConventions/ValidHookNameSniff\.php$ + /WordPress/Sniffs/Security/(EscapeOutput|NonceVerification|ValidatedSanitizedInput)Sniff\.php$ + + + + + + + - - + + + + + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 29f5e6fe61..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,130 +0,0 @@ -sudo: false - -dist: trusty - -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 - -language: - - php - -php: - - 5.4 - - 5.5 - - 5.6 - - 7.0 - - 7.1 - - 7.2 - - 7.3 - - nightly - -env: - # `master` is now 3.x. - - PHPCS_BRANCH="dev-master" LINT=1 - # Lowest supported release in the 3.x series with which WPCS is compatible (and which can run the unit tests). - - PHPCS_BRANCH="3.1.0" - # Lowest tagged release in the 2.x series with which WPCS is compatible. - - PHPCS_BRANCH="2.9.0" - -matrix: - fast_finish: true - include: - # Run PHPCS against WPCS. I just picked to run it against 7.2. - - php: 7.2 - env: PHPCS_BRANCH="dev-master" SNIFF=1 - addons: - apt: - packages: - - libxml2-utils - - # Test PHP 5.3 only against PHPCS 2.x as PHPCS 3.x has a minimum requirement of PHP 5.4. - - php: 5.3 - env: PHPCS_BRANCH="2.9.*" LINT=1 - dist: precise - # Test PHP 5.3 with short_open_tags set to On (is Off by default) - - php: 5.3 - env: PHPCS_BRANCH="2.9.0" SHORT_OPEN_TAGS=true - dist: precise - - allow_failures: - # Allow failures for unstable builds. - - php: nightly - - php: 7.3 - env: PHPCS_BRANCH="3.1.0" - - php: 7.3 - env: PHPCS_BRANCH="2.9.0" - -before_install: - # Speed up build time by disabling Xdebug. - # https://johnblackbourn.com/reducing-travis-ci-build-times-for-wordpress-projects/ - # https://twitter.com/kelunik/status/954242454676475904 - - phpenv config-rm xdebug.ini || echo 'No xdebug config.' - - export XMLLINT_INDENT=" " - - export PHPUNIT_DIR=/tmp/phpunit - - | - if [[ ${PHPCS_BRANCH:0:2} == "2." ]]; then - # --prefer-source is needed to ensure that the PHPCS unit test suite is available in PHPCS 2.9. - composer require squizlabs/php_codesniffer:${PHPCS_BRANCH} --prefer-source --update-no-dev --no-suggest --no-scripts - else - composer require squizlabs/php_codesniffer:${PHPCS_BRANCH} --update-no-dev --no-suggest --no-scripts - fi - - | - if [[ "$SNIFF" == "1" ]]; then - composer install --dev --no-suggest - # The post-install-cmd script takes care of the installed_paths. - else - # The above require already does the install. - $(pwd)/vendor/bin/phpcs --config-set installed_paths $(pwd) - fi - # Download PHPUnit 5.x for builds on PHP 7 and nightly as the PHPCS - # test suite is currently not compatible with PHPUnit 6.x. - # Fixed at a very specific PHPUnit version. - - if [[ ${TRAVIS_PHP_VERSION:0:2} != "5." ]]; then wget -P $PHPUNIT_DIR https://phar.phpunit.de/phpunit-5.7.17.phar && chmod +x $PHPUNIT_DIR/phpunit-5.7.17.phar; fi - # Selectively adjust the ini values for the build image to test ini value dependent sniff features. - - if [[ "$SHORT_OPEN_TAGS" == "true" ]]; then echo "short_open_tag = On" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi - -script: - # Lint the PHP files against parse errors. - - if [[ "$LINT" == "1" ]]; then if find . -path ./vendor -prune -o -path ./bin -prune -o -name "*.php" -exec php -l {} \; | grep "^[Parse error|Fatal error]"; then exit 1; fi; fi - # Run the unit tests. - - if [[ ${TRAVIS_PHP_VERSION:0:2} == "5." && ${PHPCS_BRANCH:0:2} == "2." ]]; then phpunit --filter WordPress $(pwd)/Test/AllTests.php; fi - - if [[ ${TRAVIS_PHP_VERSION:0:2} == "5." && ${PHPCS_BRANCH:0:2} != "2." ]]; then phpunit --filter WordPress $(pwd)/vendor/squizlabs/php_codesniffer/tests/AllTests.php; fi - - if [[ ${TRAVIS_PHP_VERSION:0:2} != "5." && ${PHPCS_BRANCH:0:2} == "2." ]]; then php $PHPUNIT_DIR/phpunit-5.7.17.phar --filter WordPress $(pwd)/Test/AllTests.php; fi - - if [[ ${TRAVIS_PHP_VERSION:0:2} != "5." && ${PHPCS_BRANCH:0:2} != "2." ]]; then php $PHPUNIT_DIR/phpunit-5.7.17.phar --filter WordPress $(pwd)/vendor/squizlabs/php_codesniffer/tests/AllTests.php; fi - # Test for fixer conflicts by running the auto-fixers of the complete WPCS over the test case files. - # This is not an exhaustive test, but should give an early indication for typical fixer conflicts. - # For the first run, the exit code will be 1 (= all fixable errors fixed). - # `travis_retry` should then kick in to run the fixer again which should now return 0 (= no fixable errors found). - # All error codes for the PHPCBF: https://github.com/squizlabs/PHP_CodeSniffer/issues/1270#issuecomment-272768413 - - if [[ "$SNIFF" == "1" ]]; then travis_retry $(pwd)/vendor/bin/phpcbf -p ./WordPress/Tests/ --standard=WordPress --extensions=inc --exclude=Generic.PHP.Syntax --report=summary; fi - # Make sure the rulesets don't thrown unexpected errors or warnings. - # This check needs to be run against a high PHP version to prevent triggering the syntax error check. - # It also needs to be run against all PHPCS versions WPCS is tested against. - - if [[ $TRAVIS_PHP_VERSION == "7.1" ]]; then $(pwd)/vendor/bin/phpcs -s ./bin/class-ruleset-test.php --standard=WordPress-Core; fi - - if [[ $TRAVIS_PHP_VERSION == "7.1" ]]; then $(pwd)/vendor/bin/phpcs -s ./bin/class-ruleset-test.php --standard=WordPress-Docs; fi - - if [[ $TRAVIS_PHP_VERSION == "7.1" ]]; then $(pwd)/vendor/bin/phpcs -s ./bin/class-ruleset-test.php --standard=WordPress-Extra; fi - - if [[ $TRAVIS_PHP_VERSION == "7.1" ]]; then $(pwd)/vendor/bin/phpcs -s ./bin/class-ruleset-test.php --standard=WordPress-VIP; fi - - if [[ $TRAVIS_PHP_VERSION == "7.1" ]]; then $(pwd)/vendor/bin/phpcs -s ./bin/class-ruleset-test.php --standard=WordPress; fi - # WordPress Coding Standards. - # @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards - # @link http://pear.php.net/package/PHP_CodeSniffer/ - - if [[ "$SNIFF" == "1" ]]; then $(pwd)/vendor/bin/phpcs --runtime-set ignore_warnings_on_exit 1; fi - # Validate the xml files. - # @link http://xmlsoft.org/xmllint.html - - if [[ "$SNIFF" == "1" ]]; then xmllint --noout ./*/ruleset.xml; fi - - if [[ "$SNIFF" == "1" ]]; then xmllint --noout ./phpcs.xml.dist.sample; fi - # Check the code-style consistency of the xml files. - - if [[ "$SNIFF" == "1" ]]; then diff -B --tabsize=4 ./WordPress/ruleset.xml <(xmllint --format "./WordPress/ruleset.xml"); fi - - if [[ "$SNIFF" == "1" ]]; then diff -B --tabsize=4 ./WordPress-Core/ruleset.xml <(xmllint --format "./WordPress-Core/ruleset.xml"); fi - - if [[ "$SNIFF" == "1" ]]; then diff -B --tabsize=4 ./WordPress-Docs/ruleset.xml <(xmllint --format "./WordPress-Docs/ruleset.xml"); fi - - if [[ "$SNIFF" == "1" ]]; then diff -B --tabsize=4 ./WordPress-Extra/ruleset.xml <(xmllint --format "./WordPress-Extra/ruleset.xml"); fi - - if [[ "$SNIFF" == "1" ]]; then diff -B --tabsize=4 ./WordPress-VIP/ruleset.xml <(xmllint --format "./WordPress-VIP/ruleset.xml"); fi - - if [[ "$SNIFF" == "1" ]]; then diff -B --tabsize=4 ./phpcs.xml.dist.sample <(xmllint --format "./phpcs.xml.dist.sample"); fi - # Validate the composer.json file. - # @link https://getcomposer.org/doc/03-cli.md#validate - - if [[ "$LINT" == "1" ]]; then composer validate --no-check-all --strict; fi diff --git a/CHANGELOG.md b/CHANGELOG.md index bbcbc75b88..d2e0cb8c58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,750 @@ This projects adheres to [Semantic Versioning](https://semver.org/) and [Keep a _No documentation available about unreleased changes as of yet._ +## [3.0.0] - 2023-08-21 + +### Important information about this release: + +At long last... WordPressCS 3.0.0 is here. + +This is an important release which makes significant changes to improve the accuracy, performance, stability and maintainability of all sniffs, as well as making WordPressCS much better at handling modern PHP. + +WordPressCS 3.0.0 contains breaking changes, both for people using ignore annotations, people maintaining custom rulesets, as well as for sniff developers who maintain a custom PHPCS standard based on WordPressCS. + +If you are an end-user or maintain a custom WordPressCS based ruleset, please start by reading the [Upgrade Guide to WordPressCS 3.0.0 for end-users](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Upgrade-Guide-to-WordPressCS-3.0.0-for-end-users) which lists the most important changes and contains a step by step guide for upgrading. + +If you are a maintainer of an external standard based on WordPressCS and any of your custom sniffs are based on or extend WordPressCS sniffs, please read the [Upgrade Guide to WordPressCS 3.0.0 for Developers](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Upgrade-Guide-to-WordPressCS-3.0.0-for-Developers-of-external-standards). + +In all cases, please read the complete changelog carefully before you upgrade. + + +### Added + +- Dependencies on the following packages: [PHPCSUtils](https://phpcsutils.com/), [PHPCSExtra](https://github.com/PHPCSStandards/PHPCSExtra) and the [Composer PHPCS plugin]. +- A best effort has been made to add support for the new PHP syntaxes/features to all WordPressCS native sniffs and utility functions (or to verify/improve existing support). + While support in external sniffs used by WordPressCS has not be exhaustively verified, a lot of work has been done to try and add support for new PHP syntaxes to those as well. + WordPressCS native sniffs and utilities have received fixes for the following syntaxes: + * PHP 7.2 + - Keyed lists. + * PHP 7.3 + - Flexible heredoc/nowdoc (providing the PHPCS scan is run on PHP 7.3 or higher). + - Trailing commas in function calls. + * PHP 7.4 + - Arrow functions. + - Array unpacking in array expressions. + - Numeric literals with underscores. + - Typed properties. + - Null coalesce equals operator. + * PHP 8.0 + - Nullsafe object operators. + - Match expressions. + - Named arguments in function calls. + - Attributes. + - Union types // including supporting the `false` and `null` types. + - Constructor property promotion. + - `$object::class` + - Throw as an expression. + * PHP 8.1 + - Enumerations. + - Explicit octal notation. + - Final class constants + - First class callables. + - Intersection types. + * PHP 8.2 + - Constants in traits. +- New `WordPress.CodeAnalysis.AssignmentInTernaryCondition` sniff to the `WordPress-Core` ruleset which partially replaces the removed `WordPress.CodeAnalysis.AssignmentInCondition` sniff. +- New `WordPress.WhiteSpace.ObjectOperatorSpacing` sniff which replaces the use of the `Squiz.WhiteSpace.ObjectOperatorSpacing` sniff in the `WordPress-Core` ruleset. +- New `WordPress.WP.ClassNameCase` sniff to the `WordPress-Core` ruleset, to check that any class name references to WP native classes and classes from external dependencies use the case of the class as per the class declaration. +- New `WordPress.WP.Capabilities` sniff to the `WordPress-Extra` ruleset. This sniff checks that valid capabilities are used, not roles or user levels. Props, amongst others, to [@grappler] and [@khacoder]. + Custom capabilities can be added to the sniff via a `custom_capabilities` ruleset property. + The sniff also supports the `minimum_wp_version` property to allow the sniff to accurately determine how the use of deprecated capabilities should be flagged. +- The `WordPress.WP.CapitalPDangit` sniff contains a new check to verify the correct spelling of `WordPress` in namespace names. +- The `WordPress.WP.I18n` sniff contains a new `EmptyTextDomain` error code for an empty text string being passed as the text domain, which overrules the default value of the parameter and renders a text untranslatable. +- The `WordPress.DB.PreparedSQLPlaceholders` sniff has been expanded with additional checks for the correct use of the `%i` placeholder, which was introduced in WP 6.2. Props [@craigfrancis]. + The sniff now also supports the `minimum_wp_version` ruleset property to determine whether the `%i` placeholder can be used. +- `WordPress-Core`: the following additional sniffs (or select error codes from these sniffs) have been added to the ruleset: `Generic.CodeAnalysis.AssignmentInCondition`, `Generic.CodeAnalysis.EmptyPHPStatement` (replaces the WordPressCS native sniff), `Generic.VersionControl.GitMergeConflict`, `Generic.WhiteSpace.IncrementDecrementSpacing`, `Generic.WhiteSpace.LanguageConstructSpacing`, `Generic.WhiteSpace.SpreadOperatorSpacingAfter`, `PSR2.Classes.ClassDeclaration`, `PSR2.Methods.FunctionClosingBrace`, `PSR12.Classes.ClassInstantiation`, `PSR12.Files.FileHeader` (select error codes only), `PSR12.Functions.NullableTypeDeclaration`, `PSR12.Functions.ReturnTypeDeclaration`, `PSR12.Traits.UseDeclaration`, `Squiz.Functions.MultiLineFunctionDeclaration` (replaces part of the `WordPress.WhiteSpace.ControlStructureSpacing` sniff), `Modernize.FunctionCalls.Dirname`, `NormalizedArrays.Arrays.ArrayBraceSpacing` (replaces part of the `WordPress.Arrays.ArrayDeclarationSpacing` sniff), `NormalizedArrays.Arrays.CommaAfterLast` (replaces the WordPressCS native sniff), `Universal.Classes.ModifierKeywordOrder`, `Universal.Classes.RequireAnonClassParentheses`, `Universal.Constants.LowercaseClassResolutionKeyword`, `Universal.Constants.ModifierKeywordOrder`, `Universal.Constants.UppercaseMagicConstants`, `Universal.Namespaces.DisallowCurlyBraceSyntax`, `Universal.Namespaces.DisallowDeclarationWithoutName`, `Universal.Namespaces.OneDeclarationPerFile`, `Universal.NamingConventions.NoReservedKeywordParameterNames`, `Universal.Operators.DisallowShortTernary` (replaces the WordPressCS native sniff), `Universal.Operators.DisallowStandalonePostIncrementDecrement`, `Universal.Operators.StrictComparisons` (replaces the WordPressCS native sniff), `Universal.Operators.TypeSeparatorSpacing`, `Universal.UseStatements.DisallowMixedGroupUse`, `Universal.UseStatements.KeywordSpacing`, `Universal.UseStatements.LowercaseFunctionConst`, `Universal.UseStatements.NoLeadingBackslash`, `Universal.UseStatements.NoUselessAliases`, `Universal.WhiteSpace.CommaSpacing`, `Universal.WhiteSpace.DisallowInlineTabs` (replaces the WordPressCS native sniff), `Universal.WhiteSpace.PrecisionAlignment` (replaces the WordPressCS native sniff), `Universal.WhiteSpace.AnonClassKeywordSpacing`. +- `WordPress-Extra`: the following additional sniffs have been added to the ruleset: `Generic.CodeAnalysis.UnusedFunctionParameter`, `Universal.Arrays.DuplicateArrayKey`, `Universal.CodeAnalysis.ConstructorDestructorReturn`, `Universal.CodeAnalysis.ForeachUniqueAssignment`, `Universal.CodeAnalysis.NoEchoSprintf`, `Universal.CodeAnalysis.StaticInFinalClass`, `Universal.ControlStructures.DisallowLonelyIf`, `Universal.Files.SeparateFunctionsFromOO`. +- `WordPress.Utils.I18nTextDomainFixer`: the `load_script_textdomain()` function to the functions the sniff looks for. +- `WordPress.WP.AlternativeFunctions`: the following PHP native functions have been added to the sniff and will now be flagged when used: `unlink()` (in a new `unlink` group) , `rename()` (in a new `rename` group), `chgrp()`, `chmod()`, `chown()`, `is_writable()` `is_writeable()`, `mkdir()`, `rmdir()`, `touch()`, `fputs()` (in the existing `file_system_operations` group, which was previously named `file_system_read`). Props [@sandeshjangam] and [@JDGrimes]. +- The `PHPUnit_Adapter_TestCase` class to the list of "known test (case) classes". +- The `antispambot()` function to the list of known "formatting" functions. +- The `esc_xml()` and `wp_kses_one_attr()` functions to the list of known "escaping" functions. +- The `wp_timezone_choice()` and `wp_readonly()` functions to the list of known "auto escaping" functions. +- The `sanitize_url()` and `wp_kses_one_attr()` functions to the list of known "sanitizing" functions. +- Metrics for blank lines at the start/end of a control structure body to the `WordPress.WhiteSpace.ControlStructureSpacing` sniff. These can be displayed using `--report=info` when the `blank_line_check` property has been set to `true`. +- End-user documentation to the following new and pre-existing sniffs: `WordPress.DateTime.RestrictedFunctions`, `WordPress.NamingConventions.PrefixAllGlobals` (props [@Ipstenu]), `WordPress.PHP.StrictInArray` (props [@marconmartins]), `WordPress.PHP.YodaConditions` (props [@Ipstenu]), `WordPress.WhiteSpace.ControlStructureSpacing` (props [@ckanitz]), `WordPress.WhiteSpace.ObjectOperatorSpacing`, `WordPress.WhiteSpace.OperatorSpacing` (props [@ckanitz]), `WordPress.WP.CapitalPDangit` (props [@NielsdeBlaauw]), `WordPress.WP.Capabilities`, `WordPress.WP.ClassNameCase`, `WordPress.WP.EnqueueResourceParameters` (props [@NielsdeBlaauw]). + This documentation can be exposed via the [`PHP_CodeSniffer` `--generator=...` command-line argument](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Usage). + Note: all sniffs which have been added from PHPCSExtra (Universal, Modernize, NormalizedArrays sniffs) are also fully documented. + +#### Added (internal/dev-only) +- New Helper classes: + - `ArrayWalkingFunctionsHelper` + - `ConstantsHelper` * + - `ContextHelper` * + - `DeprecationHelper` * + - `FormattingFunctionsHelper` + - `ListHelper` * + - `RulesetPropertyHelper` * + - `SnakeCaseHelper` * + - `UnslashingFunctionsHelper` + - `ValidationHelper` + - `VariableHelper` * + - `WPGlobalVariablesHelper` + - `WPHookHelper` +- New Helper traits: + - `EscapingFunctionsTrait` + - `IsUnitTestTrait` + - `MinimumWPVersionTrait` + - `PrintingFunctionsTrait` + - `SanitizationHelperTrait` * + - `WPDBTrait` + +These classes and traits mostly contain pre-existing functionality moved from the `Sniff` class. +The classes marked with an `*` are considered _internal_ and do not have any promise of future backward compatibility. + +More information is available in the [Upgrade Guide to WordPressCS 3.0.0 for Developers](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Upgrade-Guide-to-WordPressCS-3.0.0-for-Developers-of-external-standards). + + +### Changed + +- As of this version, installation via Composer is the only supported manner of installation. + Installing in a different manner (git clone/PEAR/PHAR) is still possible, but no longer supported. +- The minimum required `PHP_CodeSniffer` version to 3.7.2 (was 3.3.1). +- Composer: the package will now identify itself as a static analysis tool. +- The PHP `filter`, `libxml` and `XMLReader` extensions are now explicitly required. + It is recommended to also have the `Mbstring` and `iconv` extensions enabled for the most accurate results. +- The release branch has been renamed from `master` to `main`. +- The following sniffs have been moved from `WordPress-Extra` to `WordPress-Core`: the `Generic.Files.OneObjectStructurePerFile` (also changed from `warning` to `error`), + `Generic.PHP.BacktickOperator`, `PEAR.Files.IncludingFile`, `PSR2.Classes.PropertyDeclaration`, `PSR2.Methods.MethodDeclaration`, `Squiz.Scope.MethodScope`, `Squiz.WhiteSpace.ScopeKeywordSpacing` sniffs. Props, amongst others, to [@desrosj]. +- `WordPress-Core`: The `Generic.Arrays.DisallowShortArraySyntax` sniff has been replaced by the `Universal.Arrays.DisallowShortArraySyntax` sniff. + The new sniff will recognize short lists correctly and ignore them. +- `WordPress-Core`: The `Generic.Files.EndFileNewline` sniff has been replaced by the more comprehensive `PSR2.Files.EndFileNewline` sniff. +- A number of sniffs support setting the minimum WP version for the code being scanned. + This could be done in two different ways: + 1. By setting the `minimum_supported_version` property for each sniff from a ruleset. + 2. By passing `--runtime-set minimum_supported_wp_version #.#` on the command line. + The names of the property and the CLI setting have now been aligned to both use `minimum_wp_version` as the name. + Both ways of passing the value are still supported. +- `WordPress.NamingConventions.PrefixAllGlobals`: the `custom_test_class_whitelist` property has been renamed to `custom_test_classes`. +- `WordPress.NamingConventions.ValidVariableName`: the `customPropertiesWhitelist` property has been renamed to `allowed_custom_properties`. +- `WordPress.PHP.NoSilencedErrors`: the `custom_whitelist` property has been renamed to `customAllowedFunctionsList`. +- `WordPress.PHP.NoSilencedErrors`: the `use_default_whitelist` property has been renamed to `usePHPFunctionsList`. +- `WordPress.WP.GlobalVariablesOverride`: the `custom_test_class_whitelist` property has been renamed to `custom_test_classes`. +- Sniffs are now able to handle fully qualified names for custom test classes provided via a `custom_test_classes` (previously `custom_test_class_whitelist`) ruleset property. +- The default value for `minimum_supported_wp_version`, as used by a [number of sniffs detecting usage of deprecated WP features](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#minimum-wp-version-to-check-for-usage-of-deprecated-functions-classes-and-function-parameters), has been updated to `6.0`. +- `WordPress.NamingConventions.PrefixAllGlobals` now takes new pluggable constants into account as introduced in WordPress up to WP 6.3. +- `WordPress.NamingConventions.ValidPostTypeSlug` now takes new reserved post types into account as introduced in WordPress up to WP 6.3. +- `WordPress.WP.DeprecatedClasses` now detects classes deprecated in WordPress up to WP 6.3. +- `WordPress.WP.DeprecatedFunctions` now detects functions deprecated in WordPress up to WP 6.3. +- `WordPress.WP.DeprecatedParameters` now detects parameters deprecated in WordPress up to WP 6.3. +- `WordPress.WP.DeprecatedParameterValues` now detects parameter values deprecated in WordPress up to WP 6.3. +- `WordPress.Utils.I18nTextDomainFixer`: the lists of recognized plugin and theme header tags has been updated based on the current information in the plugin and theme handbooks. +- `WordPress.WP.AlternativeFunctions`: the "group" name `file_system_read`, which can be used with the `exclude` property, has been renamed to `file_system_operations`. + This also means that the error codes for individual functions flagged via that group have changed from `WordPress.WP.AlternativeFunctions.file_system_read_*` to `WordPress.WP.AlternativeFunctions.file_system_operations_*`. +- `WordPress.WP.CapitalPDangit`: the `Misspelled` error code has been split into two error codes - `MisspelledInText` and `MisspelledInComment` - to allow for more modular exclusions/selectively turning off the auto-fixer for the sniff. +- `WordPress.WP.I18n` no longer throws both the `MissingSingularPlaceholder` and the `MismatchedPlaceholders` for the same code, as the errors have an overlap. +- `WordPress-Core`: previously only the spacing around commas in arrays, function declarations and function calls was checked. Now, the spacing around commas will be checked in all contexts. +- `WordPress.Arrays.ArrayKeySpacingRestrictions`: a new `SpacesBetweenBrackets` error code has been introduced for the spacing between square brackets for array assignments without key. Previously, this would throw a `NoSpacesAroundArrayKeys` error with an unclear error message. +- `WordPress.Files.FileName` now recognizes more word separators, meaning that files using other word separators than underscores will now be flagged for not using hyphenation. +- `WordPress.Files.FileName` now checks if a file contains a test class and if so, will bow out. + This change was made to prevent issues with PHPUnit 9.1+, which strongly prefers PSR4-style file names. + Whether something is test class or not is based on a pre-defined list of "known" test case classes which can be extended and, optionally, a list of user provided test case classes provided via setting the `custom_test_classes` property in a custom ruleset or the complete test directory can be excluded via a custom ruleset. +- `WordPress.NamingConventions.PrefixAllGlobals` now allows for pluggable functions and classes, which should not be prefixed when "plugged". +- `WordPress.PHP.NoSilencedErrors`: the metric, which displays in the `info` report, has been renamed from "whitelisted function call" to "silencing allowed function call". +- `WordPress.Security.EscapeOutput` now flags the use of `get_search_query( false )` when generating output (as the `false` turns off the escaping). +- `WordPress.Security.EscapeOutput` now also examines parameters passed for exception creation in `throw` statements and expressions for correct escaping. +- `WordPress.Security.ValidatedSanitizedInput` now examines _all_ superglobal (except for `$GLOBALS`). Previously, the `$_SESSION` and `$_ENV` superglobals would not be flagged as needing validation/sanitization. +- `WordPress.WP.I18n` now recognizes the new PHP 8.0+ `h` and `H` type specifiers. +- `WordPress.WP.PostsPerPage` has improved recognition for numbers prefixed with a unary operator and non-decimal numbers. +- `WordPress.DB.PreparedSQL` will identify more precisely the code which is problematic. +- `WordPress.DB.PreparedSQLPlaceholders` will identify more precisely the code which is problematic. +- `WordPress.DB.SlowDBQuery` will identify more precisely the code which is problematic. +- `WordPress.Security.PluginMenuSlug`: the error will now be thrown more precisely on the code which triggered the error. Depending on code layout, this may mean that an error will now be thrown on a different line. +- `WordPress.WP.DiscouragedConstants`: the error will now be thrown more precisely on the code which triggered the error. Depending on code layout, this may mean that an error will now be thrown on a different line. +- `WordPress.WP.EnqueuedResourceParameters`: the error will now be thrown more precisely on the code which triggered the error. Depending on code layout, this may mean that an error will now be thrown on a different line. +- `WordPress.WP.I18n`: the errors will now be thrown more precisely on the code which triggered the error. Depending on code layout, this may mean that an error will now be thrown on a different line. +- `WordPress.WP.PostsPerPage` will identify more precisely the code which is problematic. +- `WordPress.PHP.TypeCasts.UnsetFound` has been changed from a `warning` to an `error` as the `(unset)` cast is no longer available in PHP 8.0 and higher. +- `WordPress.WP.EnqueuedResourceParameters.MissingVersion` has been changed from an `error` to a `warning`. +- `WordPress.Arrays.ArrayKeySpacingRestrictions`: improved the clarity of the error messages for the `TooMuchSpaceBeforeKey` and `TooMuchSpaceAfterKey` error codes. +- `WordPress.CodeAnalysis.EscapedNotTranslated`: improved the clarity of the error message. +- `WordPress.PHP.IniSet`: improved the clarity of the error messages for the sniff. +- `WordPress.PHP.PregQuoteDelimiter`: improved the clarity of the error message for the `Missing` error code. +- `WordPress.PHP.RestrictedFunctions`: improved the clarity of the error messages for the sniff. +- `WordPress.PHP.RestrictedPHPFunctions`: improved the error message for the `create_function_create_function` error code. +- `WordPress.PHP.TypeCast`: improved the clarity of the error message for the `UnsetFound` error code. It will no longer advise assigning `null`. +- `WordPress.Security.SafeRedirect`: improved the clarity of the error message. (very minor) +- `WordPress.Security.ValidatedSanitizedInput`: improved the clarity of the error messages for the `MissingUnslash` error code. +- `WordPress.WhiteSpace.CastStructureSpacing`: improved the clarity of the error message for the `NoSpaceBeforeOpenParenthesis` error code. +- `WordPress.WP.I18n`: improved the clarity of the error messages for the sniff. +- `WordPress.WP.I18n`: the error messages will now use the correct parameter name. +- The error messages for the `WordPress.CodeAnalysis.EscapedNotTranslated`, `WordPress.NamingConventions.PrefixAllGlobals`, `WordPress.NamingConventions.ValidPostTypeSlug`, `WordPress.PHP.IniSet`, and the `WordPress.PHP.NoSilencedErrors` sniff will now display the code sample found without comments and extranuous whitespace. +- Various updates to the README, the example ruleset and other documentation. Props, amongst others, to [@Luc45], [@slaFFik]. +- Continuous Integration checks are now run via GitHub Actions. Props [@desrosj]. +- Various other CI/QA improvements. +- Code coverage will now be monitored via [CodeCov](https://app.codecov.io/gh/WordPress/WordPress-Coding-Standards). +- All sniffs are now also being tested against PHP 8.0, 8.1, 8.2 and 8.3 for consistent sniff results. + +#### Changed (internal/dev-only) +- All non-abstract classes in WordPressCS are now `final` with the exception of the following four classes which are known to be extended by external PHPCS standards build on top of WordPressCS: `WordPress.NamingConventions.ValidHookName`, `WordPress.Security.EscapeOutput`,`WordPress.Security.NonceVerification`, `WordPress.Security.ValidatedSanitizedInput`. +- Most remaining utility properties and methods, previously contained in the `WordPressCS\WordPress\Sniff` class, have been moved to dedicated Helper classes and traits or, in some cases, to the sniff class using them. + As this change is only relevant for extenders, the full details of these moves are not included in this changelog, but can be found in the [Developers Upgrade Guide to WordPressCS 3.0.0](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Upgrade-Guide-to-WordPressCS-3.0.0-for-Developers-of-external-standards) +- A few customizable `public` properties, which were used by multiple sniffs, have been moved from `*Sniff` classes to traits. Again, the full details of these moves are not included in this changelog, but can be found in the [Developers Upgrade Guide to WordPressCS 3.0.0](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Upgrade-Guide-to-WordPressCS-3.0.0-for-Developers-of-external-standards) +- A number of non-public properties in sniffs have been renamed. + As this change is only relevant for extenders, the full details of these renames are not included in this changelog, but can be found in the [Developers Upgrade Guide to WordPressCS 3.0.0](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Upgrade-Guide-to-WordPressCS-3.0.0-for-Developers-of-external-standards) +- `AbstractFunctionRestrictionsSniff`: The `whitelist` key in the `$groups` array property has been renamed to `allow`. +- The `WordPress.NamingConventions.ValidFunctionName` sniff no longer extends the similar PHPCS native `PEAR` sniff. + + +### Removed + +- Support for the deprecated, old-style WordPressCS native ignore annotations. Use the PHPCS native selective ignore annotations instead. +- The following WordPressCS native sniffs have been removed: + - The `WordPress.Arrays.CommaAfterArrayItem` sniff (replaced by the `NormalizedArrays.Arrays.CommaAfterLast` and the `Universal.WhiteSpace.CommaSpacing` sniffs). + - The `WordPress.Classes.ClassInstantiation` sniff (replaced by the `PSR12.Classes.ClassInstantiation`, `Universal.Classes.RequireAnonClassParentheses` and `Universal.WhiteSpace.AnonClassKeywordSpacing` sniffs). + - The `WordPress.CodeAnalysis.AssignmentInCondition` sniff (replaced by the `Generic.CodeAnalysis.AssignmentInCondition` and the `WordPress.CodeAnalysis.AssignmentInTernaryCondition` sniffs). + - The `WordPress.CodeAnalysis.EmptyStatement` sniff (replaced by the `Generic.CodeAnalysis.EmptyPHPStatement` sniff). + - The `WordPress.PHP.DisallowShortTernary` sniff (replaced by the `Universal.Operators.DisallowShortTernary` sniff). + - The `WordPress.PHP.StrictComparisons` sniff (replaced by the `Universal.Operators.StrictComparisons` sniff). + - The `WordPress.WhiteSpace.DisallowInlineTabs` sniff (replaced by the `Universal.WhiteSpace.DisallowInlineTabs` sniff). + - The `WordPress.WhiteSpace.PrecisionAlignment` sniff (replaced by the `Universal.WhiteSpace.PrecisionAlignment` sniff). + - The `WordPress.WP.TimezoneChange` sniff (replaced by the `WordPress.DateTime.RestrictedFunctions` sniff). This sniff was previously already deprecated. +- `WordPress-Extra`: The `Squiz.WhiteSpace.LanguageConstructSpacing` sniff (replaced by the added, more comprehensive `Generic.WhiteSpace.LanguageConstructSpacing` sniff in the `WordPress-Core` ruleset). +- `WordPress.Arrays.ArrayDeclarationSpacing`: array brace spacing checks (replaced by the `NormalizedArrays.Arrays.ArrayBraceSpacing` sniff). +- `WordPress.WhiteSpace.ControlStructureSpacing`: checks for the spacing for function declarations (replaced by the `Squiz.Functions.MultiLineFunctionDeclaration` sniff). + Includes removal of the `spaces_before_closure_open_paren` property for this sniff. +- `WordPress.WP.I18n`: the `check_translator_comments` property. + Exclude the `WordPress.WP.I18n.MissingTranslatorsComment` and the `WordPress.WP.I18n.TranslatorsCommentWrongStyle` error codes instead. +- WordPressCS will no longer check for assigning the return value of an object instantiation by reference. + This is a PHP parse error since PHP 7.0. Use the `PHPCompatibilityWP` standard to check for PHP cross-version compatibility issues. +- The check for object instantiations will no longer check JavaScript files. +- The `WordPress.Arrays.ArrayKeySpacingRestrictions.MissingBracketCloser` error code as sniffs should not report on parse errors. +- The `WordPress.CodeAnalysis.AssignmentIn[Ternary]Condition.NonVariableAssignmentFound` error code as sniffs should not report on parse errors. +- The `Block_Supported_Styles_Test` class will no longer incorrectly be recognized as an extendable test case class. + +#### Removed (internal/dev-only) +- `AbstractArrayAssignmentRestrictionsSniff`: support for the optional `'callback'` key in the array returned by `getGroups()`. +- `WordPressCS\WordPress\PHPCSHelper` class (use the `PHPCSUtils\BackCompat\Helper` class instead). +- `WordPressCS\WordPress\Sniff::addMessage()` method (use the `PHPCSUtils\Utils\MessageHelper::addMessage()` method instead). +- `WordPressCS\WordPress\Sniff::addFixableMessage()` method (use the `PHPCSUtils\Utils\MessageHelper::addFixableMessage()` method instead). +- `WordPressCS\WordPress\Sniff::determine_namespace()` method (use the `PHPCSUtils\Utils\Namespaces::determineNamespace()` method instead). +- `WordPressCS\WordPress\Sniff::does_function_call_have_parameters()` method (use the `PHPCSUtils\Utils\PassedParameters::hasParameters()` method instead). +- `WordPressCS\WordPress\Sniff::find_array_open_close()` method (use the `PHPCSUtils\Utils\Arrays::getOpenClose()` method instead). +- `WordPressCS\WordPress\Sniff::find_list_open_close()` method (use the `PHPCSUtils\Utils\Lists::getOpenClose()` method instead). +- `WordPressCS\WordPress\Sniff::get_declared_namespace_name()` method (use the `PHPCSUtils\Utils\Namespaces::getDeclaredName()` method instead). +- `WordPressCS\WordPress\Sniff::get_function_call_parameter_count()` method (use the `PHPCSUtils\Utils\PassedParameters::getParameterCount()` method instead). +- `WordPressCS\WordPress\Sniff::get_function_call_parameters()` method (use the `PHPCSUtils\Utils\PassedParameters::getParameters()` method instead). +- `WordPressCS\WordPress\Sniff::get_function_call_parameter()` method (use the `PHPCSUtils\Utils\PassedParameters::getParameter()` method instead). +- `WordPressCS\WordPress\Sniff::get_interpolated_variables()` method (use the `PHPCSUtils\Utils\TextStrings::getEmbeds()` method instead). +- `WordPressCS\WordPress\Sniff::get_last_ptr_on_line()` method (no replacement available at this time). +- `WordPressCS\WordPress\Sniff::get_use_type()` method (use the `PHPCSUtils\Utils\UseStatements::getType()` method instead). +- `WordPressCS\WordPress\Sniff::has_whitelist_comment()` method (no replacement). +- `WordPressCS\WordPress\Sniff::$hookFunctions` property (no replacement available at this time). +- `WordPressCS\WordPress\Sniff::init()` method (no replacement). +- `WordPressCS\WordPress\Sniff::is_class_constant()` method (use the `PHPCSUtils\Utils\Scopes::isOOConstant()` method instead). +- `WordPressCS\WordPress\Sniff::is_class_property()` method (use the `PHPCSUtils\Utils\Scopes::isOOProperty()` method instead). +- `WordPressCS\WordPress\Sniff::is_foreach_as()` method (use the `PHPCSUtils\Utils\Context::inForeachCondition()` method instead). +- `WordPressCS\WordPress\Sniff::is_short_list()` method (depending on your needs, use the `PHPCSUtils\Utils\Lists::isShortList()` or the `PHPCSUtils\Utils\Arrays::isShortArray()` method instead). +- `WordPressCS\WordPress\Sniff::is_token_in_test_method()` method (no replacement available at this time). +- `WordPressCS\WordPress\Sniff::REGEX_COMPLEX_VARS` constant (use the PHPCSUtils `PHPCSUtils\Utils\TextStrings::stripEmbeds()` and `PHPCSUtils\Utils\TextStrings::getEmbeds()` methods instead). +- `WordPressCS\WordPress\Sniff::string_to_errorcode()` method (use the `PHPCSUtils\Utils\MessageHelper::stringToErrorcode()` method instead). +- `WordPressCS\WordPress\Sniff::strip_interpolated_variables()` method (use the `PHPCSUtils\Utils\TextStrings::stripEmbeds()` method instead). +- `WordPressCS\WordPress\Sniff::strip_quotes()` method (use the `PHPCSUtils\Utils\TextStrings::stripQuotes()` method instead). +- `WordPressCS\WordPress\Sniff::valid_direct_scope()` method (use the `PHPCSUtils\Utils\Scopes::validDirectScope()` method instead). +- Unused dev-only files in the (now removed) `bin` directory. + + +### Fixed + +- All sniffs which, in one way or another, check whether code represents a short list or a short array will now do so more accurately. + This fixes various false positives and false negatives. +- Sniffs supporting the `minimum_wp_version` property (previously `minimum_supported_version`) will no longer throw a "passing null to non-nullable" deprecation notice on PHP 8.1+. +- `WordPress.WhiteSpace.ControlStructureSpacing` no longer throws a `TypeError` on PHP 8.0+. +- `WordPress.NamingConventions.PrefixAllGlobals`no longer throws a "passing null to non-nullable" deprecation notice on PHP 8.1+. +- `WordPress.WP.I18n` no longer throws a "passing null to non-nullable" deprecation notice on PHP 8.1+. +- `VariableHelper::is_comparison()` (previously `Sniff::is_comparison()`): fixed risk of undefined array key notice when scanning code containing parse errors. +- `AbstractArrayAssignmentRestrictionsSniff` could previously get confused when it encountered comments in unexpected places. + This fix has a positive impact on all sniffs which are based on this abstract (2 sniffs). +- `AbstractArrayAssignmentRestrictionsSniff` no longer examines numeric string keys as PHP treats those as integer keys, which were never intended as the target of this abstract. + This fix has a positive impact on all sniffs which are based on this abstract (2 sniffs). +- `AbstractArrayAssignmentRestrictionsSniff` in case of duplicate entries, the sniff will now only examine the last value, as that's the value PHP will see. + This fix has a positive impact on all sniffs which are based on this abstract (2 sniffs). +- `AbstractArrayAssignmentRestrictionsSniff` now determines the assigned value with higher accuracy. + This fix has a positive impact on all sniffs which are based on this abstract (2 sniffs). +- `AbstractClassRestrictionsSniff` now treats the `namespace` keyword when used as an operator case-insensitively. + This fix has a positive impact on all sniffs which are based on this abstract (3 sniffs). +- `AbstractClassRestrictionsSniff` now treats the hierarchy keywords (`self`, `parent`, `static`) case-insensitively. + This fix has a positive impact on all sniffs which are based on this abstract (3 sniffs). +- `AbstractClassRestrictionsSniff` now limits itself correctly when trying to find a class name before a `::`. + This fix has a positive impact on all sniffs which are based on this abstract (3 sniffs). +- `AbstractClassRestrictionsSniff`: false negatives on class instantiation statements ending on a PHP close tag. + This fix has a positive impact on all sniffs which are based on this abstract (3 sniffs). +- `AbstractClassRestrictionsSniff`: false negatives on class instantiation statements combined with method chaining. + This fix has a positive impact on all sniffs which are based on this abstract (3 sniffs). +- `AbstractFunctionRestrictionsSniff`: false positives on function declarations when the function returns by reference. + This fix has a positive impact on all sniffs which are based on this abstract (nearly half of the WordPressCS sniffs). +- `AbstractFunctionRestrictionsSniff`: false positives on instantiation of a class with the same name as a targetted function. + This fix has a positive impact on all sniffs which are based on this abstract (nearly half of the WordPressCS sniffs). +- `AbstractFunctionRestrictionsSniff` now respects that function names in PHP are case-insensitive in more places. + This fix has a positive impact on all sniffs which are based on this abstract (nearly half of the WordPressCS sniffs). +- Various utility methods in Helper classes/traits have received fixes to correctly treat function and class names as case-insensitive. + These fixes have a positive impact on all sniffs using these methods. +- Version comparisons done by sniffs supporting the `minimum_wp_version` property (previously `minimum_supported_version`) will now be more precise. +- `WordPress.Arrays.ArrayIndentation` now ignores indentation issues for array items which are not the first thing on a line. This fixes a potential fixer conflict. +- `WordPress.Arrays.ArrayKeySpacingRestrictions`: signed positive integer keys will now be treated the same as signed negative integer keys. +- `WordPress.Arrays.ArrayKeySpacingRestrictions`: keys consisting of calculations will now be recognized more accurately. +- `WordPress.Arrays.ArrayKeySpacingRestrictions.NoSpacesAroundArrayKeys`: now has better protection in case of a fixer conflict. +- `WordPress.Classes.ClassInstantiation` could create parse errors when fixing a class instantiation using variable variables. This has been fixed by replacing the sniff with the `PSR12.Classes.ClassInstantiation` sniff (and some others). +- `WordPress.DB.DirectDatabaseQuery` could previously get confused when it encountered comments in unexpected places. +- `WordPress.DB.DirectDatabaseQuery` now respects that function (method) names in PHP are case-insensitive. +- `WordPress.DB.DirectDatabaseQuery` now only looks at the current statement to find a method call to the `$wpdb` object. +- `WordPress.DB.DirectDatabaseQuery` no longer warns about `TRUNCATE` queries as those cannot be cached and need a direct database query. +- `WordPress.DB.PreparedSQL` could previously get confused when it encountered comments in unexpected places. +- `WordPress.DB.PreparedSQL` now respects that function names in PHP are case-insensitive. +- `WordPress.DB.PreparedSQL` improved recognition of interpolated variables and expressions in the `$text` argument. This fixes both some false negatives as well as some false positives. +- `WordPress.DB.PreparedSQL` stricter recognition of the `$wpdb` variable in double quoted query strings. +- `WordPress.DB.PreparedSQL` false positive for floating point numbers concatenated into an SQL query. +- `WordPress.DB.PreparedSQLPlaceholders` could previously get confused when it encountered comments in unexpected places. +- `WordPress.DB.PreparedSQLPlaceholders` now respects that function names in PHP are case-insensitive. +- `WordPress.DB.PreparedSQLPlaceholders` stricter recognition of the `$wpdb` variable in double quotes query strings. +- `WordPress.DB.PreparedSQLPlaceholders` false positive when a fully qualified function call is encountered in an `implode( ', ', array_fill(...))` pattern. +- `WordPress.Files.FileName` no longer presumes a three character file extension. +- `WordPress.NamingConventions.PrefixAllGlobals` could previously get confused when it encountered comments in unexpected places in function calls which were being examined. +- `WordPress.NamingConventions.PrefixAllGlobals` now respects that function names in PHP are case-insensitive when checking whether a function declaration is polyfilling a PHP native function. +- `WordPress.NamingConventions.PrefixAllGlobals` improved false positive prevention for variable assignments via keyed lists. +- `WordPress.NamingConventions.PrefixAllGlobals` now only looks at the current statement when determining which variables were imported via a `global` statement. This prevents both false positives as well as false negatives. +- `WordPress.NamingConventions.PrefixAllGlobals` no longer gets confused over `global` statements in nested clsure/function declarations. +- `WordPress.NamingConventions.ValidFunctionName` now also checks the names of (global) functions when the declaration is nested within an OO method. +- `WordPress.NamingConventions.ValidFunctionName` no longer throws false positives for triple underscore methods. +- `WordPress.NamingConventions.ValidFunctionName` the suggested replacement names in the error message no longer remove underscores from a name in case of leading or trailing underscores, or multiple underscores in the middle of a name. +- `WordPress.NamingConventions.ValidFunctionName` the determination whether a name is in `snake_case` is now more accurate and has improved handling of non-ascii characters. +- `WordPress.NamingConventions.ValidFunctionName` now correctly recognizes a PHP4-style constructor when the class and the constructor method name contains non-ascii characters. +- `WordPress.NamingConventions.ValidHookName` no longer throws false positives when the hook name is generated via a function call and that function is passed string literals as parameters. +- `WordPress.NamingConventions.ValidHookName` now ignores parameters in a variable function call (like a call to a closure). +- `WordPress.NamingConventions.ValidPostTypeSlug` no longer throws false positives for interpolated text strings with complex embedded variables/expressions. +- `WordPress.NamingConventions.ValidVariableName` the suggested replacement names in the error message will no longer remove underscores from a name in case of leading or trailing underscores, or multiple underscores in the middle of a name. +- `WordPress.NamingConventions.ValidVariableName` the determination whether a name is in `snake_case` is now more accurate and has improved handling of non-ascii characters. +- `WordPress.NamingConventions.ValidVariableName` now examines all variables and variables in expressions in a text string containing interpolation. +- `WordPress.NamingConventions.ValidVariableName` now has improved recognition of variables in complex embedded variables/expressions in interpolated text strings. +- `WordPress.PHP.IniSet` no longer gets confused over comments in the code when determining whether the ini value is an allowed one. +- `WordPress.PHP.NoSilencedErrors` no longer throws an error when error silencing is encountered for function calls to the PHP native `libxml_disable_entity_loader()` and `imagecreatefromwebp()` methods. +- `WordPress.PHP.StrictInArray` no longer gets confused over comments in the code when determining whether the `$strict` parameter is used. +- `WordPress.Security.EscapeOutput` no longer throws a false positive on function calls where the parameters need escaping, when no parameters are being passed. +- `WordPress.Security.EscapeOutput` no longer throws a false positive when a fully qualified function call to the `\basename()` function is encountered within a call to `_deprecated_file()`. +- `WordPress.Security.EscapeOutput` could previously get confused when it encountered comments in the `$file` parameter for `_deprecated_file()`. +- `WordPress.Security.EscapeOutput` now ignores significantly more operators which should yield more accurate results. +- `WordPress.Security.EscapeOutput` now respects that function names in PHP are case-insensitive when checking whether a printing function is being used. +- `WordPress.Security.EscapeOutput` no longer throws an `Internal.Exception` when it encounters a constant or property mirroring the name of one of the printing functions being targetted, nor will it throw false positives for those. +- `WordPress.Security.EscapeOutput` no longer incorrectly handles method calls or calls to namespaced functions mirroring the name of one of the printing functions being targetted. +- `WordPress.Security.EscapeOutput` now ignores `exit`/`die` statements without a status being passed, preventing false positives on code after the statement. +- `WordPress.Security.EscapeOutput` now has improved recognition that `print` can also be used as an expression, not only as a statement. +- `WordPress.Security.EscapeOutput` now has much, much, much more accurate handling of code involving ternary expressions and should now correctly ignore the ternary condition in all long ternaries being examined. +- `WordPress.Security.EscapeOutput` no longer disregards the ternary condition in a short ternary. +- `WordPress.Security.EscapeOutput` no longer skips over a constant or property mirroring the name of one of the (auto-)escaping/formatting functions being targeted. +- `WordPress.Security.EscapeOutput` no longer throws false positives for `*::class`, which will always evaluate to a plain string. +- `WordPress.Security.EscapeOutput` no longer throws false positives on output generating keywords encountered in an inline expression. +- `WordPress.Security.EscapeOutput` no longer throws false positives on parameters passed to `_e()` or `_ex()`, which won't be used in the output. +- `WordPress.Security.EscapeOutput` no longer throws false positives on heredocs not using interpolation. +- `WordPress.Security.NonceVerification` now respects that function names in PHP are case-insensitive when checking whether an array comparison function is being used. +- `WordPress.Security.NonceVerification` now also checks for nonce verification when the `$_FILES` superglobal is being used. +- `WordPress.Security.NonceVerification` now ignores properties named after superglobals. +- `WordPress.Security.NonceVerification` now ignores list assignments to superglobals. +- `WordPress.Security.NonceVerification` now ignores superglobals being unset. +- `WordPress.Security.ValidatedSanitizedInput` now respects that function names in PHP are case-insensitive when checking whether an array comparison function is being used. +- `WordPress.Security.ValidatedSanitizedInput` now respects that function names in PHP are case-insensitive when checking whether a variable is being validated using `[array_]key_exists()`. +- `WordPress.Security.ValidatedSanitizedInput` improved recognition of interpolated variables and expression in the text strings. This fixes some false negatives. +- `WordPress.Security.ValidatedSanitizedInput` no longer incorrectly regards an `unset()` as variable validation. +- `WordPress.Security.ValidatedSanitizedInput` no longer incorrectly regards validation in a nested scope as validation which applies to the superglobal being examined. +- `WordPress.WP.AlternativeFunctions` could previously get confused when it encountered comments in unexpected places. +- `WordPress.WP.AlternativeFunctions` now correctly takes the `minimum_wp_version` into account when determining whether a call to `parse_url()` could switch over to using `wp_parse_url()`. +- `WordPress.WP.CapitalPDangit` now skips (keyed) list assignments to prevent false positives. +- `WordPress.WP.CapitalPDangit` now always skips all array keys, not just plain text array keys. +- `WordPress.WP.CronInterval` no longer throws a `ChangeDetected` warning for interval calculations wrapped in parentheses, but for which the value for the interval is otherwise known. +- `WordPress.WP.CronInterval` no longer throws a `ChangeDetected` warning for interval calculations using fully qualified WP native time constants, but for which the value for the interval is otherwise known. +- `WordPress.WP.DeprecatedParameters` no longer throws a false positive for function calls to `comments_number()` using the fourth parameter (which was deprecated, but has been repurposed since WP 5.4). +- `WordPress.WP.DeprecatedParameters` now looks for the correct parameter in calls to the `unregister_setting()` function. +- `WordPress.WP.DeprecatedParameters` now lists the correct WP version for the deprecation of the third parameter in function calls to `get_user_option()`. +- `WordPress.WP.DiscouragedConstants` could previously get confused when it encountered comments in unexpected places. +- `WordPress.WP.EnqueuedResources` now recognizes enqueuing in a multi-line text string correctly. +- `WordPress.WP.EnqueuedResourceParameters` could previously get confused when it encountered comments in unexpected places. +- `WordPress.WP.GlobalVariablesOverride` improved false positive prevention for variable assignments via keyed lists. +- `WordPress.WP.GlobalVariablesOverride` now only looks at the current statement when determining which variables were imported via a `global` statement. This prevents both false positives as well as false negatives. +- `WordPress.WP.I18n` improved recognition of interpolated variables and expression in the `$text` argument. This fixes some false negatives. +- `WordPress.WP.I18n` no longer potentially creates parse errors when auto-fixing an `UnorderedPlaceholders*` error involving a multi-line text string. +- `WordPress.WP.I18n` no longer throws false positives for compound parameters starting with a text string, which were previously checked as if the parameter only consisted of a text string. +- `WordPress.WP.PostsPerPage` now determines the end of statement with more precision and will no longer throw a false positive for function calls on PHP 8.0+. + + +## [2.3.0] - 2020-05-14 + +### Added +- The `WordPress.WP.I18n` sniff contains a new check for translatable text strings which are wrapped in HTML tags, like `

Translate me

`. Those tags should be moved out of the translatable string. + Note: Translatable strings wrapped in `` tags where the URL is intended to be localized will not trigger this check. + +### Changed +- The default value for `minimum_supported_wp_version`, as used by a [number of sniffs detecting usage of deprecated WP features](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#minimum-wp-version-to-check-for-usage-of-deprecated-functions-classes-and-function-parameters), has been updated to `5.1`. +- The `WordPress.WP.DeprecatedFunctions` sniff will now detect functions deprecated in WP 5.4. +- Improved grammar of an error message in the `WordPress.WP.DiscouragedFunctions` sniff. +- CI: The codebase is now - preliminary - being tested against the PHPCS 4.x development branch. + +### Fixed +- All function call detection sniffs: fixed a bug where constants with the same name as one of the targeted functions could inadvertently be recognized as if they were a called function. +- `WordPress.DB.PreparedSQL`: fixed a bug where the sniff would trigger on the namespace separator character `\\`. +- `WordPress.Security.EscapeOutput`: fixed a bug with the variable replacement in one of the error messages. + + +## [2.2.1] - 2020-02-04 + +### Added +- Metrics to the `WordPress.Arrays.CommaAfterArrayItem` sniff. These can be displayed using `--report=info`. +- The `sanitize_hex_color()` and the `sanitize_hex_color_no_hash()` functions to the `escapingFunctions` list used by the `WordPress.Security.EscapeOutput` sniff. + +### Changed +- The recommended version of the suggested [Composer PHPCS plugin] is now `^0.6`. + +### Fixed +- `WordPress.PHP.NoSilencedErrors`: depending on the custom properties set, the metrics would be different. +- `WordPress.WhiteSpace.ControlStructureSpacing`: fixed undefined index notice for closures with `use`. +- `WordPress.WP.GlobalVariablesOverride`: fixed undefined offset notice when the `treat_files_as_scoped` property would be set to `true`. +- `WordPress.WP.I18n`: fixed a _Trying to access array offset on value of type null_ error when the sniff was run on PHP 7.4 and would encounter a translation function expecting singular and plural texts for which one of these arguments was missing. + +## [2.2.0] - 2019-11-11 + +Note: The repository has moved. The new URL is https://github.com/WordPress/WordPress-Coding-Standards. +The move does not affect the package name for Packagist. This remains the same: `wp-coding-standards/wpcs`. + +### Added +- New `WordPress.DateTime.CurrentTimeTimestamp` sniff to the `WordPress-Core` ruleset, which checks against the use of the WP native `current_time()` function to retrieve a timestamp as this won't be a _real_ timestamp. Includes an auto-fixer. +- New `WordPress.DateTime.RestrictedFunctions` sniff to the `WordPress-Core` ruleset, which checks for the use of certain date/time related functions. Initially this sniff forbids the use of the PHP native `date_default_timezone_set()` and `date()` functions. +- New `WordPress.PHP.DisallowShortTernary` sniff to the `WordPress-Core` ruleset, which, as the name implies, disallows the use of short ternaries. +- New `WordPress.CodeAnalysis.EscapedNotTranslated` sniff to the `WordPress-Extra` ruleset which will warn when a text string is escaped for output, but not being translated, while the arguments passed to the function call give the impression that translation is intended. +- New `WordPress.NamingConventions.ValidPostTypeSlug` sniff to the `WordPress-Extra` ruleset which will examine calls to `register_post_type()` and throw errors when an invalid post type slug is used. +- `Generic.Arrays.DisallowShortArraySyntax` to the `WordPress-Core` ruleset. +- `WordPress.NamingConventions.PrefixAllGlobals`: the `PHP` prefix has been added to the prefix blacklist as it is reserved by PHP itself. +- The `wp_sanitize_redirect()` function to the `sanitizingFunctions` list used by the `WordPress.Security.NonceVerification`, `WordPress.Security.ValidatedSanitizedInput` and `WordPress.Security.EscapeOutput` sniffs. +- The `sanitize_key()` and the `highlight_string()` functions to the `escapingFunctions` list used by the `WordPress.Security.EscapeOutput` sniff. +- The `RECOVERY_MODE_COOKIE` constant to the list of WP Core constants which may be defined by plugins and themes and therefore don't need to be prefixed (`WordPress.NamingConventions.PrefixAllGlobals`). +- `$content_width`, `$plugin`, `$mu_plugin` and `$network_plugin` to the list of WP globals which is used by both the `WordPress.Variables.GlobalVariables` and the `WordPress.NamingConventions.PrefixAllGlobals` sniffs. +- `Sniff::is_short_list()` utility method to determine whether a _short array_ open/close token actually represents a PHP 7.1+ short list. +- `Sniff::find_list_open_close()` utility method to find the opener and closer for `list()` constructs, including short lists. +- `Sniff::get_list_variables()` utility method which will retrieve an array with the token pointers to the variables which are being assigned to in a `list()` construct. Includes support for short lists. +- `Sniff::is_function_deprecated()` static utility method to determine whether a declared function has been marked as deprecated in the function DocBlock. +- End-user documentation to the following existing sniffs: `WordPress.Arrays.ArrayIndentation`, `WordPress.Arrays.ArrayKeySpacingRestrictions`, `WordPress.Arrays.MultipleStatementAlignment`, `WordPress.Classes.ClassInstantiation`, `WordPress.NamingConventions.ValidHookName`, `WordPress.PHP.IniSet`, `WordPress.Security.SafeRedirect`, `WordPress.WhiteSpace.CastStructureSpacing`, `WordPress.WhiteSpace.DisallowInlineTabs`, `WordPress.WhiteSpace.PrecisionAlignment`, `WordPress.WP.CronInterval`, `WordPress.WP.DeprecatedClasses`, `WordPress.WP.DeprecatedFunctions`, `WordPress.WP.DeprecatedParameters`, `WordPress.WP.DeprecatedParameterValues`, `WordPress.WP.EnqueuedResources`, `WordPress.WP.PostsPerPage`. + This documentation can be exposed via the [`PHP_CodeSniffer` `--generator=...` command-line argument](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Usage). + +### Changed +- The default value for `minimum_supported_wp_version`, as used by a [number of sniffs detecting usage of deprecated WP features](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#minimum-wp-version-to-check-for-usage-of-deprecated-functions-classes-and-function-parameters), has been updated to `5.0`. +- The `WordPress.Arrays.ArrayKeySpacingRestrictions` sniff has two new error codes: `TooMuchSpaceBeforeKey` and `TooMuchSpaceAfterKey`. Both auto-fixable. + The sniff will now check that there is _exactly_ one space on the inside of the square brackets around the array key for non-string, non-numeric array keys. Previously, it only checked that there was whitespace, not how much whitespace. +- `WordPress.Arrays.ArrayKeySpacingRestrictions`: the fixers have been made more efficient and less fixer-conflict prone. +- `WordPress.NamingConventions.PrefixAllGlobals`: plugin/theme prefixes should be at least three characters long. A new `ShortPrefixPassed` error has been added for when the prefix passed does not comply with this rule. +- `WordPress.WhiteSpace.CastStructureSpacing` now allows for no whitespace before a cast when the cast is preceded by the spread `...` operator. This pre-empts a fixer conflict for when the spacing around the spread operator will start to get checked. +- The `WordPress.WP.DeprecatedClasses` sniff will now detect classes deprecated in WP 4.9 and WP 5.3. +- The `WordPress.WP.DeprecatedFunctions` sniff will now detect functions deprecated in WP 5.3. +- `WordPress.NamingConventions.ValidHookName` now has "cleaner" error messages and higher precision for the line on which an error is thrown. +- `WordPress.Security.EscapeOutput`: if an error refers to array access via a variable, the array index key will now be included in the error message. +- The processing of the `WordPress` ruleset by `PHP_CodeSniffer` will now be faster. +- Various minor code tweaks and clean up. +- Various minor documentation fixes. +- Documentation: updated the repo URL in all relevant places. + +### Deprecated +- The `WordPress.WP.TimezoneChange` sniff. Use the `WordPress.DateTime.RestrictedFunctions` instead. + The deprecated sniff will be removed in WPCS 3.0.0. + +### Fixed +- All sniffs in the `WordPress.Arrays` category will no longer treat _short lists_ as if they were a short array. +- The `WordPress.NamingConventions.ValidFunctionName` and the `WordPress.NamingConventions.PrefixAllGlobals` sniff will now ignore functions marked as `@deprecated`. +- Both the `WordPress.NamingConventions.PrefixAllGlobals` sniff as well as the `WordPress.WP.GlobalVariablesOverride` sniff have been updated to recognize variables being declared via (long/short) `list()` constructs and handle them correctly. +- Both the `WordPress.NamingConventions.PrefixAllGlobals` sniff as well as the `WordPress.WP.GlobalVariablesOverride` sniff will now take a limited list of WP global variables _which are intended to be overwritten by plugins/themes_ into account. + Initially this list contains the `$content_width` and the `$wp_cockneyreplace` variables. +- `WordPress.NamingConventions.ValidHookName`: will no longer examine a string array access index key as if it were a part of the hook name. +- `WordPress.Security.EscapeOutput`: will no longer trigger on the typical `basename( __FILE__ )` pattern if found as the first parameter passed to a call to `_deprecated_file()`. +- `WordPress.WP.CapitalPDangit`: now allows for the `.test` TLD in URLs. +- WPCS is now fully compatible with PHP 7.4. + Note: `PHP_CodeSniffer` itself is only compatible with PHP 7.4 from PHPCS 3.5.0 onwards. + + +## [2.1.1] - 2019-05-21 + +### Changed +- The `WordPress.WP.CapitalPDangit` will now ignore misspelled instances of `WordPress` within constant declarations. + This covers both constants declared using `defined()` as well as constants declared using the `const` keyword. +- The default value for `minimum_supported_wp_version`, as used by a [number of sniffs detecting usage of deprecated WP features](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#minimum-wp-version-to-check-for-usage-of-deprecated-functions-classes-and-function-parameters), has been updated to `4.9`. + +### Removed +- `paginate_comments_links()` from the list of auto-escaped functions `Sniff::$autoEscapedFunctions`. + This affects the `WordPress.Security.EscapeOutput` sniff. + +### Fixed +- The `$current_blog` and `$tag_ID` variables have been added to the list of WordPress global variables. + This fixes some false positives from the `WordPress.NamingConventions.PrefixAllGlobals` and the `WordPress.WP.GlobalVariablesOverride` sniffs. +- The generic `TestCase` class name has been added to the `$test_class_whitelist`. + This fixes some false positives from the `WordPress.NamingConventions.FileName`, `WordPress.NamingConventions.PrefixAllGlobals` and the `WordPress.WP.GlobalVariablesOverride` sniffs. +- The `WordPress.NamingConventions.ValidVariableName` sniff will now correctly recognize `$tag_ID` as a WordPress native, mixed-case variable. +- The `WordPress.Security.NonceVerification` sniff will now correctly recognize nonce verification within a nested closure or anonymous class. + + +## [2.1.0] - 2019-04-08 + +### Added +- New `WordPress.PHP.IniSet` sniff to the `WordPress-Extra` ruleset. + This sniff will detect calls to `ini_set()` and `ini_alter()` and warn against their use as changing configuration values at runtime leads to an unpredictable runtime environment, which can result in conflicts between core/plugins/themes. + - The sniff will not throw notices about a very limited set of "safe" ini directives. + - For a number of ini directives for which there are alternative, non-conflicting ways to achieve the same available, the sniff will throw an `error` and advise using the alternative. +- `doubleval()`, `count()` and `sizeof()` to `Sniff::$unslashingSanitizingFunctions` property. + While `count()` and its alias `sizeof()`, don't actually unslash or sanitize, the output of these functions is safe to use without unslashing or sanitizing. + This affects the `WordPress.Security.ValidatedSanitizedInput` and the `WordPress.Security.NonceVerification` sniffs. +- The new WP 5.1 `WP_UnitTestCase_Base` class to the `Sniff::$test_class_whitelist` property. +- New `Sniff::get_array_access_keys()` utility method to retrieve all array keys for a variable using multi-level array access. +- New `Sniff::is_class_object_call()`, `Sniff::is_token_namespaced()` utility methods. + These should help make the checking of whether or not a function call is a global function, method call or a namespaced function call more consistent. + This also implements allowing for the [namespace keyword being used as an operator](https://www.php.net/manual/en/language.namespaces.nsconstants.php#example-258). +- New `Sniff::is_in_function_call()` utility method to facilitate checking whether a token is (part of) a parameter passed to a specific (set of) function(s). +- New `Sniff::is_in_type_test()` utility method to determine if a variable is being type tested, along with a `Sniff::$typeTestFunctions` property containing the names of the functions this applies to. +- New `Sniff::is_in_array_comparison()` utility method to determine if a variable is (part of) a parameter in an array-value comparison, along with a `Sniff::$arrayCompareFunctions` property containing the names of the relevant functions. +- New `Sniff::$arrayWalkingFunctions` property containing the names of array functions which apply a callback to the array, but don't change the array by reference. +- New `Sniff::$unslashingFunctions` property containing the names of functions which unslash data passed to them and return the unslashed result. + +### Changed +- Moved the `WordPress.PHP.StrictComparisons`, `WordPress.PHP.StrictInArray` and the `WordPress.CodeAnalysis.AssignmentInCondition` sniff from the `WordPress-Extra` to the `WordPress-Core` ruleset. +- The `Squiz.Commenting.InlineComment.SpacingAfter` error is no longer included in the `WordPress-Docs` ruleset. +- The default value for `minimum_supported_wp_version`, as used by a [number of sniffs detecting usage of deprecated WP features](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#minimum-wp-version-to-check-for-usage-of-deprecated-functions-classes-and-function-parameters), has been updated to `4.8`. +- The `WordPress.WP.DeprecatedFunctions` sniff will now detect functions deprecated in WP 5.1. +- The `WordPress.Security.NonceVerification` sniff now allows for variable type testing, comparisons, unslashing and sanitization before the nonce check. A nonce check within the same scope, however, is still required. +- The `WordPress.Security.ValidatedSanitizedInput` sniff now allows for using a superglobal in an array-value comparison without sanitization, same as when the superglobal is used in a scalar value comparison. +- `WordPress.NamingConventions.PrefixAllGlobals`: some of the error messages have been made more explicit. +- The error messages for the `WordPress.Security.ValidatedSanitizedInput` sniff will now contain information on the index keys accessed. +- The error message for the `WordPress.Security.ValidatedSanitizedInput.InputNotValidated` has been reworded to make it more obvious what the actual issue being reported is. +- The error message for the `WordPress.Security.ValidatedSanitizedInput.MissingUnslash` has been reworded. +- The `Sniff::is_comparison()` method now has a new `$include_coalesce` parameter to allow for toggling whether the null coalesce operator should be seen as a comparison operator. Defaults to `true`. +- All sniffs are now also being tested against PHP 7.4 (unstable) for consistent sniff results. +- The recommended version of the suggested [Composer PHPCS plugin] is now `^0.5.0`. +- Various minor code tweaks and clean up. + +### Removed +- `ini_set` and `ini_alter` from the list of functions detected by the `WordPress.PHP.DiscouragedFunctions` sniff. + These are now covered via the new `WordPress.PHP.IniSet` sniff. +- `in_array()` and `array_key_exists()` from the list of `Sniff::$sanitizingFunctions`. These are now handled differently. + +### Fixed +- The `WordPress.NamingConventions.PrefixAllGlobals` sniff would underreport when global functions would be autoloaded via a Composer autoload `files` configuration. +- The `WordPress.Security.EscapeOutput` sniff will now recognize `map_deep()` for escaping the values in an array via a callback to an output escaping function. This should prevent false positives. +- The `WordPress.Security.NonceVerification` sniff will no longer inadvertently allow for a variable to be sanitized without a nonce check within the same scope. +- The `WordPress.Security.ValidatedSanitizedInput` sniff will no longer throw errors when a variable is only being type tested. +- The `WordPress.Security.ValidatedSanitizedInput` sniff will now correctly recognize the null coalesce (PHP 7.0) and null coalesce equal (PHP 7.4) operators and will now throw errors for missing unslashing and sanitization where relevant. +- The `WordPress.WP.AlternativeFunctions` sniff will no longer recommend using the WP_FileSystem when PHP native input streams, like `php://input`, or the PHP input stream constants are being read or written to. +- The `WordPress.WP.AlternativeFunctions` sniff will no longer report on usage of the `curl_version()` function. +- The `WordPress.WP.CronInterval` sniff now has improved function recognition which should lower the chance of false positives. +- The `WordPress.WP.EnqueuedResources` sniff will no longer throw false positives for inline jQuery code trying to access a stylesheet link tag. +- Various bugfixes for the `Sniff::has_nonce_check()` method: + - The method will no longer incorrectly identify methods/namespaced functions mirroring the name of WP native nonce verification functions as if they were the global functions. + This will prevent some false negatives. + - The method will now skip over nested closed scopes, such as closures and anonymous classes. This should prevent some false negatives for nonce verification being done while not in the correct scope. + + These fixes affect the `WordPress.Security.NonceVerification` sniff. +- The `Sniff::is_in_isset_or_empty()` method now also checks for usage of `array_key_exist()` and `key_exists()` and will regard these as correct ways to validate a variable. + This should prevent false positives for the `WordPress.Security.ValidatedSanitizedInput` and the `WordPress.Security.NonceVerification` sniffs. +- Various bugfixes for the `Sniff::is_sanitized()` method: + - The method presumed the WordPress coding style regarding code layout, which could lead to false positives. + - The method will no longer incorrectly identify methods/namespaced functions mirroring the name of WP/PHP native unslashing/sanitization functions as if they were the global functions. + This will prevent some false negatives. + - The method will now recognize `map_deep()` for sanitizing an array via a callback to a sanitization function. This should prevent false positives. + - The method will now recognize `stripslashes_deep()` and `stripslashes_from_strings_only()` as valid unslashing functions. This should prevent false positives. + All these fixes affect both the `WordPress.Security.ValidatedSanitizedInput` and the `WordPress.Security.NonceVerification` sniff. +- Various bugfixes for the `Sniff::is_validated()` method: + - The method did not verify correctly whether a variable being validated was the same variable as later used which could lead to false negatives. + - The method did not verify correctly whether a variable being validated had the same array index keys as the variable as later used which could lead to both false negatives as well as false positives. + - The method now also checks for usage of `array_key_exist()` and `key_exists()` and will regard these as correct ways to validate a variable. This should prevent some false positives. + - The methods will now recognize the null coalesce and the null coalesce equal operators as ways to validate a variable. This prevents some false positives. + The results from the `WordPress.Security.ValidatedSanitizedInput` sniff should be more accurate because of these fixes. +- A potential "Undefined index" notice from the `Sniff::is_assignment()` method. + + +## [2.0.0] - 2019-01-16 + +### Important information about this release: + +WordPressCS 2.0.0 contains breaking changes, both for people using custom rulesets as well as for sniff developers who maintain a custom PHPCS standard based on WordPressCS. + +Support for `PHP_CodeSniffer` 2.x has been dropped, the new minimum `PHP_CodeSniffer` version is 3.3.1. +Also, all previously deprecated sniffs, properties and methods have been removed. + +Please read the complete changelog carefully before you upgrade. + +If you are a maintainer of an external standard based on WordPressCS and any of your custom sniffs are based on or extend WPCS sniffs, please read the [Developers Upgrade Guide to WordPressCS 2.0.0](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Upgrade-Guide-to-WordPressCS-2.0.0-for-Developers-of-external-standards). + +### Changes since 2.0.0-RC1 + +#### Fixed + +- `WordPress-Extra`: Reverted back to including the `Squiz.WhiteSpace.LanguageConstructSpacing` sniff instead of the new `Generic.WhiteSpace.LanguageConstructSpacing` sniff as the new sniff is not (yet) available when the PEAR install of PHPCS is used. + +### Changes since 1.2.1 +For a full list of changes from the 1.2.1 version, please review the following changelog: +* https://github.com/WordPress/WordPress-Coding-Standards/releases/tag/2.0.0-RC1 + + +## [2.0.0-RC1] - 2018-12-31 + +### Important information about this release: + +This is the first release candidate for WordPressCS 2.0.0. +WordPressCS 2.0.0 contains breaking changes, both for people using custom rulesets as well as for sniff developers who maintain a custom PHPCS standard based on WordPressCS. + +Support for `PHP_CodeSniffer` 2.x has been dropped, the new minimum `PHP_CodeSniffer` version is 3.3.1. +Also, all previously deprecated sniffs, properties and methods have been removed. + +Please read the complete changelog carefully before you upgrade. + +If you are a maintainer of an external standard based on WordPressCS and any of your custom sniffs are based on or extend WPCS sniffs, please read the [Developers Upgrade Guide to WordPressCS 2.0.0](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Upgrade-Guide-to-WordPressCS-2.0.0-for-Developers-of-external-standards). + +### Added +- `Generic.PHP.DiscourageGoto`, `Generic.PHP.LowerCaseType`, `Generic.WhiteSpace.ArbitraryParenthesesSpacing` and `PSR12.Keywords.ShortFormTypeKeywords` to the `WordPress-Core` ruleset. +- Checking the spacing around the `instanceof` operator to the `WordPress.WhiteSpace.OperatorSpacing` sniff. + +### Changed +- The minimum required `PHP_CodeSniffer` version to 3.3.1 (was 2.9.0). +- The namespace used by WordPressCS has been changed from `WordPress` to `WordPressCS\WordPress`. + This was not possible while `PHP_CodeSniffer` 2.x was still supported, but WordPressCS, as a good Open Source citizen, does not want to occupy the `WordPress` namespace and is releasing its use of it now this is viable. +- The `WordPress.DB.PreparedSQL` sniff used the same error code for two different errors. + The `NotPrepared` error code remains, however an additional `InterpolatedNotPrepared` error code has been added for the second error. + If you are referencing the old error code in a ruleset XML file or in inline annotations, you may need to update it. +- The `WordPress.NamingConventions.PrefixAllGlobals` sniff used the same error code for some errors as well as warnings. + The `NonPrefixedConstantFound` error code remains for the related error, but the warning will now use the new `VariableConstantNameFound` error code. + The `NonPrefixedHooknameFound` error code remains for the related error, but the warning will now use the new `DynamicHooknameFound` error code. + If you are referencing the old error codes in a ruleset XML file or in inline annotations, you may need to update these to use the new codes instead. +- `WordPress.NamingConventions.ValidVariableName`: the error messages and error codes used by this sniff have been changed for improved usability and consistency. + - The error messages will now show a suggestion for a valid alternative name for the variable. + - The `NotSnakeCaseMemberVar` error code has been renamed to `UsedPropertyNotSnakeCase`. + - The `NotSnakeCase` error code has been renamed to `VariableNotSnakeCase`. + - The `MemberNotSnakeCase` error code has been renamed to `PropertyNotSnakeCase`. + - The `StringNotSnakeCase` error code has been renamed to `InterpolatedVariableNotSnakeCase`. + If you are referencing the old error codes in a ruleset XML file or in inline annotations, you may need to update these to use the new codes instead. +- The `WordPress.Security.NonceVerification` sniff used the same error code for both an error as well as a warning. + The old error code `NoNonceVerification` is no longer used. + The `error` now uses the `Missing` error code, while the `warning` now uses the `Recommended` error code. + If you are referencing the old error code in a ruleset XML file or in inline annotations, please update these to use the new codes instead. +- The `WordPress.WP.DiscouragedConstants` sniff used to have two error codes `UsageFound` and `DeclarationFound`. + These error codes will now be prefixed by the name of the constant found to allow for more fine-grained excluding/ignoring of warnings generated by this sniff. + If you are referencing the old error codes in a ruleset XML file or in inline annotations, you may need to update these to use the new codes instead. +- The `WordPress.WP.GlobalVariablesOverride.OverrideProhibited` error code has been replaced by the `WordPress.WP.GlobalVariablesOverride.Prohibited` error code. + If you are referencing the old error code in a ruleset XML file or in inline annotations, you may need to update it. +- `WordPress-Extra`: Replaced the inclusion of the `Generic.Files.OneClassPerFile`, `Generic.Files.OneInterfacePerFile` and the `Generic.Files.OneTraitPerFile` sniffs with the new `Generic.Files.OneObjectStructurePerFile` sniff. +- `WordPress-Extra`: Replaced the inclusion of the `Squiz.WhiteSpace.LanguageConstructSpacing` sniff with the new `Generic.WhiteSpace.LanguageConstructSpacing` sniff. +- `WordPress-Extra`: Replaced the inclusion of the `Squiz.Scope.MemberVarScope` sniff with the more comprehensive `PSR2.Classes.PropertyDeclaration` sniff. +- `WordPress.NamingConventions.ValidFunctionName`: Added a unit test confirming support for interfaces extending multiple interfaces. +- `WordPress.NamingConventions.ValidVariableName`: Added unit tests confirming support for multi-variable/property declarations. +- The `get_name_suggestion()` method has been moved from the `WordPress.NamingConventions.ValidFunctionName` sniff to the base `Sniff` class, renamed to `get_snake_case_name_suggestion()` and made static. +- The rulesets are now validated against the `PHP_CodeSniffer` XSD schema. +- Updated the [custom ruleset example](https://github.com/WordPress/WordPress-Coding-Standards/blob/develop/phpcs.xml.dist.sample) to use the recommended ruleset syntax for `PHP_CodeSniffer` 3.3.1+, including using the new [array property format](https://github.com/squizlabs/PHP_CodeSniffer/releases/tag/3.3.0) which is now supported. +- Dev: The command to run the unit tests has changed. Please see the updated instructions in the [CONTRIBUTING.md](https://github.com/WordPress/WordPress-Coding-Standards/blob/develop/.github/CONTRIBUTING.md) file. + The `bin/pre-commit` example git hook has been updated to match. Additionally a `run-tests` script has been added to the `composer.json` file for your convenience. + To facilitate this, PHPUnit has been added to `require-dev`, even though it is strictly speaking a dependency of PHPCS, not of WPCS. +- Dev: The [Composer PHPCS plugin] has been added to `require-dev`. +- Various code tweaks and clean up. +- User facing documentation, including the wiki, as well as inline documentation has been updated for all the changes contained in WordPressCS 2.0 and other recommended best practices for `PHP_CodeSniffer` 3.3.1+. + +### Deprecated +- The use of the [WordPressCS native whitelist comments](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Whitelisting-code-which-flags-errors), which were introduced in WPCS 0.4.0, have been deprecated and support will be removed in WPCS 3.0.0. + The WordPressCS native whitelist comments will continue to work for now, but a deprecation warning will be thrown when they are encountered. + You are encouraged to upgrade our whitelist comment to use the [PHPCS native selective ignore annotations](https://github.com/squizlabs/PHP_CodeSniffer/releases/tag/3.2.0) as introduced in `PHP_CodeSniffer` 3.2.0, as soon as possible. + +### Removed +- Support for PHP 5.3. PHP 5.4 is the minimum requirement for `PHP_CodeSniffer` 3.x. + Includes removing any and all workarounds which were in place to still support PHP 5.3. +- Support for `PHP_CodeSniffer` < 3.3.1. + Includes removing any and all workarounds which were in place for supporting older `PHP_CodeSniffer` versions. +- The `WordPress-VIP` standard which was deprecated since WordPressCS 1.0.0. + For checking a theme/plugin for hosting on the WordPress.com VIP platform, please use the [Automattic VIP coding standards](https://github.com/Automattic/VIP-Coding-Standards) instead. +- Support for array properties set in a custom ruleset without the `type="array"` attribute. + Support for this was deprecated in WPCS 1.0.0. + If in doubt about how properties should be set in your custom ruleset, please refer to the [Customizable sniff properties](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties) wiki page which contains XML code examples for setting each and every WPCS native sniff property. + As the minimum `PHP_CodeSniffer` version is now 3.3.1, you can now also use the [new format for setting array properties](https://github.com/squizlabs/PHP_CodeSniffer/releases/tag/3.3.0), so this would be a great moment to review and update your custom ruleset. + Note: the ability to set select properties from the command-line as comma-delimited strings is _not_ affected by this change. +- The following sniffs have been removed outright without deprecation. + If you are referencing these sniffs in a ruleset XML file or in inline annotations, please update these to reference the replacement sniffs instead. + - `WordPress.Functions.FunctionCallSignatureNoParams` - superseded by a bug fix in the upstream `PEAR.Functions.FunctionCallSignature` sniff. + - `WordPress.PHP.DiscourageGoto` - replaced by the same sniff which is now available upstream: `Generic.PHP.DiscourageGoto`. + - `WordPress.WhiteSpace.SemicolonSpacing` - superseded by a bug fix in the upstream `Squiz.WhiteSpace.SemicolonSpacing` sniff. + - `WordPress.WhiteSpace.ArbitraryParenthesesSpacing` - replaced by the same sniff which is now available upstream: `Generic.WhiteSpace.ArbitraryParenthesesSpacing`. +- The following "base" sniffs which were previously already deprecated and turned into abstract base classes, have been removed: + - `WordPress.Arrays.ArrayAssignmentRestrictions` - use the `AbstractArrayAssignmentRestrictionsSniff` class instead. + - `WordPress.Functions.FunctionRestrictions` - use the `AbstractFunctionRestrictionsSniff` class instead. + - `WordPress.Variables.VariableRestrictions` without replacement. +- The following sniffs which were previously deprecated, have been removed: + - `WordPress.Arrays.ArrayDeclaration` - use the other sniffs in the `WordPress.Arrays` category instead. + - `WordPress.CSRF.NonceVerification` - use `WordPress.Security.NonceVerification` instead. + - `WordPress.Functions.DontExtract` - use `WordPress.PHP.DontExtract` instead. + - `WordPress.Variables.GlobalVariables` - use `WordPress.WP.GlobalVariablesOverride` instead. + - `WordPress.VIP.CronInterval` - use `WordPress.WP.CronInterval` instead. + - `WordPress.VIP.DirectDatabaseQuery` - use `WordPress.DB.DirectDatabaseQuery` instead. + - `WordPress.VIP.PluginMenuSlug` - use `WordPress.Security.PluginMenuSlug` instead. + - `WordPress.VIP.SlowDBQuery` - use `WordPress.DB.SlowDBQuery` instead. + - `WordPress.VIP.TimezoneChange` - use `WordPress.WP.TimezoneChange` instead. + - `WordPress.VIP.ValidatedSanitizedInput` - use `WordPress.Security.ValidatedSanitizedInput` instead. + - `WordPress.WP.PreparedSQL` - use `WordPress.DB.PreparedSQL` instead. + - `WordPress.XSS.EscapeOutput` - use `WordPress.Security.EscapeOutput` instead. + - `WordPress.PHP.DiscouragedFunctions` without direct replacement. + The checks previously contained in this sniff were moved to separate sniffs in WPCS 0.11.0. + - `WordPress.Variables.VariableRestrictions` without replacement. + - `WordPress.VIP.AdminBarRemoval` without replacement. + - `WordPress.VIP.FileSystemWritesDisallow` without replacement. + - `WordPress.VIP.OrderByRand` without replacement. + - `WordPress.VIP.PostsPerPage` without replacement. + Part of the previous functionality was split off in WPCS 1.0.0 to the `WordPress.WP.PostsPerPage` sniff. + - `WordPress.VIP.RestrictedFunctions` without replacement. + - `WordPress.VIP.RestrictedVariables` without replacement. + - `WordPress.VIP.SessionFunctionsUsage` without replacement. + - `WordPress.VIP.SessionVariableUsage` without replacement. + - `WordPress.VIP.SuperGlobalInputUsage` without replacement. +- The `WordPress.DB.SlowDBQuery.DeprecatedWhitelistFlagFound` error code which is superseded by the blanket deprecation warning for using the now deprecated WPCS native whitelist comments. +- The `WordPress.PHP.TypeCasts.NonLowercaseFound` error code which has been replaced by the upstream `Generic.PHP.LowerCaseType` sniff. +- The `WordPress.PHP.TypeCasts.LongBoolFound` and `WordPress.PHP.TypeCasts.LongIntFound` error codes which has been replaced by the new upstream `PSR12.Keywords.ShortFormTypeKeywords` sniff. +- The `WordPress.Security.EscapeOutput.OutputNotEscapedShortEcho` error code which was only ever used if WPCS was run on PHP 5.3 with the `short_open_tag` ini directive set to `off`. +- The following sniff categories which were previously deprecated, have been removed, though select categories may be reinstated in the future: + - `CSRF` + - `Functions` + - `Variables` + - `VIP` + - `XSS` +- `WordPress.NamingConventions.ValidVariableName`: The `customVariableWhitelist` property, which had been deprecated since WordPressCS 0.11.0. Use the `customPropertiesWhitelist` property instead. +- `WordPress.Security.EscapeOutput`: The `customSanitizingFunctions` property, which had been deprecated since WordPressCS 0.5.0. Use the `customEscapingFunctions` property instead. +- `WordPress.Security.NonceVerification`: The `errorForSuperGlobals` and `warnForSuperGlobals` properties, which had been deprecated since WordPressCS 0.12.0. +- The `vip_powered_wpcom` function from the `Sniff::$autoEscapedFunctions` list which is used by the `WordPress.Security.EscapeOutput` sniff. +- The `AbstractVariableRestrictionsSniff` class, which was deprecated since WordPressCS 1.0.0. +- The `Sniff::has_html_open_tag()` utility method, which was deprecated since WordPressCS 1.0.0. +- The internal `$php_reserved_vars` property from the `WordPress.NamingConventions.ValidVariableName` sniff in favour of using a PHPCS native property which is now available. +- The class aliases and WPCS native autoloader used for PHPCS cross-version support. +- The unit test framework workarounds for PHPCS cross-version unit testing. +- Support for the `@codingStandardsChangeSetting` annotation, which is generally only used in unit tests. +- The old generic GitHub issue template which was replaced by more specific issue templates in WPCS 1.2.0. + +### Fixed +- Support for PHP 7.3. + `PHP_CodeSniffer` < 3.3.1 was not fully compatible with PHP 7.3. Now the minimum required PHPCS has been upped to `PHP_CodeSniffer` 3.3.1, WordPressCS will run on PHP 7.3 without issue. +- `WordPress.Arrays.ArrayDeclarationSpacing`: improved fixing of the placement of array items following an array item with a trailing multi-line comment. +- `WordPress.NamingConventions.ValidFunctionName`: the sniff will no longer throw false positives nor duplicate errors for methods declared in nested anonymous classes. + The error message has also been improved for methods in anonymous classes. +- `WordPress.NamingConventions.ValidFunctionName`: the sniff will no longer throw false positives for PHP 4-style class constructors/destructors where the name of the constructor/destructor method did not use the same case as the class name. + + +## [1.2.1] - 2018-12-18 + +Note: This will be the last release supporting PHP_CodeSniffer 2.x. + +### Changed +- The default value for `minimum_supported_wp_version`, as used by a [number of sniffs detecting usage of deprecated WP features](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#minimum-wp-version-to-check-for-usage-of-deprecated-functions-classes-and-function-parameters), has been updated to `4.7`. +- The `WordPress.NamingConventions.PrefixAllGlobals` sniff will now report the error for hook names and constant names declared with `define()` on the line containing the parameter for the hook/constant name. Previously, it would report the error on the line containing the function call. +- Various minor housekeeping fixes to inline documentation, rulesets, code. + +### Removed +- `comment_author_email_link()`, `comment_author_email()`, `comment_author_IP()`, `comment_author_link()`, `comment_author_rss()`, `comment_author_url_link()`, `comment_author_url()`, `comment_author()`, `comment_date()`, `comment_excerpt()`, `comment_form_title()`, `comment_form()`, `comment_id_fields()`, `comment_ID()`, `comment_reply_link()`, `comment_text_rss()`, `comment_text()`, `comment_time()`, `comment_type()`, `comments_link()`, `comments_number()`, `comments_popup_link()`, `comments_popup_script()`, `comments_rss_link()`, `delete_get_calendar_cache()`, `edit_bookmark_link()`, `edit_comment_link()`, `edit_post_link()`, `edit_tag_link()`, `get_footer()`, `get_header()`, `get_sidebar()`, `get_the_title()`, `next_comments_link()`, `next_image_link()`, `next_post_link()`, `next_posts_link()`, `permalink_anchor()`, `posts_nav_link()`, `previous_comments_link()`, `previous_image_link()`, `previous_post_link()`, `previous_posts_link()`, `sticky_class()`, `the_attachment_link()`, `the_author_link()`, `the_author_meta()`, `the_author_posts_link()`, `the_author_posts()`, `the_category_rss()`, `the_category()`, `the_content_rss()`, `the_content()`, `the_date_xml()`, `the_excerpt_rss()`, `the_excerpt()`, `the_feed_link()`, `the_ID()`, `the_meta()`, `the_modified_author()`, `the_modified_date()`, `the_modified_time()`, `the_permalink()`, `the_post_thumbnail()`, `the_search_query()`, `the_shortlink()`, `the_tags()`, `the_taxonomies()`, `the_terms()`, `the_time()`, `the_title_rss()`, `the_title()`, `wp_enqueue_script()`, `wp_meta()`, `wp_shortlink_header()` and `wp_shortlink_wp_head()` from the list of auto-escaped functions `Sniff::$autoEscapedFunctions`. This affects the `WordPress.Security.EscapeOutput` sniff. + +### Fixed +- The `WordPress.WhiteSpace.PrecisionAlignment` sniff would loose the value of a custom set `ignoreAlignmentTokens` property when scanning more than one file. + + ## [1.2.0] - 2018-11-12 ### Added @@ -36,9 +780,9 @@ _No documentation available about unreleased changes as of yet._ ### Changed - The `Sniff::valid_direct_scope()` method will now return the `$stackPtr` to the valid scope if a valid direct scope has been detected. Previously, it would return `true`. - Minor hardening and efficiency improvements to the `WordPress.NamingConventions.PrefixAllGlobals` sniff. -- The inline documentation of the `WordPress-Core` ruleset has been updated to be in line again with [the handbook](https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/). +- The inline documentation of the `WordPress-Core` ruleset has been updated to be in line again with [the handbook](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/). - The inline links to documentation about the VIP requirements have been updated. -- Updated the [custom ruleset example](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/blob/develop/phpcs.xml.dist.sample) to recommend using `PHPCompatibilityWP` rather than `PHPCompatibility`. +- Updated the [custom ruleset example](https://github.com/WordPress/WordPress-Coding-Standards/blob/develop/phpcs.xml.dist.sample) to recommend using `PHPCompatibilityWP` rather than `PHPCompatibility`. - All sniffs are now also being tested against PHP 7.3 for consistent sniff results. Note: PHP 7.3 is only supported in combination with PHPCS 3.3.1 or higher as `PHP_CodeSniffer` itself has an incompatibility in earlier versions. - Minor grammar fixes in text strings and documentation. @@ -70,7 +814,7 @@ _No documentation available about unreleased changes as of yet._ The user-defined whitelist will always be respected. By default, this property is set to `true` for the `WordPress-Core` ruleset and to `false` for the `WordPress-Extra` ruleset (which is stricter regarding these kind of best practices). - Metrics to the `WordPress.NamingConventions.PrefixAllGlobals` sniff to aid people in determining the most commonly used prefix in a legacy project. - For an example of how to use this feature, please see the detailed explanation in the [pull request](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/pull/1437). + For an example of how to use this feature, please see the detailed explanation in the [pull request](https://github.com/WordPress/WordPress-Coding-Standards/pull/1437). ### Changed - The `PEAR.Functions.FunctionCallSignature` sniff, which is part of the `WordPress-Core` ruleset, used to allow multiple function call parameters per line in multi-line function calls. This will no longer be allowed. @@ -131,7 +875,7 @@ If you are a maintainer of an external standard based on WPCS and any of your cu - New utility method `Sniff::is_use_of_global_constant()`. - A rationale to the package suggestion made via `composer.json`. - CI: Validation of the `composer.json` file on each build. -- A wiki page with instructions on how to [set up WPCS to run with Eclipse on XAMPP](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/How-to-use-WPCS-with-Eclipse-and-XAMPP). +- A wiki page with instructions on how to [set up WordPressCS to run with Eclipse on XAMPP](https://github.com/WordPress/WordPress-Coding-Standards/wiki/How-to-use-WordPressCS-with-Eclipse-and-XAMPP). - Readme: A link to an external resource with more examples for setting up PHPCS for CI. - Readme: A badge-based quick overview of the project. @@ -152,7 +896,7 @@ If you are a maintainer of an external standard based on WPCS and any of your cu - The `WordPress.VIP.PostsPerPage` sniff has been split into two distinct sniffs: - `WordPress.WP.PostsPerPage` which will check for the use of a high pagination limit and will throw a `warning` when this is encountered. For the `VIP` ruleset, the error level remains `error`. - `WordPress.VIP.PostsPerPage` wich will check for disabling of pagination. -- The default value for `minimum_supported_wp_version`, as used by a [number of sniffs detecting usage of deprecated WP features](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#minimum-wp-version-to-check-for-usage-of-deprecated-functions-classes-and-function-parameters), has been updated to `4.6`. +- The default value for `minimum_supported_wp_version`, as used by a [number of sniffs detecting usage of deprecated WP features](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#minimum-wp-version-to-check-for-usage-of-deprecated-functions-classes-and-function-parameters), has been updated to `4.6`. - The `WordPress.WP.AlternativeFunctions` sniff will now only throw a warning if/when the recommended alternative function is available in the minimum supported WP version of a project. In addition to this, certain alternatives are only valid alternatives in certain circumstances, like when the WP version only supports the first parameter of the PHP function it is trying to replace. This will now be taken into account for: @@ -175,8 +919,8 @@ If you are a maintainer of an external standard based on WPCS and any of your cu - CI: Each change will now also be checked for PHP cross-version compatibility. - CI: The rulesets will now also be tested on each change to ensure no unexpected messages are thrown. - CI: Minor changes to the script to make the build testing faster. -- Updated the [custom ruleset example](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/blob/develop/phpcs.xml.dist.sample) for the changes contained in this release and to reflect current best practices regarding the PHPCompatibility standard. -- The instructions on how to set up WPCS for various IDEs have been moved from the `README` to the [wiki](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki). +- Updated the [custom ruleset example](https://github.com/WordPress/WordPress-Coding-Standards/blob/develop/phpcs.xml.dist.sample) for the changes contained in this release and to reflect current best practices regarding the PHPCompatibility standard. +- The instructions on how to set up WPCS for various IDEs have been moved from the `README` to the [wiki](https://github.com/WordPress/WordPress-Coding-Standards/wiki). - Updated output examples in `README.md` and `CONTRIBUTING.md` and other minor changes to these files. - Updated references to the PHPCompatibility standard to reflect its new location and recommend using PHPCompatibilityWP. @@ -209,11 +953,11 @@ If you are a maintainer of an external standard based on WPCS and any of your cu - `Variables` - `XSS` - The `posts_per_page` property in the `WordPress.VIP.PostsPerPage` sniff has been deprecated as the related functionality has been moved to the `WordPress.WP.PostsPerPage` sniff. - See [WP PostsPerPage: post limit](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#wp-postsperpage-post-limit) for more information about this property. + See [WP PostsPerPage: post limit](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#wp-postsperpage-post-limit) for more information about this property. - The `exclude` property which is available to most sniffs which extend the `AbstractArrayAssignmentRestrictions`, `AbstractFunctionRestrictions` and `AbstractVariableRestrictions` classes or any of their children, used to be a `string` property and expected a comma-delimited list of groups to exclude. The type of the property has now been changed to `array`. Custom rulesets which pass this property need to be adjusted to reflect this change. Support for passing the property as a comma-delimited string has been deprecated and will be removed in WPCS 2.0.0. - See [Excluding a group of checks](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#excluding-a-group-of-checks) for more information about the sniffs affected by this change. + See [Excluding a group of checks](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#excluding-a-group-of-checks) for more information about the sniffs affected by this change. - The `AbstractVariableRestrictionsSniff` class has been deprecated as all sniffs depending on this class have been deprecated. Unless a new sniff is created in the near future which uses this class, the abstract class will be removed in WPCS 2.0.0. - The `Sniff::has_html_open_tag()` utility method has been deprecated as it is now only used by deprecated sniffs. The method will be removed in WPCS 2.0.0. @@ -228,7 +972,7 @@ If you are a maintainer of an external standard based on WPCS and any of your cu PHPCS 3.2.0 introduced new annotations which can be used inline to selectively disable/ignore certain sniffs. **Note**: The initial implementation of the new annotations was buggy. If you intend to start using these new style annotations, you are strongly advised to use PHPCS 3.3.0 or higher. For more information about these annotations, please refer to the [PHPCS Wiki](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Advanced-Usage#ignoring-parts-of-a-file). - - The [WPCS native whitelist comments](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Whitelisting-code-which-flags-errors) can now be combined with the new style PHPCS whitelist annotations in the `-- for reasons` part of the annotation. + - The [WPCS native whitelist comments](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Whitelisting-code-which-flags-errors) can now be combined with the new style PHPCS whitelist annotations in the `-- for reasons` part of the annotation. - `WordPress.Arrays.ArrayDeclarationSpacing`: the fixer will now handle the new style annotations correctly. - `WordPress.Arrays.CommaAfterArrayItem`: prevent a fixer loop when new style annotations are encountered. - `WordPress.Files.FileName`: respect the new style annotations if these would selectively disable this sniff. @@ -298,10 +1042,10 @@ If you are a maintainer of an external standard based on WPCS and any of your cu ### Added - `WordPress.Arrays.MultipleStatementAlignment` sniff to the `WordPress-Core` ruleset which will align the array assignment operator for multi-item, multi-line associative arrays. - This new sniff offers four custom properties to customize its behaviour: [`ignoreNewlines`](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#array-alignment-allow-for-new-lines), [`exact`](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#array-alignment-allow-non-exact-alignment), [`maxColumn`](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#array-alignment-maximum-column) and [`alignMultilineItems`](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#array-alignment-dealing-with-multi-line-items). + This new sniff offers four custom properties to customize its behaviour: [`ignoreNewlines`](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#array-alignment-allow-for-new-lines), [`exact`](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#array-alignment-allow-non-exact-alignment), [`maxColumn`](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#array-alignment-maximum-column) and [`alignMultilineItems`](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#array-alignment-dealing-with-multi-line-items). - `WordPress.DB.PreparedSQLPlaceholders` sniff to the `WordPress-Core` ruleset which will analyse the placeholders passed to `$wpdb->prepare()` for their validity, check whether queries using `IN ()` and `LIKE` statements are created correctly and will check whether a correct number of replacements are passed. This sniff should help detect queries which are impacted by the security fixes to `$wpdb->prepare()` which shipped with WP 4.8.2 and 4.8.3. - The sniff also adds a new ["PreparedSQLPlaceholders replacement count" whitelist comment](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Whitelisting-code-which-flags-errors#preparedsql-placeholders-vs-replacements) for pertinent replacement count vs placeholder mismatches. Please consider carefully whether something could be a bug when you are tempted to use the whitelist comment and if so, [report it](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/issues/new). + The sniff also adds a new ["PreparedSQLPlaceholders replacement count" whitelist comment](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Whitelisting-code-which-flags-errors#preparedsql-placeholders-vs-replacements) for pertinent replacement count vs placeholder mismatches. Please consider carefully whether something could be a bug when you are tempted to use the whitelist comment and if so, [report it](https://github.com/WordPress/WordPress-Coding-Standards/issues/new). - `WordPress.PHP.DiscourageGoto` sniff to the `WordPress-Core` ruleset. - `WordPress.PHP.RestrictedFunctions` sniff to the `WordPress-Core` ruleset which initially forbids the use of `create_function()`. This was previous only discouraged under certain circumstances. @@ -310,7 +1054,7 @@ If you are a maintainer of an external standard based on WPCS and any of your cu - `WordPress.WhiteSpace.SemicolonSpacing` sniff to the `WordPress-Core` ruleset which will throw a (fixable) error when whitespace is found before a semi-colon, except for when the semi-colon denotes an empty `for()` condition. - `WordPress.CodeAnalysis.AssignmentInCondition` sniff to the `WordPress-Extra` ruleset. - `WordPress.WP.DiscouragedConstants` sniff to the `WordPress-Extra` and `WordPress-VIP` rulesets to detect usage of deprecated WordPress constants, such as `STYLESHEETPATH` and `HEADER_IMAGE`. -- Ability to pass the `minimum_supported_version` to use for the `DeprecatedFunctions`, `DeprecatedClasses` and `DeprecatedParameters` sniff in one go. You can pass a `minimum_supported_wp_version` runtime variable for this [from the command line or pass it using a `config` directive in a custom ruleset](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#setting-minimum-supported-wp-version-for-all-sniffs-in-one-go-wpcs-0140). +- Ability to pass the `minimum_supported_version` to use for the `DeprecatedFunctions`, `DeprecatedClasses` and `DeprecatedParameters` sniff in one go. You can pass a `minimum_supported_wp_version` runtime variable for this [from the command line or pass it using a `config` directive in a custom ruleset](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#setting-minimum-supported-wp-version-for-all-sniffs-in-one-go-wpcs-0140). - `Generic.Formatting.MultipleStatementAlignment` - customized to have a `maxPadding` of `40` -, `Generic.Functions.FunctionCallArgumentSpacing` and `Squiz.WhiteSpace.ObjectOperatorSpacing` to the `WordPress-Core` ruleset. - `Squiz.Scope.MethodScope`, `Squiz.Scope.MemberVarScope`, `Squiz.WhiteSpace.ScopeKeywordSpacing`, `PSR2.Methods.MethodDeclaration`, `Generic.Files.OneClassPerFile`, `Generic.Files.OneInterfacePerFile`, `Generic.Files.OneTraitPerFile`, `PEAR.Files.IncludingFile`, `Squiz.WhiteSpace.LanguageConstructSpacing`, `PSR2.Namespaces.NamespaceDeclaration` to the `WordPress-Extra` ruleset. - The `is_class_constant()`, `is_class_property` and `valid_direct_scope()` utility methods to the `WordPress\Sniff` class. @@ -319,13 +1063,13 @@ If you are a maintainer of an external standard based on WPCS and any of your cu - When passing an array property via a custom ruleset to PHP_CodeSniffer, spaces around the key/value are taken as intentional and parsed as part of the array key/value. In practice, this leads to confusion and WPCS does not expect any values which could be preceded/followed by a space, so for the WordPress Coding Standard native array properties, like `customAutoEscapedFunction`, `text_domain`, `prefixes`, WPCS will now trim whitespace from the keys/values received before use. - The WPCS native whitelist comments used to only work when they were put on the _end of the line_ of the code they applied to. As of now, they will also be recognized when they are be put at the _end of the statement_ they apply to. - The `WordPress.Arrays.ArrayDeclarationSpacing` sniff used to enforce all associative arrays to be multi-line. The handbook has been updated to only require this for multi-item associative arrays and the sniff has been updated accordingly. - [The original behaviour can still be enforced](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#arrays-forcing-single-item-associative-arrays-to-be-multi-line) by setting the new `allow_single_item_single_line_associative_arrays` property to `false` in a custom ruleset. + [The original behaviour can still be enforced](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#arrays-forcing-single-item-associative-arrays-to-be-multi-line) by setting the new `allow_single_item_single_line_associative_arrays` property to `false` in a custom ruleset. - The `WordPress.NamingConventions.PrefixAllGlobals` sniff will now allow for a limited list of WP core hooks which are intended to be called by plugins and themes. - The `WordPress.PHP.DiscouragedFunctions` sniff used to include `create_function`. This check has been moved to the new `WordPress.PHP.RestrictedFunctions` sniff. - The `WordPress.PHP.StrictInArray` sniff now has a separate error code `FoundNonStrictFalse` for when the `$strict` parameter has been set to `false`. This allows for excluding the warnings for that particular situation, which will normally be intentional, via a custom ruleset. -- The `WordPress.VIP.CronInterval` sniff now allows for customizing the minimum allowed cron interval by [setting a property in a custom ruleset](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#vip-croninterval-minimum-interval). +- The `WordPress.VIP.CronInterval` sniff now allows for customizing the minimum allowed cron interval by [setting a property in a custom ruleset](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#vip-croninterval-minimum-interval). - The `WordPress.VIP.RestrictedFunctions` sniff used to prohibit the use of certain WP native functions, recommending the use of `wpcom_vip_get_term_link()`, `wpcom_vip_get_term_by()` and `wpcom_vip_get_category_by_slug()` instead, as the WP native functions were not being cached. As the results of the relevant WP native functions are cached as of WP 4.8, the advice has now been reversed i.e. use the WP native functions instead of `wpcom...` functions. -- The `WordPress.VIP.PostsPerPage` sniff now allows for customizing the `post_per_page` limit for which the sniff will trigger by [setting a property in a custom ruleset](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#vip-postsperpage-post-limit). +- The `WordPress.VIP.PostsPerPage` sniff now allows for customizing the `post_per_page` limit for which the sniff will trigger by [setting a property in a custom ruleset](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#vip-postsperpage-post-limit). - The `WordPress.WP.I18n` sniff will now allow and actively encourage omitting the text domain in I18n function calls if the text domain passed via the `text_domain` property is `default`, i.e. the domain used by Core. When `default` is one of several text domains passed via the `text_domain` property, the error thrown when the domain is missing has been downgraded to a `warning`. - The `WordPress.XSS.EscapeOutput` sniff now has a separate error code `OutputNotEscapedShortEcho` and the error message texts have been updated. @@ -338,7 +1082,7 @@ If you are a maintainer of an external standard based on WPCS and any of your cu - Various minor documentation fixes. - Improved the Atom setup instructions in the Readme. - Updated the unit testing information in Contributing. -- Updated the [custom ruleset example](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/blob/develop/phpcs.xml.dist.sample) for the changes contained in this release and to make it more explicit what is recommended versus example code. +- Updated the [custom ruleset example](https://github.com/WordPress/WordPress-Coding-Standards/blob/develop/phpcs.xml.dist.sample) for the changes contained in this release and to make it more explicit what is recommended versus example code. - The minimum recommended version for the suggested `DealerDirect/phpcodesniffer-composer-installer` Composer plugin has gone up to `0.4.3`. This patch version fixes support for PHP 5.3. ### Fixed @@ -364,7 +1108,7 @@ If you are a maintainer of an external standard based on WPCS and any of your cu ### Added - Support for PHP_CodeSniffer 3.0.2+. The minimum required PHPCS version (2.9.0) stays the same. -- Support for the PHPCS 3 `--ignore-annotations` command line option. If you pass this option, both PHPCS native `@ignore ...` annotations as well as the WPCS specific [whitelist flags](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Whitelisting-code-which-flags-errors) will be ignored. +- Support for the PHPCS 3 `--ignore-annotations` command line option. If you pass this option, both PHPCS native `@ignore ...` annotations as well as the WPCS specific [whitelist flags](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Whitelisting-code-which-flags-errors) will be ignored. ### Changed - The minimum required PHP version is now 5.3 when used in combination with PHPCS 2.x and PHP 5.4 when used in combination with PHPCS 3.x. @@ -397,9 +1141,9 @@ If you are a maintainer of an external standard based on WPCS and any of your cu - `WordPress.Classes.ClassInstantion` sniff to the `WordPress-Extra` ruleset to detect - and auto-fix - missing parentheses on object instantiation and superfluous whitespace in PHP and JS files. The sniff will also detect `new` being assigned by reference. - `WordPress.CodeAnalysis.EmptyStatement` sniff to the `WordPress-Extra` ruleset to detect - and auto-fix - superfluous semi-colons and empty PHP open-close tag combinations. - `WordPress.NamingConventions.PrefixAllGlobals` sniff to the `WordPress-Extra` ruleset to verify that all functions, classes, interfaces, traits, variables, constants and hook names which are declared/defined in the global namespace are prefixed with one of the prefixes provided via a custom property or via the command line. - To activate this sniff, [one or more allowed prefixes should be provided to the sniff](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#naming-conventions-prefix-everything-in-the-global-namespace). This can be done using a custom ruleset or via the command line. + To activate this sniff, [one or more allowed prefixes should be provided to the sniff](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#naming-conventions-prefix-everything-in-the-global-namespace). This can be done using a custom ruleset or via the command line. PHP superglobals and WP global variables are exempt from variable name prefixing. Deprecated hook names will also be disregarded when non-prefixed. Back-fills for known native PHP functionality is also accounted for. - For verified exceptions, [unprefixed code can be whitelisted](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Whitelisting-code-which-flags-errors#non-prefixed-functionclassvariableconstant-in-the-global-namespace). + For verified exceptions, [unprefixed code can be whitelisted](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Whitelisting-code-which-flags-errors#non-prefixed-functionclassvariableconstant-in-the-global-namespace). Code in unit test files is automatically exempt from this sniff. - `WordPress.WP.DeprecatedClasses` sniff to the `WordPress-Extra` ruleset to detect usage of deprecated WordPress classes. - `WordPress.WP.DeprecatedParameters` sniff to the `WordPress-Extra` ruleset to detect deprecated parameters being passed to WordPress functions with a value other than the expected default. @@ -415,7 +1159,7 @@ If you are a maintainer of an external standard based on WPCS and any of your cu - Improved support for detecting issues in code using heredoc and/or nowdoc syntax. - Improved sniff efficiency, precision and performance for a number of sniffs. - Updated a few sniffs to take advantage of new features and fixes which are included in PHP_CodeSniffer 2.9.0. -- `WordPress.Files.Filename`: The "file name mirrors the class name prefixed with 'class'" check for PHP files containing a class will no longer be applied to typical unit test classes, i.e. for classes which extend `WP_UnitTestCase`, `PHPUnit_Framework_TestCase` and `PHPUnit\Framework\TestCase`. Additional test case base classes can be passed to the sniff using the new [`custom_test_class_whitelist` property](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#custom-unit-test-classes). +- `WordPress.Files.Filename`: The "file name mirrors the class name prefixed with 'class'" check for PHP files containing a class will no longer be applied to typical unit test classes, i.e. for classes which extend `WP_UnitTestCase`, `PHPUnit_Framework_TestCase` and `PHPUnit\Framework\TestCase`. Additional test case base classes can be passed to the sniff using the new [`custom_test_class_whitelist` property](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#custom-unit-test-classes). - The `WordPress.Files.FileName` sniff allows now for more theme-specific template hierarchy based file name exceptions. - The whitelist flag for the `WordPress.VIP.SlowQuery` sniff was `tax_query` which was unintuitive. This has now been changed to `slow query` to be in line with other whitelist flags. - The `WordPress.WhiteSpace.OperatorSpacing` sniff will now ignore operator spacing within `declare()` statements. @@ -424,7 +1168,7 @@ If you are a maintainer of an external standard based on WPCS and any of your cu - The `WordPress.XSS.EscapeOutput` sniff will now also detect unescaped output when the short open echo tags ` [![Latest Stable Version](https://poser.pugx.org/wp-coding-standards/wpcs/v/stable)](https://packagist.org/packages/wp-coding-standards/wpcs) -[![Travis Build Status](https://travis-ci.org/WordPress-Coding-Standards/WordPress-Coding-Standards.svg?branch=master)](https://travis-ci.org/WordPress-Coding-Standards/WordPress-Coding-Standards) -[![Release Date of the Latest Version](https://img.shields.io/github/release-date/WordPress-Coding-Standards/WordPress-Coding-Standards.svg?maxAge=1800)](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/releases) +[![Release Date of the Latest Version](https://img.shields.io/github/release-date/WordPress/WordPress-Coding-Standards.svg?maxAge=1800)](https://github.com/WordPress/WordPress-Coding-Standards/releases) :construction: [![Latest Unstable Version](https://img.shields.io/badge/unstable-dev--develop-e68718.svg?maxAge=2419200)](https://packagist.org/packages/wp-coding-standards/wpcs#dev-develop) -[![Travis Build Status](https://travis-ci.org/WordPress-Coding-Standards/WordPress-Coding-Standards.svg?branch=develop)](https://travis-ci.org/WordPress-Coding-Standards/WordPress-Coding-Standards) -[![Last Commit to Unstable](https://img.shields.io/github/last-commit/WordPress-Coding-Standards/WordPress-Coding-Standards/develop.svg)](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/commits/develop) + +[![Basic QA checks](https://github.com/WordPress/WordPress-Coding-Standards/actions/workflows/basic-qa.yml/badge.svg)](https://github.com/WordPress/WordPress-Coding-Standards/actions/workflows/basic-qa.yml) +[![Unit Tests](https://github.com/WordPress/WordPress-Coding-Standards/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/WordPress/WordPress-Coding-Standards/actions/workflows/unit-tests.yml) +[![codecov.io](https://codecov.io/gh/WordPress/WordPress-Coding-Standards/graph/badge.svg?token=UzFYn0RzVG&branch=develop)](https://codecov.io/gh/WordPress/WordPress-Coding-Standards?branch=develop) [![Minimum PHP Version](https://img.shields.io/packagist/php-v/wp-coding-standards/wpcs.svg?maxAge=3600)](https://packagist.org/packages/wp-coding-standards/wpcs) -[![Tested on PHP 5.3 to nightly](https://img.shields.io/badge/tested%20on-PHP%205.3%20|%205.4%20|%205.5%20|%205.6%20|%207.0%20|%207.1%20|%207.2%20|%20nightly-green.svg?maxAge=2419200)](https://travis-ci.org/WordPress-Coding-Standards/WordPress-Coding-Standards) +[![Tested on PHP 5.4 to 8.3](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%20|%208.3-green.svg?maxAge=2419200)](https://github.com/WordPress/WordPress-Coding-Standards/actions/workflows/unit-tests.yml) -[![License: MIT](https://poser.pugx.org/wp-coding-standards/wpcs/license)](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/blob/develop/LICENSE) +[![License: MIT](https://poser.pugx.org/wp-coding-standards/wpcs/license)](https://github.com/WordPress/WordPress-Coding-Standards/blob/develop/LICENSE) [![Total Downloads](https://poser.pugx.org/wp-coding-standards/wpcs/downloads)](https://packagist.org/packages/wp-coding-standards/wpcs/stats) -[![Number of Contributors](https://img.shields.io/github/contributors/WordPress-Coding-Standards/WordPress-Coding-Standards.svg?maxAge=3600)](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/graphs/contributors) @@ -21,11 +21,12 @@ # WordPress Coding Standards for PHP_CodeSniffer * [Introduction](#introduction) -* [Project history](#project-history) +* [Minimum Requirements](#minimum-requirements) * [Installation](#installation) - + [Requirements](#requirements) - + [Composer](#composer) - + [Standalone](#standalone) + + [Composer Project-based Installation](#composer-project-based-installation) + + [Composer Global Installation](#composer-global-installation) + + [Updating your WordPressCS install to a newer version](#updating-your-wordpresscs-install-to-a-newer-version) + + [Using your WordPressCS install](#using-your-wordpresscs-install) * [Rulesets](#rulesets) + [Standards subsets](#standards-subsets) + [Using a custom ruleset](#using-a-custom-ruleset) @@ -33,252 +34,209 @@ + [Recommended additional rulesets](#recommended-additional-rulesets) * [How to use](#how-to-use) + [Command line](#command-line) - + [Using PHPCS and WPCS from within your IDE](#using-phpcs-and-wpcs-from-within-your-ide) -* [Running your code through WPCS automatically using CI tools](#running-your-code-through-wpcs-automatically-using-ci-tools) - + [Travis CI](#travis-ci) -* [Fixing errors or whitelisting them](#fixing-errors-or-whitelisting-them) - + [Tools shipped with WPCS](#tools-shipped-with-wpcs) + + [Using PHPCS and WordPressCS from within your IDE](#using-phpcs-and-wordpresscs-from-within-your-ide) +* [Running your code through WordPressCS automatically using Continuous Integration tools](#running-your-code-through-wordpresscs-automatically-using-continuous-integration-tools) +* [Fixing errors or ignoring them](#fixing-errors-or-ignoring-them) + + [Tools shipped with WordPressCS](#tools-shipped-with-wordpresscs) * [Contributing](#contributing) * [License](#license) + ## Introduction This project is a collection of [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) rules (sniffs) to validate code developed for WordPress. It ensures code quality and adherence to coding conventions, especially the official [WordPress Coding Standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/). -## Project history - - - On 22nd April 2009, the original project from [Urban Giraffe](https://urbangiraffe.com/articles/wordpress-codesniffer-standard/) was packaged and published. - - In May 2011 the project was forked and [added](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/commit/04fd547c691ca2baae3fa8e195a46b0c9dd671c5) to GitHub by [Chris Adams](https://chrisadams.me.uk/). - - In April 2012 [XWP](https://xwp.co/) started to dedicate resources to develop and lead the creation of the sniffs and rulesets for `WordPress-Core`, `WordPress-VIP` (WordPress.com VIP), and `WordPress-Extra`. - - In May 2015, an initial documentation ruleset was [added](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/commit/b1a4bf8232a22563ef66f8a529357275a49f47dc#diff-a17c358c3262a26e9228268eb0a7b8c8) as `WordPress-Docs`. - - In 2015, [J.D. Grimes](https://github.com/JDGrimes) began significant contributions, along with maintenance from [Gary Jones](https://github.com/GaryJones). - - In 2016, [Juliette Reinders Folmer](https://github.com/jrfnl) began contributing heavily, adding more commits in a year than anyone else in the five years since the project was added to GitHub. - - In July 2018, version [`1.0.0`](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/releases/tag/1.0.0) of the project was released. - -## Installation - -### Requirements - -The WordPress Coding Standards require PHP 5.3 or higher and [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) version **2.9.0** or higher. -As of version 0.13.0, the WordPress Coding Standards are compatible with PHPCS 3.0.2+. In that case, the minimum PHP requirement is PHP 5.4. - -### Composer - -Standards can be installed with the [Composer](https://getcomposer.org/) dependency manager: - - composer create-project wp-coding-standards/wpcs --no-dev +## Minimum Requirements -Running this command will: +The WordPress Coding Standards package requires: +* PHP 5.4 or higher with the following extensions enabled: + - [Filter](https://www.php.net/book.filter) + - [libxml](https://www.php.net/book.libxml) + - [Tokenizer](https://www.php.net/book.tokenizer) + - [XMLReader](https://www.php.net/book.xmlreader) +* [Composer](https://getcomposer.org/) -1. Install WordPress standards into `wpcs` directory. -2. Install PHP_CodeSniffer. -3. Register WordPress standards in PHP_CodeSniffer configuration. -4. Make `phpcs` command available from `wpcs/vendor/bin`. +For the best results, it is recommended to also ensure the following additional PHP extensions are enabled: + - [iconv](https://www.php.net/book.iconv) + - [Multibyte String](https://www.php.net/book.mbstring) -For the convenience of using `phpcs` as a global command, you may want to add the path to the `wpcs/vendor/scripts` (PHPCS 2.x) and/or `wpcs/vendor/bin` (PHPCS 3.x) directories to a `PATH` environment variable for your operating system. - -#### Installing WPCS as a dependency - -When installing the WordPress Coding Standards as a dependency in a larger project, the above mentioned step 3 will not be executed automatically. +## Installation -There are two actively maintained Composer plugins which can handle the registration of standards with PHP_CodeSniffer for you: -* [composer-phpcodesniffer-standards-plugin](https://github.com/higidi/composer-phpcodesniffer-standards-plugin) -* [phpcodesniffer-composer-installer](https://github.com/DealerDirect/phpcodesniffer-composer-installer):"^0.4.3" +As of WordPressCS 3.0.0, installation via Composer using the below instructions is the only supported type of installation. -It is strongly suggested to `require` one of these plugins in your project to handle the registration of external standards with PHPCS for you. +[Composer](https://getcomposer.org/) will automatically install the project dependencies and register the rulesets from WordPressCS and other external standards with PHP_CodeSniffer using the [Composer PHPCS plugin](https://github.com/PHPCSStandards/composer-installer). -### Standalone +> If you are upgrading from an older WordPressCS version to version 3.0.0, please read the [Upgrade guide for ruleset maintainers and end-users](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Upgrade-Guide-to-WordPressCS-3.0.0-for-ruleset-maintainers) first! -1. Install PHP_CodeSniffer by following its [installation instructions](https://github.com/squizlabs/PHP_CodeSniffer#installation) (via Composer, Phar file, PEAR, or Git checkout). +### Composer Project-based Installation - Do ensure that PHP_CodeSniffer's version matches our [requirements](#requirements), if, for example, you're using [VVV](https://github.com/Varying-Vagrant-Vagrants/VVV). +Run the following from the root of your project: +```bash +composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true +composer require --dev wp-coding-standards/wpcs:"^3.0" +``` -2. Clone the WordPress standards repository: +### Composer Global Installation - git clone -b master https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards.git wpcs +Alternatively, you may want to install this standard globally: +```bash +composer global config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true +composer global require --dev wp-coding-standards/wpcs:"^3.0" +``` -3. Add its path to the PHP_CodeSniffer configuration: +### Updating your WordPressCS install to a newer version - phpcs --config-set installed_paths /path/to/wpcs +If you installed WordPressCS using either of the above commands, you can upgrade to a newer version as follows: +```bash +# Project local install +composer update wp-coding-standards/wpcs --with-dependencies - **Pro-tip:** Alternatively, you can tell PHP_CodeSniffer the path to the WordPress standards by adding the following snippet to your custom ruleset: - ```xml - - ``` +# Global install +composer global update wp-coding-standards/wpcs --with-dependencies +``` -To summarize: +### Using your WordPressCS install +Once you have installed WordPressCS using either of the above commands, use it as follows: ```bash -cd ~/projects -git clone https://github.com/squizlabs/PHP_CodeSniffer.git phpcs -git clone -b master https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards.git wpcs -cd phpcs -#PHPCS 2.x -./scripts/phpcs --config-set installed_paths ../wpcs -#PHPCS 3.x -./bin/phpcs --config-set installed_paths ../wpcs +# Project local install +vendor/bin/phpcs -ps . --standard=WordPress + +# Global install +%USER_DIRECTORY%/Composer/vendor/bin/phpcs -ps . --standard=WordPress ``` -And then add the `~/projects/phpcs/scripts` (PHPCS 2.x) or `~/projects/phpcs/bin` (PHPCS 3.x) directory to your `PATH` environment variable via your `.bashrc`. +> **Pro-tip**: For the convenience of using `phpcs` as a global command, use the _Global install_ method and add the path to the `%USER_DIRECTORY%/Composer/vendor/bin` directory to the `PATH` environment variable for your operating system. -You should then see `WordPress-Core` et al listed when you run `phpcs -i`. ## Rulesets ### Standards subsets -The project encompasses a super-set of the sniffs that the WordPress community may need. If you use the `WordPress` standard you will get all the checks. Some of them might be unnecessary for your environment, for example, those specific to WordPress.com VIP coding requirements. +The project encompasses a super-set of the sniffs that the WordPress community may need. If you use the `WordPress` standard you will get all the checks. You can use the following as standard names when invoking `phpcs` to select sniffs, fitting your needs: * `WordPress` - complete set with all of the sniffs in the project - - `WordPress-Core` - main ruleset for [WordPress core coding standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/) - - `WordPress-Docs` - additional ruleset for [WordPress inline documentation standards](https://make.wordpress.org/core/handbook/best-practices/inline-documentation-standards/) - - `WordPress-Extra` - extended ruleset for recommended best practices, not sufficiently covered in the WordPress core coding standards + - `WordPress-Core` - main ruleset for [WordPress core coding standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/) + - `WordPress-Docs` - additional ruleset for [WordPress inline documentation standards](https://developer.wordpress.org/coding-standards/inline-documentation-standards/php/) + - `WordPress-Extra` - extended ruleset with recommended best practices, not sufficiently covered in the WordPress core coding standards - includes `WordPress-Core` -**Notes:** This WPCS package contains the sniffs for another ruleset, `WordPress-VIP`. This ruleset was originally intended to aid with the [WordPress.com VIP coding requirements](https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/), but this is no longer used or recommended by the WordPress.com VIP team or their clients, since they prefer to use their [official VIP coding standards](https://github.com/Automattic/VIP-Coding-Standards) ruleset instead. +### Using a custom ruleset -Before WPCS `1.0.0`, the WordPress-VIP ruleset was included as part of the complete `WordPress` ruleset. **As of `1.0.0` the `WordPress-VIP` ruleset is not part of the WordPress ruleset, and it is deprecated**. The remaining `WordPress-VIP` sniffs may still be referenced in custom rulesets, so to maintain some backwards compatibility, they will remain in WPCS until `2.0.0`. See [#1309](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/issues/1309) for more information. +If you need to further customize the selection of sniffs for your project - you can create a custom ruleset file. -### Using a custom ruleset +When you name this file either `.phpcs.xml`, `phpcs.xml`, `.phpcs.xml.dist` or `phpcs.xml.dist`, PHP_CodeSniffer will automatically locate it as long as it is placed in the directory from which you run the CodeSniffer or in a directory above it. If you follow these naming conventions you don't have to supply a `--standard` CLI argument. -If you need to further customize the selection of sniffs for your project - you can create a custom ruleset file. When you name this file either `phpcs.xml` or `phpcs.xml.dist`, PHP_CodeSniffer will automatically locate it as long as it is placed in the directory from which you run the CodeSniffer or in a directory above it. If you follow these naming conventions you don't have to supply a `--standard` arg. For more info, read about [using a default configuration file](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Advanced-Usage#using-a-default-configuration-file). See also provided [`phpcs.xml.dist.sample`](phpcs.xml.dist.sample) file and [fully annotated example](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Annotated-ruleset.xml) in the PHP_CodeSniffer documentation. +For more info, read about [using a default configuration file](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Advanced-Usage#using-a-default-configuration-file). See also the provided WordPressCS [`phpcs.xml.dist.sample`](phpcs.xml.dist.sample) file and the [fully annotated example ruleset](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Annotated-ruleset.xml) in the PHP_CodeSniffer documentation. ### Customizing sniff behaviour -The WordPress Coding Standard contains a number of sniffs which are configurable. This means that you can turn parts of the sniff on or off, or change the behaviour by setting a property for the sniff in your custom `phpcs.xml` file. +The WordPress Coding Standard contains a number of sniffs which are configurable. This means that you can turn parts of the sniff on or off, or change the behaviour by setting a property for the sniff in your custom `[.]phpcs.xml[.dist]` file. + +You can find a complete list of all the properties you can change for the WordPressCS sniffs in the [wiki](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties). + +WordPressCS also uses sniffs from PHPCSExtra and from PHP_CodeSniffer itself. +The [README for PHPCSExtra](https://github.com/PHPCSStandards/PHPCSExtra) contains information on the properties which can be set for the sniff from PHPCSExtra. +Information on custom properties which can be set for sniffs from PHP_CodeSniffer can be found in the [PHP_CodeSniffer wiki](https://github.com/squizlabs/PHP_CodeSniffer/wiki/Customisable-Sniff-Properties). -You can find a complete list of all the properties you can change in the [wiki](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties). ### Recommended additional rulesets +#### PHPCompatibility + The [PHPCompatibility](https://github.com/PHPCompatibility/PHPCompatibility) ruleset and its subset [PHPCompatibilityWP](https://github.com/PHPCompatibility/PHPCompatibilityWP) come highly recommended. -The [PHPCompatibility](https://github.com/PHPCompatibility/PHPCompatibility) sniffs are designed to analyse your code for cross-PHP version compatibility. +The [PHPCompatibility](https://github.com/PHPCompatibility/PHPCompatibility) sniffs are designed to analyse your code for cross-version PHP compatibility. The [PHPCompatibilityWP](https://github.com/PHPCompatibility/PHPCompatibilityWP) ruleset is based on PHPCompatibility, but specifically crafted to prevent false positives for projects which expect to run within the context of WordPress, i.e. core, plugins and themes. -Install either as a separate ruleset and either run it separately against your code or add it to your custom ruleset. +Install either as a separate ruleset and run it separately against your code or add it to your custom ruleset, like so: +```xml + + + *\.php$ + +``` -Whichever way you run it, do make sure you set the `testVersion` to run the sniffs against. The `testVersion` determines for which PHP versions you will receive compatibility information. The recommended setting for this at this moment is `5.2-` to support the same PHP versions as WordPress Core supports. +Whichever way you run it, do make sure you set the `testVersion` to run the sniffs against. The `testVersion` determines for which PHP versions you will receive compatibility information. The recommended setting for this at this moment is `7.0-` to support the same PHP versions as WordPress Core supports. For more information about setting the `testVersion`, see: * [PHPCompatibility: Sniffing your code for compatibility with specific PHP version(s)](https://github.com/PHPCompatibility/PHPCompatibility#sniffing-your-code-for-compatibility-with-specific-php-versions) * [PHPCompatibility: Using a custom ruleset](https://github.com/PHPCompatibility/PHPCompatibility#using-a-custom-ruleset) +#### VariableAnalysis + +For some additional checks around (undefined/unused) variables, the [`VariableAnalysis`](https://github.com/sirbrillig/phpcs-variable-analysis/) standard is a handy addition. + +#### VIP Coding Standards + +For those projects which deploy to the WordPress VIP platform, it is recommended to also use the [official WordPress VIP coding standards](https://github.com/Automattic/VIP-Coding-Standards) ruleset. + + ## How to use ### Command line Run the `phpcs` command line tool on a given file or directory, for example: - - phpcs --standard=WordPress wp-load.php +```bash +vendor/bin/phpcs --standard=WordPress wp-load.php +``` Will result in following output: - - ------------------------------------------------------------------------------------------ - FOUND 8 ERRORS AND 10 WARNINGS AFFECTING 11 LINES - ------------------------------------------------------------------------------------------ - 24 | WARNING | [ ] error_reporting() can lead to full path disclosure. - 24 | WARNING | [ ] error_reporting() found. Changing configuration at runtime is rarely - | | necessary. - 37 | WARNING | [x] "require_once" is a statement not a function; no parentheses are - | | required - 39 | WARNING | [ ] Silencing errors is discouraged - 39 | WARNING | [ ] Silencing errors is discouraged - 42 | WARNING | [x] "require_once" is a statement not a function; no parentheses are - | | required - 46 | ERROR | [ ] Inline comments must end in full-stops, exclamation marks, or - | | question marks - 46 | ERROR | [x] There must be no blank line following an inline comment - 49 | WARNING | [x] "require_once" is a statement not a function; no parentheses are - | | required - 54 | WARNING | [x] "require_once" is a statement not a function; no parentheses are - | | required - 63 | WARNING | [ ] Detected access of super global var $_SERVER, probably needs manual - | | inspection. - 63 | ERROR | [ ] Detected usage of a non-validated input variable: $_SERVER - 63 | ERROR | [ ] Missing wp_unslash() before sanitization. - 63 | ERROR | [ ] Detected usage of a non-sanitized input variable: $_SERVER - 69 | WARNING | [x] "require_once" is a statement not a function; no parentheses are - | | required - 74 | ERROR | [ ] Inline comments must end in full-stops, exclamation marks, or - | | question marks - 92 | ERROR | [ ] All output should be run through an escaping function (see the - | | Security sections in the WordPress Developer Handbooks), found - | | '$die'. - 92 | ERROR | [ ] All output should be run through an escaping function (see the - | | Security sections in the WordPress Developer Handbooks), found '__'. - ------------------------------------------------------------------------------------------ - PHPCBF CAN FIX THE 6 MARKED SNIFF VIOLATIONS AUTOMATICALLY - ------------------------------------------------------------------------------------------ - -### Using PHPCS and WPCS from within your IDE - -* **PhpStorm** : Please see "[PHP Code Sniffer with WordPress Coding Standards Integration](https://confluence.jetbrains.com/display/PhpStorm/WordPress+Development+using+PhpStorm#WordPressDevelopmentusingPhpStorm-PHPCodeSnifferwithWordPressCodingStandardsIntegrationinPhpStorm)" in the PhpStorm documentation. -* **Sublime Text** : Please see "[Setting up WPCS to work in Sublime Text](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Setting-up-WPCS-to-work-in-Sublime-Text)" in the wiki. -* **Atom**: Please see "[Setting up WPCS to work in Atom](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Setting-up-WPCS-to-work-in-Atom)" in the wiki. -* **Visual Studio**: Please see "[Setting up PHP CodeSniffer in Visual Studio Code](https://tommcfarlin.com/php-codesniffer-in-visual-studio-code/)", a tutorial by Tom McFarlin. -* **Eclipse with XAMPP**: Please see "[Setting up WPCS when using Eclipse with XAMPP](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/How-to-use-WPCS-with-Eclipse-and-XAMPP)" in the wiki. - - -## Running your code through WPCS automatically using CI tools - -### [Travis CI](https://travis-ci.org/) - -To integrate PHPCS with WPCS with Travis CI, you'll need to install both `before_install` and add the run command to the `script`. -If your project uses Composer, the typical instructions might be different. - -If you use a matrix setup in Travis to test your code against different PHP and/or WordPress versions, you don't need to run PHPCS on each variant of the matrix as the results will be same. -You can set an environment variable in the Travis matrix to only run the sniffs against one setup in the matrix. - -#### Travis CI example -```yaml -language: php - -matrix: - include: - # Arbitrary PHP version to run the sniffs against. - - php: '7.0' - env: SNIFF=1 - -before_install: - - if [[ "$SNIFF" == "1" ]]; then export PHPCS_DIR=/tmp/phpcs; fi - - if [[ "$SNIFF" == "1" ]]; then export SNIFFS_DIR=/tmp/sniffs; fi - # Install PHP_CodeSniffer. - - if [[ "$SNIFF" == "1" ]]; then git clone -b master --depth 1 https://github.com/squizlabs/PHP_CodeSniffer.git $PHPCS_DIR; fi - # Install WordPress Coding Standards. - - if [[ "$SNIFF" == "1" ]]; then git clone -b master --depth 1 https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards.git $SNIFFS_DIR; fi - # Set install path for WordPress Coding Standards. - - if [[ "$SNIFF" == "1" ]]; then $PHPCS_DIR/bin/phpcs --config-set installed_paths $SNIFFS_DIR; fi - # After CodeSniffer install you should refresh your path. - - if [[ "$SNIFF" == "1" ]]; then phpenv rehash; fi - -script: - # Run against WordPress Coding Standards. - # If you use a custom ruleset, change `--standard=WordPress` to point to your ruleset file, - # for example: `--standard=wpcs.xml`. - # You can use any of the normal PHPCS command line arguments in the command: - # https://github.com/squizlabs/PHP_CodeSniffer/wiki/Usage - - if [[ "$SNIFF" == "1" ]]; then $PHPCS_DIR/bin/phpcs -p . --standard=WordPress; fi ``` +-------------------------------------------------------------------------------- +FOUND 6 ERRORS AND 4 WARNINGS AFFECTING 5 LINES +-------------------------------------------------------------------------------- + 36 | WARNING | error_reporting() can lead to full path disclosure. + 36 | WARNING | error_reporting() found. Changing configuration values at + | | runtime is strongly discouraged. + 52 | WARNING | Silencing errors is strongly discouraged. Use proper error + | | checking instead. Found: @file_exists( dirname(... + 52 | WARNING | Silencing errors is strongly discouraged. Use proper error + | | checking instead. Found: @file_exists( dirname(... + 75 | ERROR | Overriding WordPress globals is prohibited. Found assignment + | | to $path + 78 | ERROR | Detected usage of a possibly undefined superglobal array + | | index: $_SERVER['REQUEST_URI']. Use isset() or empty() to + | | check the index exists before using it + 78 | ERROR | $_SERVER['REQUEST_URI'] not unslashed before sanitization. Use + | | wp_unslash() or similar + 78 | ERROR | Detected usage of a non-sanitized input variable: + | | $_SERVER['REQUEST_URI'] + 104 | ERROR | All output should be run through an escaping function (see the + | | Security sections in the WordPress Developer Handbooks), found + | | '$die'. + 104 | ERROR | All output should be run through an escaping function (see the + | | Security sections in the WordPress Developer Handbooks), found + | | '__'. +-------------------------------------------------------------------------------- +``` + +### Using PHPCS and WordPressCS from within your IDE + +The [wiki](https://github.com/WordPress/WordPress-Coding-Standards/wiki) contains links to various in- and external tutorials about setting up WordPressCS to work in your IDE. + + +## Running your code through WordPressCS automatically using Continuous Integration tools -More examples and advice about integrating PHPCS in your Travis build tests can be found here: https://github.com/jrfnl/make-phpcs-work-for-you/tree/master/travis-examples +- [Running in GitHub Actions](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Running-in-GitHub-Actions) +- [Running in Travis](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Running-in-Travis) -## Fixing errors or whitelisting them +## Fixing errors or ignoring them -You can find information on how to deal with some of the more frequent issues in the [wiki](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki). +You can find information on how to deal with some of the more frequent issues in the [wiki](https://github.com/WordPress/WordPress-Coding-Standards/wiki). -### Tools shipped with WPCS +### Tools shipped with WordPressCS -Since version 1.2.0, WPCS has a special sniff category `Utils`. +Since version 1.2.0, WordPressCS has a special sniff category `Utils`. This sniff category contains some tools which, generally speaking, will only be needed to be run once over a codebase and for which the fixers can be considered _risky_, i.e. very careful review by a developer is needed before accepting the fixes made by these sniffs. The sniffs in this category are disabled by default and can only be activated by adding some properties for each sniff via a custom ruleset. -At this moment, WPCS offer the following tools: +At this moment, WordPressCS offer the following tools: * `WordPress.Utils.I18nTextDomainFixer` - This sniff can replace the text domain used in a code-base. The sniff will fix the text domains in both I18n function calls as well as in a plugin/theme header. Passing the following properties will activate the sniff: diff --git a/Test/AllTests.php b/Test/AllTests.php deleted file mode 100644 index de70f8ee19..0000000000 --- a/Test/AllTests.php +++ /dev/null @@ -1,72 +0,0 @@ - - * @author Marc McIntyre - * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) - * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence - * @link http://pear.php.net/package/PHP_CodeSniffer - */ - -/* Start of WPCS adjustment */ -namespace WordPressCS\Test; - -use WordPressCS\Test\AllSniffs; -use PHP_CodeSniffer_AllTests; -use PHP_CodeSniffer_TestSuite; -/* End of WPCS adjustment */ - -/** - * A test class for running all PHP_CodeSniffer unit tests. - * - * Usage: phpunit AllTests.php - * - * @category PHP - * @package PHP_CodeSniffer - * @author Greg Sherwood - * @author Marc McIntyre - * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) - * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence - * @version Release: @package_version@ - * @link http://pear.php.net/package/PHP_CodeSniffer - */ -class AllTests extends PHP_CodeSniffer_AllTests { - - /** - * Add all PHP_CodeSniffer test suites into a single test suite. - * - * @return PHPUnit_Framework_TestSuite - */ - public static function suite() - { - $GLOBALS['PHP_CODESNIFFER_STANDARD_DIRS'] = array(); - - // Use a special PHP_CodeSniffer test suite so that we can - // unset our autoload function after the run. - $suite = new PHP_CodeSniffer_TestSuite('PHP CodeSniffer'); - - /* Start of WPCS adjustment */ - // We need to point to the WPCS version of the referenced class - // and we may as well bypass the loading of the PHPCS core unit tests - // while we're at it too. - $suite->addTest(AllSniffs::suite()); - /* End of WPCS adjustment */ - - // Unregister this here because the PEAR tester loads - // all package suites before running then, so our autoloader - // will cause problems for the packages included after us. - spl_autoload_unregister(array('PHP_CodeSniffer', 'autoload')); - - return $suite; - - }//end suite() - - -}//end class diff --git a/Test/Standards/AbstractSniffUnitTest.php b/Test/Standards/AbstractSniffUnitTest.php deleted file mode 100644 index b92b81386d..0000000000 --- a/Test/Standards/AbstractSniffUnitTest.php +++ /dev/null @@ -1,460 +0,0 @@ - - * @author Marc McIntyre - * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) - * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence - * @link http://pear.php.net/package/PHP_CodeSniffer - */ - -/* Start of WPCS adjustment */ -namespace WordPressCS\Test; - -use PHP_CodeSniffer; -use PHP_CodeSniffer_File; -use PHP_CodeSniffer_Exception; -use PHPUnit_Framework_TestCase; -use DirectoryIterator; -/* End of WPCS adjustment */ - -/** - * An abstract class that all sniff unit tests must extend. - * - * A sniff unit test checks a .inc file for expected violations of a single - * coding standard. Expected errors and warnings that are not found, or - * warnings and errors that are not expected, are considered test failures. - * - * @category PHP - * @package PHP_CodeSniffer - * @author Greg Sherwood - * @author Marc McIntyre - * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) - * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence - * @version Release: @package_version@ - * @link http://pear.php.net/package/PHP_CodeSniffer - */ -abstract class AbstractSniffUnitTest extends PHPUnit_Framework_TestCase { - - /** - * Enable or disable the backup and restoration of the $GLOBALS array. - * Overwrite this attribute in a child class of TestCase. - * Setting this attribute in setUp() has no effect! - * - * @var boolean - */ - protected $backupGlobals = false; - - /** - * The PHP_CodeSniffer object used for testing. - * - * @var PHP_CodeSniffer - */ - protected static $phpcs = null; - - /** - * The path to the directory under which the sniff's standard lives. - * - * @var string - */ - public $standardsDir = null; - - - /** - * Sets up this unit test. - * - * @return void - */ - protected function setUp() - { - if (self::$phpcs === null) { - self::$phpcs = new PHP_CodeSniffer(); - } - - $class = \get_class($this); - $this->standardsDir = $GLOBALS['PHP_CODESNIFFER_STANDARD_DIRS'][$class]; - - }//end setUp() - - - /** - * Get a list of all test files to check. - * - * These will have the same base as the sniff name but different extensions. - * We ignore the .php file as it is the class. - * - * @param string $testFileBase The base path that the unit tests files will have. - * - * @return string[] - */ - protected function getTestFiles($testFileBase) - { - $testFiles = array(); - - $dir = substr($testFileBase, 0, strrpos($testFileBase, \DIRECTORY_SEPARATOR)); - $di = new DirectoryIterator($dir); - - foreach ($di as $file) { - $path = $file->getPathname(); - if (substr($path, 0, \strlen($testFileBase)) === $testFileBase) { - - /* Start of WPCS adjustment */ - // If we're changing things anyway, we may as well exclude backup files - // from the test runs ;-) - if ($path !== $testFileBase.'php' && substr($path, -5) !== 'fixed' - && substr($path, -3) !== 'bak' && substr($path, -4) !== 'orig' - ) { - $testFiles[] = $path; - } - /* End of WPCS adjustment */ - } - } - - // Put them in order. - sort($testFiles); - - return $testFiles; - - }//end getTestFiles() - - - /** - * Should this test be skipped for some reason. - * - * @return void - */ - protected function shouldSkipTest() - { - return false; - - }//end shouldSkipTest() - - - /** - * Tests the extending classes Sniff class. - * - * @return void - * @throws PHPUnit_Framework_Error - */ - public final function testSniff() - { - // Skip this test if we can't run in this environment. - if ($this->shouldSkipTest() === true) { - $this->markTestSkipped(); - } - - // The basis for determining file locations. - $basename = substr(\get_class($this), 0, -8); - - /* Start of WPCS adjustment */ - // Support the use of PHP namespaces. - if (strpos($basename, '\\') !== false) { - $basename = str_replace('\\', '_', $basename); - } - /* End of WPCS adjustment */ - - // The name of the coding standard we are testing. - $standardName = substr($basename, 0, strpos($basename, '_')); - - // The code of the sniff we are testing. - $parts = explode('_', $basename); - $sniffCode = $parts[0].'.'.$parts[2].'.'.$parts[3]; - - $testFileBase = $this->standardsDir.\DIRECTORY_SEPARATOR.str_replace('_', \DIRECTORY_SEPARATOR, $basename).'UnitTest.'; - - // Get a list of all test files to check. - $testFiles = $this->getTestFiles($testFileBase); - - self::$phpcs->initStandard($standardName, array($sniffCode)); - self::$phpcs->setIgnorePatterns(array()); - - $failureMessages = array(); - foreach ($testFiles as $testFile) { - $filename = basename($testFile); - - try { - $cliValues = $this->getCliValues($filename); - self::$phpcs->cli->setCommandLineValues($cliValues); - $phpcsFile = self::$phpcs->processFile($testFile); - } catch (\Exception $e) { - $this->fail('An unexpected exception has been caught: '.$e->getMessage()); - } - - $failures = $this->generateFailureMessages($phpcsFile); - $failureMessages = array_merge($failureMessages, $failures); - - if ($phpcsFile->getFixableCount() > 0) { - // Attempt to fix the errors. - $phpcsFile->fixer->fixFile(); - $fixable = $phpcsFile->getFixableCount(); - if ($fixable > 0) { - $failureMessages[] = "Failed to fix $fixable fixable violations in $filename"; - } - - // Check for a .fixed file to check for accuracy of fixes. - $fixedFile = $testFile.'.fixed'; - if (file_exists($fixedFile) === true) { - $diff = $phpcsFile->fixer->generateDiff($fixedFile); - if (trim($diff) !== '') { - $filename = basename($testFile); - $fixedFilename = basename($fixedFile); - $failureMessages[] = "Fixed version of $filename does not match expected version in $fixedFilename; the diff is\n$diff"; - } - } - } - }//end foreach - - if (empty($failureMessages) === false) { - $this->fail(implode(\PHP_EOL, $failureMessages)); - } - - }//end runTest() - - - /** - * Generate a list of test failures for a given sniffed file. - * - * @param PHP_CodeSniffer_File $file The file being tested. - * - * @return array - * @throws PHP_CodeSniffer_Exception - */ - public function generateFailureMessages(PHP_CodeSniffer_File $file) - { - $testFile = $file->getFilename(); - - $foundErrors = $file->getErrors(); - $foundWarnings = $file->getWarnings(); - $expectedErrors = $this->getErrorList(basename($testFile)); - $expectedWarnings = $this->getWarningList(basename($testFile)); - - if (\is_array($expectedErrors) === false) { - throw new PHP_CodeSniffer_Exception('getErrorList() must return an array'); - } - - if (\is_array($expectedWarnings) === false) { - throw new PHP_CodeSniffer_Exception('getWarningList() must return an array'); - } - - /* - We merge errors and warnings together to make it easier - to iterate over them and produce the errors string. In this way, - we can report on errors and warnings in the same line even though - it's not really structured to allow that. - */ - - $allProblems = array(); - $failureMessages = array(); - - foreach ($foundErrors as $line => $lineErrors) { - foreach ($lineErrors as $column => $errors) { - if (isset($allProblems[$line]) === false) { - $allProblems[$line] = array( - 'expected_errors' => 0, - 'expected_warnings' => 0, - 'found_errors' => array(), - 'found_warnings' => array(), - ); - } - - $foundErrorsTemp = array(); - foreach ($allProblems[$line]['found_errors'] as $foundError) { - $foundErrorsTemp[] = $foundError; - } - - $errorsTemp = array(); - foreach ($errors as $foundError) { - $errorsTemp[] = $foundError['message'].' ('.$foundError['source'].')'; - - $source = $foundError['source']; - if (\in_array($source, $GLOBALS['PHP_CODESNIFFER_SNIFF_CODES']) === false) { - $GLOBALS['PHP_CODESNIFFER_SNIFF_CODES'][] = $source; - } - - if ($foundError['fixable'] === true - && \in_array($source, $GLOBALS['PHP_CODESNIFFER_FIXABLE_CODES']) === false - ) { - $GLOBALS['PHP_CODESNIFFER_FIXABLE_CODES'][] = $source; - } - } - - $allProblems[$line]['found_errors'] = array_merge($foundErrorsTemp, $errorsTemp); - }//end foreach - - if (isset($expectedErrors[$line]) === true) { - $allProblems[$line]['expected_errors'] = $expectedErrors[$line]; - } else { - $allProblems[$line]['expected_errors'] = 0; - } - - unset($expectedErrors[$line]); - }//end foreach - - foreach ($expectedErrors as $line => $numErrors) { - if (isset($allProblems[$line]) === false) { - $allProblems[$line] = array( - 'expected_errors' => 0, - 'expected_warnings' => 0, - 'found_errors' => array(), - 'found_warnings' => array(), - ); - } - - $allProblems[$line]['expected_errors'] = $numErrors; - } - - foreach ($foundWarnings as $line => $lineWarnings) { - foreach ($lineWarnings as $column => $warnings) { - if (isset($allProblems[$line]) === false) { - $allProblems[$line] = array( - 'expected_errors' => 0, - 'expected_warnings' => 0, - 'found_errors' => array(), - 'found_warnings' => array(), - ); - } - - $foundWarningsTemp = array(); - foreach ($allProblems[$line]['found_warnings'] as $foundWarning) { - $foundWarningsTemp[] = $foundWarning; - } - - $warningsTemp = array(); - foreach ($warnings as $warning) { - $warningsTemp[] = $warning['message'].' ('.$warning['source'].')'; - } - - $allProblems[$line]['found_warnings'] = array_merge($foundWarningsTemp, $warningsTemp); - }//end foreach - - if (isset($expectedWarnings[$line]) === true) { - $allProblems[$line]['expected_warnings'] = $expectedWarnings[$line]; - } else { - $allProblems[$line]['expected_warnings'] = 0; - } - - unset($expectedWarnings[$line]); - }//end foreach - - foreach ($expectedWarnings as $line => $numWarnings) { - if (isset($allProblems[$line]) === false) { - $allProblems[$line] = array( - 'expected_errors' => 0, - 'expected_warnings' => 0, - 'found_errors' => array(), - 'found_warnings' => array(), - ); - } - - $allProblems[$line]['expected_warnings'] = $numWarnings; - } - - // Order the messages by line number. - ksort($allProblems); - - foreach ($allProblems as $line => $problems) { - $numErrors = \count($problems['found_errors']); - $numWarnings = \count($problems['found_warnings']); - $expectedErrors = $problems['expected_errors']; - $expectedWarnings = $problems['expected_warnings']; - - $errors = ''; - $foundString = ''; - - if ($expectedErrors !== $numErrors || $expectedWarnings !== $numWarnings) { - $lineMessage = "[LINE $line]"; - $expectedMessage = 'Expected '; - $foundMessage = 'in '.basename($testFile).' but found '; - - if ($expectedErrors !== $numErrors) { - $expectedMessage .= "$expectedErrors error(s)"; - $foundMessage .= "$numErrors error(s)"; - if ($numErrors !== 0) { - $foundString .= 'error(s)'; - $errors .= implode(\PHP_EOL.' -> ', $problems['found_errors']); - } - - if ($expectedWarnings !== $numWarnings) { - $expectedMessage .= ' and '; - $foundMessage .= ' and '; - if ($numWarnings !== 0) { - if ($foundString !== '') { - $foundString .= ' and '; - } - } - } - } - - if ($expectedWarnings !== $numWarnings) { - $expectedMessage .= "$expectedWarnings warning(s)"; - $foundMessage .= "$numWarnings warning(s)"; - if ($numWarnings !== 0) { - $foundString .= 'warning(s)'; - if (empty($errors) === false) { - $errors .= \PHP_EOL.' -> '; - } - - $errors .= implode(\PHP_EOL.' -> ', $problems['found_warnings']); - } - } - - $fullMessage = "$lineMessage $expectedMessage $foundMessage."; - if ($errors !== '') { - $fullMessage .= " The $foundString found were:".\PHP_EOL." -> $errors"; - } - - $failureMessages[] = $fullMessage; - }//end if - }//end foreach - - return $failureMessages; - - }//end generateFailureMessages() - - - /** - * Get a list of CLI values to set before the file is tested. - * - * @param string $filename The name of the file being tested. - * - * @return array - */ - public function getCliValues($filename) - { - return array(); - - }//end getCliValues() - - - /** - * Returns the lines where errors should occur. - * - * The key of the array should represent the line number and the value - * should represent the number of errors that should occur on that line. - * - * @return array(int => int) - */ - protected abstract function getErrorList(); - - - /** - * Returns the lines where warnings should occur. - * - * The key of the array should represent the line number and the value - * should represent the number of warnings that should occur on that line. - * - * @return array(int => int) - */ - protected abstract function getWarningList(); - - -}//end class diff --git a/Test/Standards/AllSniffs.php b/Test/Standards/AllSniffs.php deleted file mode 100644 index c3768adfca..0000000000 --- a/Test/Standards/AllSniffs.php +++ /dev/null @@ -1,157 +0,0 @@ - - * @author Marc McIntyre - * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) - * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence - * @link http://pear.php.net/package/PHP_CodeSniffer - */ - -/* Start of WPCS adjustment */ -namespace WordPressCS\Test; - -use PHP_CodeSniffer_Standards_AllSniffs; -use PHP_CodeSniffer; -use PHPUnit_Framework_TestSuite; -use RecursiveIteratorIterator; -use RecursiveDirectoryIterator; -/* End of WPCS adjustment */ - -/** - * A test class for testing all sniffs for installed standards. - * - * Usage: phpunit AllSniffs.php - * - * This test class loads all unit tests for all installed standards into a - * single test suite and runs them. Errors are reported on the command line. - * - * @category PHP - * @package PHP_CodeSniffer - * @author Greg Sherwood - * @author Marc McIntyre - * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) - * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence - * @version Release: @package_version@ - * @link http://pear.php.net/package/PHP_CodeSniffer - */ -class AllSniffs extends PHP_CodeSniffer_Standards_AllSniffs -{ - - /** - * Add all sniff unit tests into a test suite. - * - * Sniff unit tests are found by recursing through the 'Tests' directory - * of each installed coding standard. - * - * @return PHPUnit_Framework_TestSuite - */ - public static function suite() - { - $suite = new PHPUnit_Framework_TestSuite('PHP CodeSniffer Standards'); - - /* Start of WPCS adjustment */ - // Set the correct path to PHPCS. - $isInstalled = !is_file(\PHPCS_DIR.'/CodeSniffer.php'); - /* End of WPCS adjustment */ - - // Optionally allow for ignoring the tests for one or more standards. - $ignoreTestsForStandards = getenv('PHPCS_IGNORE_TESTS'); - if ($ignoreTestsForStandards === false) { - $ignoreTestsForStandards = array(); - } else { - $ignoreTestsForStandards = explode(',', $ignoreTestsForStandards); - } - - $installedPaths = PHP_CodeSniffer::getInstalledStandardPaths(); - foreach ($installedPaths as $path) { - $path = realpath($path); - $origPath = $path; - $standards = PHP_CodeSniffer::getInstalledStandards(true, $path); - - // If the test is running PEAR installed, the built-in standards - // are split into different directories; one for the sniffs and - // a different file system location for tests. - if ($isInstalled === true - && is_dir($path.\DIRECTORY_SEPARATOR.'Generic') === true - ) { - $path = dirname(__FILE__); - } - - foreach ($standards as $standard) { - if (\in_array($standard, $ignoreTestsForStandards, true)) { - continue; - } - - $testsDir = $path.\DIRECTORY_SEPARATOR.$standard.\DIRECTORY_SEPARATOR.'Tests'.\DIRECTORY_SEPARATOR; - - if (is_dir($testsDir) === false) { - // No tests for this standard. - continue; - } - - $di = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($testsDir)); - - foreach ($di as $file) { - // Skip hidden files. - if (substr($file->getFilename(), 0, 1) === '.') { - continue; - } - - // Tests must have the extension 'php'. - $parts = explode('.', $file); - $ext = array_pop($parts); - if ($ext !== 'php') { - continue; - } - - $filePath = $file->getPathname(); - $className = str_replace($path.\DIRECTORY_SEPARATOR, '', $filePath); - $className = substr($className, 0, -4); - $className = str_replace(\DIRECTORY_SEPARATOR, '_', $className); - - // Include the sniff here so tests can use it in their setup() methods. - $parts = explode('_', $className); - if (isset($parts[0],$parts[2],$parts[3]) === true) { - $sniffPath = $origPath.\DIRECTORY_SEPARATOR.$parts[0].\DIRECTORY_SEPARATOR.'Sniffs'.\DIRECTORY_SEPARATOR.$parts[2].\DIRECTORY_SEPARATOR.$parts[3]; - $sniffPath = substr($sniffPath, 0, -8).'Sniff.php'; - - if (file_exists($sniffPath) === true) { - include_once $sniffPath; - include_once $filePath; - - /* Start of WPCS adjustment */ - // Support the use of PHP namespaces. If the class name we included - // contains namespace separators instead of underscores, use this as the - // class name from now on. - $classNameNS = str_replace('_', '\\', $className); - if (class_exists($classNameNS, false) === true) { - $className = $classNameNS; - } - /* End of WPCS adjustment */ - - $GLOBALS['PHP_CODESNIFFER_STANDARD_DIRS'][$className] = $path; - $suite->addTestSuite($className); - } else { - self::$orphanedTests[] = $filePath; - } - } else { - self::$orphanedTests[] = $filePath; - } - }//end foreach - }//end foreach - }//end foreach - - return $suite; - - }//end suite() - - -}//end class diff --git a/Test/bootstrap.php b/Test/bootstrap.php deleted file mode 100644 index 996c887ce3..0000000000 --- a/Test/bootstrap.php +++ /dev/null @@ -1,42 +0,0 @@ - true, +); + +$allStandards = PHP_CodeSniffer\Util\Standards::getInstalledStandards(); +$allStandards[] = 'Generic'; + +$standardsToIgnore = array(); +foreach ( $allStandards as $standard ) { + if ( isset( $wpcsStandards[ $standard ] ) === true ) { + continue; + } + + $standardsToIgnore[] = $standard; +} + +$standardsToIgnoreString = implode( ',', $standardsToIgnore ); + +// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_putenv -- This is not production, but test code. +putenv( "PHPCS_IGNORE_TESTS={$standardsToIgnoreString}" ); + +// Clean up. +unset( $ds, $phpcsDir, $composerPHPCSPath, $allStandards, $standardsToIgnore, $standard, $standardsToIgnoreString ); diff --git a/WordPress-Core/ruleset.xml b/WordPress-Core/ruleset.xml index b9cd23a91f..c38349e5c4 100644 --- a/WordPress-Core/ruleset.xml +++ b/WordPress-Core/ruleset.xml @@ -1,36 +1,295 @@ - - Non-controversial generally-agreed upon WordPress Coding Standards - - ./../WordPress/PHPCSAliases.php + - - + Non-controversial generally-agreed upon WordPress Coding Standards + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + - - + + + + + + + + + + + + + + + + warning + + + warning + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + + 0 + + + + + + + + + + 0 - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -39,26 +298,39 @@ - + + + + + - + + when the array contains more than one item. --> + + + + + + + + + - + - + @@ -72,7 +344,7 @@ - + @@ -91,8 +363,24 @@ + + + + + + + + + + + @@ -104,341 +392,475 @@ - - + + - + + + https://github.com/WordPress/WordPress-Coding-Standards/issues/1330 --> - - + + - + + + + + + + + - + + + + + - - - - 0 - - - 0 - - - 0 - - - 0 - + + + + + + + + - - - + + + + + + + + - - + - - + + + + + + + + + + + + + - - - - - - - - + + - - + + + - - - - - - - - - - - - - - + + + + + + + + + + + 0 + + - - - - - + + - - - - - - + + - - + + + - - - - - - - + + + - - + + - + + - - - - - - - + + + 0 + + + + + 0 + + + + 0 + + + 0 - + + + + 0 + + - + + - - - - + + + + + + + + + + + + + + 0 + + + + - - - + + + + + + + + + + + - - - - + + - - - - + + + - - - - + + + + + + + - https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/issues/751 + + - + + + warning + - - + + + + - + + + + + + + + + - - - - + - + + warning + + + + + + + + - - - - + + error The "goto" language construct should not be used. - - + error eval() is a security risk so not allowed. - + - - + + + + + + + + + + + + + + + + - + + - - + - + - + @@ -476,11 +895,16 @@ - - - - - + + + + + 0 + + + + 0 + + + + + + + + + + + + + + diff --git a/WordPress-Docs/ruleset.xml b/WordPress-Docs/ruleset.xml index 3a95dd6cff..b351118584 100644 --- a/WordPress-Docs/ruleset.xml +++ b/WordPress-Docs/ruleset.xml @@ -1,10 +1,11 @@ - + + WordPress Coding Standards for Inline Documentation and Comments @@ -61,6 +62,8 @@ + + @@ -68,6 +71,8 @@ + + @@ -81,12 +86,9 @@ - - - - + @@ -103,10 +105,5 @@ - - - - - diff --git a/WordPress-Extra/ruleset.xml b/WordPress-Extra/ruleset.xml index 2e6a412f59..6d32e45c65 100644 --- a/WordPress-Extra/ruleset.xml +++ b/WordPress-Extra/ruleset.xml @@ -1,13 +1,18 @@ - - Best practices beyond core WordPress Coding Standards + - ./../WordPress/PHPCSAliases.php + Best practices beyond core WordPress Coding Standards + + + 0 + + + https://github.com/WordPress/WordPress-Coding-Standards/pull/382 --> @@ -25,76 +30,38 @@ - - - + https://github.com/WordPress/WordPress-Coding-Standards/issues/607 --> + https://github.com/WordPress/WordPress-Coding-Standards/pull/809 --> - - - - 0 - - - 0 - - - warning - - - warning - - - warning + + + + + + + + + + + - - - - - - - - - - - - warning - Best practice suggestion: Declare only one class in a file. - - - - warning - Best practice suggestion: Declare only one interface in a file. - - - - warning - Best practice suggestion: Declare only one trait in a file. + + + 5 - - - - - - - warning + 5 @@ -106,11 +73,11 @@ + https://github.com/WordPress/WordPress-Coding-Standards/pull/1264 --> + https://github.com/WordPress/WordPress-Coding-Standards/issues/73 --> @@ -123,33 +90,28 @@ + + + + https://github.com/WordPress/WordPress-Coding-Standards/issues/35 --> + https://github.com/WordPress/WordPress-Coding-Standards/issues/26 --> - - - - - + + + https://github.com/WordPress/WordPress-Coding-Standards/issues/1146 --> - - + https://github.com/WordPress/WordPress-Coding-Standards/issues/522 --> @@ -163,36 +125,60 @@ - - + + - + - + https://github.com/WordPress/WordPress-Coding-Standards/issues/1371 --> - + - + + https://github.com/WordPress/WordPress-Coding-Standards/pull/1463 --> + + + + + + + + + + + + + + + + + + + + + + + - - - ./../WordPress/PHPCSAliases.php - - - - - - - - - - - - - - - - - - - - - - - - - - - error - - - - - error - - - error - - - - - - - - Using cURL functions is highly discouraged within VIP context. Check (Fetching Remote Data) on VIP Documentation. - - - %s() is highly discouraged, please use vip_safe_wp_remote_get() instead. - - - - - - - error - Attempting a database schema change is highly discouraged. - - - error - Usage of a direct database call without caching is prohibited on the VIP platform. Use wp_cache_get / wp_cache_set or wp_cache_delete. - - - - - - - error - - - - - error - Scheduling crons at %s sec ( less than %s minutes ) is prohibited. - - - - - - - error - - - error - - - - - - - - - 0 - - - - - 0 - - - - - 0 - - - 0 - - - 0 - - - - - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 0 - - - - - 0 - - - - - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 0 - - - - - 0 - - - 0 - - - - - 0 - - - - - 0 - - - - - 0 - - - - - 0 - - - - - 0 - - - - - 0 - - - - - 0 - - - - - 0 - - - - - 0 - - - diff --git a/WordPress/AbstractArrayAssignmentRestrictionsSniff.php b/WordPress/AbstractArrayAssignmentRestrictionsSniff.php index 1f9001a023..c72fdd910f 100644 --- a/WordPress/AbstractArrayAssignmentRestrictionsSniff.php +++ b/WordPress/AbstractArrayAssignmentRestrictionsSniff.php @@ -3,24 +3,28 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress; +namespace WordPressCS\WordPress; -use WordPress\Sniff; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\BackCompat\BCFile; +use PHPCSUtils\Utils\GetTokensAsString; +use PHPCSUtils\Utils\MessageHelper; +use PHPCSUtils\Utils\TextStrings; +use WordPressCS\WordPress\Helpers\RulesetPropertyHelper; +use WordPressCS\WordPress\Sniff; /** * Restricts array assignment of certain keys. * - * @package WPCS\WordPressCodingStandards - * - * @since 0.3.0 - * @since 0.10.0 Class became a proper abstract class. This was already the behaviour. - * Moved the file and renamed the class from - * `WordPress_Sniffs_Arrays_ArrayAssignmentRestrictionsSniff` to - * `WordPress_AbstractArrayAssignmentRestrictionsSniff`. + * @since 0.3.0 + * @since 0.10.0 Class became a proper abstract class. This was already the behaviour. + * Moved the file and renamed the class from + * `\WordPressCS\WordPress\Sniffs\Arrays\ArrayAssignmentRestrictionsSniff` to + * `\WordPressCS\WordPress\AbstractArrayAssignmentRestrictionsSniff`. */ abstract class AbstractArrayAssignmentRestrictionsSniff extends Sniff { @@ -33,7 +37,7 @@ abstract class AbstractArrayAssignmentRestrictionsSniff extends Sniff { * @since 1.0.0 This property now expects to be passed an array. * Previously a comma-delimited string was expected. * - * @var array + * @var string[] */ public $exclude = array(); @@ -42,7 +46,7 @@ abstract class AbstractArrayAssignmentRestrictionsSniff extends Sniff { * Don't use this in extended classes, override getGroups() instead. * This is only used for Unit tests. * - * @var array + * @var array */ public static $groups = array(); @@ -51,7 +55,7 @@ abstract class AbstractArrayAssignmentRestrictionsSniff extends Sniff { * * @since 0.11.0 * - * @var array + * @var array */ protected $excluded_groups = array(); @@ -60,7 +64,7 @@ abstract class AbstractArrayAssignmentRestrictionsSniff extends Sniff { * * @since 0.13.0 * - * @var array + * @var array */ protected $groups_cache = array(); @@ -90,14 +94,13 @@ public function register() { * * Example: groups => array( * 'groupname' => array( - * 'type' => 'error' | 'warning', - * 'message' => 'Dont use this one please!', - * 'keys' => array( 'key1', 'another_key' ), - * 'callback' => array( 'class', 'method' ), // Optional. + * 'type' => 'error' | 'warning', + * 'message' => 'Descriptive error message. The error message will be passed the $key and $val of the current array assignment.', + * 'keys' => array( 'key1', 'another_key' ), * ) * ) * - * @return array + * @return array */ abstract public function getGroups(); @@ -132,7 +135,7 @@ protected function setup_groups() { */ public function process_token( $stackPtr ) { - $this->excluded_groups = $this->merge_custom_array( $this->exclude ); + $this->excluded_groups = RulesetPropertyHelper::merge_custom_array( $this->exclude ); if ( array_diff_key( $this->groups_cache, $this->excluded_groups ) === array() ) { // All groups have been excluded. // Don't remove the listener as the exclude property can be changed inline. @@ -142,42 +145,65 @@ public function process_token( $stackPtr ) { $token = $this->tokens[ $stackPtr ]; if ( \T_CLOSE_SQUARE_BRACKET === $token['code'] ) { - $equal = $this->phpcsFile->findNext( \T_WHITESPACE, ( $stackPtr + 1 ), null, true ); - if ( \T_EQUAL !== $this->tokens[ $equal ]['code'] ) { - return; // This is not an assignment! + $equalPtr = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); + if ( \T_EQUAL !== $this->tokens[ $equalPtr ]['code'] + && \T_COALESCE_EQUAL !== $this->tokens[ $equalPtr ]['code'] + ) { + // This is not an assignment. Bow out. + return; } } - // Instances: Multi-dimensional array, keyed by line. + // Instances: Multi-dimensional array. $inst = array(); /* - * Covers: - * $foo = array( 'bar' => 'taz' ); - * $foo['bar'] = $taz; + * Covers array assignments: + * `$foo = array( 'bar' => 'taz' );` + * `$foo['bar'] = $taz;` */ - if ( \in_array( $token['code'], array( \T_CLOSE_SQUARE_BRACKET, \T_DOUBLE_ARROW ), true ) ) { + if ( \T_CLOSE_SQUARE_BRACKET === $token['code'] || \T_DOUBLE_ARROW === $token['code'] ) { $operator = $stackPtr; // T_DOUBLE_ARROW. if ( \T_CLOSE_SQUARE_BRACKET === $token['code'] ) { - $operator = $this->phpcsFile->findNext( \T_EQUAL, ( $stackPtr + 1 ) ); + $operator = $equalPtr; } - $keyIdx = $this->phpcsFile->findPrevious( array( \T_WHITESPACE, \T_CLOSE_SQUARE_BRACKET ), ( $operator - 1 ), null, true ); - if ( ! is_numeric( $this->tokens[ $keyIdx ]['content'] ) ) { - $key = $this->strip_quotes( $this->tokens[ $keyIdx ]['content'] ); - $valStart = $this->phpcsFile->findNext( array( \T_WHITESPACE ), ( $operator + 1 ), null, true ); - $valEnd = $this->phpcsFile->findNext( array( \T_COMMA, \T_SEMICOLON ), ( $valStart + 1 ), null, false, null, true ); - $val = $this->phpcsFile->getTokensAsString( $valStart, ( $valEnd - $valStart ) ); - $val = $this->strip_quotes( $val ); - $inst[ $key ][] = array( $val, $token['line'] ); + $keyIdx = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); + if ( isset( Tokens::$stringTokens[ $this->tokens[ $keyIdx ]['code'] ] ) + && ! is_numeric( $this->tokens[ $keyIdx ]['content'] ) + ) { + $key = TextStrings::stripQuotes( $this->tokens[ $keyIdx ]['content'] ); + $valStart = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $operator + 1 ), null, true ); + $valEnd = BCFile::findEndOfStatement( $this->phpcsFile, $valStart, \T_COLON ); + if ( \T_COMMA === $this->tokens[ $valEnd ]['code'] + || \T_SEMICOLON === $this->tokens[ $valEnd ]['code'] + ) { + // FindEndOfStatement includes the comma/semi-colon if that's the end of the statement. + // That's not what we want (and inconsistent), so remove it. + --$valEnd; + } + + $val = trim( GetTokensAsString::compact( $this->phpcsFile, $valStart, $valEnd, true ) ); + $inst[ $key ] = array( + 'value' => $val, + 'line' => $token['line'], + 'keyptr' => $keyIdx, + ); } - } elseif ( \in_array( $token['code'], array( \T_CONSTANT_ENCAPSED_STRING, \T_DOUBLE_QUOTED_STRING ), true ) ) { - // $foo = 'bar=taz&other=thing'; - if ( preg_match_all( '#(?:^|&)([a-z_]+)=([^&]*)#i', $this->strip_quotes( $token['content'] ), $matches ) <= 0 ) { + } elseif ( isset( Tokens::$stringTokens[ $token['code'] ] ) ) { + /* + * Covers assignments via query parameters: `$foo = 'bar=taz&other=thing';`. + */ + if ( preg_match_all( '#(?:^|&)([a-z_]+)=([^&]*)#i', TextStrings::stripQuotes( $token['content'] ), $matches ) <= 0 ) { return; // No assignments here, nothing to check. } - foreach ( $matches[1] as $i => $_k ) { - $inst[ $_k ][] = array( $matches[2][ $i ], $token['line'] ); + + foreach ( $matches[1] as $match_nr => $key ) { + $inst[ $key ] = array( + 'value' => $matches[2][ $match_nr ], + 'line' => $token['line'], + 'keyptr' => $stackPtr, + ); } } @@ -191,34 +217,29 @@ public function process_token( $stackPtr ) { continue; } - $callback = ( isset( $group['callback'] ) && is_callable( $group['callback'] ) ) ? $group['callback'] : array( $this, 'callback' ); - - foreach ( $inst as $key => $assignments ) { - foreach ( $assignments as $occurance ) { - list( $val, $line ) = $occurance; - - if ( ! \in_array( $key, $group['keys'], true ) ) { - continue; - } - - $output = \call_user_func( $callback, $key, $val, $line, $group ); - - if ( ! isset( $output ) || false === $output ) { - continue; - } elseif ( true === $output ) { - $message = $group['message']; - } else { - $message = $output; - } - - $this->addMessage( - $message, - $stackPtr, - ( 'error' === $group['type'] ), - $this->string_to_errorcode( $groupName . '_' . $key ), - array( $key, $val ) - ); + foreach ( $inst as $key => $assignment ) { + if ( ! \in_array( $key, $group['keys'], true ) ) { + continue; } + + $output = \call_user_func( array( $this, 'callback' ), $key, $assignment['value'], $assignment['line'], $group ); + + if ( ! isset( $output ) || false === $output ) { + continue; + } elseif ( true === $output ) { + $message = $group['message']; + } else { + $message = $output; + } + + MessageHelper::addMessage( + $this->phpcsFile, + $message, + $assignment['keyptr'], + ( 'error' === $group['type'] ), + MessageHelper::stringToErrorcode( $groupName . '_' . $key ), + array( $key, $assignment['value'] ) + ); } } } @@ -228,13 +249,13 @@ public function process_token( $stackPtr ) { * * This method must be extended to add the logic to check assignment value. * - * @param string $key Array index / key. - * @param mixed $val Assigned value. - * @param int $line Token line. - * @param array $group Group definition. - * @return mixed FALSE if no match, TRUE if matches, STRING if matches - * with custom error message passed to ->process(). + * @param string $key Array index / key. + * @param mixed $val Assigned value. + * @param int $line Token line. + * @param array $group Group definition. + * + * @return mixed FALSE if no match, TRUE if matches, STRING if matches + * with custom error message passed to ->process(). */ abstract public function callback( $key, $val, $line, $group ); - } diff --git a/WordPress/AbstractClassRestrictionsSniff.php b/WordPress/AbstractClassRestrictionsSniff.php index f41cd138e8..fbc18af36f 100644 --- a/WordPress/AbstractClassRestrictionsSniff.php +++ b/WordPress/AbstractClassRestrictionsSniff.php @@ -3,20 +3,23 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress; +namespace WordPressCS\WordPress; -use WordPress\AbstractFunctionRestrictionsSniff; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\GetTokensAsString; +use PHPCSUtils\Utils\Namespaces; +use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff; +use WordPressCS\WordPress\Helpers\RulesetPropertyHelper; /** * Restricts usage of some classes. * - * @package WPCS\WordPressCodingStandards - * - * @since 0.10.0 + * @since 0.10.0 */ abstract class AbstractClassRestrictionsSniff extends AbstractFunctionRestrictionsSniff { @@ -79,7 +82,7 @@ public function register() { /** * Processes this test, when one of its tokens is encountered. * - * {@internal Unlike in the `WordPress_AbstractFunctionRestrictionsSniff`, + * {@internal Unlike in the `AbstractFunctionRestrictionsSniff`, * we can't do a preliminary check on classes as at this point * we don't know the class name yet.}} * @@ -92,7 +95,7 @@ public function process_token( $stackPtr ) { // Reset the temporary storage before processing the token. unset( $this->classname ); - $this->excluded_groups = $this->merge_custom_array( $this->exclude ); + $this->excluded_groups = RulesetPropertyHelper::merge_custom_array( $this->exclude ); if ( array_diff_key( $this->groups, $this->excluded_groups ) === array() ) { // All groups have been excluded. // Don't remove the listener as the exclude property can be changed inline. @@ -120,28 +123,25 @@ public function is_targetted_token( $stackPtr ) { if ( \in_array( $token['code'], array( \T_NEW, \T_EXTENDS, \T_IMPLEMENTS ), true ) ) { if ( \T_NEW === $token['code'] ) { - $nameEnd = ( $this->phpcsFile->findNext( array( \T_OPEN_PARENTHESIS, \T_WHITESPACE, \T_SEMICOLON, \T_OBJECT_OPERATOR ), ( $stackPtr + 2 ) ) - 1 ); + $nameEnd = ( $this->phpcsFile->findNext( array( \T_OPEN_PARENTHESIS, \T_WHITESPACE, \T_SEMICOLON, \T_CLOSE_PARENTHESIS, \T_CLOSE_TAG ), ( $stackPtr + 2 ) ) - 1 ); } else { $nameEnd = ( $this->phpcsFile->findNext( array( \T_CLOSE_CURLY_BRACKET, \T_WHITESPACE ), ( $stackPtr + 2 ) ) - 1 ); } - $length = ( $nameEnd - ( $stackPtr + 1 ) ); - $classname = $this->phpcsFile->getTokensAsString( ( $stackPtr + 2 ), $length ); - - if ( \T_NS_SEPARATOR !== $this->tokens[ ( $stackPtr + 2 ) ]['code'] ) { - $classname = $this->get_namespaced_classname( $classname, ( $stackPtr - 1 ) ); - } + $classname = GetTokensAsString::noEmpties( $this->phpcsFile, ( $stackPtr + 2 ), $nameEnd ); + $classname = $this->get_namespaced_classname( $classname, ( $stackPtr - 1 ) ); } if ( \T_DOUBLE_COLON === $token['code'] ) { - $nameEnd = $this->phpcsFile->findPrevious( \T_STRING, ( $stackPtr - 1 ) ); - $nameStart = ( $this->phpcsFile->findPrevious( array( \T_STRING, \T_NS_SEPARATOR, \T_NAMESPACE ), ( $nameEnd - 1 ), null, true, null, true ) + 1 ); - $length = ( $nameEnd - ( $nameStart - 1 ) ); - $classname = $this->phpcsFile->getTokensAsString( $nameStart, $length ); - - if ( \T_NS_SEPARATOR !== $this->tokens[ $nameStart ]['code'] ) { - $classname = $this->get_namespaced_classname( $classname, ( $nameStart - 1 ) ); + $nameEnd = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); + if ( \T_STRING !== $this->tokens[ $nameEnd ]['code'] ) { + // Hierarchy keyword or object stored in variable. + return false; } + + $nameStart = ( $this->phpcsFile->findPrevious( Collections::namespacedNameTokens(), ( $nameEnd - 1 ), null, true ) + 1 ); + $classname = GetTokensAsString::noEmpties( $this->phpcsFile, $nameStart, $nameEnd ); + $classname = $this->get_namespaced_classname( $classname, ( $nameStart - 1 ) ); } // Stop if we couldn't determine a classname. @@ -149,8 +149,8 @@ public function is_targetted_token( $stackPtr ) { return false; } - // Nothing to do if 'parent', 'self' or 'static'. - if ( \in_array( $classname, array( 'parent', 'self', 'static' ), true ) ) { + // Nothing to do if one of the hierarchy keywords - 'parent', 'self' or 'static' - is used. + if ( \in_array( strtolower( $classname ), array( '\parent', '\self', '\static' ), true ) ) { return false; } @@ -189,6 +189,26 @@ public function check_for_matches( $stackPtr ) { return min( $skip_to ); } + /** + * Process a matched token. + * + * @since 0.11.0 Split out from the `process()` method. + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in it original case. + * + * @return int|void Integer stack pointer to skip forward or void to continue + * normal file processing. + * + * @phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod.Found + */ + public function process_matched_token( $stackPtr, $group_name, $matched_content ) { + parent::process_matched_token( $stackPtr, $group_name, $matched_content ); + } + // phpcs:enable + /** * Prepare the class name for use in a regular expression. * @@ -220,26 +240,18 @@ protected function get_namespaced_classname( $classname, $search_from ) { } // Remove the namespace keyword if used. - if ( 0 === strpos( $classname, 'namespace\\' ) ) { + if ( 0 === stripos( $classname, 'namespace\\' ) ) { $classname = substr( $classname, 10 ); } - $namespace_keyword = $this->phpcsFile->findPrevious( \T_NAMESPACE, $search_from ); - if ( false === $namespace_keyword ) { + $namespace = Namespaces::determineNamespace( $this->phpcsFile, $search_from ); + if ( '' === $namespace ) { // No namespace keyword found at all, so global namespace. $classname = '\\' . $classname; } else { - $namespace = $this->determine_namespace( $search_from ); - - if ( ! empty( $namespace ) ) { - $classname = '\\' . $namespace . '\\' . $classname; - } else { - // No actual namespace found, so global namespace. - $classname = '\\' . $classname; - } + $classname = '\\' . $namespace . '\\' . $classname; } return $classname; } - } diff --git a/WordPress/AbstractFunctionParameterSniff.php b/WordPress/AbstractFunctionParameterSniff.php index df9350eb2b..c6395a7014 100644 --- a/WordPress/AbstractFunctionParameterSniff.php +++ b/WordPress/AbstractFunctionParameterSniff.php @@ -3,20 +3,19 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress; +namespace WordPressCS\WordPress; -use WordPress\AbstractFunctionRestrictionsSniff; +use PHPCSUtils\Utils\PassedParameters; +use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff; /** * Advises about parameters used in function calls. * - * @package WPCS\WordPressCodingStandards - * - * @since 0.11.0 + * @since 0.11.0 */ abstract class AbstractFunctionParameterSniff extends AbstractFunctionRestrictionsSniff { @@ -40,7 +39,7 @@ abstract class AbstractFunctionParameterSniff extends AbstractFunctionRestrictio protected $target_functions = array(); /** - * Groups of function to restrict. + * Groups of functions to restrict. * * @return array */ @@ -60,15 +59,16 @@ public function getGroups() { * Process a matched token. * * @param int $stackPtr The position of the current token in the stack. - * @param array $group_name The name of the group which was matched. - * @param string $matched_content The token content (function name) which was matched. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. * * @return int|void Integer stack pointer to skip forward or void to continue * normal file processing. */ public function process_matched_token( $stackPtr, $group_name, $matched_content ) { - $parameters = $this->get_function_call_parameters( $stackPtr ); + $parameters = PassedParameters::getParameters( $this->phpcsFile, $stackPtr ); if ( empty( $parameters ) ) { return $this->process_no_parameters( $stackPtr, $group_name, $matched_content ); @@ -83,8 +83,9 @@ public function process_matched_token( $stackPtr, $group_name, $matched_content * This method has to be made concrete in child classes. * * @param int $stackPtr The position of the current token in the stack. - * @param array $group_name The name of the group which was matched. - * @param string $matched_content The token content (function name) which was matched. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. * @param array $parameters Array with information about the parameters. * * @return int|void Integer stack pointer to skip forward or void to continue @@ -99,14 +100,12 @@ abstract public function process_parameters( $stackPtr, $group_name, $matched_co * were parameters are expected, but none found. * * @param int $stackPtr The position of the current token in the stack. - * @param array $group_name The name of the group which was matched. - * @param string $matched_content The token content (function name) which was matched. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. * * @return int|void Integer stack pointer to skip forward or void to continue * normal file processing. */ - public function process_no_parameters( $stackPtr, $group_name, $matched_content ) { - return; - } - + public function process_no_parameters( $stackPtr, $group_name, $matched_content ) {} } diff --git a/WordPress/AbstractFunctionRestrictionsSniff.php b/WordPress/AbstractFunctionRestrictionsSniff.php index 1b2dff3cab..dfa97b5a0e 100644 --- a/WordPress/AbstractFunctionRestrictionsSniff.php +++ b/WordPress/AbstractFunctionRestrictionsSniff.php @@ -3,26 +3,28 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress; +namespace WordPressCS\WordPress; -use WordPress\Sniff; -use PHP_CodeSniffer_Tokens as Tokens; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Utils\Context; +use PHPCSUtils\Utils\MessageHelper; +use WordPressCS\WordPress\Helpers\ContextHelper; +use WordPressCS\WordPress\Helpers\RulesetPropertyHelper; +use WordPressCS\WordPress\Sniff; /** * Restricts usage of some functions. * - * @package WPCS\WordPressCodingStandards - * - * @since 0.3.0 - * @since 0.10.0 Class became a proper abstract class. This was already the behaviour. - * Moved the file and renamed the class from - * `WordPress_Sniffs_Functions_FunctionRestrictionsSniff` to - * `WordPress_AbstractFunctionRestrictionsSniff`. - * @since 0.11.0 Extends the WordPress_Sniff class. + * @since 0.3.0 + * @since 0.10.0 Class became a proper abstract class. This was already the behaviour. + * Moved the file and renamed the class from + * `\WordPressCS\WordPress\Sniffs\Functions\FunctionRestrictionsSniff` to + * `\WordPressCS\WordPress\AbstractFunctionRestrictionsSniff`. + * @since 0.11.0 Extends the WordPressCS native `Sniff` class. */ abstract class AbstractFunctionRestrictionsSniff extends Sniff { @@ -57,7 +59,7 @@ abstract class AbstractFunctionRestrictionsSniff extends Sniff { * * @var string */ - protected $regex_pattern = '`\b(?:%s)\b`i'; + protected $regex_pattern = '`^(?:%s)$`i'; /** * Cache for the group information. @@ -97,14 +99,14 @@ abstract class AbstractFunctionRestrictionsSniff extends Sniff { * 'message' => 'Use anonymous functions instead please!', * 'functions' => array( 'file_get_contents', 'create_function', 'mysql_*' ), * // Only useful when using wildcards: - * 'whitelist' => array( 'mysql_to_rfc3339' => true, ), + * 'allow' => array( 'mysql_to_rfc3339' => true, ), * ) * ) * * You can use * wildcards to target a group of functions. * When you use * wildcards, you may inadvertently restrict too many - * functions. In that case you can add the `whitelist` key to - * whitelist individual functions to prevent false positives. + * functions. In that case you can add the `allow` key to + * safe list individual functions to prevent false positives. * * @return array */ @@ -151,13 +153,23 @@ protected function setup_groups( $key ) { foreach ( $this->groups as $groupName => $group ) { if ( empty( $group[ $key ] ) ) { unset( $this->groups[ $groupName ] ); - } else { - $items = array_map( array( $this, 'prepare_name_for_regex' ), $group[ $key ] ); - $all_items[] = $items; - $items = implode( '|', $items ); + continue; + } + + // Lowercase the items and potential allows as the comparisons should be done case-insensitively. + // Note: this disregards non-ascii names, but as we don't have any of those, that is okay for now. + $items = array_map( 'strtolower', $group[ $key ] ); + $this->groups[ $groupName ][ $key ] = $items; - $this->groups[ $groupName ]['regex'] = sprintf( $this->regex_pattern, $items ); + if ( ! empty( $group['allow'] ) ) { + $this->groups[ $groupName ]['allow'] = array_change_key_case( $group['allow'], \CASE_LOWER ); } + + $items = array_map( array( $this, 'prepare_name_for_regex' ), $items ); + $all_items[] = $items; + $items = implode( '|', $items ); + + $this->groups[ $groupName ]['regex'] = sprintf( $this->regex_pattern, $items ); } if ( empty( $this->groups ) ) { @@ -182,7 +194,7 @@ protected function setup_groups( $key ) { */ public function process_token( $stackPtr ) { - $this->excluded_groups = $this->merge_custom_array( $this->exclude ); + $this->excluded_groups = RulesetPropertyHelper::merge_custom_array( $this->exclude ); if ( array_diff_key( $this->groups, $this->excluded_groups ) === array() ) { // All groups have been excluded. // Don't remove the listener as the exclude property can be changed inline. @@ -211,38 +223,56 @@ public function process_token( $stackPtr ) { * @return bool */ public function is_targetted_token( $stackPtr ) { - // Exclude function definitions, class methods, and namespaced calls. - if ( \T_STRING === $this->tokens[ $stackPtr ]['code'] && isset( $this->tokens[ ( $stackPtr - 1 ) ] ) ) { - $prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); - - if ( false !== $prev ) { - // Skip sniffing if calling a same-named method, or on function definitions. - $skipped = array( - \T_FUNCTION => \T_FUNCTION, - \T_CLASS => \T_CLASS, - \T_AS => \T_AS, // Use declaration alias. - \T_DOUBLE_COLON => \T_DOUBLE_COLON, - \T_OBJECT_OPERATOR => \T_OBJECT_OPERATOR, - ); - - if ( isset( $skipped[ $this->tokens[ $prev ]['code'] ] ) ) { - return false; - } - - // Skip namespaced functions, ie: \foo\bar() not \bar(). - if ( \T_NS_SEPARATOR === $this->tokens[ $prev ]['code'] ) { - $pprev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $prev - 1 ), null, true ); - if ( false !== $pprev && \T_STRING === $this->tokens[ $pprev ]['code'] ) { - return false; - } - } - } + if ( ContextHelper::has_object_operator_before( $this->phpcsFile, $stackPtr ) === true ) { + return false; + } + + if ( ContextHelper::is_token_namespaced( $this->phpcsFile, $stackPtr ) === true ) { + return false; + } + + if ( Context::inAttribute( $this->phpcsFile, $stackPtr ) ) { + // Class instantiation or constant in attribute, not function call. + return false; + } + $search = Tokens::$emptyTokens; + $search[ \T_BITWISE_AND ] = \T_BITWISE_AND; + + $prev = $this->phpcsFile->findPrevious( $search, ( $stackPtr - 1 ), null, true ); + + // Skip sniffing on function, OO definitions or for function aliases in use statements. + $invalid_tokens = Tokens::$ooScopeTokens; + $invalid_tokens += array( + \T_FUNCTION => \T_FUNCTION, + \T_NEW => \T_NEW, + \T_AS => \T_AS, // Use declaration alias. + ); + + if ( isset( $invalid_tokens[ $this->tokens[ $prev ]['code'] ] ) ) { + return false; + } + + // Check if this could even be a function call. + $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); + if ( false === $next ) { + return false; + } + + // Check for `use function ... (as|;)`. + if ( ( \T_STRING === $this->tokens[ $prev ]['code'] && 'function' === $this->tokens[ $prev ]['content'] ) + && ( \T_AS === $this->tokens[ $next ]['code'] || \T_SEMICOLON === $this->tokens[ $next ]['code'] ) + ) { return true; } - return false; + // If it's not a `use` statement, there should be parenthesis. + if ( \T_OPEN_PARENTHESIS !== $this->tokens[ $next ]['code'] ) { + return false; + } + + return true; } /** @@ -265,7 +295,7 @@ public function check_for_matches( $stackPtr ) { continue; } - if ( isset( $group['whitelist'][ $token_content ] ) ) { + if ( isset( $group['allow'][ $token_content ] ) ) { continue; } @@ -288,22 +318,22 @@ public function check_for_matches( $stackPtr ) { * * @param int $stackPtr The position of the current token in the stack. * @param string $group_name The name of the group which was matched. - * @param string $matched_content The token content (function name) which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. * * @return int|void Integer stack pointer to skip forward or void to continue * normal file processing. */ public function process_matched_token( $stackPtr, $group_name, $matched_content ) { - $this->addMessage( + MessageHelper::addMessage( + $this->phpcsFile, $this->groups[ $group_name ]['message'], $stackPtr, ( 'error' === $this->groups[ $group_name ]['type'] ), - $this->string_to_errorcode( $group_name . '_' . $matched_content ), + MessageHelper::stringToErrorcode( $group_name . '_' . $matched_content ), array( $matched_content ) ); - - return; } /** @@ -315,15 +345,14 @@ public function process_matched_token( $stackPtr, $group_name, $matched_content * * @since 0.10.0 * - * @param string $function Function name. + * @param string $function_name Function name. * @return string Regex escaped function name. */ - protected function prepare_name_for_regex( $function ) { - $function = str_replace( array( '.*', '*' ), '@@', $function ); // Replace wildcards with placeholder. - $function = preg_quote( $function, '`' ); - $function = str_replace( '@@', '.*', $function ); // Replace placeholder with regex wildcard. + protected function prepare_name_for_regex( $function_name ) { + $function_name = str_replace( array( '.*', '*' ), '@@', $function_name ); // Replace wildcards with placeholder. + $function_name = preg_quote( $function_name, '`' ); + $function_name = str_replace( '@@', '.*', $function_name ); // Replace placeholder with regex wildcard. - return $function; + return $function_name; } - } diff --git a/WordPress/AbstractVariableRestrictionsSniff.php b/WordPress/AbstractVariableRestrictionsSniff.php deleted file mode 100644 index 6ebd64fa0b..0000000000 --- a/WordPress/AbstractVariableRestrictionsSniff.php +++ /dev/null @@ -1,240 +0,0 @@ -setup_groups() ) { - return array(); - } - - return array( - \T_VARIABLE, - \T_OBJECT_OPERATOR, - \T_DOUBLE_COLON, - \T_OPEN_SQUARE_BRACKET, - \T_DOUBLE_QUOTED_STRING, - \T_HEREDOC, - ); - } - - /** - * Groups of variables to restrict. - * - * This method should be overridden in extending classes. - * - * Example: groups => array( - * 'wpdb' => array( - * 'type' => 'error' | 'warning', - * 'message' => 'Dont use this one please!', - * 'variables' => array( '$val', '$var' ), - * 'object_vars' => array( '$foo->bar', .. ), - * 'array_members' => array( '$foo['bar']', .. ), - * ) - * ) - * - * @return array - */ - abstract public function getGroups(); - - /** - * Cache the groups. - * - * @since 0.13.0 - * - * @return bool True if the groups were setup. False if not. - */ - protected function setup_groups() { - $this->groups_cache = $this->getGroups(); - - if ( empty( $this->groups_cache ) && empty( self::$groups ) ) { - return false; - } - - // Allow for adding extra unit tests. - if ( ! empty( self::$groups ) ) { - $this->groups_cache = array_merge( $this->groups_cache, self::$groups ); - } - - return true; - } - - /** - * Processes this test, when one of its tokens is encountered. - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return int|void Integer stack pointer to skip forward or void to continue - * normal file processing. - */ - public function process_token( $stackPtr ) { - - $token = $this->tokens[ $stackPtr ]; - - $this->excluded_groups = $this->merge_custom_array( $this->exclude ); - if ( array_diff_key( $this->groups_cache, $this->excluded_groups ) === array() ) { - // All groups have been excluded. - // Don't remove the listener as the exclude property can be changed inline. - return; - } - - // Check if it is a function not a variable. - if ( \in_array( $token['code'], array( \T_OBJECT_OPERATOR, \T_DOUBLE_COLON ), true ) ) { // This only works for object vars and array members. - $method = $this->phpcsFile->findNext( \T_WHITESPACE, ( $stackPtr + 1 ), null, true ); - $possible_parenthesis = $this->phpcsFile->findNext( \T_WHITESPACE, ( $method + 1 ), null, true ); - if ( \T_OPEN_PARENTHESIS === $this->tokens[ $possible_parenthesis ]['code'] ) { - return; // So .. it is a function after all ! - } - } - - foreach ( $this->groups_cache as $groupName => $group ) { - - if ( isset( $this->excluded_groups[ $groupName ] ) ) { - continue; - } - - $patterns = array(); - - // Simple variable. - if ( \in_array( $token['code'], array( \T_VARIABLE, \T_DOUBLE_QUOTED_STRING, \T_HEREDOC ), true ) && ! empty( $group['variables'] ) ) { - $patterns = array_merge( $patterns, $group['variables'] ); - $var = $token['content']; - - } - - if ( \in_array( $token['code'], array( \T_OBJECT_OPERATOR, \T_DOUBLE_COLON, \T_DOUBLE_QUOTED_STRING, \T_HEREDOC ), true ) && ! empty( $group['object_vars'] ) ) { - // Object var, ex: $foo->bar / $foo::bar / Foo::bar / Foo::$bar . - $patterns = array_merge( $patterns, $group['object_vars'] ); - - $owner = $this->phpcsFile->findPrevious( array( \T_VARIABLE, \T_STRING ), $stackPtr ); - $child = $this->phpcsFile->findNext( array( \T_STRING, \T_VARIABLE ), $stackPtr ); - $var = implode( '', array( $this->tokens[ $owner ]['content'], $token['content'], $this->tokens[ $child ]['content'] ) ); - - } - - if ( \in_array( $token['code'], array( \T_OPEN_SQUARE_BRACKET, \T_DOUBLE_QUOTED_STRING, \T_HEREDOC ), true ) && ! empty( $group['array_members'] ) ) { - // Array members. - $patterns = array_merge( $patterns, $group['array_members'] ); - - if ( isset( $token['bracket_closer'] ) ) { - $owner = $this->phpcsFile->findPrevious( \T_VARIABLE, $stackPtr ); - $inside = $this->phpcsFile->getTokensAsString( $stackPtr, ( $token['bracket_closer'] - $stackPtr + 1 ) ); - $var = implode( '', array( $this->tokens[ $owner ]['content'], $inside ) ); - } - } - - if ( empty( $patterns ) ) { - continue; - } - - $patterns = array_map( array( $this, 'test_patterns' ), $patterns ); - $pattern = implode( '|', $patterns ); - $delim = ( \T_OPEN_SQUARE_BRACKET !== $token['code'] && \T_HEREDOC !== $token['code'] ) ? '\b' : ''; - - if ( \T_DOUBLE_QUOTED_STRING === $token['code'] || \T_HEREDOC === $token['code'] ) { - $var = $token['content']; - } - - if ( empty( $var ) || preg_match( '#(' . $pattern . ')' . $delim . '#', $var, $match ) !== 1 ) { - continue; - } - - $this->addMessage( - $group['message'], - $stackPtr, - ( 'error' === $group['type'] ), - $this->string_to_errorcode( $groupName . '_' . $match[1] ), - array( $var ) - ); - - return; // Show one error only. - } - } - - /** - * Transform a wildcard pattern to a usable regex pattern. - * - * @param string $pattern Pattern. - * @return string - */ - private function test_patterns( $pattern ) { - $pattern = preg_quote( $pattern, '#' ); - $pattern = preg_replace( - array( '#\\\\\*#', '[\'"]' ), - array( '.*', '\'' ), - $pattern - ); - return $pattern; - } - -} diff --git a/WordPress/Docs/Arrays/ArrayIndentationStandard.xml b/WordPress/Docs/Arrays/ArrayIndentationStandard.xml new file mode 100644 index 0000000000..0833cce3bf --- /dev/null +++ b/WordPress/Docs/Arrays/ArrayIndentationStandard.xml @@ -0,0 +1,116 @@ + + + + + + + + 22, +); + ]]> + + + 22, + ); + ]]> + + + + + + + + 22, + 'comment_count' => array( + 'value' => 25, + 'compare' => '>=', + ), + 'post_type' => array( + 'post', + 'page', + ), +); + ]]> + + + 22, + 'comment_count' => array( + 'value' => 25, + 'compare' => '>=', + ), + 'post_type' => array( + 'post', + 'page', + ), +); + ]]> + + + + + + + + 'start of phrase' + . 'concatented additional phrase' + . 'more text', +); + ]]> + + + 'start of phrase' +. 'concatented additional phrase' +. 'more text', +); + ]]> + + + + + + + + << + start of phrase + concatented additional phrase + more text +EOD +, +); + ]]> + + + diff --git a/WordPress/Docs/Arrays/ArrayKeySpacingRestrictionsStandard.xml b/WordPress/Docs/Arrays/ArrayKeySpacingRestrictionsStandard.xml new file mode 100644 index 0000000000..b42ca4cfec --- /dev/null +++ b/WordPress/Docs/Arrays/ArrayKeySpacingRestrictionsStandard.xml @@ -0,0 +1,31 @@ + + + + + + + + [ $post_id ]; +$post_title = $post[ 'concatenated' . $title ]; +$post = $posts[ HOME_PAGE ]; +$post = $posts[123]; +$post_title = $post['post_title']; + ]]> + + + [$post_id]; +$post_title = $post['concatenated' . $title ]; +$post = $posts[HOME_PAGE]; +$post = $posts[ 123 ]; +$post_title = $post[ 'post_title' ]; + ]]> + + + diff --git a/WordPress/Docs/Arrays/MultipleStatementAlignmentStandard.xml b/WordPress/Docs/Arrays/MultipleStatementAlignmentStandard.xml new file mode 100644 index 0000000000..e5c856769c --- /dev/null +++ b/WordPress/Docs/Arrays/MultipleStatementAlignmentStandard.xml @@ -0,0 +1,50 @@ + + + + + + + + => 22 ); +$bar = array( 'year' => $current_year ); + ]]> + + + =>22 ); +$bar = array( 'year'=> $current_year ); + ]]> + + + + + + + + => 22, + 'year' => $current_year, + 'monthnum' => $current_month, +); + ]]> + + + => 22, + 'year' => $current_year, + 'monthnum' => $current_month, +); + ]]> + + + diff --git a/WordPress/Docs/CodeAnalysis/EscapedNotTranslatedStandard.xml b/WordPress/Docs/CodeAnalysis/EscapedNotTranslatedStandard.xml new file mode 100644 index 0000000000..43950a0925 --- /dev/null +++ b/WordPress/Docs/CodeAnalysis/EscapedNotTranslatedStandard.xml @@ -0,0 +1,24 @@ + + + + + + + + esc_html__( 'text', 'domain' ); + ]]> + + + esc_html( 'text', 'domain' ); + ]]> + + + diff --git a/WordPress/Docs/DateTime/CurrentTimeTimestampStandard.xml b/WordPress/Docs/DateTime/CurrentTimeTimestampStandard.xml new file mode 100644 index 0000000000..e5a2c1bcb7 --- /dev/null +++ b/WordPress/Docs/DateTime/CurrentTimeTimestampStandard.xml @@ -0,0 +1,35 @@ + + + + + + + + time(); + ]]> + + + current_time( 'timestamp', true ); + ]]> + + + + + 'Y-m-d' ); + ]]> + + + current_time( 'U', false ); + ]]> + + + diff --git a/WordPress/Docs/DateTime/RestrictedFunctionsStandard.xml b/WordPress/Docs/DateTime/RestrictedFunctionsStandard.xml new file mode 100644 index 0000000000..845869d00b --- /dev/null +++ b/WordPress/Docs/DateTime/RestrictedFunctionsStandard.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + DateTime(); +$date->setTimezone( + new DateTimeZone( 'Europe/Amsterdam' ) +); + ]]> + + + date_default_timezone_set( 'Europe/Amsterdam' ); + ]]> + + + + + + + + gmdate( + 'Y-m-d\TH:i:s', + strtotime( $plugin['last_updated'] ) +); + ]]> + + + date( + 'Y-m-d\TH:i:s', + strtotime( $plugin['last_updated'] ) +); + ]]> + + + diff --git a/WordPress/Docs/NamingConventions/PrefixAllGlobalsStandard.xml b/WordPress/Docs/NamingConventions/PrefixAllGlobalsStandard.xml new file mode 100644 index 0000000000..a97d139438 --- /dev/null +++ b/WordPress/Docs/NamingConventions/PrefixAllGlobalsStandard.xml @@ -0,0 +1,119 @@ + + + + + + + + 'ECPT_VERSION', '1.0' ); + +$ecpt_admin = new ECPT_Admin_Page(); + +class ECPT_Admin_Page {} + +apply_filter( + 'ecpt_modify_content', + $ecpt_content +); + ]]> + + + 'PLUGIN_VERSION', '1.0' ); + +$admin = new Admin_Page(); + +class Admin_Page {} + +apply_filter( + 'modify_content', + $content +); + ]]> + + + + + ECPT_Plugin\Admin; + +// Constants declared using `const` will +// be namespaced and therefore prefixed. +const VERSION = 1.0; + +// A class declared in a (prefixed) namespace +// is automatically prefixed. +class Admin_Page {} + +// Variables in a namespaced file are not +// namespaced, so still need prefixing. +$ecpt_admin = new Admin_Page(); + +// Hook names are not subject to namespacing. +apply_filter( + 'ecpt_modify_content', + $ecpt_content +); + ]]> + + + Admin; + +// As the namespace is not prefixed, this +// is still bad. +const VERSION = 1.0; + +// As the namespace is not prefixed, this +// is still bad. +class Admin_Page {} + ]]> + + + + + + + + mycoolplugin_save_post() {} + ]]> + + + wp_save_post() {} + ]]> + + + + + + + + MyPluginIsCool {} + ]]> + + + My {} + ]]> + + + diff --git a/WordPress/Docs/NamingConventions/ValidHookNameStandard.xml b/WordPress/Docs/NamingConventions/ValidHookNameStandard.xml new file mode 100644 index 0000000000..387769d91f --- /dev/null +++ b/WordPress/Docs/NamingConventions/ValidHookNameStandard.xml @@ -0,0 +1,35 @@ + + + + + + + + 'prefix_hook_name', $var ); + ]]> + + + 'Prefix_Hook_NAME', $var ); + ]]> + + + + + 'prefix_hook_name', $var ); + ]]> + + + 'prefix\hook-name', $var ); + ]]> + + + diff --git a/WordPress/Docs/NamingConventions/ValidPostTypeSlugStandard.xml b/WordPress/Docs/NamingConventions/ValidPostTypeSlugStandard.xml new file mode 100644 index 0000000000..fe88dfd6e7 --- /dev/null +++ b/WordPress/Docs/NamingConventions/ValidPostTypeSlugStandard.xml @@ -0,0 +1,121 @@ + + + + + + + + 'my_short_slug', + array() +); + ]]> + + + 'my_own_post_type_too_long', + array() +); + ]]> + + + + + + + + 'my_post_type_slug', + array() +); + ]]> + + + 'my/post/type/slug', + array() +); + ]]> + + + + + + + + 'my_post_active', + array() +); + ]]> + + + "my_post_{$status}", + array() +); + ]]> + + + + + + + + 'prefixed_author', + array() +); + ]]> + + + 'author', + array() +); + ]]> + + + + + + + + 'prefixed_author', + array() +); + ]]> + + + 'wp_author', + array() +); + ]]> + + + diff --git a/WordPress/Docs/PHP/IniSetStandard.xml b/WordPress/Docs/PHP/IniSetStandard.xml new file mode 100644 index 0000000000..91c73c584c --- /dev/null +++ b/WordPress/Docs/PHP/IniSetStandard.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + 'short_open_tag', 'off' ); + ]]> + + + + + + + + wp_raise_memory_limit(); + ]]> + + + 'memory_limit', '256M' ); + ]]> + + + diff --git a/WordPress/Docs/PHP/StrictInArrayStandard.xml b/WordPress/Docs/PHP/StrictInArrayStandard.xml new file mode 100644 index 0000000000..744ec9ed97 --- /dev/null +++ b/WordPress/Docs/PHP/StrictInArrayStandard.xml @@ -0,0 +1,53 @@ + + + + + + + + true ) ) {} + ]]> + + + ) ) {} + ]]> + + + + + + true ); + ]]> + + + ); + ]]> + + + + + + true ); + ]]> + + + ); + ]]> + + + diff --git a/WordPress/Docs/PHP/YodaConditionsStandard.xml b/WordPress/Docs/PHP/YodaConditionsStandard.xml new file mode 100644 index 0000000000..cb2aa366db --- /dev/null +++ b/WordPress/Docs/PHP/YodaConditionsStandard.xml @@ -0,0 +1,27 @@ + + + + + + + + true === $the_force ) { + $victorious = you_will( $be ); +} + ]]> + + + $the_force === false ) { + $victorious = you_will_not( $be ); +} + ]]> + + + diff --git a/WordPress/Docs/Security/SafeRedirectStandard.xml b/WordPress/Docs/Security/SafeRedirectStandard.xml new file mode 100644 index 0000000000..aee63a6219 --- /dev/null +++ b/WordPress/Docs/Security/SafeRedirectStandard.xml @@ -0,0 +1,23 @@ + + + + + + + + wp_safe_redirect( $location ); + ]]> + + + wp_redirect( $location ); + ]]> + + + diff --git a/WordPress/Docs/WP/CapabilitiesStandard.xml b/WordPress/Docs/WP/CapabilitiesStandard.xml new file mode 100644 index 0000000000..a65848e8e1 --- /dev/null +++ b/WordPress/Docs/WP/CapabilitiesStandard.xml @@ -0,0 +1,69 @@ + + + + + + + + 'manage_sites' ) ) { } + ]]> + + + 'manage_site', $user->ID ); + ]]> + + + + + + + + 'manage_options', + 'options_page_slug', + 'project_options_page_cb' +); + ]]> + + + 'author', + 'options_page_slug', + 'project_options_page_cb' +); + ]]> + + + + + + + + 'read' ) ) { } + ]]> + + + 'level_6' ) ) { } + ]]> + + + diff --git a/WordPress/Docs/WP/CapitalPDangitStandard.xml b/WordPress/Docs/WP/CapitalPDangitStandard.xml new file mode 100644 index 0000000000..8df7940f72 --- /dev/null +++ b/WordPress/Docs/WP/CapitalPDangitStandard.xml @@ -0,0 +1,43 @@ + + + + + + + + WordPress_Example { + + /** + * This function is about WordPress. + */ + public function explain() { + echo 'This is an explanation + about WordPress.'; + } +} + ]]> + + + Wordpress_Example { + + /** + * This function is about Wordpress. + */ + public function explain() { + echo 'This is an explanation + about wordpress.'; + } +} + ]]> + + + diff --git a/WordPress/Docs/WP/ClassNameCaseStandard.xml b/WordPress/Docs/WP/ClassNameCaseStandard.xml new file mode 100644 index 0000000000..b17ec3f955 --- /dev/null +++ b/WordPress/Docs/WP/ClassNameCaseStandard.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/WordPress/Docs/WP/CronIntervalStandard.xml b/WordPress/Docs/WP/CronIntervalStandard.xml new file mode 100644 index 0000000000..03c75b8ab2 --- /dev/null +++ b/WordPress/Docs/WP/CronIntervalStandard.xml @@ -0,0 +1,45 @@ + + + + + + + + HOUR_IN_SECONDS, + 'display' => __( 'Every hour' ) + ); + return $schedules; +} + +add_filter( + 'cron_schedules', + 'adjust_schedules' +); + ]]> + + + 9 * 60, + 'display' => __( 'Every 9 minutes' ) + ); + return $schedules; +} + +add_filter( + 'cron_schedules', + 'adjust_schedules' +); + ]]> + + + diff --git a/WordPress/Docs/WP/DeprecatedClassesStandard.xml b/WordPress/Docs/WP/DeprecatedClassesStandard.xml new file mode 100644 index 0000000000..2de4e18e8b --- /dev/null +++ b/WordPress/Docs/WP/DeprecatedClassesStandard.xml @@ -0,0 +1,23 @@ + + + + + + + + WP_User_Query(); + ]]> + + + WP_User_Search(); // Deprecated WP 3.1. + ]]> + + + diff --git a/WordPress/Docs/WP/DeprecatedFunctionsStandard.xml b/WordPress/Docs/WP/DeprecatedFunctionsStandard.xml new file mode 100644 index 0000000000..7bb63d8b99 --- /dev/null +++ b/WordPress/Docs/WP/DeprecatedFunctionsStandard.xml @@ -0,0 +1,23 @@ + + + + + + + + get_sites(); + ]]> + + + wp_get_sites(); // Deprecated WP 4.6. + ]]> + + + diff --git a/WordPress/Docs/WP/DeprecatedParameterValuesStandard.xml b/WordPress/Docs/WP/DeprecatedParameterValuesStandard.xml new file mode 100644 index 0000000000..de9164c3ef --- /dev/null +++ b/WordPress/Docs/WP/DeprecatedParameterValuesStandard.xml @@ -0,0 +1,23 @@ + + + + + + + + 'url' ); + ]]> + + + 'home' ); // Deprecated WP 2.2.0. + ]]> + + + diff --git a/WordPress/Docs/WP/DeprecatedParametersStandard.xml b/WordPress/Docs/WP/DeprecatedParametersStandard.xml new file mode 100644 index 0000000000..195e27098b --- /dev/null +++ b/WordPress/Docs/WP/DeprecatedParametersStandard.xml @@ -0,0 +1,40 @@ + + + + after the deprecated parameter, only ever pass the default value. + ]]> + + + + + + + $string ); + ]]> + + + + + '', 'yes' ); + ]]> + + + 'oops', 'yes' ); + ]]> + + + diff --git a/WordPress/Docs/WP/EnqueuedResourceParametersStandard.xml b/WordPress/Docs/WP/EnqueuedResourceParametersStandard.xml new file mode 100644 index 0000000000..4060d79e04 --- /dev/null +++ b/WordPress/Docs/WP/EnqueuedResourceParametersStandard.xml @@ -0,0 +1,92 @@ + + + + + + + + '1.0.0' +); + ]]> + + + + + + + + + + + '1.0.0', + true +); + ]]> + + + false, + true +); + ]]> + + + + + + + + true +); + ]]> + + + + + + diff --git a/WordPress/Docs/WP/EnqueuedResourcesStandard.xml b/WordPress/Docs/WP/EnqueuedResourcesStandard.xml new file mode 100644 index 0000000000..1f7013068d --- /dev/null +++ b/WordPress/Docs/WP/EnqueuedResourcesStandard.xml @@ -0,0 +1,57 @@ + + + + + + + + wp_enqueue_script( + 'someScript-js', + $path_to_file, + array( 'jquery' ), + '1.0.0', + true +); + ]]> + + + ', + esc_url( $path_to_file ) +); + ]]> + + + + + + + + wp_enqueue_style( + 'style-name', + $path_to_file, + array(), + '1.0.0' +); + ]]> + + + ', + esc_url( $path_to_file ) +); + ]]> + + + diff --git a/WordPress/Docs/WP/PostsPerPageStandard.xml b/WordPress/Docs/WP/PostsPerPageStandard.xml new file mode 100644 index 0000000000..55715ca80d --- /dev/null +++ b/WordPress/Docs/WP/PostsPerPageStandard.xml @@ -0,0 +1,73 @@ + + + + + + + + -1, +); +$args = array( + 'posts_per_page' => 100, +); +$args = array( + 'posts_per_page' => '10', +); + +$query_args['posts_per_page'] = 100; + +_query_posts( 'nopaging=1&posts_per_page=50' ); + ]]> + + + 101, +); + +$query_args['posts_per_page'] = 200; + +_query_posts( 'nopaging=1&posts_per_page=999' ); + ]]> + + + + + -1, +); +$args = array( + 'numberposts' => 100, +); +$args = array( + 'numberposts' => '10', +); + +$query_args['numberposts'] = '-1'; + +_query_posts( 'numberposts=50' ); + ]]> + + + 101, +); + +$query_args['numberposts'] = '200'; + +_query_posts( 'numberposts=999' ); + ]]> + + + diff --git a/WordPress/Docs/WhiteSpace/CastStructureSpacingStandard.xml b/WordPress/Docs/WhiteSpace/CastStructureSpacingStandard.xml new file mode 100644 index 0000000000..a087dd5417 --- /dev/null +++ b/WordPress/Docs/WhiteSpace/CastStructureSpacingStandard.xml @@ -0,0 +1,27 @@ + + + + + + + + (int) '420'; + +// No space between spread operator and cast. +$a = function_call( ...(array) $mixed ); + ]]> + + + =(int) '420'; + ]]> + + + diff --git a/WordPress/Docs/WhiteSpace/ControlStructureSpacingStandard.xml b/WordPress/Docs/WhiteSpace/ControlStructureSpacingStandard.xml new file mode 100644 index 0000000000..943967a620 --- /dev/null +++ b/WordPress/Docs/WhiteSpace/ControlStructureSpacingStandard.xml @@ -0,0 +1,150 @@ + + + + + + + + ( have_posts() ) {} + +// For multi-line conditions, +// a new line is also accepted. +if ( true === $condition + && $count > 10 +) {} + ]]> + + + (have_posts()){} + +// Too much space. +while ( have_posts() ) {} + ]]> + + + + + + + + { + // Do something. +} catch ( + ExceptionA | ExceptionB $e +) { +} + ]]> + + + { + // Do something. +} catch ( Exception $e ) +( +} + ]]> + + + + + { + // Do something. +} + ]]> + + + { + // Do something. +} + ]]> + + + + + + + + : + // Do something. +endforeach; + ]]> + + + : + // Do something. +endforeach; + ]]> + + + + + + + + + + + + + +} + ]]> + + + + + + + + + + + + + echo $a; + + +} + ]]> + + + diff --git a/WordPress/Docs/WhiteSpace/ObjectOperatorSpacingStandard.xml b/WordPress/Docs/WhiteSpace/ObjectOperatorSpacingStandard.xml new file mode 100644 index 0000000000..47ffb1e8e6 --- /dev/null +++ b/WordPress/Docs/WhiteSpace/ObjectOperatorSpacingStandard.xml @@ -0,0 +1,19 @@ + + + , ?->, ::) should not have any spaces around them, though new lines are allowed except for use with the `::class` constant. + ]]> + + + + ->bar(); + ]]> + + + ?-> bar(); + ]]> + + + diff --git a/WordPress/Docs/WhiteSpace/OperatorSpacingStandard.xml b/WordPress/Docs/WhiteSpace/OperatorSpacingStandard.xml new file mode 100644 index 0000000000..f9e0719f1a --- /dev/null +++ b/WordPress/Docs/WhiteSpace/OperatorSpacingStandard.xml @@ -0,0 +1,61 @@ + + + + + + + + === $b && $b === $c ) {} +if ( ! $var ) {} + ]]> + + + && $b === $c ) {} +if ( ! $var ) {} + +// Too little space. +if ( $a===$b &&$b ===$c ) {} +if ( !$var ) {} + ]]> + + + + + + && $b === $c +) {} + ]]> + + + + && $b === $c +) {} + ]]> + + + + + = 'foo'; +$all = 'foobar'; + ]]> + + + = 'foo'; +$all ='foobar'; + ]]> + + + diff --git a/WordPress/Helpers/ArrayWalkingFunctionsHelper.php b/WordPress/Helpers/ArrayWalkingFunctionsHelper.php new file mode 100644 index 0000000000..76623d7dd9 --- /dev/null +++ b/WordPress/Helpers/ArrayWalkingFunctionsHelper.php @@ -0,0 +1,108 @@ + + */ + private static $arrayWalkingFunctions = array( + 'array_map' => array( + 'position' => 1, + 'name' => 'callback', + ), + 'map_deep' => array( + 'position' => 2, + 'name' => 'callback', + ), + ); + + /** + * Retrieve a list of the supported "array walking" functions. + * + * @since 3.0.0 + * + * @return array + */ + public static function get_functions() { + return \array_fill_keys( \array_keys( self::$arrayWalkingFunctions ), true ); + } + + /** + * Check if a particular function is an "array walking" function. + * + * @since 3.0.0 + * + * @param string $functionName The name of the function to check. + * + * @return bool + */ + public static function is_array_walking_function( $functionName ) { + return isset( self::$arrayWalkingFunctions[ strtolower( $functionName ) ] ); + } + + /** + * Retrieve the parameter information for the callback parameter for an array walking function. + * + * @since 3.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position of function call name token. + * + * @return array|false Array with information on the callback parameter. + * Or `FALSE` if the parameter is not found. + * See the PHPCSUtils PassedParameters::getParameters() documentation + * for the format of the returned (single-dimensional) array. + */ + public static function get_callback_parameter( File $phpcsFile, $stackPtr ) { + $tokens = $phpcsFile->getTokens(); + if ( isset( $tokens[ $stackPtr ] ) === false ) { + return false; + } + + $functionName = strtolower( $tokens[ $stackPtr ]['content'] ); + if ( isset( self::$arrayWalkingFunctions[ $functionName ] ) === false ) { + return false; + } + + return PassedParameters::getParameter( + $phpcsFile, + $stackPtr, + self::$arrayWalkingFunctions[ $functionName ]['position'], + self::$arrayWalkingFunctions[ $functionName ]['name'] + ); + } +} diff --git a/WordPress/Helpers/ConstantsHelper.php b/WordPress/Helpers/ConstantsHelper.php new file mode 100644 index 0000000000..080f3cf223 --- /dev/null +++ b/WordPress/Helpers/ConstantsHelper.php @@ -0,0 +1,135 @@ +getTokens(); + + // Check for the existence of the token. + if ( ! isset( $tokens[ $stackPtr ] ) ) { + return false; + } + + // Is this one of the tokens this function handles ? + if ( \T_STRING !== $tokens[ $stackPtr ]['code'] ) { + return false; + } + + $next = $phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); + if ( false !== $next + && ( \T_OPEN_PARENTHESIS === $tokens[ $next ]['code'] + || \T_DOUBLE_COLON === $tokens[ $next ]['code'] ) + ) { + // Function call or declaration. + return false; + } + + // Array of tokens which if found preceding the $stackPtr indicate that a T_STRING is not a global constant. + $tokens_to_ignore = array( + \T_NAMESPACE => true, + \T_USE => true, + \T_EXTENDS => true, + \T_IMPLEMENTS => true, + \T_NEW => true, + \T_FUNCTION => true, + \T_INSTANCEOF => true, + \T_INSTEADOF => true, + \T_GOTO => true, + ); + $tokens_to_ignore += Tokens::$ooScopeTokens; + $tokens_to_ignore += Collections::objectOperators(); + $tokens_to_ignore += Tokens::$scopeModifiers; + + $prev = $phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); + if ( isset( $tokens_to_ignore[ $tokens[ $prev ]['code'] ] ) ) { + // Not the use of a constant. + return false; + } + + if ( ContextHelper::is_token_namespaced( $phpcsFile, $stackPtr ) === true ) { + // Namespaced constant of the same name. + return false; + } + + if ( \T_CONST === $tokens[ $prev ]['code'] + && Scopes::isOOConstant( $phpcsFile, $prev ) + ) { + // Class constant declaration of the same name. + return false; + } + + /* + * Deal with a number of variations of use statements. + */ + for ( $i = $stackPtr; $i > 0; $i-- ) { + if ( $tokens[ $i ]['line'] !== $tokens[ $stackPtr ]['line'] ) { + break; + } + } + + $firstOnLine = $phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true ); + if ( false !== $firstOnLine && \T_USE === $tokens[ $firstOnLine ]['code'] ) { + $nextOnLine = $phpcsFile->findNext( Tokens::$emptyTokens, ( $firstOnLine + 1 ), null, true ); + if ( false !== $nextOnLine ) { + if ( \T_STRING === $tokens[ $nextOnLine ]['code'] + && 'const' === $tokens[ $nextOnLine ]['content'] + ) { + $hasNsSep = $phpcsFile->findNext( \T_NS_SEPARATOR, ( $nextOnLine + 1 ), $stackPtr ); + if ( false !== $hasNsSep ) { + // Namespaced const (group) use statement. + return false; + } + } else { + // Not a const use statement. + return false; + } + } + } + + return true; + } +} diff --git a/WordPress/Helpers/ContextHelper.php b/WordPress/Helpers/ContextHelper.php new file mode 100644 index 0000000000..8b5c178666 --- /dev/null +++ b/WordPress/Helpers/ContextHelper.php @@ -0,0 +1,394 @@ + Key is token constant, value irrelevant. + */ + private static $safe_casts = array( + \T_INT_CAST => true, + \T_DOUBLE_CAST => true, + \T_BOOL_CAST => true, + \T_UNSET_CAST => true, + ); + + /** + * List of PHP native functions to test the type of a variable. + * + * Using these functions is safe in combination with superglobals without + * unslashing or sanitization. + * + * They should, however, not be regarded as unslashing or sanitization functions. + * + * @since 2.1.0 + * @since 3.0.0 - Moved from the Sniff class to this class. + * - The property visibility was changed from `protected` to `private static`. + * + * @var array Key is function name, value irrelevant. + */ + private static $typeTestFunctions = array( + 'is_array' => true, + 'is_bool' => true, + 'is_callable' => true, + 'is_countable' => true, + 'is_double' => true, + 'is_float' => true, + 'is_int' => true, + 'is_integer' => true, + 'is_iterable' => true, + 'is_long' => true, + 'is_null' => true, + 'is_numeric' => true, + 'is_object' => true, + 'is_real' => true, + 'is_resource' => true, + 'is_scalar' => true, + 'is_string' => true, + ); + + /** + * List of PHP native functions to check if an array index exists. + * + * @since 3.0.0 + * + * @var array Key is function name, value irrelevant. + */ + private static $key_exists_functions = array( + 'array_key_exists' => true, + 'key_exists' => true, // Alias. + ); + + /** + * Array functions to compare a $needle to a predefined set of values. + * + * If the value is set to an array, the parameter specified in the array is + * required for the function call to be considered as a comparison. + * + * @since 2.1.0 + * @since 3.0.0 - Moved from the Sniff class to this class. + * - The property visibility was changed from `protected` to `private static`. + * + * @var array + */ + private static $arrayCompareFunctions = array( + 'in_array' => true, + 'array_search' => true, + 'array_keys' => array( + 'position' => 2, + 'name' => 'filter_value', + ), + ); + + /** + * Check if a particular token acts - statically or non-statically - on an object. + * + * {@internal Note: this may still mistake a namespaced function imported via a `use` statement for + * a global function!} + * + * @since 2.1.0 + * @since 3.0.0 - Moved from the Sniff class to this class. + * - The method was renamed from `is_class_object_call() to `has_object_operator_before()`. + * - The method visibility was changed from `protected` to `public static`. + * - The `$phpcsFile` parameter was added. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The index of the token in the stack. + * + * @return bool + */ + public static function has_object_operator_before( File $phpcsFile, $stackPtr ) { + $tokens = $phpcsFile->getTokens(); + if ( isset( $tokens[ $stackPtr ] ) === false ) { + return false; + } + + $before = $phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); + + return isset( Collections::objectOperators()[ $tokens[ $before ]['code'] ] ); + } + + /** + * Check if a particular token is prefixed with a namespace. + * + * {@internal This will give a false positive if the file is not namespaced and the token is prefixed + * with `namespace\`.} + * + * @since 2.1.0 + * @since 3.0.0 - Moved from the Sniff class to this class. + * - The method visibility was changed from `protected` to `public static`. + * - The `$phpcsFile` parameter was added. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The index of the token in the stack. + * + * @return bool + */ + public static function is_token_namespaced( File $phpcsFile, $stackPtr ) { + $tokens = $phpcsFile->getTokens(); + if ( isset( $tokens[ $stackPtr ] ) === false ) { + return false; + } + + $prev = $phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); + + if ( \T_NS_SEPARATOR !== $tokens[ $prev ]['code'] ) { + return false; + } + + $before_prev = $phpcsFile->findPrevious( Tokens::$emptyTokens, ( $prev - 1 ), null, true ); + if ( \T_STRING !== $tokens[ $before_prev ]['code'] + && \T_NAMESPACE !== $tokens[ $before_prev ]['code'] + ) { + return false; + } + + return true; + } + + /** + * Check if a token is (part of) a parameter for a function call to a select list of functions. + * + * This is useful, for instance, when trying to determine the context a variable is used in. + * + * For example: this function could be used to determine if the variable `$foo` is used + * in a global function call to the function `is_foo()`. + * In that case, a call to this function would return the stackPtr to the T_STRING `is_foo` + * for code like: `is_foo( $foo, 'some_other_param' )`, while it would return `false` for + * the following code `is_bar( $foo, 'some_other_param' )`. + * + * @since 2.1.0 + * @since 3.0.0 - Moved from the Sniff class to this class. + * - The method visibility was changed from `protected` to `public static`. + * - The `$phpcsFile` parameter was added. + * - The `$global` parameter was renamed to `$global_function`. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The index of the token in the stack. + * @param array $valid_functions List of valid function names. + * Note: The keys to this array should be the function names + * in lowercase. Values are irrelevant. + * @param bool $global_function Optional. Whether to make sure that the function call is + * to a global function. If `false`, calls to methods, be it static + * `Class::method()` or via an object `$obj->method()`, and + * namespaced function calls, like `MyNS\function_name()` will + * also be accepted. + * Defaults to `true`. + * @param bool $allow_nested Optional. Whether to allow for nested function calls within the + * call to this function. + * I.e. when checking whether a token is within a function call + * to `strtolower()`, whether to accept `strtolower( trim( $var ) )` + * or only `strtolower( $var )`. + * Defaults to `false`. + * + * @return int|bool Stack pointer to the function call T_STRING token or false otherwise. + */ + public static function is_in_function_call( File $phpcsFile, $stackPtr, array $valid_functions, $global_function = true, $allow_nested = false ) { + $tokens = $phpcsFile->getTokens(); + if ( ! isset( $tokens[ $stackPtr ]['nested_parenthesis'] ) ) { + return false; + } + + $nested_parenthesis = $tokens[ $stackPtr ]['nested_parenthesis']; + if ( false === $allow_nested ) { + $nested_parenthesis = array_reverse( $nested_parenthesis, true ); + } + + foreach ( $nested_parenthesis as $open => $close ) { + $prev_non_empty = $phpcsFile->findPrevious( Tokens::$emptyTokens, ( $open - 1 ), null, true ); + if ( false === $prev_non_empty || \T_STRING !== $tokens[ $prev_non_empty ]['code'] ) { + continue; + } + + if ( isset( $valid_functions[ strtolower( $tokens[ $prev_non_empty ]['content'] ) ] ) === false ) { + if ( false === $allow_nested ) { + // Function call encountered, but not to one of the allowed functions. + return false; + } + + continue; + } + + if ( false === $global_function ) { + return $prev_non_empty; + } + + /* + * Now, make sure it is a global function. + */ + if ( self::has_object_operator_before( $phpcsFile, $prev_non_empty ) === true ) { + continue; + } + + if ( self::is_token_namespaced( $phpcsFile, $prev_non_empty ) === true ) { + continue; + } + + return $prev_non_empty; + } + + return false; + } + + /** + * Check if a token is inside of an is_...() statement. + * + * @since 2.1.0 + * @since 3.0.0 - Moved from the Sniff class to this class. + * - The method visibility was changed from `protected` to `public static`. + * - The `$phpcsFile` parameter was added. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The index of the token in the stack. + * + * @return bool Whether the token is being type tested. + */ + public static function is_in_type_test( File $phpcsFile, $stackPtr ) { + /* + * Casting the potential integer stack pointer return value to boolean here is fine. + * The return can never be `0` as there will always be a PHP open tag before the + * function call. + */ + return (bool) self::is_in_function_call( $phpcsFile, $stackPtr, self::$typeTestFunctions ); + } + + /** + * Check if a token is inside of an isset(), empty() or array_key_exists() statement. + * + * @since 0.5.0 + * @since 2.1.0 Now checks for the token being used as the array parameter + * in function calls to array_key_exists() and key_exists() as well. + * @since 3.0.0 - Moved from the Sniff class to this class. + * - The method visibility was changed from `protected` to `public static`. + * - The `$phpcsFile` parameter was added. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The index of the token in the stack. + * + * @return bool Whether the token is inside an isset() or empty() statement. + */ + public static function is_in_isset_or_empty( File $phpcsFile, $stackPtr ) { + if ( Parentheses::lastOwnerIn( $phpcsFile, $stackPtr, array( \T_ISSET, \T_EMPTY ) ) !== false ) { + return true; + } + + $functionPtr = self::is_in_function_call( $phpcsFile, $stackPtr, self::$key_exists_functions ); + if ( false !== $functionPtr ) { + /* + * Both functions being checked have the same parameters. If the function list would + * be expanded, this needs to be revisited. + */ + $array_param = PassedParameters::getParameter( $phpcsFile, $functionPtr, 2, 'array' ); + if ( false !== $array_param + && ( $stackPtr >= $array_param['start'] && $stackPtr <= $array_param['end'] ) + ) { + return true; + } + } + + return false; + } + + /** + * Retrieve a list of the tokens which are regarded as "safe casts". + * + * @since 3.0.0 + * + * @return array + */ + public static function get_safe_cast_tokens() { + return self::$safe_casts; + } + + /** + * Check if something is being casted to a safe value. + * + * @since 0.5.0 + * @since 3.0.0 - Moved from the Sniff class to this class. + * - The method visibility was changed from `protected` to `public static`. + * - The `$phpcsFile` parameter was added. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The index of the token in the stack. + * + * @return bool Whether the token being casted. + */ + public static function is_safe_casted( File $phpcsFile, $stackPtr ) { + $tokens = $phpcsFile->getTokens(); + if ( isset( $tokens[ $stackPtr ] ) === false ) { + return false; + } + + $prev = $phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); + + return isset( self::$safe_casts[ $tokens[ $prev ]['code'] ] ); + } + + /** + * Check if a token is inside of an array-value comparison function. + * + * @since 2.1.0 + * @since 3.0.0 - Moved from the Sniff class to this class. + * - The method visibility was changed from `protected` to `public static`. + * - The `$phpcsFile` parameter was added. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The index of the token in the stack. + * + * @return bool Whether the token is (part of) a parameter to an + * array-value comparison function. + */ + public static function is_in_array_comparison( File $phpcsFile, $stackPtr ) { + $function_ptr = self::is_in_function_call( $phpcsFile, $stackPtr, self::$arrayCompareFunctions, true, true ); + if ( false === $function_ptr ) { + return false; + } + + $tokens = $phpcsFile->getTokens(); + $function_name = strtolower( $tokens[ $function_ptr ]['content'] ); + if ( true === self::$arrayCompareFunctions[ $function_name ] ) { + return true; + } + + $target_param = self::$arrayCompareFunctions[ $function_name ]; + $found_param = PassedParameters::getParameter( $phpcsFile, $function_ptr, $target_param['position'], $target_param['name'] ); + if ( false !== $found_param ) { + return true; + } + + return false; + } +} diff --git a/WordPress/Helpers/DeprecationHelper.php b/WordPress/Helpers/DeprecationHelper.php new file mode 100644 index 0000000000..13273496c7 --- /dev/null +++ b/WordPress/Helpers/DeprecationHelper.php @@ -0,0 +1,84 @@ +getTokens(); + if ( isset( $tokens[ $stackPtr ] ) === false ) { + return false; + } + + $ignore = Tokens::$methodPrefixes; + $ignore[ \T_WHITESPACE ] = \T_WHITESPACE; + + for ( $comment_end = ( $stackPtr - 1 ); $comment_end >= 0; $comment_end-- ) { + if ( isset( $ignore[ $tokens[ $comment_end ]['code'] ] ) === true ) { + continue; + } + + if ( \T_ATTRIBUTE_END === $tokens[ $comment_end ]['code'] + && isset( $tokens[ $comment_end ]['attribute_opener'] ) === true + ) { + $comment_end = $tokens[ $comment_end ]['attribute_opener']; + continue; + } + + break; + } + + if ( \T_DOC_COMMENT_CLOSE_TAG !== $tokens[ $comment_end ]['code'] ) { + // Function doesn't have a doc comment or is using the wrong type of comment. + return false; + } + + $comment_start = $tokens[ $comment_end ]['comment_opener']; + foreach ( $tokens[ $comment_start ]['comment_tags'] as $tag ) { + if ( '@deprecated' === $tokens[ $tag ]['content'] ) { + return true; + } + } + + return false; + } +} diff --git a/WordPress/Helpers/EscapingFunctionsTrait.php b/WordPress/Helpers/EscapingFunctionsTrait.php new file mode 100644 index 0000000000..4ebff51396 --- /dev/null +++ b/WordPress/Helpers/EscapingFunctionsTrait.php @@ -0,0 +1,255 @@ + + */ + private $escapingFunctions = array( + 'absint' => true, + 'esc_attr__' => true, + 'esc_attr_e' => true, + 'esc_attr_x' => true, + 'esc_attr' => true, + 'esc_html__' => true, + 'esc_html_e' => true, + 'esc_html_x' => true, + 'esc_html' => true, + 'esc_js' => true, + 'esc_sql' => true, + 'esc_textarea' => true, + 'esc_url_raw' => true, + 'esc_url' => true, + 'esc_xml' => true, + 'filter_input' => true, + 'filter_var' => true, + 'floatval' => true, + 'highlight_string' => true, + 'intval' => true, + 'json_encode' => true, + 'like_escape' => true, + 'number_format' => true, + 'rawurlencode' => true, + 'sanitize_hex_color' => true, + 'sanitize_hex_color_no_hash' => true, + 'sanitize_html_class' => true, + 'sanitize_key' => true, + 'sanitize_user_field' => true, + 'tag_escape' => true, + 'urlencode_deep' => true, + 'urlencode' => true, + 'wp_json_encode' => true, + 'wp_kses_allowed_html' => true, + 'wp_kses_data' => true, + 'wp_kses_one_attr' => true, + 'wp_kses_post' => true, + 'wp_kses' => true, + ); + + /** + * Functions whose output is automatically escaped for display. + * + * @since 0.5.0 + * @since 0.11.0 Changed from public static to protected non-static. + * @since 3.0.0 - Moved from the Sniff class to this trait. + * - Visibility changed from protected to private. + * + * @var array + */ + private $autoEscapedFunctions = array( + 'allowed_tags' => true, + 'bloginfo' => true, + 'body_class' => true, + 'calendar_week_mod' => true, + 'category_description' => true, + 'checked' => true, + 'comment_class' => true, + 'count' => true, + 'disabled' => true, + 'do_shortcode' => true, + 'do_shortcode_tag' => true, + 'get_archives_link' => true, + 'get_attachment_link' => true, + 'get_avatar' => true, + 'get_bookmark_field' => true, + 'get_calendar' => true, + 'get_comment_author_link' => true, + 'get_current_blog_id' => true, + 'get_delete_post_link' => true, + 'get_search_form' => true, + 'get_search_query' => true, + 'get_the_author_link' => true, + 'get_the_author' => true, + 'get_the_date' => true, + 'get_the_ID' => true, + 'get_the_post_thumbnail' => true, + 'get_the_term_list' => true, + 'post_type_archive_title' => true, + 'readonly' => true, + 'selected' => true, + 'single_cat_title' => true, + 'single_month_title' => true, + 'single_post_title' => true, + 'single_tag_title' => true, + 'single_term_title' => true, + 'tag_description' => true, + 'term_description' => true, + 'the_author' => true, + 'the_date' => true, + 'the_title_attribute' => true, + 'walk_nav_menu_tree' => true, + 'wp_dropdown_categories' => true, + 'wp_dropdown_users' => true, + 'wp_generate_tag_cloud' => true, + 'wp_get_archives' => true, + 'wp_get_attachment_image' => true, + 'wp_get_attachment_link' => true, + 'wp_link_pages' => true, + 'wp_list_authors' => true, + 'wp_list_bookmarks' => true, + 'wp_list_categories' => true, + 'wp_list_comments' => true, + 'wp_login_form' => true, + 'wp_loginout' => true, + 'wp_nav_menu' => true, + 'wp_readonly' => true, + 'wp_register' => true, + 'wp_tag_cloud' => true, + 'wp_timezone_choice' => true, + 'wp_title' => true, + ); + + /** + * Cache of previously added custom functions. + * + * Prevents having to do the same merges over and over again. + * + * @since 0.4.0 + * @since 0.11.0 - Changed from public static to protected non-static. + * - Changed the format from simple bool to array. + * @since 3.0.0 - Moved from the EscapeOutput Sniff class to this trait. + * - Visibility changed from protected to private. + * + * @var array + */ + private $addedCustomEscapingFunctions = array( + 'escape' => array(), + 'autoescape' => array(), + ); + + /** + * Combined list of WP native and custom escaping functions. + * + * @since 3.0.0 + * + * @var array + */ + private $allEscapingFunctions = array(); + + /** + * Combined list of WP native and custom auto-escaping functions. + * + * @since 3.0.0 + * + * @var array + */ + private $allAutoEscapedFunctions = array(); + + /** + * Check if a particular function is regarded as an escaping function. + * + * @since 3.0.0 + * + * @param string $functionName The name of the function to check. + * + * @return bool + */ + final public function is_escaping_function( $functionName ) { + if ( array() === $this->allEscapingFunctions + || $this->customEscapingFunctions !== $this->addedCustomEscapingFunctions['escape'] + ) { + $this->allEscapingFunctions = RulesetPropertyHelper::merge_custom_array( + $this->customEscapingFunctions, + $this->escapingFunctions + ); + + $this->addedCustomEscapingFunctions['escape'] = $this->customEscapingFunctions; + } + + return isset( $this->allEscapingFunctions[ strtolower( $functionName ) ] ); + } + + /** + * Check if a particular function is regarded as an auto-escaped function. + * + * @since 3.0.0 + * + * @param string $functionName The name of the function to check. + * + * @return bool + */ + final public function is_auto_escaped_function( $functionName ) { + if ( array() === $this->allAutoEscapedFunctions + || $this->customAutoEscapedFunctions !== $this->addedCustomEscapingFunctions['autoescape'] + ) { + $this->allAutoEscapedFunctions = RulesetPropertyHelper::merge_custom_array( + $this->customAutoEscapedFunctions, + $this->autoEscapedFunctions + ); + + $this->addedCustomEscapingFunctions['autoescape'] = $this->customAutoEscapedFunctions; + } + + return isset( $this->allAutoEscapedFunctions[ strtolower( $functionName ) ] ); + } +} diff --git a/WordPress/Helpers/FormattingFunctionsHelper.php b/WordPress/Helpers/FormattingFunctionsHelper.php new file mode 100644 index 0000000000..e04298ab46 --- /dev/null +++ b/WordPress/Helpers/FormattingFunctionsHelper.php @@ -0,0 +1,60 @@ + + */ + private static $formattingFunctions = array( + 'antispambot' => true, + 'array_fill' => true, + 'ent2ncr' => true, + 'implode' => true, + 'join' => true, + 'nl2br' => true, + 'sprintf' => true, + 'vsprintf' => true, + 'wp_sprintf' => true, + ); + + /** + * Check if a particular function is regarded as a formatting function. + * + * @since 3.0.0 + * + * @param string $functionName The name of the function to check. + * + * @return bool + */ + public static function is_formatting_function( $functionName ) { + return isset( self::$formattingFunctions[ strtolower( $functionName ) ] ); + } +} diff --git a/WordPress/Helpers/IsUnitTestTrait.php b/WordPress/Helpers/IsUnitTestTrait.php new file mode 100644 index 0000000000..a6e3b36d32 --- /dev/null +++ b/WordPress/Helpers/IsUnitTestTrait.php @@ -0,0 +1,237 @@ + + * + * + * + * + * + * + * + * ``` + * + * Note: it is strongly _recommended_ to exclude your test directories for + * select error codes of those particular sniffs instead of relying on this + * property/trait. + * + * @since 0.11.0 + * @since 3.0.0 Moved from the Sniff class to this dedicated Trait. + * Renamed from `$custom_test_class_whitelist` to `$custom_test_classes`. + * + * @var string[] + */ + public $custom_test_classes = array(); + + /** + * List of PHPUnit and WP native classes which test classes can extend. + * + * {internal These are the test cases provided in the `/tests/phpunit/includes/` + * directory of WP Core.} + * + * @since 0.11.0 + * @since 3.0.0 - Moved from the Sniff class to this dedicated Trait. + * - Renamed from `$test_class_whitelist` to `$known_test_classes`. + * - Visibility changed from protected to private. + * + * @var array Key is class name, value irrelevant. + */ + private $known_test_classes = array( + // Base test cases. + 'WP_UnitTestCase' => true, + 'WP_UnitTestCase_Base' => true, + 'PHPUnit_Adapter_TestCase' => true, + + // Domain specific base test cases. + 'WP_Ajax_UnitTestCase' => true, + 'WP_Canonical_UnitTestCase' => true, + 'WP_Test_REST_Controller_Testcase' => true, + 'WP_Test_REST_Post_Type_Controller_Testcase' => true, + 'WP_Test_REST_TestCase' => true, + 'WP_Test_XML_TestCase' => true, + 'WP_XMLRPC_UnitTestCase' => true, + + // PHPUnit native test cases. + 'PHPUnit_Framework_TestCase' => true, + 'PHPUnit\\Framework\\TestCase' => true, + // PHPUnit native TestCase class when imported via use statement. + 'TestCase' => true, + ); + + /** + * Cache of previously added custom test classes. + * + * Prevents having to do the same merges over and over again. + * + * @since 3.0.0 + * + * @var string[] + */ + private $added_custom_test_classes = array(); + + /** + * Combined list of WP/PHPUnit native and custom test classes. + * + * @since 3.0.0 + * + * @var array + */ + private $all_test_classes = array(); + + /** + * Retrieve a list of all registered test classes, both WP/PHPUnit native as well as custom. + * + * @since 3.0.0 + * + * @return array + */ + final protected function get_all_test_classes() { + if ( array() === $this->all_test_classes + || $this->custom_test_classes !== $this->added_custom_test_classes + ) { + /* + * Show some tolerance for user input. + * The custom test class names should be passed as FQN without a prefixing `\`. + */ + $custom_test_classes = array(); + if ( ! empty( $this->custom_test_classes ) ) { + foreach ( $this->custom_test_classes as $v ) { + $custom_test_classes[] = ltrim( $v, '\\' ); + } + } + + /* + * Lowercase all names, both custom as well as "known", as PHP treats namespaced names case-insensitively. + */ + $custom_test_classes = array_map( 'strtolower', $custom_test_classes ); + $known_test_classes = array_change_key_case( $this->known_test_classes, \CASE_LOWER ); + + $this->all_test_classes = RulesetPropertyHelper::merge_custom_array( + $custom_test_classes, + $known_test_classes + ); + + // Store the original value so the comparison can succeed. + $this->added_custom_test_classes = $this->custom_test_classes; + } + + return $this->all_test_classes; + } + + /** + * Check if a class token is part of a unit test suite. + * + * Unit test classes are identified as such: + * - Class which either extends one of the known test cases, such as `WP_UnitTestCase` + * or `PHPUnit_Framework_TestCase` or extends a custom unit test class as listed in the + * `custom_test_classes` property. + * + * @since 0.12.0 Split off from the `is_token_in_test_method()` method. + * @since 1.0.0 Improved recognition of namespaced class names. + * @since 3.0.0 - Moved from the Sniff class to this dedicated Trait. + * - The `$phpcsFile` parameter was added. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the token to be examined. + * This should be a class, anonymous class or trait token. + * + * @return bool True if the class is a unit test class, false otherwise. + */ + final protected function is_test_class( File $phpcsFile, $stackPtr ) { + + $tokens = $phpcsFile->getTokens(); + + if ( isset( $tokens[ $stackPtr ], Tokens::$ooScopeTokens[ $tokens[ $stackPtr ]['code'] ] ) === false ) { + return false; + } + + // Add any potentially extra custom test classes to the known test classes list. + $known_test_classes = $this->get_all_test_classes(); + + $namespace = strtolower( Namespaces::determineNamespace( $phpcsFile, $stackPtr ) ); + + // Is the class/trait one of the known test classes ? + $className = ObjectDeclarations::getName( $phpcsFile, $stackPtr ); + if ( empty( $className ) === false ) { + $className = strtolower( $className ); + if ( '' !== $namespace ) { + if ( isset( $known_test_classes[ $namespace . '\\' . $className ] ) ) { + return true; + } + } elseif ( isset( $known_test_classes[ $className ] ) ) { + return true; + } + } + + // Does the class/trait extend one of the known test classes ? + $extendedClassName = ObjectDeclarations::findExtendedClassName( $phpcsFile, $stackPtr ); + if ( false === $extendedClassName ) { + return false; + } + + $extendedClassName = strtolower( $extendedClassName ); + + if ( '\\' === $extendedClassName[0] ) { + if ( isset( $known_test_classes[ substr( $extendedClassName, 1 ) ] ) ) { + return true; + } + } elseif ( '' !== $namespace ) { + if ( isset( $known_test_classes[ $namespace . '\\' . $extendedClassName ] ) ) { + return true; + } + } elseif ( isset( $known_test_classes[ $extendedClassName ] ) ) { + return true; + } + + /* + * Not examining imported classes via `use` statements as with the variety of syntaxes, + * this would get very complicated. + * After all, users can add an `` for a particular sniff to their + * custom ruleset to selectively exclude the test directory. + */ + + return false; + } +} diff --git a/WordPress/Helpers/ListHelper.php b/WordPress/Helpers/ListHelper.php new file mode 100644 index 0000000000..aa1e22477f --- /dev/null +++ b/WordPress/Helpers/ListHelper.php @@ -0,0 +1,101 @@ +getTokens(); + + // Is this one of the tokens this function handles ? + if ( isset( $tokens[ $stackPtr ], Collections::listOpenTokensBC()[ $tokens[ $stackPtr ]['code'] ] ) === false ) { + return array(); + } + + if ( isset( Collections::shortArrayListOpenTokensBC()[ $tokens[ $stackPtr ]['code'] ] ) + && Lists::isShortList( $phpcsFile, $stackPtr ) === false + ) { + return array(); + } + + try { + $assignments = Lists::getAssignments( $phpcsFile, $stackPtr ); + } catch ( RuntimeException $e ) { + // Parse error/live coding. + return array(); + } + + $var_pointers = array(); + + foreach ( $assignments as $assign ) { + if ( true === $assign['is_empty'] ) { + continue; + } + + if ( true === $assign['is_nested_list'] ) { + /* + * Recurse into the nested list and get the variables. + * No need to `catch` any errors as only lists can be nested in lists. + */ + $var_pointers += self::get_list_variables( $phpcsFile, $assign['assignment_token'] ); + continue; + } + + /* + * Ok, so this must be a "normal" assignment in the list. + * Set the variable pointer both as the key as well as the value, so we can use array join + * for nested lists (above). + */ + $var_pointers[ $assign['assignment_token'] ] = $assign['assignment_token']; + } + + return $var_pointers; + } +} diff --git a/WordPress/Helpers/MinimumWPVersionTrait.php b/WordPress/Helpers/MinimumWPVersionTrait.php new file mode 100644 index 0000000000..92df2a4a6e --- /dev/null +++ b/WordPress/Helpers/MinimumWPVersionTrait.php @@ -0,0 +1,159 @@ + + * + * + * + * + * + * Alternatively, the value can be passed in one go for all sniffs using it via + * the command line or by setting a `` value in a custom phpcs.xml ruleset. + * + * CL: `phpcs --runtime-set minimum_wp_version 5.7` + * Ruleset: `` + * + * @since 0.14.0 Previously the individual sniffs each contained this property. + * @since 3.0.0 - Moved from the Sniff class to this dedicated Trait. + * - The property has been renamed from `$minimum_supported_version` to `$minimum_wp_version`. + * - The CLI option has been renamed from `minimum_supported_wp_version` to `minimum_wp_version`. + * + * @var string WordPress version. + */ + public $minimum_wp_version; + + /** + * Default minimum supported WordPress version. + * + * By default, the minimum_wp_version presumes that a project will support the current + * WP version and up to three releases before. + * + * {@internal This should be a constant, but constants in traits are not supported + * until PHP 8.2.}} + * + * @since 3.0.0 + * + * @var string WordPress version. + */ + private $default_minimum_wp_version = '6.0'; + + /** + * Overrule the minimum supported WordPress version with a command-line/config value. + * + * Handle setting the minimum supported WP version in one go for all sniffs which + * expect it via the command line or via a `` variable in a ruleset. + * The config variable overrules the default `$minimum_wp_version` and/or a + * `$minimum_wp_version` set for individual sniffs through the ruleset. + * + * @since 0.14.0 + * @since 3.0.0 - Moved from the Sniff class to this dedicated Trait. + * - Renamed from `get_wp_version_from_cl()` to `set_minimum_wp_version()`. + * + * @return void + */ + final protected function set_minimum_wp_version() { + $minimum_wp_version = ''; + + // Use a ruleset provided value if available. + if ( ! empty( $this->minimum_wp_version ) ) { + $minimum_wp_version = $this->minimum_wp_version; + } + + // A CLI provided value overrules a ruleset provided value. + $cli_supported_version = Helper::getConfigData( 'minimum_wp_version' ); + if ( ! empty( $cli_supported_version ) ) { + $minimum_wp_version = $cli_supported_version; + } + + // If no valid value was provided, use the default. + if ( filter_var( $minimum_wp_version, \FILTER_VALIDATE_FLOAT ) === false ) { + $minimum_wp_version = $this->default_minimum_wp_version; + } + + $this->minimum_wp_version = $minimum_wp_version; + } + + /** + * Compares two version numbers. + * + * @since 3.0.0 + * + * @param string $version1 First version number. + * @param string $version2 Second version number. + * @param string $operator Comparison operator. + * + * @return bool + */ + final protected function wp_version_compare( $version1, $version2, $operator ) { + $version1 = $this->normalize_version_number( $version1 ); + $version2 = $this->normalize_version_number( $version2 ); + + return version_compare( $version1, $version2, $operator ); + } + + /** + * Normalize a version number. + * + * Ensures that a version number is comparable via the PHP version_compare() function + * by making sure it complies with the minimum "PHP-standardized" version number requirements. + * + * Presumes the input is a numeric version number string. The behaviour with other input is undefined. + * + * @since 3.0.0 + * + * @param string $version Version number. + * + * @return string + */ + private function normalize_version_number( $version ) { + if ( preg_match( '`^\d+\.\d+$`', $version ) ) { + $version .= '.0'; + } + + return $version; + } +} diff --git a/WordPress/Helpers/PrintingFunctionsTrait.php b/WordPress/Helpers/PrintingFunctionsTrait.php new file mode 100644 index 0000000000..edd6dbe211 --- /dev/null +++ b/WordPress/Helpers/PrintingFunctionsTrait.php @@ -0,0 +1,122 @@ + + */ + private $printingFunctions = array( + '_deprecated_argument' => true, + '_deprecated_constructor' => true, + '_deprecated_file' => true, + '_deprecated_function' => true, + '_deprecated_hook' => true, + '_doing_it_wrong' => true, + '_e' => true, + '_ex' => true, + 'printf' => true, + 'trigger_error' => true, + 'user_error' => true, + 'vprintf' => true, + 'wp_die' => true, + 'wp_dropdown_pages' => true, + ); + + /** + * Cache of previously added custom functions. + * + * Prevents having to do the same merges over and over again. + * + * @since 0.4.0 + * @since 0.11.0 - Changed from public static to protected non-static. + * - Changed the format from simple bool to array. + * @since 3.0.0 - Moved from the EscapeOutput Sniff class to this trait. + * - Visibility changed from protected to private. + * + * @var string[] + */ + private $addedCustomPrintingFunctions = array(); + + /** + * Combined list of WP/PHP native and custom printing functions. + * + * @since 3.0.0 + * + * @var array + */ + private $allPrintingFunctions = array(); + + /** + * Retrieve a list of all known printing functions. + * + * @since 3.0.0 + * + * @return array + */ + final public function get_printing_functions() { + if ( array() === $this->allPrintingFunctions + || $this->customPrintingFunctions !== $this->addedCustomPrintingFunctions + ) { + $this->allPrintingFunctions = RulesetPropertyHelper::merge_custom_array( + $this->customPrintingFunctions, + $this->printingFunctions + ); + + $this->addedCustomPrintingFunctions = $this->customPrintingFunctions; + } + + return $this->allPrintingFunctions; + } + + /** + * Check if a particular function is regarded as a printing function. + * + * @since 3.0.0 + * + * @param string $functionName The name of the function to check. + * + * @return bool + */ + final public function is_printing_function( $functionName ) { + return isset( $this->get_printing_functions()[ strtolower( $functionName ) ] ); + } +} diff --git a/WordPress/Helpers/RulesetPropertyHelper.php b/WordPress/Helpers/RulesetPropertyHelper.php new file mode 100644 index 0000000000..b20c6ed3bf --- /dev/null +++ b/WordPress/Helpers/RulesetPropertyHelper.php @@ -0,0 +1,73 @@ + true` format. + * * Any custom items will be given the value `false` to be able to + * distinguish them from pre-set (base array) values. + * * Will filter previously added custom items out from the base array + * before merging/returning to allow for resetting to the base array. + * + * {@internal Function is static as it doesn't use any of the properties or others + * methods anyway.} + * + * @since 0.11.0 + * @since 2.0.0 No longer supports custom array properties which were incorrectly + * passed as a string. + * @since 3.0.0 Moved from the Sniff class to this class. + * + * @param array $custom Custom list as provided via a ruleset. + * @param array $base Optional. Base list. Defaults to an empty array. + * Expects `value => true` format when `$flip` is true. + * @param bool $flip Optional. Whether or not to flip the custom list. + * Defaults to true. + * @return array + */ + public static function merge_custom_array( $custom, array $base = array(), $flip = true ) { + if ( true === $flip ) { + $base = array_filter( $base ); + } + + if ( empty( $custom ) || ! \is_array( $custom ) ) { + return $base; + } + + if ( true === $flip ) { + $custom = array_fill_keys( $custom, false ); + } + + if ( empty( $base ) ) { + return $custom; + } + + return array_merge( $base, $custom ); + } +} diff --git a/WordPress/Helpers/SanitizationHelperTrait.php b/WordPress/Helpers/SanitizationHelperTrait.php new file mode 100644 index 0000000000..c041dfe5c1 --- /dev/null +++ b/WordPress/Helpers/SanitizationHelperTrait.php @@ -0,0 +1,417 @@ + + */ + private $sanitizingFunctions = array( + '_wp_handle_upload' => true, + 'esc_url_raw' => true, + 'filter_input' => true, + 'filter_var' => true, + 'hash_equals' => true, + 'is_email' => true, + 'number_format' => true, + 'sanitize_bookmark_field' => true, + 'sanitize_bookmark' => true, + 'sanitize_email' => true, + 'sanitize_file_name' => true, + 'sanitize_hex_color_no_hash' => true, + 'sanitize_hex_color' => true, + 'sanitize_html_class' => true, + 'sanitize_meta' => true, + 'sanitize_mime_type' => true, + 'sanitize_option' => true, + 'sanitize_sql_orderby' => true, + 'sanitize_term_field' => true, + 'sanitize_term' => true, + 'sanitize_text_field' => true, + 'sanitize_textarea_field' => true, + 'sanitize_title_for_query' => true, + 'sanitize_title_with_dashes' => true, + 'sanitize_title' => true, + 'sanitize_url' => true, + 'sanitize_user_field' => true, + 'sanitize_user' => true, + 'validate_file' => true, + 'wp_handle_sideload' => true, + 'wp_handle_upload' => true, + 'wp_kses_allowed_html' => true, + 'wp_kses_data' => true, + 'wp_kses_one_attr' => true, + 'wp_kses_post' => true, + 'wp_kses' => true, + 'wp_parse_id_list' => true, + 'wp_redirect' => true, + 'wp_safe_redirect' => true, + 'wp_sanitize_redirect' => true, + 'wp_strip_all_tags' => true, + ); + + /** + * Sanitizing functions that implicitly unslash the data passed to them. + * + * This list is complementary to the `$sanitizingFunctions` list. + * Sanitizing functions should be added to this list if they also + * implicitely unslash data and to the `$sanitizingFunctions` list + * if they don't. + * + * @since 0.5.0 + * @since 0.11.0 Changed from public static to protected non-static. + * @since 3.0.0 - Moved from the Sniff class to this trait. + * - Visibility changed from protected to private. + * + * @var array + */ + private $unslashingSanitizingFunctions = array( + 'absint' => true, + 'boolval' => true, + 'count' => true, + 'doubleval' => true, + 'floatval' => true, + 'intval' => true, + 'sanitize_key' => true, + 'sizeof' => true, + ); + + /** + * Cache of previously added custom functions. + * + * Prevents having to do the same merges over and over again. + * + * @since 0.4.0 + * @since 0.11.0 - Changed from public static to protected non-static. + * - Changed the format from simple bool to array. + * @since 3.0.0 - Moved from the NonceVerification and the ValidatedSanitizedInput sniff classes to this class. + * - Visibility changed from protected to private. + * + * @var array + */ + private $addedCustomSanitizingFunctions = array( + 'sanitize' => array(), + 'unslashsanitize' => array(), + ); + + /** + * Combined list of WP/PHP native and custom sanitizing functions. + * + * @since 3.0.0 + * + * @var array + */ + private $allSanitizingFunctions = array(); + + /** + * Combined list of WP/PHP native and custom sanitizing and unslashing functions. + * + * @since 3.0.0 + * + * @var array + */ + private $allUnslashingSanitizingFunctions = array(); + + /** + * Retrieve a list of all known sanitizing functions. + * + * @since 3.0.0 + * + * @return array + */ + final public function get_sanitizing_functions() { + if ( array() === $this->allSanitizingFunctions + || $this->customSanitizingFunctions !== $this->addedCustomSanitizingFunctions['sanitize'] + ) { + $this->allSanitizingFunctions = RulesetPropertyHelper::merge_custom_array( + $this->customSanitizingFunctions, + $this->sanitizingFunctions + ); + + $this->addedCustomSanitizingFunctions['sanitize'] = $this->customSanitizingFunctions; + } + + return $this->allSanitizingFunctions; + } + + /** + * Retrieve a list of all known sanitizing and unslashing functions. + * + * @since 3.0.0 + * + * @return array + */ + final public function get_sanitizing_and_unslashing_functions() { + if ( array() === $this->allUnslashingSanitizingFunctions + || $this->customUnslashingSanitizingFunctions !== $this->addedCustomSanitizingFunctions['unslashsanitize'] + ) { + $this->allUnslashingSanitizingFunctions = RulesetPropertyHelper::merge_custom_array( + $this->customUnslashingSanitizingFunctions, + $this->unslashingSanitizingFunctions + ); + + $this->addedCustomSanitizingFunctions['unslashsanitize'] = $this->customUnslashingSanitizingFunctions; + } + + return $this->allUnslashingSanitizingFunctions; + } + + /** + * Check if a particular function is regarded as a sanitizing function. + * + * @since 3.0.0 + * + * @param string $functionName The name of the function to check. + * + * @return bool + */ + final public function is_sanitizing_function( $functionName ) { + return isset( $this->get_sanitizing_functions()[ strtolower( $functionName ) ] ); + } + + /** + * Check if a particular function is regarded as a sanitizing and unslashing function. + * + * @since 3.0.0 + * + * @param string $functionName The name of the function to check. + * + * @return bool + */ + final public function is_sanitizing_and_unslashing_function( $functionName ) { + return isset( $this->get_sanitizing_and_unslashing_functions()[ strtolower( $functionName ) ] ); + } + + /** + * Check if something is only being sanitized. + * + * @since 0.5.0 + * @since 3.0.0 - Moved from the Sniff class to this class. + * - The method visibility was changed from `protected` to `public`. + * - The `$phpcsFile` parameter was added. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The index of the token in the stack. + * + * @return bool Whether the token is only within a sanitization. + */ + final public function is_only_sanitized( File $phpcsFile, $stackPtr ) { + $tokens = $phpcsFile->getTokens(); + + // If it isn't being sanitized at all. + if ( ! $this->is_sanitized( $phpcsFile, $stackPtr ) ) { + return false; + } + + // If the token isn't in parentheses, we know the value must have only been casted, because + // is_sanitized() would have returned `false` otherwise. + if ( ! isset( $tokens[ $stackPtr ]['nested_parenthesis'] ) ) { + return true; + } + + // At this point we're expecting the value to have not been casted. If it + // was, it wasn't *only* casted, because it's also in a function. + if ( ContextHelper::is_safe_casted( $phpcsFile, $stackPtr ) ) { + return false; + } + + // The only parentheses should belong to the sanitizing function. If there's + // more than one set, this isn't *only* sanitization. + return ( \count( $tokens[ $stackPtr ]['nested_parenthesis'] ) === 1 ); + } + + /** + * Check if something is being sanitized. + * + * @since 0.5.0 + * @since 3.0.0 - Moved from the Sniff class to this class. + * - The method visibility was changed from `protected` to `public`. + * - The `$phpcsFile` parameter was added. + * - The $require_unslash parameter has been changed from + * a boolean toggle to a ?callable $unslash_callback parameter to + * allow a sniff calling this method to handle their "unslashing" + * related messaging itself. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The index of the token in the stack. + * @param callable|null $unslash_callback Optional. When passed, this method will check if + * an unslashing function is used on the variable before + * sanitization and if not, the callback will be called + * to handle the missing unslashing. + * The callback will receive the $phpcsFile object and + * the $stackPtr. + * When not passed or `null`, this method will **not** + * check for unslashing issues. + * Defaults to `null` (skip unslashing checks). + * + * @return bool Whether the token is being sanitized. + */ + final public function is_sanitized( File $phpcsFile, $stackPtr, $unslash_callback = null ) { + $tokens = $phpcsFile->getTokens(); + $require_unslash = is_callable( $unslash_callback ); + + if ( isset( $tokens[ $stackPtr ] ) === false ) { + return false; + } + + // If the variable is just being unset, the value isn't used at all, so it's safe. + if ( Context::inUnset( $phpcsFile, $stackPtr ) ) { + return true; + } + + // First we check if it is being casted to a safe value. + if ( ContextHelper::is_safe_casted( $phpcsFile, $stackPtr ) ) { + return true; + } + + // If this isn't within a function call, we know already that it's not safe. + if ( ! isset( $tokens[ $stackPtr ]['nested_parenthesis'] ) ) { + if ( $require_unslash ) { + call_user_func( $unslash_callback, $phpcsFile, $stackPtr ); + } + + return false; + } + + $sanitizing_functions = $this->get_sanitizing_functions(); + $sanitizing_functions += $this->get_sanitizing_and_unslashing_functions(); + $sanitizing_functions += ArrayWalkingFunctionsHelper::get_functions(); + $valid_functions = $sanitizing_functions + UnslashingFunctionsHelper::get_functions(); + + // Get the function that it's in. + $functionPtr = ContextHelper::is_in_function_call( $phpcsFile, $stackPtr, $valid_functions ); + + // If this isn't a call to one of the valid functions, it sure isn't a sanitizing function. + if ( false === $functionPtr ) { + if ( true === $require_unslash ) { + call_user_func( $unslash_callback, $phpcsFile, $stackPtr ); + } + + return false; + } + + $functionName = $tokens[ $functionPtr ]['content']; + + // Check if an unslashing function is being used. + $is_unslashed = false; + if ( UnslashingFunctionsHelper::is_unslashing_function( $functionName ) ) { + $is_unslashed = true; + + // Check whether this function call is wrapped within a sanitizing function. + $higherFunctionPtr = ContextHelper::is_in_function_call( $phpcsFile, $functionPtr, $sanitizing_functions ); + + // If there is no other valid function being used, this value is unsanitized. + if ( false === $higherFunctionPtr ) { + return false; + } + + $functionPtr = $higherFunctionPtr; + $functionName = $tokens[ $functionPtr ]['content']; + } + + // Arrays might be sanitized via an array walking function using a callback. + if ( ArrayWalkingFunctionsHelper::is_array_walking_function( $functionName ) ) { + // Get the callback parameter. + $callback = ArrayWalkingFunctionsHelper::get_callback_parameter( $phpcsFile, $functionPtr ); + + if ( ! empty( $callback ) ) { + /* + * If this is a function callback (not a method callback array) and we're able + * to resolve the function name, do so. + */ + $first_non_empty = $phpcsFile->findNext( + Tokens::$emptyTokens, + $callback['start'], + ( $callback['end'] + 1 ), + true + ); + + if ( false !== $first_non_empty && \T_CONSTANT_ENCAPSED_STRING === $tokens[ $first_non_empty ]['code'] ) { + $functionName = TextStrings::stripQuotes( $tokens[ $first_non_empty ]['content'] ); + } + } + } + + // If slashing is required, give an error. + if ( false === $is_unslashed + && true === $require_unslash + && ! $this->is_sanitizing_and_unslashing_function( $functionName ) + ) { + call_user_func( $unslash_callback, $phpcsFile, $stackPtr ); + } + + // Check if this is a sanitizing function. + return ( $this->is_sanitizing_function( $functionName ) || $this->is_sanitizing_and_unslashing_function( $functionName ) ); + } +} diff --git a/WordPress/Helpers/SnakeCaseHelper.php b/WordPress/Helpers/SnakeCaseHelper.php new file mode 100644 index 0000000000..9937e5e22e --- /dev/null +++ b/WordPress/Helpers/SnakeCaseHelper.php @@ -0,0 +1,60 @@ + + */ + private static $unslashingFunctions = array( + 'stripslashes_deep' => true, + 'stripslashes_from_strings_only' => true, + 'wp_unslash' => true, + ); + + /** + * Retrieve a list of the unslashing functions. + * + * @since 3.0.0 + * + * @return array + */ + public static function get_functions() { + return self::$unslashingFunctions; + } + + /** + * Check if a particular function is regarded as a unslashing function. + * + * @since 3.0.0 + * + * @param string $functionName The name of the function to check. + * + * @return bool + */ + public static function is_unslashing_function( $functionName ) { + return isset( self::$unslashingFunctions[ strtolower( $functionName ) ] ); + } +} diff --git a/WordPress/Helpers/ValidationHelper.php b/WordPress/Helpers/ValidationHelper.php new file mode 100644 index 0000000000..d5a22d7d56 --- /dev/null +++ b/WordPress/Helpers/ValidationHelper.php @@ -0,0 +1,349 @@ + + */ + private static $targets = array( + \T_ISSET => 'construct', + \T_EMPTY => 'construct', + \T_STRING => 'function_call', + \T_COALESCE => 'coalesce', + \T_COALESCE_EQUAL => 'coalesce', + ); + + /** + * List of PHP native functions to check if an array index exists. + * + * @since 3.0.0 + * + * @var array + */ + private static $key_exists_functions = array( + 'array_key_exists' => true, + 'key_exists' => true, // Alias. + ); + + /** + * Check if the existence of a variable is validated with isset(), empty(), array_key_exists() + * or key_exists(). + * + * When $in_condition_only is `false`, (which is the default), this is considered + * valid: + * + * ```php + * if ( isset( $var ) ) { + * // Do stuff, like maybe return or exit (but could be anything) + * } + * + * foo( $var ); + * ``` + * + * When it is `true`, that would be invalid; the use of the variable must be within + * the scope of the validating condition, like this: + * + * ```php + * if ( isset( $var ) ) { + * foo( $var ); + * } + * ``` + * + * @since 0.5.0 + * @since 2.1.0 Now recognizes array_key_exists() and key_exists() as validation functions. + * @since 2.1.0 Stricter check on whether the correct variable and the correct + * array keys are being validated. + * @since 3.0.0 - Moved from the Sniff class to this class. + * - The method visibility was changed from `protected` to `public static`. + * - The `$phpcsFile` parameter was added. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The index of this token in the stack. + * @param array|string $array_keys An array key to check for ("bar" in $foo['bar']) + * or an array of keys for multi-level array access. + * @param bool $in_condition_only Whether to require that this use of the + * variable occurs within the scope of the + * validating condition, or just in the same + * scope (default). + * + * @return bool Whether the var is validated. + */ + public static function is_validated( File $phpcsFile, $stackPtr, $array_keys = array(), $in_condition_only = false ) { + $tokens = $phpcsFile->getTokens(); + if ( isset( $tokens[ $stackPtr ] ) === false ) { + return false; + } + + if ( $in_condition_only ) { + /* + * This is a stricter check, requiring the variable to be used only + * within the validation condition. + */ + $conditionPtr = Conditions::getLastCondition( $phpcsFile, $stackPtr ); + if ( false === $conditionPtr ) { + // If there are no conditions, there's no validation. + return false; + } + + $condition = $tokens[ $conditionPtr ]; + if ( ! isset( $condition['parenthesis_opener'] ) ) { + // Live coding or parse error. + return false; + } + + $scope_start = $condition['parenthesis_opener']; + $scope_end = $condition['parenthesis_closer']; + + } else { + /* + * We are more loose, requiring only that the variable be validated + * in the same function/file scope as it is used. + */ + $scope_start = 0; + + /* + * Check if we are in a function. + * + * Note: PHP 7.4+ arrow functions are not taken into account as those are not + * included in the "conditions" array. Additionally, arrow functions have + * access to variables outside their direct scope. + */ + $function = Conditions::getLastCondition( $phpcsFile, $stackPtr, array( \T_FUNCTION, \T_CLOSURE ) ); + + // If so, we check only within the function, otherwise the whole file. + if ( false !== $function ) { + $scope_start = $tokens[ $function ]['scope_opener']; + } + + $scope_end = $stackPtr; + } + + if ( ! empty( $array_keys ) && ! is_array( $array_keys ) ) { + $array_keys = (array) $array_keys; + } + + $bare_array_keys = self::strip_quotes_from_array_values( $array_keys ); + + // phpcs:ignore Generic.CodeAnalysis.JumbledIncrementer.Found -- On purpose, see below. + for ( $i = ( $scope_start + 1 ); $i < $scope_end; $i++ ) { + + if ( isset( Collections::closedScopes()[ $tokens[ $i ]['code'] ] ) + && isset( $tokens[ $i ]['scope_closer'] ) + ) { + // Jump over nested closed scopes as validation done within those does not apply. + $i = $tokens[ $i ]['scope_closer']; + continue; + } + + if ( \T_FN === $tokens[ $i ]['code'] + && isset( $tokens[ $i ]['scope_closer'] ) + && $tokens[ $i ]['scope_closer'] < $scope_end + ) { + // Jump over nested arrow functions as long as the current variable isn't *in* the arrow function. + $i = $tokens[ $i ]['scope_closer']; + continue; + } + + if ( isset( self::$targets[ $tokens[ $i ]['code'] ] ) === false ) { + continue; + } + + switch ( self::$targets[ $tokens[ $i ]['code'] ] ) { + case 'construct': + $issetOpener = $phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true ); + if ( false === $issetOpener + || \T_OPEN_PARENTHESIS !== $tokens[ $issetOpener ]['code'] + || isset( $tokens[ $issetOpener ]['parenthesis_closer'] ) === false + ) { + // Parse error or live coding. + continue 2; + } + + $issetCloser = $tokens[ $issetOpener ]['parenthesis_closer']; + + // Look for this variable. We purposely stomp $i from the parent loop. + for ( $i = ( $issetOpener + 1 ); $i < $issetCloser; $i++ ) { + + if ( \T_VARIABLE !== $tokens[ $i ]['code'] ) { + continue; + } + + if ( $tokens[ $stackPtr ]['content'] !== $tokens[ $i ]['content'] ) { + continue; + } + + // If we're checking for specific array keys (ex: 'hello' in + // $_POST['hello']), that must match too. Quote-style, however, doesn't matter. + if ( ! empty( $bare_array_keys ) ) { + $found_keys = VariableHelper::get_array_access_keys( $phpcsFile, $i ); + $found_keys = self::strip_quotes_from_array_values( $found_keys ); + $diff = array_diff_assoc( $bare_array_keys, $found_keys ); + if ( ! empty( $diff ) ) { + continue; + } + } + + return true; + } + + break; + + case 'function_call': + // Only check calls to array_key_exists() and key_exists(). + if ( isset( self::$key_exists_functions[ strtolower( $tokens[ $i ]['content'] ) ] ) === false ) { + continue 2; + } + + $next_non_empty = $phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true ); + if ( false === $next_non_empty || \T_OPEN_PARENTHESIS !== $tokens[ $next_non_empty ]['code'] ) { + // Not a function call. + continue 2; + } + + if ( Context::inAttribute( $phpcsFile, $i ) === true ) { + // Definitely not the function call as those are not allowed in attributes. + continue 2; + } + + if ( ContextHelper::has_object_operator_before( $phpcsFile, $i ) === true ) { + // Method call. + continue 2; + } + + if ( ContextHelper::is_token_namespaced( $phpcsFile, $i ) === true ) { + // Namespaced function call. + continue 2; + } + + $params = PassedParameters::getParameters( $phpcsFile, $i ); + + // As `key_exists()` is an alias of `array_key_exists()`, the param positions and names are the same. + $array_param = PassedParameters::getParameterFromStack( $params, 2, 'array' ); + if ( false === $array_param ) { + continue 2; + } + + $array_param_first_token = $phpcsFile->findNext( Tokens::$emptyTokens, $array_param['start'], ( $array_param['end'] + 1 ), true ); + if ( false === $array_param_first_token + || \T_VARIABLE !== $tokens[ $array_param_first_token ]['code'] + || $tokens[ $array_param_first_token ]['content'] !== $tokens[ $stackPtr ]['content'] + ) { + continue 2; + } + + if ( ! empty( $bare_array_keys ) ) { + // Prevent the original array from being altered. + $bare_keys = $bare_array_keys; + $last_key = array_pop( $bare_keys ); + + /* + * For multi-level array access, the complete set of keys could be split between + * the $key and the $array parameter, but could also be completely in the $array + * parameter, so we need to check both options. + */ + $found_keys = VariableHelper::get_array_access_keys( $phpcsFile, $array_param_first_token ); + $found_keys = self::strip_quotes_from_array_values( $found_keys ); + + // First try matching the complete set against the array parameter. + $diff = array_diff_assoc( $bare_array_keys, $found_keys ); + if ( empty( $diff ) ) { + return true; + } + + // If that failed, try getting an exact match for the subset against the + // $array parameter and the last key against the first. + $key_param = PassedParameters::getParameterFromStack( $params, 1, 'key' ); + if ( false !== $key_param + && $bare_keys === $found_keys + && TextStrings::stripQuotes( $key_param['raw'] ) === $last_key + ) { + return true; + } + + // Didn't find the correct array keys. + continue 2; + } + + return true; + + case 'coalesce': + $prev = $i; + do { + $prev = $phpcsFile->findPrevious( Tokens::$emptyTokens, ( $prev - 1 ), null, true ); + // Skip over array keys, like `$_GET['key']['subkey']`. + if ( \T_CLOSE_SQUARE_BRACKET === $tokens[ $prev ]['code'] ) { + $prev = $tokens[ $prev ]['bracket_opener']; + continue; + } + + break; + } while ( $prev >= ( $scope_start + 1 ) ); + + // We should now have reached the variable. + if ( \T_VARIABLE !== $tokens[ $prev ]['code'] ) { + continue 2; + } + + if ( $tokens[ $prev ]['content'] !== $tokens[ $stackPtr ]['content'] ) { + continue 2; + } + + if ( ! empty( $bare_array_keys ) ) { + $found_keys = VariableHelper::get_array_access_keys( $phpcsFile, $prev ); + $found_keys = self::strip_quotes_from_array_values( $found_keys ); + $diff = array_diff_assoc( $bare_array_keys, $found_keys ); + if ( ! empty( $diff ) ) { + continue 2; + } + } + + // Right variable, correct key. + return true; + } + } + + return false; + } + + /** + * Strip quotes of all the values in an array containing only text strings. + * + * @since 3.0.0 + * + * @param string[] $text_strings The input array. + * + * @return string[] + */ + private static function strip_quotes_from_array_values( array $text_strings ) { + return array_map( array( 'PHPCSUtils\Utils\TextStrings', 'stripQuotes' ), $text_strings ); + } +} diff --git a/WordPress/Helpers/VariableHelper.php b/WordPress/Helpers/VariableHelper.php new file mode 100644 index 0000000000..e1076ee42c --- /dev/null +++ b/WordPress/Helpers/VariableHelper.php @@ -0,0 +1,262 @@ +getTokens(); + $keys = array(); + + if ( isset( $tokens[ $stackPtr ] ) === false + || \T_VARIABLE !== $tokens[ $stackPtr ]['code'] + ) { + return $keys; + } + + $current = $stackPtr; + + do { + // Find the next non-empty token. + $open_bracket = $phpcsFile->findNext( + Tokens::$emptyTokens, + ( $current + 1 ), + null, + true + ); + + // If it isn't a bracket, this isn't an array-access. + if ( false === $open_bracket + || \T_OPEN_SQUARE_BRACKET !== $tokens[ $open_bracket ]['code'] + || ! isset( $tokens[ $open_bracket ]['bracket_closer'] ) + ) { + break; + } + + $key = GetTokensAsString::compact( + $phpcsFile, + ( $open_bracket + 1 ), + ( $tokens[ $open_bracket ]['bracket_closer'] - 1 ), + true + ); + + $keys[] = trim( $key ); + $current = $tokens[ $open_bracket ]['bracket_closer']; + } while ( isset( $tokens[ $current ] ) && true === $all ); + + return $keys; + } + + /** + * Get the index key of an array variable. + * + * E.g., "bar" in $foo['bar']. + * + * @since 0.5.0 + * @since 2.1.0 Now uses get_array_access_keys() under the hood. + * @since 3.0.0 - Moved from the Sniff class to this class. + * - Visibility is now `public` (was `protected`) and the method `static`. + * - The `$phpcsFile` parameter was added. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The index of the variable token in the stack. + * + * @return string|false The array index key whose value is being accessed. + */ + public static function get_array_access_key( File $phpcsFile, $stackPtr ) { + $keys = self::get_array_access_keys( $phpcsFile, $stackPtr, false ); + if ( isset( $keys[0] ) ) { + return $keys[0]; + } + + return false; + } + + /** + * Check whether a variable is being compared to another value. + * + * E.g., $var === 'foo', 1 <= $var, etc. + * + * Also recognizes `switch ( $var )` and `match ( $var )`. + * + * @since 0.5.0 + * @since 2.1.0 Added the $include_coalesce parameter. + * @since 3.0.0 - Moved from the Sniff class to this class. + * - Visibility is now `public` (was `protected`) and the method `static`. + * - The `$phpcsFile` parameter was added. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The index of this token in the stack. + * @param bool $include_coalesce Optional. Whether or not to regard the null + * coalesce operator - ?? - as a comparison operator. + * Defaults to true. + * Null coalesce is a special comparison operator in this + * sense as it doesn't compare a variable to whatever is + * on the other side of the comparison operator. + * + * @return bool Whether this is a comparison. + */ + public static function is_comparison( File $phpcsFile, $stackPtr, $include_coalesce = true ) { + $tokens = $phpcsFile->getTokens(); + if ( isset( $tokens[ $stackPtr ] ) === false ) { + return false; + } + + $comparisonTokens = Tokens::$comparisonTokens; + if ( false === $include_coalesce ) { + unset( $comparisonTokens[ \T_COALESCE ] ); + } + + // We first check if this is a switch or match statement (switch ( $var )). + if ( Parentheses::lastOwnerIn( $phpcsFile, $stackPtr, array( \T_SWITCH, \T_MATCH ) ) !== false ) { + return true; + } + + // Find the previous non-empty token. We check before the var first because + // yoda conditions are usually expected. + $previous_token = $phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); + + if ( isset( $comparisonTokens[ $tokens[ $previous_token ]['code'] ] ) ) { + return true; + } + + // Maybe the comparison operator is after this. + $next_token = $phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); + + // This might be an opening square bracket in the case of arrays ($var['a']). + while ( false !== $next_token + && \T_OPEN_SQUARE_BRACKET === $tokens[ $next_token ]['code'] + && isset( $tokens[ $next_token ]['bracket_closer'] ) + ) { + $next_token = $phpcsFile->findNext( + Tokens::$emptyTokens, + ( $tokens[ $next_token ]['bracket_closer'] + 1 ), + null, + true + ); + } + + if ( false !== $next_token && isset( $comparisonTokens[ $tokens[ $next_token ]['code'] ] ) ) { + return true; + } + + return false; + } + + /** + * Check if this variable is being assigned a value. + * + * E.g., $var = 'foo'; + * + * Also handles array assignments to arbitrary depth: + * + * $array['key'][ $foo ][ something() ] = $bar; + * + * @since 0.5.0 + * @since 3.0.0 - Moved from the Sniff class to this class. + * - Visibility is now `public` (was `protected`) and the method `static`. + * - The `$phpcsFile` parameter was added. + * - The `$include_coalesce` parameter was added. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The index of the token in the stack. + * This must point to either a T_VARIABLE or + * T_CLOSE_SQUARE_BRACKET token. + * @param bool $include_coalesce Optional. Whether or not to regard the null + * coalesce operator - ?? - as a comparison operator. + * Defaults to true. + * Null coalesce is a special comparison operator in this + * sense as it doesn't compare a variable to whatever is + * on the other side of the comparison operator. + * + * @return bool Whether the token is a variable being assigned a value. + */ + public static function is_assignment( File $phpcsFile, $stackPtr, $include_coalesce = true ) { + $tokens = $phpcsFile->getTokens(); + if ( isset( $tokens[ $stackPtr ] ) === false ) { + return false; + } + + static $valid = array( + \T_VARIABLE => true, + \T_CLOSE_SQUARE_BRACKET => true, + ); + + // Must be a variable or closing square bracket (see below). + if ( ! isset( $valid[ $tokens[ $stackPtr ]['code'] ] ) ) { + return false; + } + + $next_non_empty = $phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true, null, true ); + + // No token found. + if ( false === $next_non_empty ) { + return false; + } + + $assignmentTokens = Tokens::$assignmentTokens; + if ( false === $include_coalesce ) { + unset( $assignmentTokens[ \T_COALESCE_EQUAL ] ); + } + + // If the next token is an assignment, that's all we need to know. + if ( isset( $assignmentTokens[ $tokens[ $next_non_empty ]['code'] ] ) ) { + return true; + } + + // Check if this is an array assignment, e.g., `$var['key'] = 'val';` . + if ( \T_OPEN_SQUARE_BRACKET === $tokens[ $next_non_empty ]['code'] + && isset( $tokens[ $next_non_empty ]['bracket_closer'] ) + ) { + return self::is_assignment( $phpcsFile, $tokens[ $next_non_empty ]['bracket_closer'], $include_coalesce ); + } + + return false; + } +} diff --git a/WordPress/Helpers/WPDBTrait.php b/WordPress/Helpers/WPDBTrait.php new file mode 100644 index 0000000000..1f325effc0 --- /dev/null +++ b/WordPress/Helpers/WPDBTrait.php @@ -0,0 +1,115 @@ +getTokens(); + if ( isset( $tokens[ $stackPtr ] ) === false ) { + return false; + } + + // Check for wpdb. + if ( ( \T_VARIABLE === $tokens[ $stackPtr ]['code'] && '$wpdb' !== $tokens[ $stackPtr ]['content'] ) + || ( \T_STRING === $tokens[ $stackPtr ]['code'] && 'wpdb' !== strtolower( $tokens[ $stackPtr ]['content'] ) ) + ) { + return false; + } + + // Check that this is a method call. + $is_object_call = $phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); + if ( false === $is_object_call + || isset( Collections::objectOperators()[ $tokens[ $is_object_call ]['code'] ] ) === false + ) { + return false; + } + + $methodPtr = $phpcsFile->findNext( Tokens::$emptyTokens, ( $is_object_call + 1 ), null, true, null, true ); + if ( false === $methodPtr ) { + return false; + } + + if ( \T_STRING === $tokens[ $methodPtr ]['code'] && property_exists( $this, 'methodPtr' ) ) { + $this->methodPtr = $methodPtr; + } + + // Find the opening parenthesis. + $opening_paren = $phpcsFile->findNext( Tokens::$emptyTokens, ( $methodPtr + 1 ), null, true, null, true ); + + if ( false === $opening_paren ) { + return false; + } + + if ( property_exists( $this, 'i' ) ) { + $this->i = $opening_paren; + } + + if ( \T_OPEN_PARENTHESIS !== $tokens[ $opening_paren ]['code'] + || ! isset( $tokens[ $opening_paren ]['parenthesis_closer'] ) + ) { + return false; + } + + // Check that this is one of the methods that we are interested in. + if ( ! isset( $target_methods[ strtolower( $tokens[ $methodPtr ]['content'] ) ] ) ) { + return false; + } + + // Find the end of the first parameter. + $end = BCFile::findEndOfStatement( $phpcsFile, $opening_paren + 1 ); + + if ( \T_COMMA !== $tokens[ $end ]['code'] ) { + ++$end; + } + + if ( property_exists( $this, 'end' ) ) { + $this->end = $end; + } + + return true; + } +} diff --git a/WordPress/Helpers/WPGlobalVariablesHelper.php b/WordPress/Helpers/WPGlobalVariablesHelper.php new file mode 100644 index 0000000000..b509793164 --- /dev/null +++ b/WordPress/Helpers/WPGlobalVariablesHelper.php @@ -0,0 +1,312 @@ + The key is the name of a WP global variable, the value is irrelevant. + */ + private static $wp_globals = array( + '_links_add_base' => true, + '_links_add_target' => true, + '_menu_item_sort_prop' => true, + '_nav_menu_placeholder' => true, + '_new_bundled_files' => true, + '_old_files' => true, + '_parent_pages' => true, + '_registered_pages' => true, + '_updated_user_settings' => true, + '_wp_additional_image_sizes' => true, + '_wp_admin_css_colors' => true, + '_wp_default_headers' => true, + '_wp_deprecated_widgets_callbacks' => true, + '_wp_last_object_menu' => true, + '_wp_last_utility_menu' => true, + '_wp_menu_nopriv' => true, + '_wp_nav_menu_max_depth' => true, + '_wp_post_type_features' => true, + '_wp_real_parent_file' => true, + '_wp_registered_nav_menus' => true, + '_wp_sidebars_widgets' => true, + '_wp_submenu_nopriv' => true, + '_wp_suspend_cache_invalidation' => true, + '_wp_theme_features' => true, + '_wp_using_ext_object_cache' => true, + 'action' => true, + 'active_signup' => true, + 'admin_body_class' => true, + 'admin_page_hooks' => true, + 'all_links' => true, + 'allowedentitynames' => true, + 'allowedposttags' => true, + 'allowedtags' => true, + 'auth_secure_cookie' => true, + 'authordata' => true, + 'avail_post_mime_types' => true, + 'avail_post_stati' => true, + 'blog_id' => true, + 'blog_title' => true, + 'blogname' => true, + 'cat' => true, + 'cat_id' => true, + 'charset_collate' => true, + 'comment' => true, + 'comment_alt' => true, + 'comment_depth' => true, + 'comment_status' => true, + 'comment_thread_alt' => true, + 'comment_type' => true, + 'comments' => true, + 'compress_css' => true, + 'compress_scripts' => true, + 'concatenate_scripts' => true, + 'content_width' => true, + 'current_blog' => true, + 'current_screen' => true, + 'current_site' => true, + 'current_user' => true, + 'currentcat' => true, + 'currentday' => true, + 'currentmonth' => true, + 'custom_background' => true, + 'custom_image_header' => true, + 'default_menu_order' => true, + 'descriptions' => true, + 'domain' => true, + 'editor_styles' => true, + 'error' => true, + 'errors' => true, + 'EZSQL_ERROR' => true, + 'feeds' => true, + 'GETID3_ERRORARRAY' => true, + 'hook_suffix' => true, + 'HTTP_RAW_POST_DATA' => true, + 'id' => true, + 'in_comment_loop' => true, + 'interim_login' => true, + 'is_apache' => true, + 'is_chrome' => true, + 'is_gecko' => true, + 'is_IE' => true, + 'is_IIS' => true, + 'is_iis7' => true, + 'is_macIE' => true, + 'is_NS4' => true, + 'is_opera' => true, + 'is_safari' => true, + 'is_winIE' => true, + 'l10n' => true, + 'link' => true, + 'link_id' => true, + 'locale' => true, + 'locked_post_status' => true, + 'lost' => true, + 'm' => true, + 'map' => true, + 'menu' => true, + 'menu_order' => true, + 'merged_filters' => true, + 'mode' => true, + 'monthnum' => true, + 'more' => true, + 'mu_plugin' => true, + 'multipage' => true, + 'names' => true, + 'nav_menu_selected_id' => true, + 'network_plugin' => true, + 'new_whitelist_options' => true, + 'numpages' => true, + 'one_theme_location_no_menus' => true, + 'opml' => true, + 'order' => true, + 'orderby' => true, + 'overridden_cpage' => true, + 'page' => true, + 'paged' => true, + 'pagenow' => true, + 'pages' => true, + 'parent_file' => true, + 'pass_allowed_html' => true, + 'pass_allowed_protocols' => true, + 'path' => true, + 'per_page' => true, + 'PHP_SELF' => true, + 'phpmailer' => true, + 'plugin_page' => true, + 'plugin' => true, + 'plugins' => true, + 'post' => true, + 'post_default_category' => true, + 'post_default_title' => true, + 'post_ID' => true, + 'post_id' => true, + 'post_mime_types' => true, + 'post_type' => true, + 'post_type_object' => true, + 'posts' => true, + 'preview' => true, + 'previouscat' => true, + 'previousday' => true, + 'previousweekday' => true, + 'redir_tab' => true, + 'required_mysql_version' => true, + 'required_php_version' => true, + 'rnd_value' => true, + 'role' => true, + 's' => true, + 'search' => true, + 'self' => true, + 'shortcode_tags' => true, + 'show_admin_bar' => true, + 'sidebars_widgets' => true, + 'status' => true, + 'submenu' => true, + 'submenu_file' => true, + 'super_admins' => true, + 'tab' => true, + 'table_prefix' => true, + 'tabs' => true, + 'tag' => true, + 'tag_ID' => true, + 'targets' => true, + 'tax' => true, + 'taxnow' => true, + 'taxonomy' => true, + 'term' => true, + 'text_direction' => true, + 'theme_field_defaults' => true, + 'themes_allowedtags' => true, + 'timeend' => true, + 'timestart' => true, + 'tinymce_version' => true, + 'title' => true, + 'totals' => true, + 'type' => true, + 'typenow' => true, + 'updated_timestamp' => true, + 'upgrading' => true, + 'urls' => true, + 'user_email' => true, + 'user_ID' => true, + 'user_identity' => true, + 'user_level' => true, + 'user_login' => true, + 'user_url' => true, + 'userdata' => true, + 'usersearch' => true, + 'whitelist_options' => true, + 'withcomments' => true, + 'wp' => true, + 'wp_actions' => true, + 'wp_admin_bar' => true, + 'wp_cockneyreplace' => true, + 'wp_current_db_version' => true, + 'wp_current_filter' => true, + 'wp_customize' => true, + 'wp_dashboard_control_callbacks' => true, + 'wp_db_version' => true, + 'wp_did_header' => true, + 'wp_embed' => true, + 'wp_file_descriptions' => true, + 'wp_filesystem' => true, + 'wp_filter' => true, + 'wp_hasher' => true, + 'wp_header_to_desc' => true, + 'wp_importers' => true, + 'wp_json' => true, + 'wp_list_table' => true, + 'wp_local_package' => true, + 'wp_locale' => true, + 'wp_meta_boxes' => true, + 'wp_object_cache' => true, + 'wp_plugin_paths' => true, + 'wp_post_statuses' => true, + 'wp_post_types' => true, + 'wp_queries' => true, + 'wp_query' => true, + 'wp_registered_sidebars' => true, + 'wp_registered_widget_controls' => true, + 'wp_registered_widget_updates' => true, + 'wp_registered_widgets' => true, + 'wp_rewrite' => true, + 'wp_rich_edit' => true, + 'wp_rich_edit_exists' => true, + 'wp_roles' => true, + 'wp_scripts' => true, + 'wp_settings_errors' => true, + 'wp_settings_fields' => true, + 'wp_settings_sections' => true, + 'wp_smiliessearch' => true, + 'wp_styles' => true, + 'wp_taxonomies' => true, + 'wp_the_query' => true, + 'wp_theme_directories' => true, + 'wp_themes' => true, + 'wp_user_roles' => true, + 'wp_version' => true, + 'wp_widget_factory' => true, + 'wp_xmlrpc_server' => true, + 'wpcommentsjavascript' => true, + 'wpcommentspopupfile' => true, + 'wpdb' => true, + 'wpsmiliestrans' => true, + 'year' => true, + ); + + /** + * Retrieve a list with the names of global WP variables. + * + * @since 3.0.0 + * + * @return array Array with the variables names as keys. The value is irrelevant. + */ + public static function get_names() { + return self::$wp_globals; + } + + /** + * Verify if a given variable name is the name of a WP global variable. + * + * @since 3.0.0 + * + * @param string $name The full variable name with or without leading dollar sign. + * This allows for passing an array key variable name, such as + * `'_GET'` retrieved from `$GLOBALS['_GET']`. + * > Note: when passing an array key, string quotes are expected + * to have been stripped already. + * + * @return bool + */ + public static function is_wp_global( $name ) { + if ( strpos( $name, '$' ) === 0 ) { + $name = substr( $name, 1 ); + } + + return isset( self::$wp_globals[ $name ] ); + } +} diff --git a/WordPress/Helpers/WPHookHelper.php b/WordPress/Helpers/WPHookHelper.php new file mode 100644 index 0000000000..b5b4f4a133 --- /dev/null +++ b/WordPress/Helpers/WPHookHelper.php @@ -0,0 +1,113 @@ +> Function name as key, array with target + * parameter position and name(s) as value. + */ + private static $hookInvokeFunctions = array( + 'do_action' => array( + 'position' => 1, + 'name' => 'hook_name', + ), + 'do_action_ref_array' => array( + 'position' => 1, + 'name' => 'hook_name', + ), + 'do_action_deprecated' => array( + 'position' => 1, + 'name' => 'hook_name', + ), + 'apply_filters' => array( + 'position' => 1, + 'name' => 'hook_name', + ), + 'apply_filters_ref_array' => array( + 'position' => 1, + 'name' => 'hook_name', + ), + 'apply_filters_deprecated' => array( + 'position' => 1, + 'name' => 'hook_name', + ), + ); + + /** + * Retrieve a list of the WordPress functions which invoke hooks. + * + * @since 3.0.0 + * + * @param bool $include_deprecated Whether to include the names of functions + * which are used to invoke deprecated hooks. + * Defaults to `true`. + * + * @return array Array with the function names as keys. The value is irrelevant. + */ + public static function get_functions( $include_deprecated = true ) { + $hooks = array_fill_keys( array_keys( self::$hookInvokeFunctions ), true ); + if ( false === $include_deprecated ) { + unset( + $hooks['do_action_deprecated'], + $hooks['apply_filters_deprecated'] + ); + } + + return $hooks; + } + + /** + * Retrieve the parameter information for the hook name parameter from a stack of parameters + * passed to one of the WP hook functions. + * + * @since 3.0.0 + * + * @param string $function_name The name of the WP hook function which the parameters were passed to. + * @param array $parameters The output of a previous call to PassedParameters::getParameters(). + * + * @return array|false Array with information on the parameter at the specified offset, + * or with the specified name. + * Or `FALSE` if the specified parameter is not found. + * See the PHPCSUtils PassedParameters::getParameters() documentation + * for the format of the returned (single-dimensional) array. + */ + public static function get_hook_name_param( $function_name, array $parameters ) { + $function_lc = strtolower( $function_name ); + if ( isset( self::$hookInvokeFunctions[ $function_lc ] ) === false ) { + return false; + } + + return PassedParameters::getParameterFromStack( + $parameters, + self::$hookInvokeFunctions[ $function_lc ]['position'], + self::$hookInvokeFunctions[ $function_lc ]['name'] + ); + } +} diff --git a/WordPress/PHPCSAliases.php b/WordPress/PHPCSAliases.php deleted file mode 100644 index f82bac41a1..0000000000 --- a/WordPress/PHPCSAliases.php +++ /dev/null @@ -1,83 +0,0 @@ -` ruleset directive. - * - * {@internal The PHPCS files have been reorganized in PHPCS 3.x, quite - * a few "old" classes have been split and spread out over several "new" - * classes. In other words, this will only work for a limited number - * of classes.}} - * - * {@internal The `class_exists` wrappers are needed to play nice with other - * external PHPCS standards creating cross-version compatibility in the same - * manner.}} - */ -if ( ! \defined( 'WPCS_PHPCS_ALIASES_SET' ) ) { - // PHPCS base classes/interface. - if ( ! interface_exists( '\PHP_CodeSniffer_Sniff' ) ) { - class_alias( 'PHP_CodeSniffer\Sniffs\Sniff', '\PHP_CodeSniffer_Sniff' ); - } - if ( ! class_exists( '\PHP_CodeSniffer_File' ) ) { - class_alias( 'PHP_CodeSniffer\Files\File', '\PHP_CodeSniffer_File' ); - } - if ( ! class_exists( '\PHP_CodeSniffer_Tokens' ) ) { - class_alias( 'PHP_CodeSniffer\Util\Tokens', '\PHP_CodeSniffer_Tokens' ); - } - - // PHPCS classes which are being extended by WPCS sniffs. - if ( ! class_exists( '\PHP_CodeSniffer_Standards_AbstractVariableSniff' ) ) { - class_alias( 'PHP_CodeSniffer\Sniffs\AbstractVariableSniff', '\PHP_CodeSniffer_Standards_AbstractVariableSniff' ); - } - if ( ! class_exists( '\PEAR_Sniffs_NamingConventions_ValidFunctionNameSniff' ) ) { - class_alias( 'PHP_CodeSniffer\Standards\PEAR\Sniffs\NamingConventions\ValidFunctionNameSniff', '\PEAR_Sniffs_NamingConventions_ValidFunctionNameSniff' ); - } - if ( ! class_exists( '\Squiz_Sniffs_WhiteSpace_OperatorSpacingSniff' ) ) { - class_alias( 'PHP_CodeSniffer\Standards\Squiz\Sniffs\WhiteSpace\OperatorSpacingSniff', '\Squiz_Sniffs_WhiteSpace_OperatorSpacingSniff' ); - } - if ( ! class_exists( '\Squiz_Sniffs_WhiteSpace_SemicolonSpacingSniff' ) ) { - class_alias( 'PHP_CodeSniffer\Standards\Squiz\Sniffs\WhiteSpace\SemicolonSpacingSniff', '\Squiz_Sniffs_WhiteSpace_SemicolonSpacingSniff' ); - } - - define( 'WPCS_PHPCS_ALIASES_SET', true ); - - /* - * Register our own autoloader for the WPCS abstract classes & the helper class. - * - * This can be removed once the minimum required version of WPCS for the - * PHPCS 3.x branch has gone up to 3.1.0 (unreleased as of yet) or - * whichever version contains the fix for upstream #1591. - * - * @link https://github.com/squizlabs/PHP_CodeSniffer/issues/1564 - * @link https://github.com/squizlabs/PHP_CodeSniffer/issues/1591 - */ - spl_autoload_register( - function ( $class ) { - // Only try & load our own classes. - if ( stripos( $class, 'WordPress' ) !== 0 ) { - return; - } - - // PHPCS handles the Test and Sniff classes without problem. - if ( stripos( $class, '\Tests\\' ) !== false || stripos( $class, '\Sniffs\\' ) !== false ) { - return; - } - - $file = dirname( __DIR__ ) . DIRECTORY_SEPARATOR . strtr( $class, '\\', DIRECTORY_SEPARATOR ) . '.php'; - - if ( file_exists( $file ) ) { - include_once $file; - } - } - ); -} diff --git a/WordPress/PHPCSHelper.php b/WordPress/PHPCSHelper.php deleted file mode 100644 index 7f20081559..0000000000 --- a/WordPress/PHPCSHelper.php +++ /dev/null @@ -1,141 +0,0 @@ -config->tabWidth ) && $phpcsFile->config->tabWidth > 0 ) { - $tab_width = $phpcsFile->config->tabWidth; - } - } else { - // PHPCS 2.x. - $cli_values = $phpcsFile->phpcs->cli->getCommandLineValues(); - if ( isset( $cli_values['tabWidth'] ) && $cli_values['tabWidth'] > 0 ) { - $tab_width = $cli_values['tabWidth']; - } - } - - return $tab_width; - } - - /** - * Check whether the `--ignore-annotations` option has been used. - * - * @since 0.13.0 - * - * @param \PHP_CodeSniffer\Files\File $phpcsFile Optional. The current file being processed. - * - * @return bool True if annotations should be ignored, false otherwise. - */ - public static function ignore_annotations( File $phpcsFile = null ) { - if ( class_exists( '\PHP_CodeSniffer\Config' ) ) { - // PHPCS 3.x. - if ( isset( $phpcsFile, $phpcsFile->config->annotations ) ) { - return ! $phpcsFile->config->annotations; - } else { - $annotations = \PHP_CodeSniffer\Config::getConfigData( 'annotations' ); - if ( isset( $annotations ) ) { - return ! $annotations; - } - } - } - - // PHPCS 2.x does not support `--ignore-annotations`. - return false; - } - -} diff --git a/WordPress/Sniff.php b/WordPress/Sniff.php index 07a0f4693d..eaa7c22b55 100644 --- a/WordPress/Sniff.php +++ b/WordPress/Sniff.php @@ -3,884 +3,24 @@ * Represents a PHP_CodeSniffer sniff for sniffing WordPress coding standards. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress; +namespace WordPressCS\WordPress; -use PHP_CodeSniffer_Sniff as PHPCS_Sniff; -use PHP_CodeSniffer_File as File; -use PHP_CodeSniffer_Tokens as Tokens; -use WordPress\PHPCSHelper; +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff as PHPCS_Sniff; /** * Represents a PHP_CodeSniffer sniff for sniffing WordPress coding standards. * * Provides a bootstrap for the sniffs, to reduce code duplication. * - * @package WPCS\WordPressCodingStandards - * @since 0.4.0 - * - * {@internal This class contains numerous properties where the array format looks - * like `'string' => true`, i.e. the array item is set as the array key. - * This allows for sniffs to verify whether something is in one of these - * lists using `isset()` rather than `in_array()` which is a much more - * efficient (faster) check to execute and therefore improves the - * performance of the sniffs. - * The `true` value in those cases is used as a placeholder and has no - * meaning in and of itself. - * In the rare few cases where the array values *do* have meaning, this - * is documented in the property documentation.}} + * @since 0.4.0 */ abstract class Sniff implements PHPCS_Sniff { - /** - * Regex to get complex variables from T_DOUBLE_QUOTED_STRING or T_HEREDOC. - * - * @since 0.14.0 - * - * @var string - */ - const REGEX_COMPLEX_VARS = '`(?:(\{)?(?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)(?:->\$?(?P>varname)|\[[^\]]+\]|::\$?(?P>varname)|\([^\)]*\))*(?(3)\}|)(?(2)\}|)(?(1)\}|)`'; - - /** - * Minimum supported WordPress version. - * - * Currently used by the `WordPress.WP.AlternativeFunctions`, - * `WordPress.WP.DeprecatedClasses`, `WordPress.WP.DeprecatedFunctions` - * and the `WordPress.WP.DeprecatedParameter` sniff. - * - * These sniffs will throw an error when usage of a deprecated class/function/parameter - * is detected if the class/function/parameter was deprecated before the minimum - * supported WP version; a warning otherwise. - * By default, it is set to presume that a project will support the current - * WP version and up to three releases before. - * - * This property allows changing the minimum supported WP version used by - * these sniffs by setting a property in a custom phpcs.xml ruleset. - * This property will need to be set for each sniff which uses it. - * - * Example usage: - * - * - * - * - * - * - * Alternatively, the value can be passed in one go for all sniff using it via - * the command line or by setting a `` value in a custom phpcs.xml ruleset. - * Note: the `_wp_` in the command line property name! - * - * CL: `phpcs --runtime-set minimum_supported_wp_version 4.5` - * Ruleset: `` - * - * @since 0.14.0 Previously the individual sniffs each contained this property. - * - * @internal When the value of this property is changed, it will also need - * to be changed in the `WP/AlternativeFunctionsUnitTest.inc` file. - * - * @var string WordPress version. - */ - public $minimum_supported_version = '4.6'; - - /** - * Custom list of classes which test classes can extend. - * - * This property allows end-users to add to the $test_class_whitelist via their ruleset. - * This property will need to be set for each sniff which uses the - * `is_test_class()` method. - * Currently the method is used by the `WordPress.WP.GlobalVariablesOverride`, - * `WordPress.NamingConventions.PrefixAllGlobals` and the `WordPress.Files.Filename` sniffs. - * - * Example usage: - * - * - * - * - * - * - * @since 0.11.0 - * - * @var string|string[] - */ - public $custom_test_class_whitelist = array(); - - /** - * List of the functions which verify nonces. - * - * @since 0.5.0 - * @since 0.11.0 Changed from public static to protected non-static. - * - * @var array - */ - protected $nonceVerificationFunctions = array( - 'wp_verify_nonce' => true, - 'check_admin_referer' => true, - 'check_ajax_referer' => true, - ); - - /** - * Functions that escape values for display. - * - * @since 0.5.0 - * @since 0.11.0 Changed from public static to protected non-static. - * - * @var array - */ - protected $escapingFunctions = array( - 'absint' => true, - 'esc_attr__' => true, - 'esc_attr_e' => true, - 'esc_attr_x' => true, - 'esc_attr' => true, - 'esc_html__' => true, - 'esc_html_e' => true, - 'esc_html_x' => true, - 'esc_html' => true, - 'esc_js' => true, - 'esc_sql' => true, - 'esc_textarea' => true, - 'esc_url_raw' => true, - 'esc_url' => true, - 'filter_input' => true, - 'filter_var' => true, - 'floatval' => true, - 'intval' => true, - 'json_encode' => true, - 'like_escape' => true, - 'number_format' => true, - 'rawurlencode' => true, - 'sanitize_html_class' => true, - 'sanitize_user_field' => true, - 'tag_escape' => true, - 'urlencode_deep' => true, - 'urlencode' => true, - 'wp_json_encode' => true, - 'wp_kses_allowed_html' => true, - 'wp_kses_data' => true, - 'wp_kses_post' => true, - 'wp_kses' => true, - ); - - /** - * Functions whose output is automatically escaped for display. - * - * @since 0.5.0 - * @since 0.11.0 Changed from public static to protected non-static. - * - * @var array - */ - protected $autoEscapedFunctions = array( - 'allowed_tags' => true, - 'bloginfo' => true, - 'body_class' => true, - 'calendar_week_mod' => true, - 'category_description' => true, - 'checked' => true, - 'comment_author_email_link' => true, - 'comment_author_email' => true, - 'comment_author_IP' => true, - 'comment_author_link' => true, - 'comment_author_rss' => true, - 'comment_author_url_link' => true, - 'comment_author_url' => true, - 'comment_author' => true, - 'comment_class' => true, - 'comment_date' => true, - 'comment_excerpt' => true, - 'comment_form_title' => true, - 'comment_form' => true, - 'comment_id_fields' => true, - 'comment_ID' => true, - 'comment_reply_link' => true, - 'comment_text_rss' => true, - 'comment_text' => true, - 'comment_time' => true, - 'comment_type' => true, - 'comments_link' => true, - 'comments_number' => true, - 'comments_popup_link' => true, - 'comments_popup_script' => true, - 'comments_rss_link' => true, - 'count' => true, - 'delete_get_calendar_cache' => true, - 'disabled' => true, - 'do_shortcode' => true, - 'do_shortcode_tag' => true, - 'edit_bookmark_link' => true, - 'edit_comment_link' => true, - 'edit_post_link' => true, - 'edit_tag_link' => true, - 'get_archives_link' => true, - 'get_attachment_link' => true, - 'get_avatar' => true, - 'get_bookmark_field' => true, - 'get_calendar' => true, - 'get_comment_author_link' => true, - 'get_current_blog_id' => true, - 'get_delete_post_link' => true, - 'get_footer' => true, - 'get_header' => true, - 'get_search_form' => true, - 'get_search_query' => true, - 'get_sidebar' => true, - 'get_the_author_link' => true, - 'get_the_author' => true, - 'get_the_date' => true, - 'get_the_ID' => true, - 'get_the_post_thumbnail' => true, - 'get_the_term_list' => true, - 'get_the_title' => true, - 'next_comments_link' => true, - 'next_image_link' => true, - 'next_post_link' => true, - 'next_posts_link' => true, - 'paginate_comments_links' => true, - 'permalink_anchor' => true, - 'post_type_archive_title' => true, - 'posts_nav_link' => true, - 'previous_comments_link' => true, - 'previous_image_link' => true, - 'previous_post_link' => true, - 'previous_posts_link' => true, - 'readonly' => true, - 'selected' => true, - 'single_cat_title' => true, - 'single_month_title' => true, - 'single_post_title' => true, - 'single_tag_title' => true, - 'single_term_title' => true, - 'sticky_class' => true, - 'tag_description' => true, - 'term_description' => true, - 'the_attachment_link' => true, - 'the_author_link' => true, - 'the_author_meta' => true, - 'the_author_posts_link' => true, - 'the_author_posts' => true, - 'the_author' => true, - 'the_category_rss' => true, - 'the_category' => true, - 'the_content_rss' => true, - 'the_content' => true, - 'the_date_xml' => true, - 'the_date' => true, - 'the_excerpt_rss' => true, - 'the_excerpt' => true, - 'the_feed_link' => true, - 'the_ID' => true, - 'the_meta' => true, - 'the_modified_author' => true, - 'the_modified_date' => true, - 'the_modified_time' => true, - 'the_permalink' => true, - 'the_post_thumbnail' => true, - 'the_search_query' => true, - 'the_shortlink' => true, - 'the_tags' => true, - 'the_taxonomies' => true, - 'the_terms' => true, - 'the_time' => true, - 'the_title_attribute' => true, - 'the_title_rss' => true, - 'the_title' => true, - 'vip_powered_wpcom' => true, - 'walk_nav_menu_tree' => true, - 'wp_dropdown_categories' => true, - 'wp_dropdown_users' => true, - 'wp_enqueue_script' => true, - 'wp_generate_tag_cloud' => true, - 'wp_get_archives' => true, - 'wp_get_attachment_image' => true, - 'wp_get_attachment_link' => true, - 'wp_link_pages' => true, - 'wp_list_authors' => true, - 'wp_list_bookmarks' => true, - 'wp_list_categories' => true, - 'wp_list_comments' => true, - 'wp_login_form' => true, - 'wp_loginout' => true, - 'wp_meta' => true, - 'wp_nav_menu' => true, - 'wp_register' => true, - 'wp_shortlink_header' => true, - 'wp_shortlink_wp_head' => true, - 'wp_tag_cloud' => true, - 'wp_title' => true, - ); - - /** - * Functions that sanitize values. - * - * This list is complementary to the `$unslashingSanitizingFunctions` - * list. - * Sanitizing functions should be added to this list if they do *not* - * implicitely unslash data and to the `$unslashingsanitizingFunctions` - * list if they do. - * - * @since 0.5.0 - * @since 0.11.0 Changed from public static to protected non-static. - * - * @var array - */ - protected $sanitizingFunctions = array( - '_wp_handle_upload' => true, - 'array_key_exists' => true, - 'esc_url_raw' => true, - 'filter_input' => true, - 'filter_var' => true, - 'hash_equals' => true, - 'in_array' => true, - 'is_email' => true, - 'number_format' => true, - 'sanitize_bookmark_field' => true, - 'sanitize_bookmark' => true, - 'sanitize_email' => true, - 'sanitize_file_name' => true, - 'sanitize_hex_color_no_hash' => true, - 'sanitize_hex_color' => true, - 'sanitize_html_class' => true, - 'sanitize_meta' => true, - 'sanitize_mime_type' => true, - 'sanitize_option' => true, - 'sanitize_sql_orderby' => true, - 'sanitize_term_field' => true, - 'sanitize_term' => true, - 'sanitize_text_field' => true, - 'sanitize_textarea_field' => true, - 'sanitize_title_for_query' => true, - 'sanitize_title_with_dashes' => true, - 'sanitize_title' => true, - 'sanitize_user_field' => true, - 'sanitize_user' => true, - 'validate_file' => true, - 'wp_handle_sideload' => true, - 'wp_handle_upload' => true, - 'wp_kses_allowed_html' => true, - 'wp_kses_data' => true, - 'wp_kses_post' => true, - 'wp_kses' => true, - 'wp_parse_id_list' => true, - 'wp_redirect' => true, - 'wp_safe_redirect' => true, - 'wp_strip_all_tags' => true, - ); - - /** - * Sanitizing functions that implicitly unslash the data passed to them. - * - * This list is complementary to the `$sanitizingFunctions` list. - * Sanitizing functions should be added to this list if they also - * implicitely unslash data and to the `$sanitizingFunctions` list - * if they don't. - * - * @since 0.5.0 - * @since 0.11.0 Changed from public static to protected non-static. - * - * @var array - */ - protected $unslashingSanitizingFunctions = array( - 'absint' => true, - 'boolval' => true, - 'floatval' => true, - 'intval' => true, - 'is_array' => true, - 'sanitize_key' => true, - ); - - /** - * Token which when they preceed code indicate the value is safely casted. - * - * @since 1.1.0 - * - * @var array - */ - protected $safe_casts = array( - \T_INT_CAST => true, - \T_DOUBLE_CAST => true, - \T_BOOL_CAST => true, - ); - - /** - * Functions that format strings. - * - * These functions are often used for formatting values just before output, and - * it is common practice to escape the individual parameters passed to them as - * needed instead of escaping the entire result. This is especially true when the - * string being formatted contains HTML, which makes escaping the full result - * more difficult. - * - * @since 0.5.0 - * @since 0.11.0 Changed from public static to protected non-static. - * - * @var array - */ - protected $formattingFunctions = array( - 'array_fill' => true, - 'ent2ncr' => true, - 'implode' => true, - 'join' => true, - 'nl2br' => true, - 'sprintf' => true, - 'vsprintf' => true, - 'wp_sprintf' => true, - ); - - /** - * Functions which print output incorporating the values passed to them. - * - * @since 0.5.0 - * @since 0.11.0 Changed from public static to protected non-static. - * - * @var array - */ - protected $printingFunctions = array( - '_deprecated_argument' => true, - '_deprecated_constructor' => true, - '_deprecated_file' => true, - '_deprecated_function' => true, - '_deprecated_hook' => true, - '_doing_it_wrong' => true, - '_e' => true, - '_ex' => true, - 'printf' => true, - 'trigger_error' => true, - 'user_error' => true, - 'vprintf' => true, - 'wp_die' => true, - 'wp_dropdown_pages' => true, - ); - - /** - * Functions that escape values for use in SQL queries. - * - * @since 0.9.0 - * @since 0.11.0 Changed from public static to protected non-static. - * - * @var array - */ - protected $SQLEscapingFunctions = array( - 'absint' => true, - 'esc_sql' => true, - 'floatval' => true, - 'intval' => true, - 'like_escape' => true, - ); - - /** - * Functions whose output is automatically escaped for use in SQL queries. - * - * @since 0.9.0 - * @since 0.11.0 Changed from public static to protected non-static. - * - * @var array - */ - protected $SQLAutoEscapedFunctions = array( - 'count' => true, - ); - - /** - * A list of functions that get data from the cache. - * - * @since 0.6.0 - * @since 0.11.0 Changed from public static to protected non-static. - * - * @var array - */ - protected $cacheGetFunctions = array( - 'wp_cache_get' => true, - ); - - /** - * A list of functions that set data in the cache. - * - * @since 0.6.0 - * @since 0.11.0 Changed from public static to protected non-static. - * - * @var array - */ - protected $cacheSetFunctions = array( - 'wp_cache_set' => true, - 'wp_cache_add' => true, - ); - - /** - * A list of functions that delete data from the cache. - * - * @since 0.6.0 - * @since 0.11.0 Changed from public static to protected non-static. - * - * @var array - */ - protected $cacheDeleteFunctions = array( - 'wp_cache_delete' => true, - 'clean_attachment_cache' => true, - 'clean_blog_cache' => true, - 'clean_bookmark_cache' => true, - 'clean_category_cache' => true, - 'clean_comment_cache' => true, - 'clean_network_cache' => true, - 'clean_object_term_cache' => true, - 'clean_page_cache' => true, - 'clean_post_cache' => true, - 'clean_term_cache' => true, - 'clean_user_cache' => true, - ); - - /** - * A list of functions that invoke WP hooks (filters/actions). - * - * @since 0.10.0 - * @since 0.11.0 Changed from public static to protected non-static. - * - * @var array - */ - protected $hookInvokeFunctions = array( - 'do_action' => true, - 'do_action_ref_array' => true, - 'do_action_deprecated' => true, - 'apply_filters' => true, - 'apply_filters_ref_array' => true, - 'apply_filters_deprecated' => true, - ); - - /** - * A list of functions that are used to interact with the WP plugins API. - * - * @since 0.10.0 - * @since 0.11.0 Changed from public static to protected non-static. - * - * @var array => - */ - protected $hookFunctions = array( - 'has_filter' => 1, - 'add_filter' => 1, - 'remove_filter' => 1, - 'remove_all_filters' => 1, - 'doing_filter' => 1, // Hook name optional. - 'has_action' => 1, - 'add_action' => 1, - 'doing_action' => 1, // Hook name optional. - 'did_action' => 1, - 'remove_action' => 1, - 'remove_all_actions' => 1, - 'current_filter' => 0, // No hook name argument. - ); - - /** - * List of global WP variables. - * - * @since 0.3.0 - * @since 0.11.0 Changed visibility from public to protected. - * @since 0.12.0 Renamed from `$globals` to `$wp_globals` to be more descriptive. - * @since 0.12.0 Moved from WordPress_Sniffs_Variables_GlobalVariablesSniff to WordPress_Sniff - * - * @var array - */ - protected $wp_globals = array( - '_links_add_base' => true, - '_links_add_target' => true, - '_menu_item_sort_prop' => true, - '_nav_menu_placeholder' => true, - '_new_bundled_files' => true, - '_old_files' => true, - '_parent_pages' => true, - '_registered_pages' => true, - '_updated_user_settings' => true, - '_wp_additional_image_sizes' => true, - '_wp_admin_css_colors' => true, - '_wp_default_headers' => true, - '_wp_deprecated_widgets_callbacks' => true, - '_wp_last_object_menu' => true, - '_wp_last_utility_menu' => true, - '_wp_menu_nopriv' => true, - '_wp_nav_menu_max_depth' => true, - '_wp_post_type_features' => true, - '_wp_real_parent_file' => true, - '_wp_registered_nav_menus' => true, - '_wp_sidebars_widgets' => true, - '_wp_submenu_nopriv' => true, - '_wp_suspend_cache_invalidation' => true, - '_wp_theme_features' => true, - '_wp_using_ext_object_cache' => true, - 'action' => true, - 'active_signup' => true, - 'admin_body_class' => true, - 'admin_page_hooks' => true, - 'all_links' => true, - 'allowedentitynames' => true, - 'allowedposttags' => true, - 'allowedtags' => true, - 'auth_secure_cookie' => true, - 'authordata' => true, - 'avail_post_mime_types' => true, - 'avail_post_stati' => true, - 'blog_id' => true, - 'blog_title' => true, - 'blogname' => true, - 'cat' => true, - 'cat_id' => true, - 'charset_collate' => true, - 'comment' => true, - 'comment_alt' => true, - 'comment_depth' => true, - 'comment_status' => true, - 'comment_thread_alt' => true, - 'comment_type' => true, - 'comments' => true, - 'compress_css' => true, - 'compress_scripts' => true, - 'concatenate_scripts' => true, - 'current_screen' => true, - 'current_site' => true, - 'current_user' => true, - 'currentcat' => true, - 'currentday' => true, - 'currentmonth' => true, - 'custom_background' => true, - 'custom_image_header' => true, - 'default_menu_order' => true, - 'descriptions' => true, - 'domain' => true, - 'editor_styles' => true, - 'error' => true, - 'errors' => true, - 'EZSQL_ERROR' => true, - 'feeds' => true, - 'GETID3_ERRORARRAY' => true, - 'hook_suffix' => true, - 'HTTP_RAW_POST_DATA' => true, - 'id' => true, - 'in_comment_loop' => true, - 'interim_login' => true, - 'is_apache' => true, - 'is_chrome' => true, - 'is_gecko' => true, - 'is_IE' => true, - 'is_IIS' => true, - 'is_iis7' => true, - 'is_macIE' => true, - 'is_NS4' => true, - 'is_opera' => true, - 'is_safari' => true, - 'is_winIE' => true, - 'l10n' => true, - 'link' => true, - 'link_id' => true, - 'locale' => true, - 'locked_post_status' => true, - 'lost' => true, - 'm' => true, - 'map' => true, - 'menu' => true, - 'menu_order' => true, - 'merged_filters' => true, - 'mode' => true, - 'monthnum' => true, - 'more' => true, - 'multipage' => true, - 'names' => true, - 'nav_menu_selected_id' => true, - 'new_whitelist_options' => true, - 'numpages' => true, - 'one_theme_location_no_menus' => true, - 'opml' => true, - 'order' => true, - 'orderby' => true, - 'overridden_cpage' => true, - 'page' => true, - 'paged' => true, - 'pagenow' => true, - 'pages' => true, - 'parent_file' => true, - 'pass_allowed_html' => true, - 'pass_allowed_protocols' => true, - 'path' => true, - 'per_page' => true, - 'PHP_SELF' => true, - 'phpmailer' => true, - 'plugin_page' => true, - 'plugins' => true, - 'post' => true, - 'post_default_category' => true, - 'post_default_title' => true, - 'post_ID' => true, - 'post_id' => true, - 'post_mime_types' => true, - 'post_type' => true, - 'post_type_object' => true, - 'posts' => true, - 'preview' => true, - 'previouscat' => true, - 'previousday' => true, - 'previousweekday' => true, - 'redir_tab' => true, - 'required_mysql_version' => true, - 'required_php_version' => true, - 'rnd_value' => true, - 'role' => true, - 's' => true, - 'search' => true, - 'self' => true, - 'shortcode_tags' => true, - 'show_admin_bar' => true, - 'sidebars_widgets' => true, - 'status' => true, - 'submenu' => true, - 'submenu_file' => true, - 'super_admins' => true, - 'tab' => true, - 'table_prefix' => true, - 'tabs' => true, - 'tag' => true, - 'targets' => true, - 'tax' => true, - 'taxnow' => true, - 'taxonomy' => true, - 'term' => true, - 'text_direction' => true, - 'theme_field_defaults' => true, - 'themes_allowedtags' => true, - 'timeend' => true, - 'timestart' => true, - 'tinymce_version' => true, - 'title' => true, - 'totals' => true, - 'type' => true, - 'typenow' => true, - 'updated_timestamp' => true, - 'upgrading' => true, - 'urls' => true, - 'user_email' => true, - 'user_ID' => true, - 'user_identity' => true, - 'user_level' => true, - 'user_login' => true, - 'user_url' => true, - 'userdata' => true, - 'usersearch' => true, - 'whitelist_options' => true, - 'withcomments' => true, - 'wp' => true, - 'wp_actions' => true, - 'wp_admin_bar' => true, - 'wp_cockneyreplace' => true, - 'wp_current_db_version' => true, - 'wp_current_filter' => true, - 'wp_customize' => true, - 'wp_dashboard_control_callbacks' => true, - 'wp_db_version' => true, - 'wp_did_header' => true, - 'wp_embed' => true, - 'wp_file_descriptions' => true, - 'wp_filesystem' => true, - 'wp_filter' => true, - 'wp_hasher' => true, - 'wp_header_to_desc' => true, - 'wp_importers' => true, - 'wp_json' => true, - 'wp_list_table' => true, - 'wp_local_package' => true, - 'wp_locale' => true, - 'wp_meta_boxes' => true, - 'wp_object_cache' => true, - 'wp_plugin_paths' => true, - 'wp_post_statuses' => true, - 'wp_post_types' => true, - 'wp_queries' => true, - 'wp_query' => true, - 'wp_registered_sidebars' => true, - 'wp_registered_widget_controls' => true, - 'wp_registered_widget_updates' => true, - 'wp_registered_widgets' => true, - 'wp_rewrite' => true, - 'wp_rich_edit' => true, - 'wp_rich_edit_exists' => true, - 'wp_roles' => true, - 'wp_scripts' => true, - 'wp_settings_errors' => true, - 'wp_settings_fields' => true, - 'wp_settings_sections' => true, - 'wp_smiliessearch' => true, - 'wp_styles' => true, - 'wp_taxonomies' => true, - 'wp_the_query' => true, - 'wp_theme_directories' => true, - 'wp_themes' => true, - 'wp_user_roles' => true, - 'wp_version' => true, - 'wp_widget_factory' => true, - 'wp_xmlrpc_server' => true, - 'wpcommentsjavascript' => true, - 'wpcommentspopupfile' => true, - 'wpdb' => true, - 'wpsmiliestrans' => true, - 'year' => true, - ); - - /** - * A list of superglobals that incorporate user input. - * - * @since 0.5.0 - * @since 0.11.0 Changed from static to non-static. - * - * @var string[] - */ - protected $input_superglobals = array( - '$_COOKIE', - '$_GET', - '$_FILES', - '$_POST', - '$_REQUEST', - '$_SERVER', - ); - - /** - * Whitelist of classes which test classes can extend. - * - * @since 0.11.0 - * - * @var string[] - */ - protected $test_class_whitelist = array( - 'WP_UnitTestCase' => true, - 'WP_Ajax_UnitTestCase' => true, - 'WP_Canonical_UnitTestCase' => true, - 'WP_Test_REST_TestCase' => true, - 'WP_Test_REST_Controller_Testcase' => true, - 'WP_Test_REST_Post_Type_Controller_Testcase' => true, - 'WP_XMLRPC_UnitTestCase' => true, - 'PHPUnit_Framework_TestCase' => true, - 'PHPUnit\Framework\TestCase' => true, - ); - - /** - * The token "type" values for the PHPCS 3.2+ whitelist comments. - * - * PHPCS cross-version compatibility layer to allow sniffs to - * allow for the new PHPCS annotation comments without breaking in older - * PHPCS versions. - * - * @internal Can be replaced with using the PHPCS native Token::$phpcsCommentTokens - * array once the minimum WPCS requirement for PHPCS has gone up - * to PHPCS 3.2.3. - * Note: The PHPCS native property uses the constants/ token "code", - * so code referring to this property will need to be adjusted when - * the property is removed. - * - * @since 1.0.0 - * - * @var array - */ - protected $phpcsCommentTokens = array( - 'T_PHPCS_ENABLE' => true, - 'T_PHPCS_DISABLE' => true, - 'T_PHPCS_SET' => true, - 'T_PHPCS_IGNORE' => true, - 'T_PHPCS_IGNORE_FILE' => true, - ); - /** * The current file being sniffed. * @@ -912,7 +52,9 @@ abstract class Sniff implements PHPCS_Sniff { * normal file processing. */ public function process( File $phpcsFile, $stackPtr ) { - $this->init( $phpcsFile ); + $this->phpcsFile = $phpcsFile; + $this->tokens = $phpcsFile->getTokens(); + return $this->process_token( $stackPtr ); } @@ -927,1846 +69,4 @@ public function process( File $phpcsFile, $stackPtr ) { * normal file processing. */ abstract public function process_token( $stackPtr ); - - /** - * Initialize the class for the current process. - * - * This method must be called by child classes before using many of the methods - * below. - * - * @since 0.4.0 - * - * @param \PHP_CodeSniffer\Files\File $phpcsFile The file currently being processed. - */ - protected function init( File $phpcsFile ) { - $this->phpcsFile = $phpcsFile; - $this->tokens = $phpcsFile->getTokens(); - } - - /** - * Strip quotes surrounding an arbitrary string. - * - * Intended for use with the contents of a T_CONSTANT_ENCAPSED_STRING / T_DOUBLE_QUOTED_STRING. - * - * @since 0.11.0 - * - * @param string $string The raw string. - * @return string String without quotes around it. - */ - public function strip_quotes( $string ) { - return preg_replace( '`^([\'"])(.*)\1$`Ds', '$2', $string ); - } - - /** - * Add a PHPCS message to the output stack as either a warning or an error. - * - * @since 0.11.0 - * - * @param string $message The message. - * @param int $stackPtr The position of the token the message relates to. - * @param bool $is_error Optional. Whether to report the message as an 'error' or 'warning'. - * Defaults to true (error). - * @param string $code Optional error code for the message. Defaults to 'Found'. - * @param array $data Optional input for the data replacements. - * @param int $severity Optional. Severity level. Defaults to 0 which will translate to - * the PHPCS default severity level. - * @return bool - */ - protected function addMessage( $message, $stackPtr, $is_error = true, $code = 'Found', $data = array(), $severity = 0 ) { - return $this->throwMessage( $message, $stackPtr, $is_error, $code, $data, $severity, false ); - } - - /** - * Add a fixable PHPCS message to the output stack as either a warning or an error. - * - * @since 0.11.0 - * - * @param string $message The message. - * @param int $stackPtr The position of the token the message relates to. - * @param bool $is_error Optional. Whether to report the message as an 'error' or 'warning'. - * Defaults to true (error). - * @param string $code Optional error code for the message. Defaults to 'Found'. - * @param array $data Optional input for the data replacements. - * @param int $severity Optional. Severity level. Defaults to 0 which will translate to - * the PHPCS default severity level. - * @return bool - */ - protected function addFixableMessage( $message, $stackPtr, $is_error = true, $code = 'Found', $data = array(), $severity = 0 ) { - return $this->throwMessage( $message, $stackPtr, $is_error, $code, $data, $severity, true ); - } - - /** - * Add a PHPCS message to the output stack as either a warning or an error. - * - * @since 0.11.0 - * - * @param string $message The message. - * @param int $stackPtr The position of the token the message relates to. - * @param bool $is_error Optional. Whether to report the message as an 'error' or 'warning'. - * Defaults to true (error). - * @param string $code Optional error code for the message. Defaults to 'Found'. - * @param array $data Optional input for the data replacements. - * @param int $severity Optional. Severity level. Defaults to 0 which will translate to - * the PHPCS default severity level. - * @param bool $fixable Optional. Whether this is a fixable error. Defaults to false. - * @return bool - */ - private function throwMessage( $message, $stackPtr, $is_error = true, $code = 'Found', $data = array(), $severity = 0, $fixable = false ) { - - $method = 'add'; - if ( true === $fixable ) { - $method .= 'Fixable'; - } - - if ( true === $is_error ) { - $method .= 'Error'; - } else { - $method .= 'Warning'; - } - - return \call_user_func( array( $this->phpcsFile, $method ), $message, $stackPtr, $code, $data, $severity ); - } - - /** - * Convert an arbitrary string to an alphanumeric string with underscores. - * - * Pre-empt issues with arbitrary strings being used as error codes in XML and PHP. - * - * @since 0.11.0 - * - * @param string $base_string Arbitrary string. - * - * @return string - */ - protected function string_to_errorcode( $base_string ) { - return preg_replace( '`[^a-z0-9_]`i', '_', $base_string ); - } - - /** - * Merge a pre-set array with a ruleset provided array or inline provided string. - * - * - Will correctly handle custom array properties which were set without - * the `type="array"` indicator. - * This also allows for making these custom array properties testable using - * a `@codingStandardsChangeSetting` comment in the unit tests. - * - By default flips custom lists to allow for using `isset()` instead - * of `in_array()`. - * - When `$flip` is true: - * * Presumes the base array is in a `'value' => true` format. - * * Any custom items will be given the value `false` to be able to - * distinguish them from pre-set (base array) values. - * * Will filter previously added custom items out from the base array - * before merging/returning to allow for resetting to the base array. - * - * {@internal Function is static as it doesn't use any of the properties or others - * methods anyway and this way the `WordPress_Sniffs_NamingConventions_ValidVariableNameSniff` - * which extends an upstream sniff can also use it.}} - * - * @since 0.11.0 - * - * @param array|string $custom Custom list as provided via a ruleset. - * Can be either a comma-delimited string or - * an array of values. - * @param array $base Optional. Base list. Defaults to an empty array. - * Expects `value => true` format when `$flip` is true. - * @param bool $flip Optional. Whether or not to flip the custom list. - * Defaults to true. - * @return array - */ - public static function merge_custom_array( $custom, $base = array(), $flip = true ) { - if ( true === $flip ) { - $base = array_filter( $base ); - } - - if ( empty( $custom ) || ( ! \is_array( $custom ) && ! \is_string( $custom ) ) ) { - return $base; - } - - // Allow for a comma delimited list. - if ( \is_string( $custom ) ) { - $custom = explode( ',', $custom ); - } - - // Always trim whitespace from the values. - $custom = array_filter( array_map( 'trim', $custom ) ); - - if ( true === $flip ) { - $custom = array_fill_keys( $custom, false ); - } - - if ( empty( $base ) ) { - return $custom; - } - - return array_merge( $base, $custom ); - } - - /** - * Get the last pointer in a line. - * - * @since 0.4.0 - * - * @param integer $stackPtr The position of the current token in the stack passed - * in $tokens. - * - * @return integer Position of the last pointer on that line. - */ - protected function get_last_ptr_on_line( $stackPtr ) { - - $tokens = $this->tokens; - $currentLine = $tokens[ $stackPtr ]['line']; - $nextPtr = ( $stackPtr + 1 ); - - while ( isset( $tokens[ $nextPtr ] ) && $tokens[ $nextPtr ]['line'] === $currentLine ) { - $nextPtr++; - // Do nothing, we just want the last token of the line. - } - - // We've made it to the next line, back up one to the last in the previous line. - // We do this for micro-optimization of the above loop. - $lastPtr = ( $nextPtr - 1 ); - - return $lastPtr; - } - - /** - * Overrule the minimum supported WordPress version with a command-line/config value. - * - * Handle setting the minimum supported WP version in one go for all sniffs which - * expect it via the command line or via a `` variable in a ruleset. - * The config variable overrules the default `$minimum_supported_version` and/or a - * `$minimum_supported_version` set for individual sniffs through the ruleset. - * - * @since 0.14.0 - */ - protected function get_wp_version_from_cl() { - $cl_supported_version = trim( PHPCSHelper::get_config_data( 'minimum_supported_wp_version' ) ); - if ( ! empty( $cl_supported_version ) - && filter_var( $cl_supported_version, \FILTER_VALIDATE_FLOAT ) !== false - ) { - $this->minimum_supported_version = $cl_supported_version; - } - } - - /** - * Find whitelisting comment. - * - * Comment must be at the end of the line or at the end of the statement - * and must use // format. - * It can be prefixed or suffixed with anything e.g. "foobar" will match: - * ... // foobar okay - * ... // WPCS: foobar whitelist. - * - * There is an exception, and that is when PHP is being interspersed with HTML. - * In that case, the comment should always come at the end of the statement (right - * before the closing tag, ?>). For example: - * - * - * - * @since 0.4.0 - * @since 0.14.0 Whitelist comments at the end of the statement are now also accepted. - * - * @param string $comment Comment to find. - * @param integer $stackPtr The position of the current token in the stack passed - * in $tokens. - * - * @return boolean True if whitelisting comment was found, false otherwise. - */ - protected function has_whitelist_comment( $comment, $stackPtr ) { - - // Respect the PHPCS 3.x --ignore-annotations setting. - if ( true === PHPCSHelper::ignore_annotations( $this->phpcsFile ) ) { - return false; - } - - $regex = '#\b' . preg_quote( $comment, '#' ) . '\b#i'; - - // There is a findEndOfStatement() method, but it considers more tokens than - // we need to consider here. - $end_of_statement = $this->phpcsFile->findNext( array( \T_CLOSE_TAG, \T_SEMICOLON ), $stackPtr ); - - if ( false !== $end_of_statement ) { - // If the statement was ended by a semicolon, check if there is a whitelist comment directly after it. - if ( \T_SEMICOLON === $this->tokens[ $end_of_statement ]['code'] ) { - $lastPtr = $this->phpcsFile->findNext( \T_WHITESPACE, ( $end_of_statement + 1 ), null, true ); - } elseif ( \T_CLOSE_TAG === $this->tokens[ $end_of_statement ]['code'] ) { - // If the semicolon was left out and it was terminated by an ending tag, we need to look backwards. - $lastPtr = $this->phpcsFile->findPrevious( \T_WHITESPACE, ( $end_of_statement - 1 ), null, true ); - } - - if ( ( ( \T_COMMENT === $this->tokens[ $lastPtr ]['code'] - && strpos( $this->tokens[ $lastPtr ]['content'], '@codingStandardsChangeSetting' ) === false ) - || ( isset( $this->phpcsCommentTokens[ $this->tokens[ $lastPtr ]['type'] ] ) - && 'T_PHPCS_SET' !== $this->tokens[ $lastPtr ]['type'] ) ) - && $this->tokens[ $lastPtr ]['line'] === $this->tokens[ $end_of_statement ]['line'] - && preg_match( $regex, $this->tokens[ $lastPtr ]['content'] ) === 1 - ) { - return true; - } - } - - // No whitelist comment found so far. Check at the end of the stackPtr line. - // Note: a T_COMMENT includes the new line character, so may be the last token on the line! - $end_of_line = $this->get_last_ptr_on_line( $stackPtr ); - $lastPtr = $this->phpcsFile->findPrevious( \T_WHITESPACE, $end_of_line, null, true ); - - if ( ( ( \T_COMMENT === $this->tokens[ $lastPtr ]['code'] - && strpos( $this->tokens[ $lastPtr ]['content'], '@codingStandardsChangeSetting' ) === false ) - || ( isset( $this->phpcsCommentTokens[ $this->tokens[ $lastPtr ]['type'] ] ) - && 'T_PHPCS_SET' !== $this->tokens[ $lastPtr ]['type'] ) ) - && $this->tokens[ $lastPtr ]['line'] === $this->tokens[ $stackPtr ]['line'] - && preg_match( $regex, $this->tokens[ $lastPtr ]['content'] ) === 1 - ) { - return true; - } - - return false; - } - - /** - * Check if a token is used within a unit test. - * - * Unit test methods are identified as such: - * - Method is within a known unit test class; - * - or Method is within a class/trait which extends a known unit test class. - * - * @since 0.11.0 - * @since 1.1.0 Supports anonymous test classes and improved handling of nested scopes. - * - * @param int $stackPtr The position of the token to be examined. - * - * @return bool True if the token is within a unit test, false otherwise. - */ - protected function is_token_in_test_method( $stackPtr ) { - // Is the token inside of a function definition ? - $functionToken = $this->phpcsFile->getCondition( $stackPtr, \T_FUNCTION ); - if ( false === $functionToken ) { - // No conditions or no function condition. - return false; - } - - /* - * Is this a method inside of a class or a trait ? If so, it is a test class/trait ? - * - * {@internal Once the minimum supported PHPCS version has gone up to 3.1.0, the - * local array here can be replace with Tokens::$ooScopeTokens.}} - */ - $oo_tokens = array( - \T_CLASS => true, - \T_TRAIT => true, - \T_ANON_CLASS => true, - ); - $conditions = $this->tokens[ $stackPtr ]['conditions']; - - foreach ( $conditions as $token => $condition ) { - if ( $token === $functionToken ) { - // Only examine the conditions the function is nested in, not those nested within the function. - break; - } - - if ( isset( $oo_tokens[ $condition ] ) ) { - $is_test_class = $this->is_test_class( $token ); - if ( true === $is_test_class ) { - return true; - } - } - } - - return false; - } - - /** - * Check if a class token is part of a unit test suite. - * - * Unit test classes are identified as such: - * - Class which either extends WP_UnitTestCase or PHPUnit_Framework_TestCase - * or a custom whitelisted unit test class. - * - * @since 0.12.0 Split off from the `is_token_in_test_method()` method. - * @since 1.0.0 Improved recognition of namespaced class names. - * - * @param int $stackPtr The position of the token to be examined. - * This should be a class, anonymous class or trait token. - * - * @return bool True if the class is a unit test class, false otherwise. - */ - protected function is_test_class( $stackPtr ) { - - if ( ! isset( $this->tokens[ $stackPtr ] ) - || \in_array( $this->tokens[ $stackPtr ]['type'], array( 'T_CLASS', 'T_ANON_CLASS', 'T_TRAIT' ), true ) === false - ) { - return false; - } - - // Add any potentially whitelisted custom test classes to the whitelist. - $whitelist = $this->merge_custom_array( - $this->custom_test_class_whitelist, - $this->test_class_whitelist - ); - - /* - * Show some tolerance for user input. - * The custom test class names should be passed as FQN without a prefixing `\`. - */ - foreach ( $whitelist as $k => $v ) { - $whitelist[ $k ] = ltrim( $v, '\\' ); - } - - // Is the class/trait one of the whitelisted test classes ? - $namespace = $this->determine_namespace( $stackPtr ); - $className = $this->phpcsFile->getDeclarationName( $stackPtr ); - if ( '' !== $namespace ) { - if ( isset( $whitelist[ $namespace . '\\' . $className ] ) ) { - return true; - } - } elseif ( isset( $whitelist[ $className ] ) ) { - return true; - } - - // Does the class/trait extend one of the whitelisted test classes ? - $extendedClassName = $this->phpcsFile->findExtendedClassName( $stackPtr ); - if ( '\\' === $extendedClassName[0] ) { - if ( isset( $whitelist[ substr( $extendedClassName, 1 ) ] ) ) { - return true; - } - } elseif ( '' !== $namespace ) { - if ( isset( $whitelist[ $namespace . '\\' . $extendedClassName ] ) ) { - return true; - } - } elseif ( isset( $whitelist[ $extendedClassName ] ) ) { - return true; - } - - /* - * Not examining imported classes via `use` statements as with the variety of syntaxes, - * this would get very complicated. - * After all, users can add an `` for a particular sniff to their - * custom ruleset to selectively exclude the test directory. - */ - - return false; - } - - /** - * Check if this variable is being assigned a value. - * - * E.g., $var = 'foo'; - * - * Also handles array assignments to arbitrary depth: - * - * $array['key'][ $foo ][ something() ] = $bar; - * - * @since 0.5.0 - * - * @param int $stackPtr The index of the token in the stack. This must point to - * either a T_VARIABLE or T_CLOSE_SQUARE_BRACKET token. - * - * @return bool Whether the token is a variable being assigned a value. - */ - protected function is_assignment( $stackPtr ) { - - static $valid = array( - \T_VARIABLE => true, - \T_CLOSE_SQUARE_BRACKET => true, - ); - - // Must be a variable, constant or closing square bracket (see below). - if ( ! isset( $valid[ $this->tokens[ $stackPtr ]['code'] ] ) ) { - return false; - } - - $next_non_empty = $this->phpcsFile->findNext( - Tokens::$emptyTokens, - ( $stackPtr + 1 ), - null, - true, - null, - true - ); - - // No token found. - if ( false === $next_non_empty ) { - return false; - } - - // If the next token is an assignment, that's all we need to know. - if ( isset( Tokens::$assignmentTokens[ $this->tokens[ $next_non_empty ]['code'] ] ) ) { - return true; - } - - // Check if this is an array assignment, e.g., `$var['key'] = 'val';` . - if ( \T_OPEN_SQUARE_BRACKET === $this->tokens[ $next_non_empty ]['code'] ) { - return $this->is_assignment( $this->tokens[ $next_non_empty ]['bracket_closer'] ); - } - - return false; - } - - /** - * Check if this token has an associated nonce check. - * - * @since 0.5.0 - * - * @param int $stackPtr The position of the current token in the stack of tokens. - * - * @return bool - */ - protected function has_nonce_check( $stackPtr ) { - - /** - * A cache of the scope that we last checked for nonce verification in. - * - * @var array { - * @var string $file The name of the file. - * @var int $start The index of the token where the scope started. - * @var int $end The index of the token where the scope ended. - * @var bool|int $nonce_check The index of the token where an nonce check - * was found, or false if none was found. - * } - */ - static $last; - - $start = 0; - $end = $stackPtr; - - $tokens = $this->phpcsFile->getTokens(); - - // If we're in a function, only look inside of it. - $f = $this->phpcsFile->getCondition( $stackPtr, \T_FUNCTION ); - if ( false !== $f ) { - $start = $tokens[ $f ]['scope_opener']; - } else { - $f = $this->phpcsFile->getCondition( $stackPtr, \T_CLOSURE ); - if ( false !== $f ) { - $start = $tokens[ $f ]['scope_opener']; - } - } - - $in_isset = $this->is_in_isset_or_empty( $stackPtr ); - - // We allow for isset( $_POST['var'] ) checks to come before the nonce check. - // If this is inside an isset(), check after it as well, all the way to the - // end of the scope. - if ( $in_isset ) { - $end = ( 0 === $start ) ? $this->phpcsFile->numTokens : $tokens[ $start ]['scope_closer']; - } - - // Check if we've looked here before. - $filename = $this->phpcsFile->getFilename(); - - if ( - $filename === $last['file'] - && $start === $last['start'] - ) { - - if ( false !== $last['nonce_check'] ) { - // If we have already found an nonce check in this scope, we just - // need to check whether it comes before this token. It is OK if the - // check is after the token though, if this was only a isset() check. - return ( $in_isset || $last['nonce_check'] < $stackPtr ); - } elseif ( $end <= $last['end'] ) { - // If not, we can still go ahead and return false if we've already - // checked to the end of the search area. - return false; - } - - // We haven't checked this far yet, but we can still save work by - // skipping over the part we've already checked. - $start = $last['end']; - } else { - $last = array( - 'file' => $filename, - 'start' => $start, - 'end' => $end, - ); - } - - // Loop through the tokens looking for nonce verification functions. - for ( $i = $start; $i < $end; $i++ ) { - - // If this isn't a function name, skip it. - if ( \T_STRING !== $tokens[ $i ]['code'] ) { - continue; - } - - // If this is one of the nonce verification functions, we can bail out. - if ( isset( $this->nonceVerificationFunctions[ $tokens[ $i ]['content'] ] ) ) { - $last['nonce_check'] = $i; - return true; - } - } - - // We're still here, so no luck. - $last['nonce_check'] = false; - - return false; - } - - /** - * Check if a token is inside of an isset() or empty() statement. - * - * @since 0.5.0 - * - * @param int $stackPtr The index of the token in the stack. - * - * @return bool Whether the token is inside an isset() or empty() statement. - */ - protected function is_in_isset_or_empty( $stackPtr ) { - - if ( ! isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) { - return false; - } - - $nested_parenthesis = $this->tokens[ $stackPtr ]['nested_parenthesis']; - - end( $nested_parenthesis ); - $open_parenthesis = key( $nested_parenthesis ); - - $previous_non_empty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $open_parenthesis - 1 ), null, true, null, true ); - return in_array( $this->tokens[ $previous_non_empty ]['code'], array( \T_ISSET, \T_EMPTY ), true ); - } - - /** - * Check if something is only being sanitized. - * - * @since 0.5.0 - * - * @param int $stackPtr The index of the token in the stack. - * - * @return bool Whether the token is only within a sanitization. - */ - protected function is_only_sanitized( $stackPtr ) { - - // If it isn't being sanitized at all. - if ( ! $this->is_sanitized( $stackPtr ) ) { - return false; - } - - // If this isn't set, we know the value must have only been casted, because - // is_sanitized() would have returned false otherwise. - if ( ! isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) { - return true; - } - - // At this point we're expecting the value to have not been casted. If it - // was, it wasn't *only* casted, because it's also in a function. - if ( $this->is_safe_casted( $stackPtr ) ) { - return false; - } - - // The only parentheses should belong to the sanitizing function. If there's - // more than one set, this isn't *only* sanitization. - return ( \count( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) === 1 ); - } - - /** - * Check if something is being casted to a safe value. - * - * @since 0.5.0 - * - * @param int $stackPtr The index of the token in the stack. - * - * @return bool Whether the token being casted. - */ - protected function is_safe_casted( $stackPtr ) { - - // Get the last non-empty token. - $prev = $this->phpcsFile->findPrevious( - Tokens::$emptyTokens, - ( $stackPtr - 1 ), - null, - true - ); - - if ( false === $prev ) { - return false; - } - - // Check if it is a safe cast. - return isset( $this->safe_casts[ $this->tokens[ $prev ]['code'] ] ); - } - - /** - * Check if something is being sanitized. - * - * @since 0.5.0 - * - * @param int $stackPtr The index of the token in the stack. - * @param bool $require_unslash Whether to give an error if wp_unslash() isn't - * used on the variable before sanitization. - * - * @return bool Whether the token being sanitized. - */ - protected function is_sanitized( $stackPtr, $require_unslash = false ) { - - // First we check if it is being casted to a safe value. - if ( $this->is_safe_casted( $stackPtr ) ) { - return true; - } - - // If this isn't within a function call, we know already that it's not safe. - if ( ! isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) { - if ( $require_unslash ) { - $this->add_unslash_error( $stackPtr ); - } - return false; - } - - // Get the function that it's in. - $nested_parenthesis = $this->tokens[ $stackPtr ]['nested_parenthesis']; - $function_closer = end( $nested_parenthesis ); - $function_opener = key( $nested_parenthesis ); - $function = $this->tokens[ ( $function_opener - 1 ) ]; - - // If it is just being unset, the value isn't used at all, so it's safe. - if ( \T_UNSET === $function['code'] ) { - return true; - } - - // If this isn't a call to a function, it sure isn't sanitizing function. - if ( \T_STRING !== $function['code'] ) { - if ( $require_unslash ) { - $this->add_unslash_error( $stackPtr ); - } - return false; - } - - $functionName = $function['content']; - - // Check if wp_unslash() is being used. - if ( 'wp_unslash' === $functionName ) { - - $is_unslashed = true; - $function_closer = prev( $nested_parenthesis ); - - // If there is no other function being used, this value is unsanitized. - if ( ! $function_closer ) { - return false; - } - - $function_opener = key( $nested_parenthesis ); - $functionName = $this->tokens[ ( $function_opener - 1 ) ]['content']; - - } else { - - $is_unslashed = false; - } - - // Arrays might be sanitized via array_map(). - if ( 'array_map' === $functionName ) { - - // Get the first parameter. - $callback = $this->get_function_call_parameter( ( $function_opener - 1 ), 1 ); - - if ( ! empty( $callback ) ) { - /* - * If this is a function callback (not a method callback array) and we're able - * to resolve the function name, do so. - */ - $first_non_empty = $this->phpcsFile->findNext( - Tokens::$emptyTokens, - $callback['start'], - ( $callback['end'] + 1 ), - true - ); - - if ( false !== $first_non_empty && \T_CONSTANT_ENCAPSED_STRING === $this->tokens[ $first_non_empty ]['code'] ) { - $functionName = $this->strip_quotes( $this->tokens[ $first_non_empty ]['content'] ); - } - } - } - - // If slashing is required, give an error. - if ( ! $is_unslashed && $require_unslash && ! isset( $this->unslashingSanitizingFunctions[ $functionName ] ) ) { - $this->add_unslash_error( $stackPtr ); - } - - // Check if this is a sanitizing function. - if ( isset( $this->sanitizingFunctions[ $functionName ] ) || isset( $this->unslashingSanitizingFunctions[ $functionName ] ) ) { - return true; - } - - return false; - } - - /** - * Add an error for missing use of wp_unslash(). - * - * @since 0.5.0 - * - * @param int $stackPtr The index of the token in the stack. - */ - public function add_unslash_error( $stackPtr ) { - - $this->phpcsFile->addError( - 'Missing wp_unslash() before sanitization.', - $stackPtr, - 'MissingUnslash', - array( $this->tokens[ $stackPtr ]['content'] ) - ); - } - - /** - * Get the index key of an array variable. - * - * E.g., "bar" in $foo['bar']. - * - * @since 0.5.0 - * - * @param int $stackPtr The index of the token in the stack. - * - * @return string|false The array index key whose value is being accessed. - */ - protected function get_array_access_key( $stackPtr ) { - - // Find the next non-empty token. - $open_bracket = $this->phpcsFile->findNext( - Tokens::$emptyTokens, - ( $stackPtr + 1 ), - null, - true - ); - - // If it isn't a bracket, this isn't an array-access. - if ( false === $open_bracket || \T_OPEN_SQUARE_BRACKET !== $this->tokens[ $open_bracket ]['code'] ) { - return false; - } - - $key = $this->phpcsFile->getTokensAsString( - ( $open_bracket + 1 ), - ( $this->tokens[ $open_bracket ]['bracket_closer'] - $open_bracket - 1 ) - ); - - return trim( $key ); - } - - /** - * Check if the existence of a variable is validated with isset() or empty(). - * - * When $in_condition_only is false, (which is the default), this is considered - * valid: - * - * ```php - * if ( isset( $var ) ) { - * // Do stuff, like maybe return or exit (but could be anything) - * } - * - * foo( $var ); - * ``` - * - * When it is true, that would be invalid, the use of the variable must be within - * the scope of the validating condition, like this: - * - * ```php - * if ( isset( $var ) ) { - * foo( $var ); - * } - * ``` - * - * @since 0.5.0 - * - * @param int $stackPtr The index of this token in the stack. - * @param string $array_key An array key to check for ("bar" in $foo['bar']). - * @param bool $in_condition_only Whether to require that this use of the - * variable occur within the scope of the - * validating condition, or just in the same - * scope as it (default). - * - * @return bool Whether the var is validated. - */ - protected function is_validated( $stackPtr, $array_key = null, $in_condition_only = false ) { - - if ( $in_condition_only ) { - /* - * This is a stricter check, requiring the variable to be used only - * within the validation condition. - */ - - // If there are no conditions, there's no validation. - if ( empty( $this->tokens[ $stackPtr ]['conditions'] ) ) { - return false; - } - - $conditions = $this->tokens[ $stackPtr ]['conditions']; - end( $conditions ); // Get closest condition. - $conditionPtr = key( $conditions ); - $condition = $this->tokens[ $conditionPtr ]; - - if ( ! isset( $condition['parenthesis_opener'] ) ) { - // Live coding or parse error. - return false; - } - - $scope_start = $condition['parenthesis_opener']; - $scope_end = $condition['parenthesis_closer']; - - } else { - /* - * We are are more loose, requiring only that the variable be validated - * in the same function/file scope as it is used. - */ - - $scope_start = 0; - - // Check if we are in a function. - $function = $this->phpcsFile->getCondition( $stackPtr, \T_FUNCTION ); - - // If so, we check only within the function, otherwise the whole file. - if ( false !== $function ) { - $scope_start = $this->tokens[ $function ]['scope_opener']; - } else { - // Check if we are in a closure. - $closure = $this->phpcsFile->getCondition( $stackPtr, \T_CLOSURE ); - - // If so, we check only within the closure. - if ( false !== $closure ) { - $scope_start = $this->tokens[ $closure ]['scope_opener']; - } - } - - $scope_end = $stackPtr; - - } - - $bare_array_key = $this->strip_quotes( $array_key ); - - for ( $i = ( $scope_start + 1 ); $i < $scope_end; $i++ ) { - - if ( ! \in_array( $this->tokens[ $i ]['code'], array( \T_ISSET, \T_EMPTY, \T_UNSET ), true ) ) { - continue; - } - - $issetOpener = $this->phpcsFile->findNext( \T_OPEN_PARENTHESIS, $i ); - $issetCloser = $this->tokens[ $issetOpener ]['parenthesis_closer']; - - // Look for this variable. We purposely stomp $i from the parent loop. - for ( $i = ( $issetOpener + 1 ); $i < $issetCloser; $i++ ) { - - if ( \T_VARIABLE !== $this->tokens[ $i ]['code'] ) { - continue; - } - - // If we're checking for a specific array key (ex: 'hello' in - // $_POST['hello']), that must match too. Quote-style, however, doesn't matter. - if ( isset( $array_key ) - && $this->strip_quotes( $this->get_array_access_key( $i ) ) !== $bare_array_key ) { - continue; - } - - return true; - } - } - - return false; - } - - /** - * Check whether a variable is being compared to another value. - * - * E.g., $var === 'foo', 1 <= $var, etc. - * - * Also recognizes `switch ( $var )`. - * - * @since 0.5.0 - * - * @param int $stackPtr The index of this token in the stack. - * - * @return bool Whether this is a comparison. - */ - protected function is_comparison( $stackPtr ) { - - // We first check if this is a switch statement (switch ( $var )). - if ( isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) { - $nested_parenthesis = $this->tokens[ $stackPtr ]['nested_parenthesis']; - $close_parenthesis = end( $nested_parenthesis ); - - if ( - isset( $this->tokens[ $close_parenthesis ]['parenthesis_owner'] ) - && \T_SWITCH === $this->tokens[ $this->tokens[ $close_parenthesis ]['parenthesis_owner'] ]['code'] - ) { - return true; - } - } - - // Find the previous non-empty token. We check before the var first because - // yoda conditions are usually expected. - $previous_token = $this->phpcsFile->findPrevious( - Tokens::$emptyTokens, - ( $stackPtr - 1 ), - null, - true - ); - - if ( isset( Tokens::$comparisonTokens[ $this->tokens[ $previous_token ]['code'] ] ) ) { - return true; - } - - // Maybe the comparison operator is after this. - $next_token = $this->phpcsFile->findNext( - Tokens::$emptyTokens, - ( $stackPtr + 1 ), - null, - true - ); - - // This might be an opening square bracket in the case of arrays ($var['a']). - while ( \T_OPEN_SQUARE_BRACKET === $this->tokens[ $next_token ]['code'] ) { - - $next_token = $this->phpcsFile->findNext( - Tokens::$emptyTokens, - ( $this->tokens[ $next_token ]['bracket_closer'] + 1 ), - null, - true - ); - } - - if ( isset( Tokens::$comparisonTokens[ $this->tokens[ $next_token ]['code'] ] ) ) { - return true; - } - - return false; - } - - /** - * Check what type of 'use' statement a token is part of. - * - * The T_USE token has multiple different uses: - * - * 1. In a closure: function () use ( $var ) {} - * 2. In a class, to import a trait: use Trait_Name - * 3. In a namespace, to import a class: use Some\Class; - * - * This function will check the token and return 'closure', 'trait', or 'class', - * based on which of these uses the use is being used for. - * - * @since 0.7.0 - * - * @param int $stackPtr The position of the token to check. - * - * @return string The type of use. - */ - protected function get_use_type( $stackPtr ) { - - // USE keywords inside closures. - $next = $this->phpcsFile->findNext( \T_WHITESPACE, ( $stackPtr + 1 ), null, true ); - - if ( \T_OPEN_PARENTHESIS === $this->tokens[ $next ]['code'] ) { - return 'closure'; - } - - // USE keywords for traits. - $valid_scopes = array( - 'T_CLASS' => true, - 'T_ANON_CLASS' => true, - 'T_TRAIT' => true, - ); - if ( false !== $this->valid_direct_scope( $stackPtr, $valid_scopes ) ) { - return 'trait'; - } - - // USE keywords for classes to import to a namespace. - return 'class'; - } - - /** - * Get the interpolated variable names from a string. - * - * Check if '$' is followed by a valid variable name, and that it is not preceded by an escape sequence. - * - * @since 0.9.0 - * - * @param string $string The contents of a T_DOUBLE_QUOTED_STRING or T_HEREDOC token. - * - * @return array Variable names (without '$' sigil). - */ - protected function get_interpolated_variables( $string ) { - $variables = array(); - if ( preg_match_all( '/(?P\\\\*)\$(?P\w+)/', $string, $match_sets, \PREG_SET_ORDER ) ) { - foreach ( $match_sets as $matches ) { - if ( ! isset( $matches['backslashes'] ) || ( \strlen( $matches['backslashes'] ) % 2 ) === 0 ) { - $variables[] = $matches['symbol']; - } - } - } - return $variables; - } - - /** - * Strip variables from an arbitrary double quoted/heredoc string. - * - * Intended for use with the contents of a T_DOUBLE_QUOTED_STRING or T_HEREDOC token. - * - * @since 0.14.0 - * - * @param string $string The raw string. - * - * @return string String without variables in it. - */ - public function strip_interpolated_variables( $string ) { - if ( strpos( $string, '$' ) === false ) { - return $string; - } - - return preg_replace( self::REGEX_COMPLEX_VARS, '', $string ); - } - - /** - * Checks if a function call has parameters. - * - * Expects to be passed the T_STRING stack pointer for the function call. - * If passed a T_STRING which is *not* a function call, the behaviour is unreliable. - * - * Extra feature: If passed an T_ARRAY or T_OPEN_SHORT_ARRAY stack pointer, it - * will detect whether the array has values or is empty. - * - * @link https://github.com/PHPCompatibility/PHPCompatibility/issues/120 - * @link https://github.com/PHPCompatibility/PHPCompatibility/issues/152 - * - * @since 0.11.0 - * - * @param int $stackPtr The position of the function call token. - * - * @return bool - */ - public function does_function_call_have_parameters( $stackPtr ) { - - // Check for the existence of the token. - if ( false === isset( $this->tokens[ $stackPtr ] ) ) { - return false; - } - - // Is this one of the tokens this function handles ? - if ( false === \in_array( $this->tokens[ $stackPtr ]['code'], array( \T_STRING, \T_ARRAY, \T_OPEN_SHORT_ARRAY ), true ) ) { - return false; - } - - $next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true, null, true ); - - // Deal with short array syntax. - if ( 'T_OPEN_SHORT_ARRAY' === $this->tokens[ $stackPtr ]['type'] ) { - if ( false === isset( $this->tokens[ $stackPtr ]['bracket_closer'] ) ) { - return false; - } - - if ( $next_non_empty === $this->tokens[ $stackPtr ]['bracket_closer'] ) { - // No parameters. - return false; - } else { - return true; - } - } - - // Deal with function calls & long arrays. - // Next non-empty token should be the open parenthesis. - if ( false === $next_non_empty && \T_OPEN_PARENTHESIS !== $this->tokens[ $next_non_empty ]['code'] ) { - return false; - } - - if ( false === isset( $this->tokens[ $next_non_empty ]['parenthesis_closer'] ) ) { - return false; - } - - $close_parenthesis = $this->tokens[ $next_non_empty ]['parenthesis_closer']; - $next_next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next_non_empty + 1 ), ( $close_parenthesis + 1 ), true ); - - if ( $next_next_non_empty === $close_parenthesis ) { - // No parameters. - return false; - } - - return true; - } - - /** - * Count the number of parameters a function call has been passed. - * - * Expects to be passed the T_STRING stack pointer for the function call. - * If passed a T_STRING which is *not* a function call, the behaviour is unreliable. - * - * Extra feature: If passed an T_ARRAY or T_OPEN_SHORT_ARRAY stack pointer, - * it will return the number of values in the array. - * - * @link https://github.com/PHPCompatibility/PHPCompatibility/issues/111 - * @link https://github.com/PHPCompatibility/PHPCompatibility/issues/114 - * @link https://github.com/PHPCompatibility/PHPCompatibility/issues/151 - * - * @since 0.11.0 - * - * @param int $stackPtr The position of the function call token. - * - * @return int - */ - public function get_function_call_parameter_count( $stackPtr ) { - if ( false === $this->does_function_call_have_parameters( $stackPtr ) ) { - return 0; - } - - return \count( $this->get_function_call_parameters( $stackPtr ) ); - } - - /** - * Get information on all parameters passed to a function call. - * - * Expects to be passed the T_STRING stack pointer for the function call. - * If passed a T_STRING which is *not* a function call, the behaviour is unreliable. - * - * Extra feature: If passed an T_ARRAY or T_OPEN_SHORT_ARRAY stack pointer, - * it will tokenize the values / key/value pairs contained in the array call. - * - * @since 0.11.0 - * - * @param int $stackPtr The position of the function call token. - * - * @return array Multi-dimentional array with parameter details or - * empty array if no parameters are found. - * - * @type int $position 1-based index position of the parameter. { - * @type int $start Stack pointer for the start of the parameter. - * @type int $end Stack pointer for the end of parameter. - * @type int $raw Trimmed raw parameter content. - * } - */ - public function get_function_call_parameters( $stackPtr ) { - if ( false === $this->does_function_call_have_parameters( $stackPtr ) ) { - return array(); - } - - /* - * Ok, we know we have a T_STRING, T_ARRAY or T_OPEN_SHORT_ARRAY with parameters - * and valid open & close brackets/parenthesis. - */ - - // Mark the beginning and end tokens. - if ( 'T_OPEN_SHORT_ARRAY' === $this->tokens[ $stackPtr ]['type'] ) { - $opener = $stackPtr; - $closer = $this->tokens[ $stackPtr ]['bracket_closer']; - - $nestedParenthesisCount = 0; - } else { - $opener = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true, null, true ); - $closer = $this->tokens[ $opener ]['parenthesis_closer']; - - $nestedParenthesisCount = 1; - } - - // Which nesting level is the one we are interested in ? - if ( isset( $this->tokens[ $opener ]['nested_parenthesis'] ) ) { - $nestedParenthesisCount += \count( $this->tokens[ $opener ]['nested_parenthesis'] ); - } - - $parameters = array(); - $next_comma = $opener; - $param_start = ( $opener + 1 ); - $cnt = 1; - while ( $next_comma = $this->phpcsFile->findNext( array( \T_COMMA, $this->tokens[ $closer ]['code'], \T_OPEN_SHORT_ARRAY, \T_CLOSURE ), ( $next_comma + 1 ), ( $closer + 1 ) ) ) { - // Ignore anything within short array definition brackets. - if ( 'T_OPEN_SHORT_ARRAY' === $this->tokens[ $next_comma ]['type'] - && ( isset( $this->tokens[ $next_comma ]['bracket_opener'] ) - && $this->tokens[ $next_comma ]['bracket_opener'] === $next_comma ) - && isset( $this->tokens[ $next_comma ]['bracket_closer'] ) - ) { - // Skip forward to the end of the short array definition. - $next_comma = $this->tokens[ $next_comma ]['bracket_closer']; - continue; - } - - // Skip past closures passed as function parameters. - if ( 'T_CLOSURE' === $this->tokens[ $next_comma ]['type'] - && ( isset( $this->tokens[ $next_comma ]['scope_condition'] ) - && $this->tokens[ $next_comma ]['scope_condition'] === $next_comma ) - && isset( $this->tokens[ $next_comma ]['scope_closer'] ) - ) { - // Skip forward to the end of the closure declaration. - $next_comma = $this->tokens[ $next_comma ]['scope_closer']; - continue; - } - - // Ignore comma's at a lower nesting level. - if ( \T_COMMA === $this->tokens[ $next_comma ]['code'] - && isset( $this->tokens[ $next_comma ]['nested_parenthesis'] ) - && \count( $this->tokens[ $next_comma ]['nested_parenthesis'] ) !== $nestedParenthesisCount - ) { - continue; - } - - // Ignore closing parenthesis/bracket if not 'ours'. - if ( $this->tokens[ $next_comma ]['type'] === $this->tokens[ $closer ]['type'] && $next_comma !== $closer ) { - continue; - } - - // Ok, we've reached the end of the parameter. - $parameters[ $cnt ]['start'] = $param_start; - $parameters[ $cnt ]['end'] = ( $next_comma - 1 ); - $parameters[ $cnt ]['raw'] = trim( $this->phpcsFile->getTokensAsString( $param_start, ( $next_comma - $param_start ) ) ); - - /* - * Check if there are more tokens before the closing parenthesis. - * Prevents code like the following from setting a third parameter: - * functionCall( $param1, $param2, ); - */ - $has_next_param = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next_comma + 1 ), $closer, true, null, true ); - if ( false === $has_next_param ) { - break; - } - - // Prepare for the next parameter. - $param_start = ( $next_comma + 1 ); - $cnt++; - } - - return $parameters; - } - - /** - * Get information on a specific parameter passed to a function call. - * - * Expects to be passed the T_STRING stack pointer for the function call. - * If passed a T_STRING which is *not* a function call, the behaviour is unreliable. - * - * Will return a array with the start token pointer, end token pointer and the raw value - * of the parameter at a specific offset. - * If the specified parameter is not found, will return false. - * - * @since 0.11.0 - * - * @param int $stackPtr The position of the function call token. - * @param int $param_offset The 1-based index position of the parameter to retrieve. - * - * @return array|false - */ - public function get_function_call_parameter( $stackPtr, $param_offset ) { - $parameters = $this->get_function_call_parameters( $stackPtr ); - - if ( false === isset( $parameters[ $param_offset ] ) ) { - return false; - } - - return $parameters[ $param_offset ]; - } - - /** - * Find the array opener & closer based on a T_ARRAY or T_OPEN_SHORT_ARRAY token. - * - * @since 0.12.0 - * - * @param int $stackPtr The stack pointer to the array token. - * - * @return array|bool Array with two keys `opener`, `closer` or false if - * either or these could not be determined. - */ - protected function find_array_open_close( $stackPtr ) { - /* - * Determine the array opener & closer. - */ - if ( \T_ARRAY === $this->tokens[ $stackPtr ]['code'] ) { - if ( isset( $this->tokens[ $stackPtr ]['parenthesis_opener'] ) ) { - $opener = $this->tokens[ $stackPtr ]['parenthesis_opener']; - - if ( isset( $this->tokens[ $opener ]['parenthesis_closer'] ) ) { - $closer = $this->tokens[ $opener ]['parenthesis_closer']; - } - } - } else { - // Short array syntax. - $opener = $stackPtr; - - if ( isset( $this->tokens[ $stackPtr ]['bracket_closer'] ) ) { - $closer = $this->tokens[ $stackPtr ]['bracket_closer']; - } - } - - if ( isset( $opener, $closer ) ) { - return array( - 'opener' => $opener, - 'closer' => $closer, - ); - } - - return false; - } - - /** - * Determine the namespace name an arbitrary token lives in. - * - * @since 0.10.0 - * @since 0.12.0 Moved from the WordPress_AbstractClassRestrictionsSniff to this sniff. - * - * @param int $stackPtr The token position for which to determine the namespace. - * - * @return string Namespace name or empty string if it couldn't be determined or no namespace applies. - */ - public function determine_namespace( $stackPtr ) { - - // Check for the existence of the token. - if ( ! isset( $this->tokens[ $stackPtr ] ) ) { - return ''; - } - - // Check for scoped namespace {}. - if ( ! empty( $this->tokens[ $stackPtr ]['conditions'] ) ) { - $namespacePtr = $this->phpcsFile->getCondition( $stackPtr, \T_NAMESPACE ); - if ( false !== $namespacePtr ) { - $namespace = $this->get_declared_namespace_name( $namespacePtr ); - if ( false !== $namespace ) { - return $namespace; - } - - // We are in a scoped namespace, but couldn't determine the name. - // Searching for a global namespace is futile. - return ''; - } - } - - /* - * Not in a scoped namespace, so let's see if we can find a non-scoped namespace instead. - * Keeping in mind that: - * - there can be multiple non-scoped namespaces in a file (bad practice, but it happens). - * - the namespace keyword can also be used as part of a function/method call and such. - * - that a non-named namespace resolves to the global namespace. - */ - $previousNSToken = $stackPtr; - $namespace = false; - do { - $previousNSToken = $this->phpcsFile->findPrevious( \T_NAMESPACE, ( $previousNSToken - 1 ) ); - - // Stop if we encounter a scoped namespace declaration as we already know we're not in one. - if ( ! empty( $this->tokens[ $previousNSToken ]['scope_condition'] ) - && $this->tokens[ $previousNSToken ]['scope_condition'] === $previousNSToken - ) { - break; - } - - $namespace = $this->get_declared_namespace_name( $previousNSToken ); - - } while ( false === $namespace && false !== $previousNSToken ); - - // If we still haven't got a namespace, return an empty string. - if ( false === $namespace ) { - return ''; - } - - return $namespace; - } - - /** - * Get the complete namespace name for a namespace declaration. - * - * For hierarchical namespaces, the name will be composed of several tokens, - * i.e. MyProject\Sub\Level which will be returned together as one string. - * - * @since 0.12.0 A lesser variant of this method previously existed in the - * WordPress_AbstractClassRestrictionsSniff. - * - * @param int|bool $stackPtr The position of a T_NAMESPACE token. - * - * @return string|false Namespace name or false if not a namespace declaration. - * Namespace name can be an empty string for global namespace declaration. - */ - public function get_declared_namespace_name( $stackPtr ) { - - // Check for the existence of the token. - if ( false === $stackPtr || ! isset( $this->tokens[ $stackPtr ] ) ) { - return false; - } - - if ( \T_NAMESPACE !== $this->tokens[ $stackPtr ]['code'] ) { - return false; - } - - $nextToken = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true, null, true ); - if ( \T_NS_SEPARATOR === $this->tokens[ $nextToken ]['code'] ) { - // Not a namespace declaration, but use of, i.e. `namespace\someFunction();`. - return false; - } - - if ( \T_OPEN_CURLY_BRACKET === $this->tokens[ $nextToken ]['code'] ) { - // Declaration for global namespace when using multiple namespaces in a file. - // I.e.: `namespace {}`. - return ''; - } - - // Ok, this should be a namespace declaration, so get all the parts together. - $acceptedTokens = array( - \T_STRING => true, - \T_NS_SEPARATOR => true, - ); - $validTokens = $acceptedTokens + Tokens::$emptyTokens; - - $namespaceName = ''; - while ( isset( $validTokens[ $this->tokens[ $nextToken ]['code'] ] ) ) { - if ( isset( $acceptedTokens[ $this->tokens[ $nextToken ]['code'] ] ) ) { - $namespaceName .= trim( $this->tokens[ $nextToken ]['content'] ); - } - ++$nextToken; - } - - return $namespaceName; - } - - /** - * Check if a content string contains a specific html open tag. - * - * @since 0.11.0 - * @since 0.13.0 No longer allows for the PHP 5.2 bug for which the function was - * originally created. - * @since 0.13.0 The $stackPtr parameter is now optional. Either that or the - * $content parameter has to be passed. - * @deprecated 1.0.0 This method is only used by deprecated sniffs. - * - * @param string $tag_name The name of the HTML tag without brackets. So if - * searching for ' open tag, false otherwise. - */ - public function has_html_open_tag( $tag_name, $stackPtr = null, $content = false ) { - if ( false === $content && isset( $stackPtr ) ) { - $content = $this->tokens[ $stackPtr ]['content']; - } - - if ( ! empty( $content ) && false !== strpos( $content, '<' . $tag_name ) ) { - return true; - } - - return false; - } - - /** - * Check whether a T_CONST token is a class constant declaration. - * - * @since 0.14.0 - * - * @param int $stackPtr The position in the stack of the T_CONST token to verify. - * - * @return bool - */ - public function is_class_constant( $stackPtr ) { - if ( ! isset( $this->tokens[ $stackPtr ] ) || \T_CONST !== $this->tokens[ $stackPtr ]['code'] ) { - return false; - } - - // Note: traits can not declare constants. - $valid_scopes = array( - 'T_CLASS' => true, - 'T_ANON_CLASS' => true, - 'T_INTERFACE' => true, - ); - - return is_int( $this->valid_direct_scope( $stackPtr, $valid_scopes ) ); - } - - /** - * Check whether a T_VARIABLE token is a class property declaration. - * - * @since 0.14.0 - * - * @param int $stackPtr The position in the stack of the T_VARIABLE token to verify. - * - * @return bool - */ - public function is_class_property( $stackPtr ) { - if ( ! isset( $this->tokens[ $stackPtr ] ) || \T_VARIABLE !== $this->tokens[ $stackPtr ]['code'] ) { - return false; - } - - // Note: interfaces can not declare properties. - $valid_scopes = array( - 'T_CLASS' => true, - 'T_ANON_CLASS' => true, - 'T_TRAIT' => true, - ); - - $scopePtr = $this->valid_direct_scope( $stackPtr, $valid_scopes ); - if ( false !== $scopePtr ) { - // Make sure it's not a method parameter. - if ( empty( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) { - return true; - } else { - $parenthesis = array_keys( $this->tokens[ $stackPtr ]['nested_parenthesis'] ); - $deepest_open = array_pop( $parenthesis ); - if ( $deepest_open < $scopePtr - || isset( $this->tokens[ $deepest_open ]['parenthesis_owner'] ) === false - || T_FUNCTION !== $this->tokens[ $this->tokens[ $deepest_open ]['parenthesis_owner'] ]['code'] - ) { - return true; - } - } - } - - return false; - } - - /** - * Check whether the direct wrapping scope of a token is within a limited set of - * acceptable tokens. - * - * Used to check, for instance, if a T_CONST is a class constant. - * - * @since 0.14.0 - * - * @param int $stackPtr The position in the stack of the token to verify. - * @param array $valid_scopes Array of token types. - * Keys should be the token types in string format - * to allow for newer token types. - * Value is irrelevant. - * - * @return int|bool StackPtr to the scope if valid, false otherwise. - */ - protected function valid_direct_scope( $stackPtr, array $valid_scopes ) { - if ( empty( $this->tokens[ $stackPtr ]['conditions'] ) ) { - return false; - } - - /* - * Check only the direct wrapping scope of the token. - */ - $conditions = array_keys( $this->tokens[ $stackPtr ]['conditions'] ); - $ptr = array_pop( $conditions ); - - if ( ! isset( $this->tokens[ $ptr ] ) ) { - return false; - } - - if ( isset( $valid_scopes[ $this->tokens[ $ptr ]['type'] ] ) ) { - return $ptr; - } - - return false; - } - - /** - * Checks whether this is a call to a $wpdb method that we want to sniff. - * - * If available in the child class, the $methodPtr, $i and $end properties are - * automatically set to correspond to the start and end of the method call. - * The $i property is also set if this is not a method call but rather the - * use of a $wpdb property. - * - * @since 0.8.0 - * @since 0.9.0 The return value is now always boolean. The $end and $i member - * vars are automatically updated. - * @since 0.14.0 Moved this method from the `PreparedSQL` sniff to the base WP sniff. - * - * {@internal This method should probably be refactored.}} - * - * @param int $stackPtr The index of the $wpdb variable. - * @param array $target_methods Array of methods. Key(s) should be method name. - * - * @return bool Whether this is a $wpdb method call. - */ - protected function is_wpdb_method_call( $stackPtr, $target_methods ) { - - // Check for wpdb. - if ( ( \T_VARIABLE === $this->tokens[ $stackPtr ]['code'] && '$wpdb' !== $this->tokens[ $stackPtr ]['content'] ) - || ( \T_STRING === $this->tokens[ $stackPtr ]['code'] && 'wpdb' !== $this->tokens[ $stackPtr ]['content'] ) - ) { - return false; - } - - // Check that this is a method call. - $is_object_call = $this->phpcsFile->findNext( - array( \T_OBJECT_OPERATOR, \T_DOUBLE_COLON ), - ( $stackPtr + 1 ), - null, - false, - null, - true - ); - if ( false === $is_object_call ) { - return false; - } - - $methodPtr = $this->phpcsFile->findNext( \T_WHITESPACE, ( $is_object_call + 1 ), null, true, null, true ); - if ( false === $methodPtr ) { - return false; - } - - if ( \T_STRING === $this->tokens[ $methodPtr ]['code'] && property_exists( $this, 'methodPtr' ) ) { - $this->methodPtr = $methodPtr; - } - - // Find the opening parenthesis. - $opening_paren = $this->phpcsFile->findNext( \T_WHITESPACE, ( $methodPtr + 1 ), null, true, null, true ); - - if ( false === $opening_paren ) { - return false; - } - - if ( property_exists( $this, 'i' ) ) { - $this->i = $opening_paren; - } - - if ( \T_OPEN_PARENTHESIS !== $this->tokens[ $opening_paren ]['code'] - || ! isset( $this->tokens[ $opening_paren ]['parenthesis_closer'] ) - ) { - return false; - } - - // Check that this is one of the methods that we are interested in. - if ( ! isset( $target_methods[ $this->tokens[ $methodPtr ]['content'] ] ) ) { - return false; - } - - // Find the end of the first parameter. - $end = $this->phpcsFile->findEndOfStatement( $opening_paren + 1 ); - - if ( \T_COMMA !== $this->tokens[ $end ]['code'] ) { - ++$end; - } - - if ( property_exists( $this, 'end' ) ) { - $this->end = $end; - } - - return true; - } - - /** - * Determine whether an arbitrary T_STRING token is the use of a global constant. - * - * @since 1.0.0 - * - * @param int $stackPtr The position of the function call token. - * - * @return bool - */ - public function is_use_of_global_constant( $stackPtr ) { - // Check for the existence of the token. - if ( ! isset( $this->tokens[ $stackPtr ] ) ) { - return false; - } - - // Is this one of the tokens this function handles ? - if ( \T_STRING !== $this->tokens[ $stackPtr ]['code'] ) { - return false; - } - - $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); - if ( false !== $next - && ( \T_OPEN_PARENTHESIS === $this->tokens[ $next ]['code'] - || \T_DOUBLE_COLON === $this->tokens[ $next ]['code'] ) - ) { - // Function call or declaration. - return false; - } - - // Array of tokens which if found preceding the $stackPtr indicate that a T_STRING is not a global constant. - $tokens_to_ignore = array( - 'T_NAMESPACE' => true, - 'T_USE' => true, - 'T_CLASS' => true, - 'T_TRAIT' => true, - 'T_INTERFACE' => true, - 'T_EXTENDS' => true, - 'T_IMPLEMENTS' => true, - 'T_NEW' => true, - 'T_FUNCTION' => true, - 'T_DOUBLE_COLON' => true, - 'T_OBJECT_OPERATOR' => true, - 'T_INSTANCEOF' => true, - 'T_INSTEADOF' => true, - 'T_GOTO' => true, - 'T_AS' => true, - 'T_PUBLIC' => true, - 'T_PROTECTED' => true, - 'T_PRIVATE' => true, - ); - - $prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); - if ( false !== $prev - && isset( $tokens_to_ignore[ $this->tokens[ $prev ]['type'] ] ) - ) { - // Not the use of a constant. - return false; - } - - if ( false !== $prev - && \T_NS_SEPARATOR === $this->tokens[ $prev ]['code'] - && \T_STRING === $this->tokens[ ( $prev - 1 ) ]['code'] - ) { - // Namespaced constant of the same name. - return false; - } - - if ( false !== $prev - && \T_CONST === $this->tokens[ $prev ]['code'] - && $this->is_class_constant( $prev ) - ) { - // Class constant declaration of the same name. - return false; - } - - /* - * Deal with a number of variations of use statements. - */ - for ( $i = $stackPtr; $i > 0; $i-- ) { - if ( $this->tokens[ $i ]['line'] !== $this->tokens[ $stackPtr ]['line'] ) { - break; - } - } - - $firstOnLine = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true ); - if ( false !== $firstOnLine && \T_USE === $this->tokens[ $firstOnLine ]['code'] ) { - $nextOnLine = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $firstOnLine + 1 ), null, true ); - if ( false !== $nextOnLine ) { - if ( \T_STRING === $this->tokens[ $nextOnLine ]['code'] - && 'const' === $this->tokens[ $nextOnLine ]['content'] - ) { - $hasNsSep = $this->phpcsFile->findNext( \T_NS_SEPARATOR, ( $nextOnLine + 1 ), $stackPtr ); - if ( false !== $hasNsSep ) { - // Namespaced const (group) use statement. - return false; - } - } else { - // Not a const use statement. - return false; - } - } - } - - return true; - } - - /** - * Determine if a variable is in the `as $key => $value` part of a foreach condition. - * - * @since 1.0.0 - * @since 1.1.0 Moved from the PrefixAllGlobals sniff to the Sniff base class. - * - * @param int $stackPtr Pointer to the variable. - * - * @return bool True if it is. False otherwise. - */ - protected function is_foreach_as( $stackPtr ) { - if ( ! isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) { - return false; - } - - $nested_parenthesis = $this->tokens[ $stackPtr ]['nested_parenthesis']; - $close_parenthesis = end( $nested_parenthesis ); - $open_parenthesis = key( $nested_parenthesis ); - if ( ! isset( $this->tokens[ $close_parenthesis ]['parenthesis_owner'] ) ) { - return false; - } - - if ( \T_FOREACH !== $this->tokens[ $this->tokens[ $close_parenthesis ]['parenthesis_owner'] ]['code'] ) { - return false; - } - - $as_ptr = $this->phpcsFile->findNext( \T_AS, ( $open_parenthesis + 1 ), $close_parenthesis ); - if ( false === $as_ptr ) { - // Should never happen. - return false; - } - - return ( $stackPtr > $as_ptr ); - } - } diff --git a/WordPress/Sniffs/Arrays/ArrayAssignmentRestrictionsSniff.php b/WordPress/Sniffs/Arrays/ArrayAssignmentRestrictionsSniff.php deleted file mode 100644 index 43aa75e94f..0000000000 --- a/WordPress/Sniffs/Arrays/ArrayAssignmentRestrictionsSniff.php +++ /dev/null @@ -1,64 +0,0 @@ - array( - * 'groupname' => array( - * 'type' => 'error' | 'warning', - * 'message' => 'Dont use this one please!', - * 'keys' => array( 'key1', 'another_key' ), - * 'callback' => array( 'class', 'method' ), // Optional. - * ) - * ) - * - * @return array - */ - public function getGroups() { - return array(); - } - - /** - * Callback to process each confirmed key, to check value. - * - * @param string $key Array index / key. - * @param mixed $val Assigned value. - * @param int $line Token line. - * @param array $group Group definition. - * @return mixed FALSE if no match, TRUE if matches, STRING if matches - * with custom error message passed to ->process(). - */ - public function callback( $key, $val, $line, $group ) { - return true; - } - -} diff --git a/WordPress/Sniffs/Arrays/ArrayDeclarationSniff.php b/WordPress/Sniffs/Arrays/ArrayDeclarationSniff.php deleted file mode 100644 index 52d987f243..0000000000 --- a/WordPress/Sniffs/Arrays/ArrayDeclarationSniff.php +++ /dev/null @@ -1,61 +0,0 @@ - \T_ARRAY, - \T_OPEN_SHORT_ARRAY => \T_OPEN_SHORT_ARRAY, - ); - /** * Returns an array of tokens this test wants to listen for. * @@ -74,7 +58,7 @@ class ArrayDeclarationSpacingSniff extends Sniff { * @return array */ public function register() { - return $this->targets; + return Collections::arrayOpenTokensBC(); } /** @@ -88,10 +72,18 @@ public function register() { * @return void */ public function process_token( $stackPtr ) { + + if ( isset( Collections::shortArrayListOpenTokensBC()[ $this->tokens[ $stackPtr ]['code'] ] ) + && Arrays::isShortArray( $this->phpcsFile, $stackPtr ) === false + ) { + // Short list, not short array. + return; + } + /* * Determine the array opener & closer. */ - $array_open_close = $this->find_array_open_close( $stackPtr ); + $array_open_close = Arrays::getOpenClose( $this->phpcsFile, $stackPtr ); if ( false === $array_open_close ) { // Array open/close could not be determined. return; @@ -101,74 +93,16 @@ public function process_token( $stackPtr ) { $closer = $array_open_close['closer']; unset( $array_open_close ); - /* - * Long arrays only: Check for space between the array keyword and the open parenthesis. - */ - if ( \T_ARRAY === $this->tokens[ $stackPtr ]['code'] ) { - - if ( ( $stackPtr + 1 ) !== $opener ) { - $error = 'There must be no space between the "array" keyword and the opening parenthesis'; - $error_code = 'SpaceAfterKeyword'; - - $nextNonWhitespace = $this->phpcsFile->findNext( \T_WHITESPACE, ( $stackPtr + 1 ), ( $opener + 1 ), true ); - if ( $nextNonWhitespace !== $opener ) { - // Don't auto-fix: Something other than whitespace found between keyword and open parenthesis. - $this->phpcsFile->addError( $error, $stackPtr, $error_code ); - } else { - - $fix = $this->phpcsFile->addFixableError( $error, $stackPtr, $error_code ); - - if ( true === $fix ) { - $this->phpcsFile->fixer->beginChangeset(); - for ( $i = ( $stackPtr + 1 ); $i < $opener; $i++ ) { - $this->phpcsFile->fixer->replaceToken( $i, '' ); - } - $this->phpcsFile->fixer->endChangeset(); - unset( $i ); - } - } - unset( $error, $error_code, $nextNonWhitespace, $fix ); - } - } - - /* - * Check for empty arrays. - */ - $nextNonWhitespace = $this->phpcsFile->findNext( \T_WHITESPACE, ( $opener + 1 ), ( $closer + 1 ), true ); - if ( $nextNonWhitespace === $closer ) { - - if ( ( $opener + 1 ) !== $closer ) { - $fix = $this->phpcsFile->addFixableError( - 'Empty array declaration must have no space between the parentheses', - $stackPtr, - 'SpaceInEmptyArray' - ); - - if ( true === $fix ) { - $this->phpcsFile->fixer->beginChangeset(); - for ( $i = ( $opener + 1 ); $i < $closer; $i++ ) { - $this->phpcsFile->fixer->replaceToken( $i, '' ); - } - $this->phpcsFile->fixer->endChangeset(); - unset( $i ); - } - } - - // This array is empty, so the below checks aren't necessary. - return; - } - unset( $nextNonWhitespace ); - // Pass off to either the single line or multi-line array analysis. if ( $this->tokens[ $opener ]['line'] === $this->tokens[ $closer ]['line'] ) { $this->process_single_line_array( $stackPtr, $opener, $closer ); } else { - $this->process_multi_line_array( $stackPtr, $opener, $closer ); + $this->process_multi_line_array( $stackPtr, $opener ); } } /** - * Process a single-line array. + * Check that associative arrays are always multi-line. * * @since 0.13.0 The actual checks contained in this method used to * be in the `process()` method. @@ -180,148 +114,47 @@ public function process_token( $stackPtr ) { * @return void */ protected function process_single_line_array( $stackPtr, $opener, $closer ) { - /* - * Check that associative arrays are always multi-line. - */ - $array_has_keys = $this->phpcsFile->findNext( \T_DOUBLE_ARROW, $opener, $closer ); - if ( false !== $array_has_keys ) { - - $array_items = $this->get_function_call_parameters( $stackPtr ); - - if ( ( false === $this->allow_single_item_single_line_associative_arrays - && ! empty( $array_items ) ) - || ( true === $this->allow_single_item_single_line_associative_arrays - && \count( $array_items ) > 1 ) - ) { - /* - * Make sure the double arrow is for *this* array, not for a nested one. - */ - $array_has_keys = false; // Reset before doing more detailed check. - foreach ( $array_items as $item ) { - for ( $ptr = $item['start']; $ptr <= $item['end']; $ptr++ ) { - if ( \T_DOUBLE_ARROW === $this->tokens[ $ptr ]['code'] ) { - $array_has_keys = true; - break 2; - } - - // Skip passed any nested arrays. - if ( isset( $this->targets[ $this->tokens[ $ptr ]['code'] ] ) ) { - $nested_array_open_close = $this->find_array_open_close( $ptr ); - if ( false === $nested_array_open_close ) { - // Nested array open/close could not be determined. - continue; - } - - $ptr = $nested_array_open_close['closer']; - } - } - } - - if ( true === $array_has_keys ) { - - $phrase = 'an'; - if ( true === $this->allow_single_item_single_line_associative_arrays ) { - $phrase = 'a multi-item'; - } - $fix = $this->phpcsFile->addFixableError( - 'When %s array uses associative keys, each value should start on a new line.', - $closer, - 'AssociativeArrayFound', - array( $phrase ) - ); - - if ( true === $fix ) { - - $this->phpcsFile->fixer->beginChangeset(); - - foreach ( $array_items as $item ) { - /* - * Add a line break before the first non-empty token in the array item. - * Prevents extraneous whitespace at the start of the line which could be - * interpreted as alignment whitespace. - */ - $first_non_empty = $this->phpcsFile->findNext( - Tokens::$emptyTokens, - $item['start'], - ( $item['end'] + 1 ), - true - ); - if ( false === $first_non_empty ) { - continue; - } - - if ( $item['start'] <= ( $first_non_empty - 1 ) - && \T_WHITESPACE === $this->tokens[ ( $first_non_empty - 1 ) ]['code'] - ) { - // Remove whitespace which would otherwise becoming trailing - // (as it gives problems with the fixed file). - $this->phpcsFile->fixer->replaceToken( ( $first_non_empty - 1 ), '' ); - } - - $this->phpcsFile->fixer->addNewlineBefore( $first_non_empty ); - } - - $this->phpcsFile->fixer->endChangeset(); - } - - // No need to check for spacing around opener/closer as this array should be multi-line. - return; - } - } + $array_items = PassedParameters::getParameters( $this->phpcsFile, $stackPtr ); + if ( ( false === $this->allow_single_item_single_line_associative_arrays + && empty( $array_items ) ) + || ( true === $this->allow_single_item_single_line_associative_arrays + && \count( $array_items ) === 1 ) + ) { + return; } /* - * Check that there is a single space after the array opener and before the array closer. + * Make sure the double arrow is for *this* array, not for a nested one. */ - if ( \T_WHITESPACE !== $this->tokens[ ( $opener + 1 ) ]['code'] ) { - - $fix = $this->phpcsFile->addFixableError( - 'Missing space after array opener.', - $opener, - 'NoSpaceAfterArrayOpener' - ); - - if ( true === $fix ) { - $this->phpcsFile->fixer->addContent( $opener, ' ' ); - } - } elseif ( ' ' !== $this->tokens[ ( $opener + 1 ) ]['content'] ) { - - $fix = $this->phpcsFile->addFixableError( - 'Expected 1 space after array opener, found %s.', - $opener, - 'SpaceAfterArrayOpener', - array( \strlen( $this->tokens[ ( $opener + 1 ) ]['content'] ) ) - ); - - if ( true === $fix ) { - $this->phpcsFile->fixer->replaceToken( ( $opener + 1 ), ' ' ); + $array_has_keys = false; + foreach ( $array_items as $item ) { + if ( Arrays::getDoubleArrowPtr( $this->phpcsFile, $item['start'], $item['end'] ) !== false ) { + $array_has_keys = true; + break; } } - if ( \T_WHITESPACE !== $this->tokens[ ( $closer - 1 ) ]['code'] ) { - - $fix = $this->phpcsFile->addFixableError( - 'Missing space before array closer.', - $closer, - 'NoSpaceBeforeArrayCloser' - ); - - if ( true === $fix ) { - $this->phpcsFile->fixer->addContentBefore( $closer, ' ' ); - } - } elseif ( ' ' !== $this->tokens[ ( $closer - 1 ) ]['content'] ) { - - $fix = $this->phpcsFile->addFixableError( - 'Expected 1 space before array closer, found %s.', - $closer, - 'SpaceBeforeArrayCloser', - array( \strlen( $this->tokens[ ( $closer - 1 ) ]['content'] ) ) - ); - - if ( true === $fix ) { - $this->phpcsFile->fixer->replaceToken( ( $closer - 1 ), ' ' ); - } + if ( false === $array_has_keys ) { + return; } + $error = 'When an array uses associative keys, each value should start on %s.'; + if ( true === $this->allow_single_item_single_line_associative_arrays ) { + $error = 'When a multi-item array uses associative keys, each value should start on %s.'; + } + + /* + * Just add a new line before the array closer. + * The multi-line array fixer will then fix the individual array items in the next fixer loop. + */ + SpacesFixer::checkAndFix( + $this->phpcsFile, + $closer, + $this->phpcsFile->findPrevious( \T_WHITESPACE, ( $closer - 1 ), null, true ), + 'newline', + $error, + 'AssociativeArrayFound', + 'error' + ); } /** @@ -329,46 +162,18 @@ protected function process_single_line_array( $stackPtr, $opener, $closer ) { * * @since 0.13.0 The actual checks contained in this method used to * be in the `ArrayDeclaration` sniff. + * @since 3.0.0 Removed the `$closer` parameter. * * @param int $stackPtr The position of the current token in the stack. * @param int $opener The position of the array opener. - * @param int $closer The position of the array closer. * * @return void */ - protected function process_multi_line_array( $stackPtr, $opener, $closer ) { - /* - * Check that the closing bracket is on a new line. - */ - $last_content = $this->phpcsFile->findPrevious( \T_WHITESPACE, ( $closer - 1 ), $opener, true ); - if ( false !== $last_content - && $this->tokens[ $last_content ]['line'] === $this->tokens[ $closer ]['line'] - ) { - $fix = $this->phpcsFile->addFixableError( - 'Closing parenthesis of array declaration must be on a new line', - $closer, - 'CloseBraceNewLine' - ); - if ( true === $fix ) { - $this->phpcsFile->fixer->beginChangeset(); - - if ( $last_content < ( $closer - 1 ) - && \T_WHITESPACE === $this->tokens[ ( $closer - 1 ) ]['code'] - ) { - // Remove whitespace which would otherwise becoming trailing - // (as it gives problems with the fixed file). - $this->phpcsFile->fixer->replaceToken( ( $closer - 1 ), '' ); - } - - $this->phpcsFile->fixer->addNewlineBefore( $closer ); - $this->phpcsFile->fixer->endChangeset(); - } - } - + protected function process_multi_line_array( $stackPtr, $opener ) { /* * Check that each array item starts on a new line. */ - $array_items = $this->get_function_call_parameters( $stackPtr ); + $array_items = PassedParameters::getParameters( $this->phpcsFile, $stackPtr ); $end_of_last_item = $opener; foreach ( $array_items as $item ) { @@ -383,20 +188,39 @@ protected function process_multi_line_array( $stackPtr, $opener, $closer ) { ); // Ignore comments after array items if the next real content starts on a new line. - if ( \T_COMMENT === $this->tokens[ $first_content ]['code'] - || isset( $this->phpcsCommentTokens[ $this->tokens[ $first_content ]['type'] ] ) + if ( $this->tokens[ $first_content ]['line'] === $this->tokens[ $end_of_last_item ]['line'] + && ( \T_COMMENT === $this->tokens[ $first_content ]['code'] + || isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $first_content ]['code'] ] ) ) ) { + $end_of_comment = $first_content; + + // Find the end of (multi-line) /* */- style trailing comments. + if ( substr( ltrim( $this->tokens[ $end_of_comment ]['content'] ), 0, 2 ) === '/*' ) { + while ( ( \T_COMMENT === $this->tokens[ $end_of_comment ]['code'] + || isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $end_of_comment ]['code'] ] ) ) + && substr( rtrim( $this->tokens[ $end_of_comment ]['content'] ), -2 ) !== '*/' + && ( $end_of_comment + 1 ) < $end_of_this_item + ) { + ++$end_of_comment; + } + + if ( $this->tokens[ $end_of_comment ]['line'] !== $this->tokens[ $end_of_last_item ]['line'] ) { + // Multi-line trailing comment. + $end_of_last_item = $end_of_comment; + } + } + $next = $this->phpcsFile->findNext( array( \T_WHITESPACE, \T_DOC_COMMENT_WHITESPACE ), - ( $first_content + 1 ), + ( $end_of_comment + 1 ), $end_of_this_item, true ); if ( false === $next ) { // Shouldn't happen, but just in case. - $end_of_last_item = $end_of_this_item; - continue; + $end_of_last_item = $end_of_this_item; // @codeCoverageIgnore + continue; // @codeCoverageIgnore } if ( $this->tokens[ $next ]['line'] !== $this->tokens[ $first_content ]['line'] ) { @@ -406,37 +230,23 @@ protected function process_multi_line_array( $stackPtr, $opener, $closer ) { if ( false === $first_content ) { // Shouldn't happen, but just in case. - $end_of_last_item = $end_of_this_item; - continue; + $end_of_last_item = $end_of_this_item; // @codeCoverageIgnore + continue; // @codeCoverageIgnore } if ( $this->tokens[ $end_of_last_item ]['line'] === $this->tokens[ $first_content ]['line'] ) { - - $fix = $this->phpcsFile->addFixableError( - 'Each item in a multi-line array must be on a new line', + SpacesFixer::checkAndFix( + $this->phpcsFile, $first_content, - 'ArrayItemNoNewLine' + $end_of_last_item, + 'newline', + 'Each item in a multi-line array must be on %s. Found: %s', + 'ArrayItemNoNewLine', + 'error' ); - - if ( true === $fix ) { - - $this->phpcsFile->fixer->beginChangeset(); - - if ( $item['start'] <= ( $first_content - 1 ) - && \T_WHITESPACE === $this->tokens[ ( $first_content - 1 ) ]['code'] - ) { - // Remove whitespace which would otherwise becoming trailing - // (as it gives problems with the fixed file). - $this->phpcsFile->fixer->replaceToken( ( $first_content - 1 ), '' ); - } - - $this->phpcsFile->fixer->addNewlineBefore( $first_content ); - $this->phpcsFile->fixer->endChangeset(); - } } $end_of_last_item = $end_of_this_item; } } - } diff --git a/WordPress/Sniffs/Arrays/ArrayIndentationSniff.php b/WordPress/Sniffs/Arrays/ArrayIndentationSniff.php index 96eb4f0dfb..76bcda60c1 100644 --- a/WordPress/Sniffs/Arrays/ArrayIndentationSniff.php +++ b/WordPress/Sniffs/Arrays/ArrayIndentationSniff.php @@ -3,30 +3,31 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\Arrays; +namespace WordPressCS\WordPress\Sniffs\Arrays; -use WordPress\Sniff; -use WordPress\PHPCSHelper; -use PHP_CodeSniffer_Tokens as Tokens; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\BackCompat\Helper; +use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\Arrays; +use PHPCSUtils\Utils\PassedParameters; +use WordPressCS\WordPress\Sniff; /** * Enforces WordPress array indentation for multi-line arrays. * - * @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#indentation + * @link https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#indentation * - * @package WPCS\WordPressCodingStandards - * - * @since 0.12.0 - * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.12.0 + * @since 0.13.0 Class name changed: this class is now namespaced. * * {@internal This sniff should eventually be pulled upstream as part of a solution * for https://github.com/squizlabs/PHP_CodeSniffer/issues/582 }} */ -class ArrayIndentationSniff extends Sniff { +final class ArrayIndentationSniff extends Sniff { /** * Should tabs be used for indenting? @@ -74,10 +75,7 @@ public function register() { unset( $this->ignore_tokens[ \T_START_HEREDOC ], $this->ignore_tokens[ \T_START_NOWDOC ] ); $this->ignore_tokens[ \T_INLINE_HTML ] = \T_INLINE_HTML; - return array( - \T_ARRAY, - \T_OPEN_SHORT_ARRAY, - ); + return Collections::arrayOpenTokensBC(); } /** @@ -85,17 +83,25 @@ public function register() { * * @param int $stackPtr The position of the current token in the stack. * - * @return void + * @return int|void Integer stack pointer to skip forward or void to continue + * normal file processing. */ public function process_token( $stackPtr ) { if ( ! isset( $this->tab_width ) ) { - $this->tab_width = PHPCSHelper::get_tab_width( $this->phpcsFile ); + $this->tab_width = Helper::getTabWidth( $this->phpcsFile ); + } + + if ( isset( Collections::shortArrayListOpenTokensBC()[ $this->tokens[ $stackPtr ]['code'] ] ) + && Arrays::isShortArray( $this->phpcsFile, $stackPtr ) === false + ) { + // Short list, not short array. + return; } /* * Determine the array opener & closer. */ - $array_open_close = $this->find_array_open_close( $stackPtr ); + $array_open_close = Arrays::getOpenClose( $this->phpcsFile, $stackPtr ); if ( false === $array_open_close ) { // Array open/close could not be determined. return; @@ -157,7 +163,7 @@ public function process_token( $stackPtr ) { /* * Verify & correct the array item indentation. */ - $array_items = $this->get_function_call_parameters( $stackPtr ); + $array_items = PassedParameters::getParameters( $this->phpcsFile, $stackPtr ); if ( empty( $array_items ) ) { // Strange, no array items found. return; @@ -198,13 +204,22 @@ public function process_token( $stackPtr ) { // Bow out from reporting and fixing mixed multi-line/single-line arrays. // That is handled by the ArrayDeclarationSpacingSniff. - if ( $this->tokens[ $first_content ]['line'] === $this->tokens[ $end_of_previous_item ]['line'] - || ( 1 !== $this->tokens[ $first_content ]['column'] - && \T_WHITESPACE !== $this->tokens[ ( $first_content - 1 ) ]['code'] ) - ) { + if ( $this->tokens[ $first_content ]['line'] === $this->tokens[ $end_of_previous_item ]['line'] ) { return $closer; } + // Ignore this item if there is anything but whitespace before the start of the next item. + if ( 1 !== $this->tokens[ $first_content ]['column'] ) { + // Go to the start of the line. + $i = $first_content; + while ( 1 !== $this->tokens[ --$i ]['column'] ); + + if ( \T_WHITESPACE !== $this->tokens[ $i ]['code'] ) { + $end_of_previous_item = $end_of_this_item; + continue; + } + } + $found_spaces = ( $this->tokens[ $first_content ]['column'] - 1 ); if ( $found_spaces !== $expected_spaces ) { @@ -412,9 +427,7 @@ protected function ignore_token( $ptr ) { * If it's a subsequent line of a multi-line sting, it will not start with a quote * character, nor just *be* a quote character. */ - if ( \T_CONSTANT_ENCAPSED_STRING === $token_code - || \T_DOUBLE_QUOTED_STRING === $token_code - ) { + if ( isset( Tokens::$stringTokens[ $token_code ] ) === true ) { // Deal with closing quote of a multi-line string being on its own line. if ( "'" === $this->tokens[ $ptr ]['content'] || '"' === $this->tokens[ $ptr ]['content'] @@ -508,6 +521,8 @@ protected function get_indentation_string( $nr ) { * @param int $expected Expected nr of spaces (tabs translated to space value). * @param int $found Found nr of spaces (tabs translated to space value). * @param string $new_indent Whitespace indent replacement content. + * + * @return void */ protected function add_array_alignment_error( $ptr, $error, $error_code, $expected, $found, $new_indent ) { @@ -522,6 +537,8 @@ protected function add_array_alignment_error( $ptr, $error, $error_code, $expect * * @param int $ptr Stack pointer to the first content on the line. * @param string $new_indent Whitespace indent replacement content. + * + * @return void */ protected function fix_alignment_error( $ptr, $new_indent ) { if ( 1 === $this->tokens[ $ptr ]['column'] ) { @@ -530,5 +547,4 @@ protected function fix_alignment_error( $ptr, $new_indent ) { $this->phpcsFile->fixer->replaceToken( ( $ptr - 1 ), $new_indent ); } } - } diff --git a/WordPress/Sniffs/Arrays/ArrayKeySpacingRestrictionsSniff.php b/WordPress/Sniffs/Arrays/ArrayKeySpacingRestrictionsSniff.php index 9cd040fc14..b32fb26ab0 100644 --- a/WordPress/Sniffs/Arrays/ArrayKeySpacingRestrictionsSniff.php +++ b/WordPress/Sniffs/Arrays/ArrayKeySpacingRestrictionsSniff.php @@ -3,27 +3,27 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\Arrays; +namespace WordPressCS\WordPress\Sniffs\Arrays; -use WordPress\Sniff; +use PHPCSUtils\Fixers\SpacesFixer; +use WordPressCS\WordPress\Sniff; /** * Check for proper spacing in array key references. * - * @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#space-usage + * @link https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#space-usage * - * @package WPCS\WordPressCodingStandards - * - * @since 0.3.0 - * @since 0.7.0 This sniff now has the ability to fix a number of the issues it flags. - * @since 0.12.0 This class now extends WordPress_Sniff. - * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.3.0 + * @since 0.7.0 This sniff now has the ability to fix a number of the issues it flags. + * @since 0.12.0 This class now extends the WordPressCS native `Sniff` class. + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 2.2.0 The sniff now also checks the size of the spacing, if applicable. */ -class ArrayKeySpacingRestrictionsSniff extends Sniff { +final class ArrayKeySpacingRestrictionsSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. @@ -47,44 +47,128 @@ public function process_token( $stackPtr ) { $token = $this->tokens[ $stackPtr ]; if ( ! isset( $token['bracket_closer'] ) ) { - $this->phpcsFile->addWarning( 'Missing bracket closer.', $stackPtr, 'MissingBracketCloser' ); return; } - $need_spaces = $this->phpcsFile->findNext( - array( \T_CONSTANT_ENCAPSED_STRING, \T_LNUMBER, \T_WHITESPACE, \T_MINUS ), - ( $stackPtr + 1 ), - $token['bracket_closer'], - true - ); + /* + * Handle square brackets without a key (array assignments) first. + */ + $first_non_ws = $this->phpcsFile->findNext( \T_WHITESPACE, ( $stackPtr + 1 ), null, true ); + if ( $first_non_ws === $token['bracket_closer'] ) { + $error = 'There should be %1$s between the square brackets for an array assignment without an explicit key. Found: %2$s'; + SpacesFixer::checkAndFix( + $this->phpcsFile, + $stackPtr, + $token['bracket_closer'], + 0, + $error, + 'SpacesBetweenBrackets' + ); + + return; + } + + /* + * Handle the spaces around explicit array keys. + */ + $needs_spaces = true; - $spaced1 = ( \T_WHITESPACE === $this->tokens[ ( $stackPtr + 1 ) ]['code'] ); - $spaced2 = ( \T_WHITESPACE === $this->tokens[ ( $token['bracket_closer'] - 1 ) ]['code'] ); + // Skip over a potential plus/minus sign for integers. + $first_effective = $first_non_ws; + if ( \T_MINUS === $this->tokens[ $first_effective ]['code'] || \T_PLUS === $this->tokens[ $first_effective ]['code'] ) { + $first_effective = $this->phpcsFile->findNext( \T_WHITESPACE, ( $first_effective + 1 ), null, true ); + } + + $next_non_ws = $this->phpcsFile->findNext( \T_WHITESPACE, ( $first_effective + 1 ), null, true ); + if ( ( \T_CONSTANT_ENCAPSED_STRING === $this->tokens[ $first_effective ]['code'] + || \T_LNUMBER === $this->tokens[ $first_effective ]['code'] ) + && $next_non_ws === $token['bracket_closer'] + ) { + $needs_spaces = false; + } + + $has_space_after_opener = ( \T_WHITESPACE === $this->tokens[ ( $stackPtr + 1 ) ]['code'] ); + $has_space_before_close = ( \T_WHITESPACE === $this->tokens[ ( $token['bracket_closer'] - 1 ) ]['code'] ); - // It should have spaces unless if it only has strings or numbers as the key. - if ( false !== $need_spaces && ! ( $spaced1 && $spaced2 ) ) { + // The array key should be surrounded by spaces unless the key only consists of a string or an integer. + if ( true === $needs_spaces + && ( false === $has_space_after_opener || false === $has_space_before_close ) + ) { $error = 'Array keys must be surrounded by spaces unless they contain a string or an integer.'; $fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'NoSpacesAroundArrayKeys' ); if ( true === $fix ) { - if ( ! $spaced1 ) { - $this->phpcsFile->fixer->addContentBefore( ( $stackPtr + 1 ), ' ' ); + $this->phpcsFile->fixer->beginChangeset(); + + if ( false === $has_space_after_opener ) { + $this->phpcsFile->fixer->addContent( $stackPtr, ' ' ); } - if ( ! $spaced2 ) { + + if ( false === $has_space_before_close ) { $this->phpcsFile->fixer->addContentBefore( $token['bracket_closer'], ' ' ); } + + $this->phpcsFile->fixer->endChangeset(); } - } elseif ( false === $need_spaces && ( $spaced1 || $spaced2 ) ) { + } elseif ( false === $needs_spaces && ( $has_space_after_opener || $has_space_before_close ) ) { $error = 'Array keys must NOT be surrounded by spaces if they only contain a string or an integer.'; $fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'SpacesAroundArrayKeys' ); if ( true === $fix ) { - if ( $spaced1 ) { - $this->phpcsFile->fixer->replaceToken( ( $stackPtr + 1 ), '' ); + if ( $has_space_after_opener ) { + $this->phpcsFile->fixer->beginChangeset(); + + for ( $i = ( $stackPtr + 1 ); $i < $token['bracket_closer']; $i++ ) { + if ( \T_WHITESPACE !== $this->tokens[ $i ]['code'] ) { + break; + } + + $this->phpcsFile->fixer->replaceToken( $i, '' ); + } + + $this->phpcsFile->fixer->endChangeset(); } - if ( $spaced2 ) { - $this->phpcsFile->fixer->replaceToken( ( $token['bracket_closer'] - 1 ), '' ); + + if ( $has_space_before_close ) { + $this->phpcsFile->fixer->beginChangeset(); + + for ( $i = ( $token['bracket_closer'] - 1 ); $i > $stackPtr; $i-- ) { + if ( \T_WHITESPACE !== $this->tokens[ $i ]['code'] ) { + break; + } + + $this->phpcsFile->fixer->replaceToken( $i, '' ); + } + + $this->phpcsFile->fixer->endChangeset(); } } } - } + // If spaces are needed, check that there is only one space. + if ( true === $needs_spaces ) { + if ( $has_space_after_opener ) { + $error = 'There should be exactly %1$s before the array key. Found: %2$s'; + SpacesFixer::checkAndFix( + $this->phpcsFile, + $stackPtr, + $first_non_ws, + 1, + $error, + 'TooMuchSpaceBeforeKey' + ); + } + + if ( $has_space_before_close ) { + $last_non_ws = $this->phpcsFile->findPrevious( \T_WHITESPACE, ( $token['bracket_closer'] - 1 ), null, true ); + $error = 'There should be exactly %1$s after the array key. Found: %2$s'; + SpacesFixer::checkAndFix( + $this->phpcsFile, + $last_non_ws, + $token['bracket_closer'], + 1, + $error, + 'TooMuchSpaceAfterKey' + ); + } + } + } } diff --git a/WordPress/Sniffs/Arrays/CommaAfterArrayItemSniff.php b/WordPress/Sniffs/Arrays/CommaAfterArrayItemSniff.php deleted file mode 100644 index 80ffde2727..0000000000 --- a/WordPress/Sniffs/Arrays/CommaAfterArrayItemSniff.php +++ /dev/null @@ -1,291 +0,0 @@ -find_array_open_close( $stackPtr ); - if ( false === $array_open_close ) { - // Array open/close could not be determined. - return; - } - - $opener = $array_open_close['opener']; - $closer = $array_open_close['closer']; - unset( $array_open_close ); - - // This array is empty, so the below checks aren't necessary. - if ( ( $opener + 1 ) === $closer ) { - return; - } - - $single_line = true; - if ( $this->tokens[ $opener ]['line'] !== $this->tokens[ $closer ]['line'] ) { - $single_line = false; - } - - $array_items = $this->get_function_call_parameters( $stackPtr ); - if ( empty( $array_items ) ) { - // Strange, no array items found. - return; - } - - $array_item_count = \count( $array_items ); - - // Note: $item_index is 1-based and the array items are split on the commas! - foreach ( $array_items as $item_index => $item ) { - $maybe_comma = ( $item['end'] + 1 ); - $is_comma = false; - if ( isset( $this->tokens[ $maybe_comma ] ) && \T_COMMA === $this->tokens[ $maybe_comma ]['code'] ) { - $is_comma = true; - } - - /* - * Check if this is a comma at the end of the last item in a single line array. - */ - if ( true === $single_line && $item_index === $array_item_count ) { - - if ( true === $is_comma ) { - $fix = $this->phpcsFile->addFixableError( - 'Comma not allowed after last value in single-line array declaration', - $maybe_comma, - 'CommaAfterLast' - ); - - if ( true === $fix ) { - $this->phpcsFile->fixer->replaceToken( $maybe_comma, '' ); - } - } - - /* - * No need to do the spacing checks for the last item in a single line array. - * This is handled by another sniff checking the spacing before the array closer. - */ - continue; - } - - $last_content = $this->phpcsFile->findPrevious( - Tokens::$emptyTokens, - $item['end'], - $item['start'], - true - ); - - if ( false === $last_content ) { - // Shouldn't be able to happen, but just in case, ignore this array item. - continue; - } - - /** - * Make sure every item in a multi-line array has a comma at the end. - * - * Should in reality only be triggered by the last item in a multi-line array - * as otherwise we'd have a parse error already. - */ - if ( false === $is_comma && false === $single_line ) { - - $fix = $this->phpcsFile->addFixableError( - 'Each array item in a multi-line array declaration must end in a comma', - $last_content, - 'NoComma' - ); - - if ( true === $fix ) { - $this->phpcsFile->fixer->addContent( $last_content, ',' ); - } - } - - if ( false === $is_comma ) { - // Can't check spacing around the comma if there is no comma. - continue; - } - - /* - * Check for whitespace at the end of the array item. - */ - if ( $last_content !== $item['end'] - // Ignore whitespace at the end of a multi-line item if it is the end of a heredoc/nowdoc. - && ( true === $single_line - || ! isset( Tokens::$heredocTokens[ $this->tokens[ $last_content ]['code'] ] ) ) - ) { - $newlines = 0; - $spaces = 0; - for ( $i = $item['end']; $i > $last_content; $i-- ) { - - if ( \T_WHITESPACE === $this->tokens[ $i ]['code'] ) { - if ( $this->tokens[ $i ]['content'] === $this->phpcsFile->eolChar ) { - $newlines++; - } else { - $spaces += $this->tokens[ $i ]['length']; - } - } elseif ( \T_COMMENT === $this->tokens[ $i ]['code'] - || isset( $this->phpcsCommentTokens[ $this->tokens[ $i ]['type'] ] ) - ) { - break; - } - } - - $space_phrases = array(); - if ( $spaces > 0 ) { - $space_phrases[] = $spaces . ' spaces'; - } - if ( $newlines > 0 ) { - $space_phrases[] = $newlines . ' newlines'; - } - unset( $newlines, $spaces ); - - $fix = $this->phpcsFile->addFixableError( - 'Expected 0 spaces between "%s" and comma; %s found', - $maybe_comma, - 'SpaceBeforeComma', - array( - $this->tokens[ $last_content ]['content'], - implode( ' and ', $space_phrases ), - ) - ); - - if ( true === $fix ) { - $this->phpcsFile->fixer->beginChangeset(); - for ( $i = $item['end']; $i > $last_content; $i-- ) { - - if ( \T_WHITESPACE === $this->tokens[ $i ]['code'] ) { - $this->phpcsFile->fixer->replaceToken( $i, '' ); - - } elseif ( \T_COMMENT === $this->tokens[ $i ]['code'] - || isset( $this->phpcsCommentTokens[ $this->tokens[ $i ]['type'] ] ) - ) { - // We need to move the comma to before the comment. - $this->phpcsFile->fixer->addContent( $last_content, ',' ); - $this->phpcsFile->fixer->replaceToken( $maybe_comma, '' ); - - /* - * No need to worry about removing too much whitespace in - * combination with a `//` comment as in that case, the newline - * is part of the comment, so we're good. - */ - - break; - } - } - $this->phpcsFile->fixer->endChangeset(); - } - } - - if ( ! isset( $this->tokens[ ( $maybe_comma + 1 ) ] ) ) { - // Shouldn't be able to happen, but just in case. - continue; - } - - /* - * Check whitespace after the comma. - */ - $next_token = $this->tokens[ ( $maybe_comma + 1 ) ]; - - if ( \T_WHITESPACE === $next_token['code'] ) { - - if ( false === $single_line && $this->phpcsFile->eolChar === $next_token['content'] ) { - continue; - } - - $next_non_whitespace = $this->phpcsFile->findNext( - \T_WHITESPACE, - ( $maybe_comma + 1 ), - $closer, - true - ); - - if ( false === $next_non_whitespace - || ( false === $single_line - && $this->tokens[ $next_non_whitespace ]['line'] === $this->tokens[ $maybe_comma ]['line'] - && ( \T_COMMENT === $this->tokens[ $next_non_whitespace ]['code'] - || isset( $this->phpcsCommentTokens[ $this->tokens[ $next_non_whitespace ]['type'] ] ) ) ) - ) { - continue; - } - - $space_length = $next_token['length']; - if ( 1 === $space_length ) { - continue; - } - - $fix = $this->phpcsFile->addFixableError( - 'Expected 1 space between comma and "%s"; %s found', - $maybe_comma, - 'SpaceAfterComma', - array( - $this->tokens[ $next_non_whitespace ]['content'], - $space_length, - ) - ); - - if ( true === $fix ) { - $this->phpcsFile->fixer->replaceToken( ( $maybe_comma + 1 ), ' ' ); - } - } else { - // This is either a comment or a mixed single/multi-line array. - // Just add a space and let other sniffs sort out the array layout. - $fix = $this->phpcsFile->addFixableError( - 'Expected 1 space between comma and "%s"; 0 found', - $maybe_comma, - 'NoSpaceAfterComma', - array( $next_token['content'] ) - ); - - if ( true === $fix ) { - $this->phpcsFile->fixer->addContent( $maybe_comma, ' ' ); - } - } - } - } - -} diff --git a/WordPress/Sniffs/Arrays/MultipleStatementAlignmentSniff.php b/WordPress/Sniffs/Arrays/MultipleStatementAlignmentSniff.php index d06994b883..87630c867d 100644 --- a/WordPress/Sniffs/Arrays/MultipleStatementAlignmentSniff.php +++ b/WordPress/Sniffs/Arrays/MultipleStatementAlignmentSniff.php @@ -3,13 +3,16 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\Arrays; +namespace WordPressCS\WordPress\Sniffs\Arrays; -use WordPress\Sniff; +use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\Arrays; +use PHPCSUtils\Utils\PassedParameters; +use WordPressCS\WordPress\Sniff; /** * Enforces alignment of the double arrow assignment operator for multi-item, multi-line arrays. @@ -19,16 +22,14 @@ * - Allows for new line(s) before a double arrow (configurable). * - Allows for handling multi-line array items differently if so desired (configurable). * - * @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#indentation + * @link https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#indentation * - * @package WPCS\WordPressCodingStandards - * - * @since 0.14.0 + * @since 0.14.0 * * {@internal This sniff should eventually be pulled upstream as part of a solution * for https://github.com/squizlabs/PHP_CodeSniffer/issues/582 }} */ -class MultipleStatementAlignmentSniff extends Sniff { +final class MultipleStatementAlignmentSniff extends Sniff { /** * Whether or not to ignore an array item for the purpose of alignment @@ -147,10 +148,7 @@ class MultipleStatementAlignmentSniff extends Sniff { * @return array */ public function register() { - return array( - \T_ARRAY, - \T_OPEN_SHORT_ARRAY, - ); + return Collections::arrayOpenTokensBC(); } /** @@ -164,10 +162,18 @@ public function register() { * normal file processing. */ public function process_token( $stackPtr ) { + + if ( isset( Collections::shortArrayListOpenTokensBC()[ $this->tokens[ $stackPtr ]['code'] ] ) + && Arrays::isShortArray( $this->phpcsFile, $stackPtr ) === false + ) { + // Short list, not short array. + return; + } + /* * Determine the array opener & closer. */ - $array_open_close = $this->find_array_open_close( $stackPtr ); + $array_open_close = Arrays::getOpenClose( $this->phpcsFile, $stackPtr ); if ( false === $array_open_close ) { // Array open/close could not be determined. return; @@ -176,7 +182,7 @@ public function process_token( $stackPtr ) { $opener = $array_open_close['opener']; $closer = $array_open_close['closer']; - $array_items = $this->get_function_call_parameters( $stackPtr ); + $array_items = PassedParameters::getParameters( $this->phpcsFile, $stackPtr ); if ( empty( $array_items ) ) { return; } @@ -285,34 +291,9 @@ protected function process_multi_line_array( $stackPtr, $items, $opener, $closer $total_items = \count( $items ); foreach ( $items as $key => $item ) { - if ( strpos( $item['raw'], '=>' ) === false ) { - // Ignore items without assignment operators. - unset( $items[ $key ] ); - continue; - } - - // Find the position of the first double arrow. - $double_arrow = $this->phpcsFile->findNext( - \T_DOUBLE_ARROW, - $item['start'], - ( $item['end'] + 1 ) - ); - + // Find the double arrow if there is one. + $double_arrow = Arrays::getDoubleArrowPtr( $this->phpcsFile, $item['start'], $item['end'] ); if ( false === $double_arrow ) { - // Shouldn't happen, just in case. - unset( $items[ $key ] ); - continue; - } - - // Make sure the arrow is for this item and not for a nested array value assignment. - $has_array_opener = $this->phpcsFile->findNext( - $this->register(), - $item['start'], - $double_arrow - ); - - if ( false !== $has_array_opener ) { - // Double arrow is for a nested array. unset( $items[ $key ] ); continue; } @@ -325,12 +306,6 @@ protected function process_multi_line_array( $stackPtr, $items, $opener, $closer true ); - if ( false === $last_index_token ) { - // Shouldn't happen, but just in case. - unset( $items[ $key ] ); - continue; - } - if ( true === $this->ignoreNewlines && $this->tokens[ $last_index_token ]['line'] !== $this->tokens[ $double_arrow ]['line'] ) { @@ -348,7 +323,7 @@ protected function process_multi_line_array( $stackPtr, $items, $opener, $closer $items[ $key ]['single_line'] = true; } else { $items[ $key ]['single_line'] = false; - $multi_line_count++; + ++$multi_line_count; } if ( ( $index_end_position + 2 ) <= $this->maxColumn ) { @@ -358,10 +333,10 @@ protected function process_multi_line_array( $stackPtr, $items, $opener, $closer if ( ! isset( $double_arrow_cols[ $this->tokens[ $double_arrow ]['column'] ] ) ) { $double_arrow_cols[ $this->tokens[ $double_arrow ]['column'] ] = 1; } else { - $double_arrow_cols[ $this->tokens[ $double_arrow ]['column'] ]++; + ++$double_arrow_cols[ $this->tokens[ $double_arrow ]['column'] ]; } } - unset( $key, $item, $double_arrow, $has_array_opener, $last_index_token ); + unset( $key, $item, $double_arrow, $last_index_token ); if ( empty( $items ) || empty( $index_end_cols ) ) { // No actionable array items found. @@ -403,7 +378,7 @@ protected function process_multi_line_array( $stackPtr, $items, $opener, $closer if ( ! isset( $double_arrow_cols[ $this->tokens[ $item['operatorPtr'] ]['column'] ] ) ) { $double_arrow_cols[ $this->tokens[ $item['operatorPtr'] ]['column'] ] = 1; } else { - $double_arrow_cols[ $this->tokens[ $item['operatorPtr'] ]['column'] ]++; + ++$double_arrow_cols[ $this->tokens[ $item['operatorPtr'] ]['column'] ]; } } } @@ -455,12 +430,10 @@ protected function process_multi_line_array( $stackPtr, $items, $opener, $closer if ( \T_WHITESPACE !== $this->tokens[ ( $item['operatorPtr'] - 1 ) ]['code'] ) { $before = 0; + } elseif ( $this->tokens[ $item['last_index_token'] ]['line'] !== $this->tokens[ $item['operatorPtr'] ]['line'] ) { + $before = 'newline'; } else { - if ( $this->tokens[ $item['last_index_token'] ]['line'] !== $this->tokens[ $item['operatorPtr'] ]['line'] ) { - $before = 'newline'; - } else { - $before = $this->tokens[ ( $item['operatorPtr'] - 1 ) ]['length']; - } + $before = $this->tokens[ ( $item['operatorPtr'] - 1 ) ]['length']; } /* @@ -570,6 +543,8 @@ protected function process_multi_line_array( $stackPtr, $items, $opener, $closer * This message may be thrown more than once if the property is being changed inline in a file. * * @since 0.14.0 + * + * @return void */ protected function validate_align_multiline_items() { $alignMultilineItems = $this->alignMultilineItems; @@ -605,5 +580,4 @@ protected function validate_align_multiline_items() { // Reset to the default if an invalid value was received. $this->alignMultilineItems = 'always'; } - } diff --git a/WordPress/Sniffs/CSRF/NonceVerificationSniff.php b/WordPress/Sniffs/CSRF/NonceVerificationSniff.php deleted file mode 100644 index a0185ed3ff..0000000000 --- a/WordPress/Sniffs/CSRF/NonceVerificationSniff.php +++ /dev/null @@ -1,75 +0,0 @@ - false, - 'FoundPropertyForDeprecatedSniff' => false, - ); - - /** - * Don't use. - * - * @deprecated 1.0.0 - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return void|int - */ - public function process_token( $stackPtr ) { - if ( false === $this->thrown['DeprecatedSniff'] ) { - $this->thrown['DeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.CSRF.NonceVerification" sniff has been renamed to "WordPress.Security.NonceVerification". Please update your custom ruleset.', - 0, - 'DeprecatedSniff' - ); - } - - if ( false === $this->thrown['FoundPropertyForDeprecatedSniff'] - && ( ( array() !== $this->customNonceVerificationFunctions && $this->customNonceVerificationFunctions !== $this->addedCustomFunctions['nonce'] ) - || ( array() !== $this->customSanitizingFunctions && $this->customSanitizingFunctions !== $this->addedCustomFunctions['sanitize'] ) - || ( array() !== $this->customUnslashingSanitizingFunctions && $this->customUnslashingSanitizingFunctions !== $this->addedCustomFunctions['unslashsanitize'] ) ) - ) { - $this->thrown['FoundPropertyForDeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.CSRF.NonceVerification" sniff has been renamed to "WordPress.Security.NonceVerification". Please update your custom ruleset.', - 0, - 'FoundPropertyForDeprecatedSniff' - ); - } - - return parent::process_token( $stackPtr ); - } - -} diff --git a/WordPress/Sniffs/Classes/ClassInstantiationSniff.php b/WordPress/Sniffs/Classes/ClassInstantiationSniff.php deleted file mode 100644 index e68f0733b2..0000000000 --- a/WordPress/Sniffs/Classes/ClassInstantiationSniff.php +++ /dev/null @@ -1,204 +0,0 @@ -classname_tokens = Tokens::$emptyTokens; - $this->classname_tokens[ \T_NS_SEPARATOR ] = \T_NS_SEPARATOR; - $this->classname_tokens[ \T_STRING ] = \T_STRING; - $this->classname_tokens[ \T_SELF ] = \T_SELF; - $this->classname_tokens[ \T_STATIC ] = \T_STATIC; - $this->classname_tokens[ \T_PARENT ] = \T_PARENT; - $this->classname_tokens[ \T_ANON_CLASS ] = \T_ANON_CLASS; - - // Classname in a variable. - $this->classname_tokens[ \T_VARIABLE ] = \T_VARIABLE; - $this->classname_tokens[ \T_DOUBLE_COLON ] = \T_DOUBLE_COLON; - $this->classname_tokens[ \T_OBJECT_OPERATOR ] = \T_OBJECT_OPERATOR; - $this->classname_tokens[ \T_OPEN_SQUARE_BRACKET ] = \T_OPEN_SQUARE_BRACKET; - $this->classname_tokens[ \T_CLOSE_SQUARE_BRACKET ] = \T_CLOSE_SQUARE_BRACKET; - $this->classname_tokens[ \T_CONSTANT_ENCAPSED_STRING ] = \T_CONSTANT_ENCAPSED_STRING; - $this->classname_tokens[ \T_LNUMBER ] = \T_LNUMBER; - - return array( - \T_NEW, - \T_STRING, // JS. - ); - } - - /** - * Processes this test, when one of its tokens is encountered. - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return void - */ - public function process_token( $stackPtr ) { - // Make sure we have the right token, JS vs PHP. - if ( ( 'PHP' === $this->phpcsFile->tokenizerType && \T_NEW !== $this->tokens[ $stackPtr ]['code'] ) - || ( 'JS' === $this->phpcsFile->tokenizerType - && ( \T_STRING !== $this->tokens[ $stackPtr ]['code'] - || 'new' !== strtolower( $this->tokens[ $stackPtr ]['content'] ) ) ) - ) { - return; - } - - /* - * Check for new by reference used in PHP files. - */ - if ( 'PHP' === $this->phpcsFile->tokenizerType ) { - $prev_non_empty = $this->phpcsFile->findPrevious( - Tokens::$emptyTokens, - ( $stackPtr - 1 ), - null, - true - ); - - if ( false !== $prev_non_empty && 'T_BITWISE_AND' === $this->tokens[ $prev_non_empty ]['type'] ) { - $this->phpcsFile->recordMetric( $stackPtr, 'Assigning new by reference', 'yes' ); - - $this->phpcsFile->addError( - 'Assigning the return value of new by reference is no longer supported by PHP.', - $stackPtr, - 'NewByReferenceFound' - ); - } else { - $this->phpcsFile->recordMetric( $stackPtr, 'Assigning new by reference', 'no' ); - } - } - - /* - * Check for parenthesis & correct placement thereof. - */ - $next_non_empty_after_class_name = $this->phpcsFile->findNext( - $this->classname_tokens, - ( $stackPtr + 1 ), - null, - true, - null, - true - ); - - if ( false === $next_non_empty_after_class_name ) { - // Live coding. - return; - } - - // Walk back to the last part of the class name. - $has_comment = false; - for ( $classname_ptr = ( $next_non_empty_after_class_name - 1 ); $classname_ptr >= $stackPtr; $classname_ptr-- ) { - if ( ! isset( Tokens::$emptyTokens[ $this->tokens[ $classname_ptr ]['code'] ] ) ) { - // Prevent a false positive on variable variables, disregard them for now. - if ( $stackPtr === $classname_ptr ) { - return; - } - - break; - } - - if ( \T_WHITESPACE !== $this->tokens[ $classname_ptr ]['code'] ) { - $has_comment = true; - } - } - - if ( \T_OPEN_PARENTHESIS !== $this->tokens[ $next_non_empty_after_class_name ]['code'] ) { - $this->phpcsFile->recordMetric( $stackPtr, 'Object instantiation with parenthesis', 'no' ); - - $fix = $this->phpcsFile->addFixableError( - 'Parenthesis should always be used when instantiating a new object.', - $classname_ptr, - 'MissingParenthesis' - ); - - if ( true === $fix ) { - $this->phpcsFile->fixer->addContent( $classname_ptr, '()' ); - } - } else { - $this->phpcsFile->recordMetric( $stackPtr, 'Object instantiation with parenthesis', 'yes' ); - - if ( ( $next_non_empty_after_class_name - 1 ) !== $classname_ptr ) { - $this->phpcsFile->recordMetric( - $stackPtr, - 'Space between classname and parenthesis', - ( $next_non_empty_after_class_name - $classname_ptr ) - ); - - $error = 'There must be no spaces between the class name and the open parenthesis when instantiating a new object.'; - $error_code = 'SpaceBeforeParenthesis'; - - if ( false === $has_comment ) { - $fix = $this->phpcsFile->addFixableError( $error, $next_non_empty_after_class_name, $error_code ); - - if ( true === $fix ) { - $this->phpcsFile->fixer->beginChangeset(); - for ( $i = ( $next_non_empty_after_class_name - 1 ); $i > $classname_ptr; $i-- ) { - $this->phpcsFile->fixer->replaceToken( $i, '' ); - } - $this->phpcsFile->fixer->endChangeset(); - } - } else { - $this->phpcsFile->addError( $error, $next_non_empty_after_class_name, $error_code ); - } - } else { - $this->phpcsFile->recordMetric( $stackPtr, 'Space between classname and parenthesis', 0 ); - } - } - } - -} diff --git a/WordPress/Sniffs/CodeAnalysis/AssignmentInConditionSniff.php b/WordPress/Sniffs/CodeAnalysis/AssignmentInConditionSniff.php deleted file mode 100644 index 901df2503e..0000000000 --- a/WordPress/Sniffs/CodeAnalysis/AssignmentInConditionSniff.php +++ /dev/null @@ -1,235 +0,0 @@ -assignment_tokens = Tokens::$assignmentTokens; - unset( $this->assignment_tokens[ \T_DOUBLE_ARROW ] ); - - $starters = Tokens::$booleanOperators; - $starters[ \T_SEMICOLON ] = \T_SEMICOLON; - $starters[ \T_OPEN_PARENTHESIS ] = \T_OPEN_PARENTHESIS; - $starters[ \T_INLINE_ELSE ] = \T_INLINE_ELSE; - - $this->condition_start_tokens = $starters; - - return array( - \T_IF, - \T_ELSEIF, - \T_FOR, - \T_SWITCH, - \T_CASE, - \T_WHILE, - \T_INLINE_THEN, - ); - } - - /** - * Processes this test, when one of its tokens is encountered. - * - * @since 0.14.0 - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return void - */ - public function process_token( $stackPtr ) { - - $token = $this->tokens[ $stackPtr ]; - - // Find the condition opener/closer. - if ( \T_FOR === $token['code'] ) { - if ( isset( $token['parenthesis_opener'], $token['parenthesis_closer'] ) === false ) { - return; - } - - $semicolon = $this->phpcsFile->findNext( \T_SEMICOLON, ( $token['parenthesis_opener'] + 1 ), $token['parenthesis_closer'] ); - if ( false === $semicolon ) { - return; - } - - $opener = $semicolon; - $semicolon = $this->phpcsFile->findNext( \T_SEMICOLON, ( $opener + 1 ), $token['parenthesis_closer'] ); - if ( false === $semicolon ) { - return; - } - - $closer = $semicolon; - unset( $semicolon ); - - } elseif ( \T_CASE === $token['code'] ) { - if ( isset( $token['scope_opener'] ) === false ) { - return; - } - - $opener = $stackPtr; - $closer = $token['scope_opener']; - - } elseif ( \T_INLINE_THEN === $token['code'] ) { - // Check if the condition for the ternary is bracketed. - $prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); - if ( false === $prev ) { - // Shouldn't happen, but in that case we don't have anything to examine anyway. - return; - } - - if ( \T_CLOSE_PARENTHESIS === $this->tokens[ $prev ]['code'] ) { - if ( ! isset( $this->tokens[ $prev ]['parenthesis_opener'] ) ) { - return; - } - - $opener = $this->tokens[ $prev ]['parenthesis_opener']; - $closer = $prev; - } elseif ( isset( $token['nested_parenthesis'] ) ) { - $closer = end( $token['nested_parenthesis'] ); - $opener = key( $token['nested_parenthesis'] ); - - $next_statement_closer = $this->phpcsFile->findEndOfStatement( $stackPtr, array( \T_COLON, \T_CLOSE_PARENTHESIS, \T_CLOSE_SQUARE_BRACKET ) ); - if ( false !== $next_statement_closer && $next_statement_closer < $closer ) { - // Parentheses are unrelated to the ternary. - return; - } - - $prev_statement_closer = $this->phpcsFile->findStartOfStatement( $stackPtr, array( \T_COLON, \T_OPEN_PARENTHESIS, \T_OPEN_SQUARE_BRACKET ) ); - if ( false !== $prev_statement_closer && $opener < $prev_statement_closer ) { - // Parentheses are unrelated to the ternary. - return; - } - - if ( $closer > $stackPtr ) { - $closer = $stackPtr; - } - } else { - // No parenthesis found, can't determine where the conditional part of the ternary starts. - return; - } - } else { - if ( isset( $token['parenthesis_opener'], $token['parenthesis_closer'] ) === false ) { - return; - } - - $opener = $token['parenthesis_opener']; - $closer = $token['parenthesis_closer']; - } - - $startPos = $opener; - - do { - $hasAssignment = $this->phpcsFile->findNext( $this->assignment_tokens, ( $startPos + 1 ), $closer ); - if ( false === $hasAssignment ) { - return; - } - - // Examine whether the left side is a variable. - $hasVariable = false; - $conditionStart = $startPos; - $altConditionStart = $this->phpcsFile->findPrevious( - $this->condition_start_tokens, - ( $hasAssignment - 1 ), - $startPos - ); - if ( false !== $altConditionStart ) { - $conditionStart = $altConditionStart; - } - - for ( $i = $hasAssignment; $i > $conditionStart; $i-- ) { - if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) { - continue; - } - - // If this is a variable or array, we've seen all we need to see. - if ( \T_VARIABLE === $this->tokens[ $i ]['code'] - || \T_CLOSE_SQUARE_BRACKET === $this->tokens[ $i ]['code'] - ) { - $hasVariable = true; - break; - } - - // If this is a function call or something, we are OK. - if ( \T_CLOSE_PARENTHESIS === $this->tokens[ $i ]['code'] ) { - break; - } - } - - if ( true === $hasVariable ) { - $errorCode = 'Found'; - if ( \T_WHILE === $token['code'] ) { - $errorCode = 'FoundInWhileCondition'; - } elseif ( \T_INLINE_THEN === $token['code'] ) { - $errorCode = 'FoundInTernaryCondition'; - } - - $this->phpcsFile->addWarning( - 'Variable assignment found within a condition. Did you mean to do a comparison?', - $hasAssignment, - $errorCode - ); - } else { - $this->phpcsFile->addWarning( - 'Assignment found within a condition. Did you mean to do a comparison?', - $hasAssignment, - 'NonVariableAssignmentFound' - ); - } - - $startPos = $hasAssignment; - - } while ( $startPos < $closer ); - } - -} diff --git a/WordPress/Sniffs/CodeAnalysis/AssignmentInTernaryConditionSniff.php b/WordPress/Sniffs/CodeAnalysis/AssignmentInTernaryConditionSniff.php new file mode 100644 index 0000000000..a74b5f29be --- /dev/null +++ b/WordPress/Sniffs/CodeAnalysis/AssignmentInTernaryConditionSniff.php @@ -0,0 +1,173 @@ +assignment_tokens = Tokens::$assignmentTokens; + unset( $this->assignment_tokens[ \T_DOUBLE_ARROW ] ); + + $starters = Tokens::$booleanOperators; + $starters[ \T_SEMICOLON ] = \T_SEMICOLON; + $starters[ \T_OPEN_PARENTHESIS ] = \T_OPEN_PARENTHESIS; + $starters[ \T_INLINE_ELSE ] = \T_INLINE_ELSE; + + $this->condition_start_tokens = $starters; + + return array( + \T_INLINE_THEN, + ); + } + + /** + * Processes this test, when one of its tokens is encountered. + * + * @since 0.14.0 + * + * @param int $stackPtr The position of the current token in the stack. + * + * @return void + */ + public function process_token( $stackPtr ) { + + $token = $this->tokens[ $stackPtr ]; + + // Check if the condition for the ternary is bracketed. + $prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); + if ( \T_CLOSE_PARENTHESIS === $this->tokens[ $prev ]['code'] ) { + if ( ! isset( $this->tokens[ $prev ]['parenthesis_opener'] ) ) { + return; + } + + $opener = $this->tokens[ $prev ]['parenthesis_opener']; + $closer = $prev; + } elseif ( isset( $token['nested_parenthesis'] ) ) { + $opener = Parentheses::getLastOpener( $this->phpcsFile, $stackPtr ); + $closer = Parentheses::getLastCloser( $this->phpcsFile, $stackPtr ); + + $next_statement_closer = BCFile::findEndOfStatement( $this->phpcsFile, $stackPtr, array( \T_COLON, \T_CLOSE_PARENTHESIS, \T_CLOSE_SQUARE_BRACKET ) ); + if ( false !== $next_statement_closer && $next_statement_closer < $closer ) { + // Parentheses are unrelated to the ternary. + return; + } + + $prev_statement_closer = BCFile::findStartOfStatement( $this->phpcsFile, $stackPtr, array( \T_COLON, \T_OPEN_PARENTHESIS, \T_OPEN_SQUARE_BRACKET ) ); + if ( false !== $prev_statement_closer && $opener < $prev_statement_closer ) { + // Parentheses are unrelated to the ternary. + return; + } + + if ( $closer > $stackPtr ) { + $closer = $stackPtr; + } + } else { + // No parenthesis found, can't determine where the conditional part of the ternary starts. + return; + } + + $startPos = $opener; + + do { + $hasAssignment = $this->phpcsFile->findNext( $this->assignment_tokens, ( $startPos + 1 ), $closer ); + if ( false === $hasAssignment ) { + return; + } + + // Examine whether the left side is a variable. + $hasVariable = false; + $conditionStart = $startPos; + $altConditionStart = $this->phpcsFile->findPrevious( + $this->condition_start_tokens, + ( $hasAssignment - 1 ), + $startPos + ); + if ( false !== $altConditionStart ) { + $conditionStart = $altConditionStart; + } + + for ( $i = $hasAssignment; $i > $conditionStart; $i-- ) { + if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) { + continue; + } + + // If this is a variable or array, we've seen all we need to see. + if ( \T_VARIABLE === $this->tokens[ $i ]['code'] + || \T_CLOSE_SQUARE_BRACKET === $this->tokens[ $i ]['code'] + ) { + $hasVariable = true; + break; + } + + // If this is a function call or something, we are OK. + if ( \T_CLOSE_PARENTHESIS === $this->tokens[ $i ]['code'] ) { + break; + } + } + + if ( true === $hasVariable ) { + $this->phpcsFile->addWarning( + 'Variable assignment found within a condition. Did you mean to do a comparison?', + $hasAssignment, + 'FoundInTernaryCondition' + ); + } + + $startPos = $hasAssignment; + + } while ( $startPos < $closer ); + } +} diff --git a/WordPress/Sniffs/CodeAnalysis/EmptyStatementSniff.php b/WordPress/Sniffs/CodeAnalysis/EmptyStatementSniff.php deleted file mode 100644 index 2eff890987..0000000000 --- a/WordPress/Sniffs/CodeAnalysis/EmptyStatementSniff.php +++ /dev/null @@ -1,160 +0,0 @@ -tokens[ $stackPtr ]['type'] ) { - /* - * Detect `something();;`. - */ - case 'T_SEMICOLON': - $prevNonEmpty = $this->phpcsFile->findPrevious( - Tokens::$emptyTokens, - ( $stackPtr - 1 ), - null, - true - ); - - if ( false === $prevNonEmpty - || ( \T_SEMICOLON !== $this->tokens[ $prevNonEmpty ]['code'] - && \T_OPEN_TAG !== $this->tokens[ $prevNonEmpty ]['code'] - && \T_OPEN_TAG_WITH_ECHO !== $this->tokens[ $prevNonEmpty ]['code'] ) - ) { - return; - } - - if ( isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) { - $nested = $this->tokens[ $stackPtr ]['nested_parenthesis']; - $last_closer = array_pop( $nested ); - if ( isset( $this->tokens[ $last_closer ]['parenthesis_owner'] ) - && \T_FOR === $this->tokens[ $this->tokens[ $last_closer ]['parenthesis_owner'] ]['code'] - ) { - // Empty for() condition. - return; - } - } - - $fix = $this->phpcsFile->addFixableWarning( - 'Empty PHP statement detected: superfluous semi-colon.', - $stackPtr, - 'SemicolonWithoutCodeDetected' - ); - if ( true === $fix ) { - $this->phpcsFile->fixer->beginChangeset(); - - if ( \T_OPEN_TAG === $this->tokens[ $prevNonEmpty ]['code'] - || \T_OPEN_TAG_WITH_ECHO === $this->tokens[ $prevNonEmpty ]['code'] - ) { - /* - * Check for superfluous whitespace after the semi-colon which will be - * removed as the `tokens[ ( $stackPtr + 1 ) ]['code'] ) { - $replacement = str_replace( ' ', '', $this->tokens[ ( $stackPtr + 1 ) ]['content'] ); - $this->phpcsFile->fixer->replaceToken( ( $stackPtr + 1 ), $replacement ); - } - } - - for ( $i = $stackPtr; $i > $prevNonEmpty; $i-- ) { - if ( \T_SEMICOLON !== $this->tokens[ $i ]['code'] - && \T_WHITESPACE !== $this->tokens[ $i ]['code'] - ) { - break; - } - $this->phpcsFile->fixer->replaceToken( $i, '' ); - } - - $this->phpcsFile->fixer->endChangeset(); - } - break; - - /* - * Detect ``. - */ - case 'T_CLOSE_TAG': - $prevNonEmpty = $this->phpcsFile->findPrevious( - \T_WHITESPACE, - ( $stackPtr - 1 ), - null, - true - ); - - if ( false === $prevNonEmpty - || ( \T_OPEN_TAG !== $this->tokens[ $prevNonEmpty ]['code'] - && \T_OPEN_TAG_WITH_ECHO !== $this->tokens[ $prevNonEmpty ]['code'] ) - ) { - return; - } - - $fix = $this->phpcsFile->addFixableWarning( - 'Empty PHP open/close tag combination detected.', - $prevNonEmpty, - 'EmptyPHPOpenCloseTagsDetected' - ); - if ( true === $fix ) { - $this->phpcsFile->fixer->beginChangeset(); - for ( $i = $prevNonEmpty; $i <= $stackPtr; $i++ ) { - $this->phpcsFile->fixer->replaceToken( $i, '' ); - } - $this->phpcsFile->fixer->endChangeset(); - } - break; - - default: - /* Deliberately left empty. */ - break; - } - } - -} diff --git a/WordPress/Sniffs/CodeAnalysis/EscapedNotTranslatedSniff.php b/WordPress/Sniffs/CodeAnalysis/EscapedNotTranslatedSniff.php new file mode 100644 index 0000000000..45084498a0 --- /dev/null +++ b/WordPress/Sniffs/CodeAnalysis/EscapedNotTranslatedSniff.php @@ -0,0 +1,89 @@ + Key is the name of the function being matched, value the alternative to use. + */ + protected $target_functions = array( + 'esc_html' => 'esc_html__', + 'esc_attr' => 'esc_attr__', + ); + + /** + * Process the parameters of a matched function. + * + * @since 2.2.0 + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * @param array $parameters Array with information about the parameters. + * + * @return void + */ + public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { + if ( \count( $parameters ) === 1 ) { + return; + } + + /* + * We already know that there will be a valid open+close parenthesis, otherwise the sniff + * would have bowed out long before. + */ + $opener = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); + $closer = $this->tokens[ $opener ]['parenthesis_closer']; + + $data = array( + $matched_content, + $this->target_functions[ $matched_content ], + GetTokensAsString::compact( $this->phpcsFile, $stackPtr, $closer, true ), + ); + + $this->phpcsFile->addWarning( + '%s() expects only a $text parameter. Did you mean to use %s() ? Found: %s', + $stackPtr, + 'Found', + $data + ); + } +} diff --git a/WordPress/Sniffs/DB/DirectDatabaseQuerySniff.php b/WordPress/Sniffs/DB/DirectDatabaseQuerySniff.php index b1339e778f..590f6478ea 100644 --- a/WordPress/Sniffs/DB/DirectDatabaseQuerySniff.php +++ b/WordPress/Sniffs/DB/DirectDatabaseQuerySniff.php @@ -3,36 +3,39 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\DB; +namespace WordPressCS\WordPress\Sniffs\DB; -use WordPress\Sniff; -use PHP_CodeSniffer_Tokens as Tokens; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\Conditions; +use PHPCSUtils\Utils\TextStrings; +use WordPressCS\WordPress\Helpers\RulesetPropertyHelper; +use WordPressCS\WordPress\Sniff; /** * Flag Database direct queries. * - * @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#direct-database-queries + * @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#direct-database-queries * - * @package WPCS\WordPressCodingStandards - * - * @since 0.3.0 - * @since 0.6.0 Removed the add_unique_message() function as it is no longer needed. - * @since 0.11.0 This class now extends WordPress_Sniff. - * @since 0.13.0 Class name changed: this class is now namespaced. - * @since 1.0.0 This sniff has been moved from the `VIP` category to the `DB` category. + * @since 0.3.0 + * @since 0.6.0 Removed the add_unique_message() function as it is no longer needed. + * @since 0.11.0 This class now extends the WordPressCS native `Sniff` class. + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 1.0.0 This sniff has been moved from the `VIP` category to the `DB` category. + * @since 3.0.0 Support for the very sniff specific WPCS native ignore comment syntax has been removed. */ -class DirectDatabaseQuerySniff extends Sniff { +final class DirectDatabaseQuerySniff extends Sniff { /** * List of custom cache get functions. * * @since 0.6.0 * - * @var string|string[] + * @var string[] */ public $customCacheGetFunctions = array(); @@ -41,7 +44,7 @@ class DirectDatabaseQuerySniff extends Sniff { * * @since 0.6.0 * - * @var string|string[] + * @var string[] */ public $customCacheSetFunctions = array(); @@ -50,7 +53,7 @@ class DirectDatabaseQuerySniff extends Sniff { * * @since 0.6.0 * - * @var string|string[] + * @var string[] */ public $customCacheDeleteFunctions = array(); @@ -69,6 +72,57 @@ class DirectDatabaseQuerySniff extends Sniff { 'cachedelete' => array(), ); + /** + * A list of functions that get data from the cache. + * + * @since 0.6.0 + * @since 0.11.0 Changed from public static to protected non-static. + * @since 3.0.0 Moved from the generic `Sniff` class to this class. + * + * @var array + */ + protected $cacheGetFunctions = array( + 'wp_cache_get' => true, + ); + + /** + * A list of functions that set data in the cache. + * + * @since 0.6.0 + * @since 0.11.0 Changed from public static to protected non-static. + * @since 3.0.0 Moved from the generic `Sniff` class to this class. + * + * @var array + */ + protected $cacheSetFunctions = array( + 'wp_cache_set' => true, + 'wp_cache_add' => true, + ); + + /** + * A list of functions that delete data from the cache. + * + * @since 0.6.0 + * @since 0.11.0 Changed from public static to protected non-static. + * @since 3.0.0 Moved from the generic `Sniff` class to this class. + * + * @var array + */ + protected $cacheDeleteFunctions = array( + 'wp_cache_delete' => true, + 'clean_attachment_cache' => true, + 'clean_blog_cache' => true, + 'clean_bookmark_cache' => true, + 'clean_category_cache' => true, + 'clean_comment_cache' => true, + 'clean_network_cache' => true, + 'clean_object_term_cache' => true, + 'clean_page_cache' => true, + 'clean_post_cache' => true, + 'clean_term_cache' => true, + 'clean_user_cache' => true, + ); + /** * The lists of $wpdb methods. * @@ -109,7 +163,8 @@ public function register() { * * @param int $stackPtr The position of the current token in the stack. * - * @return void + * @return int|void Integer stack pointer to skip forward or void to continue + * normal file processing. */ public function process_token( $stackPtr ) { @@ -118,13 +173,17 @@ public function process_token( $stackPtr ) { return; } - $is_object_call = $this->phpcsFile->findNext( \T_OBJECT_OPERATOR, ( $stackPtr + 1 ), null, false, null, true ); - if ( false === $is_object_call ) { - return; // This is not a call to the wpdb object. + $is_object_call = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); + if ( false === $is_object_call + || ( \T_OBJECT_OPERATOR !== $this->tokens[ $is_object_call ]['code'] + && \T_NULLSAFE_OBJECT_OPERATOR !== $this->tokens[ $is_object_call ]['code'] ) + ) { + // This is not a call to the wpdb object. + return; } - $methodPtr = $this->phpcsFile->findNext( array( \T_WHITESPACE ), ( $is_object_call + 1 ), null, true, null, true ); - $method = $this->tokens[ $methodPtr ]['content']; + $methodPtr = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $is_object_call + 1 ), null, true ); + $method = strtolower( $this->tokens[ $methodPtr ]['content'] ); $this->mergeFunctionLists(); @@ -132,88 +191,67 @@ public function process_token( $stackPtr ) { return; } - $endOfStatement = $this->phpcsFile->findNext( \T_SEMICOLON, ( $stackPtr + 1 ), null, false, null, true ); - $endOfLineComment = ''; - for ( $i = ( $endOfStatement + 1 ); $i < $this->phpcsFile->numTokens; $i++ ) { - - if ( $this->tokens[ $i ]['line'] !== $this->tokens[ $endOfStatement ]['line'] ) { - break; - } - - if ( \T_COMMENT === $this->tokens[ $i ]['code'] ) { - $endOfLineComment .= $this->tokens[ $i ]['content']; - } - } - - $whitelisted_db_call = false; - if ( preg_match( '/db call\W*(?:ok|pass|clear|whitelist)/i', $endOfLineComment ) ) { - $whitelisted_db_call = true; + $endOfStatement = $this->phpcsFile->findNext( array( \T_SEMICOLON, \T_CLOSE_TAG ), ( $stackPtr + 1 ) ); + if ( false === $endOfStatement ) { + return; } - // Check for Database Schema Changes. + // Check for Database Schema Changes/ table truncation. for ( $_pos = ( $stackPtr + 1 ); $_pos < $endOfStatement; $_pos++ ) { - $_pos = $this->phpcsFile->findNext( Tokens::$textStringTokens, $_pos, $endOfStatement, false, null, true ); + $_pos = $this->phpcsFile->findNext( Tokens::$textStringTokens, $_pos, $endOfStatement ); if ( false === $_pos ) { break; } + if ( strpos( strtoupper( TextStrings::stripQuotes( $this->tokens[ $_pos ]['content'] ) ), 'TRUNCATE ' ) === 0 ) { + // Ignore queries to truncate the database as caching those is irrelevant and they need a direct db query. + return; + } + if ( preg_match( '#\b(?:ALTER|CREATE|DROP)\b#i', $this->tokens[ $_pos ]['content'] ) > 0 ) { $this->phpcsFile->addWarning( 'Attempting a database schema change is discouraged.', $_pos, 'SchemaChange' ); } } - // Flag instance if not whitelisted. - if ( ! $whitelisted_db_call ) { - $this->phpcsFile->addWarning( 'Usage of a direct database call is discouraged.', $stackPtr, 'DirectQuery' ); - } + $this->phpcsFile->addWarning( 'Use of a direct database call is discouraged.', $stackPtr, 'DirectQuery' ); if ( ! isset( $this->methods['cachable'][ $method ] ) ) { return $endOfStatement; } - $whitelisted_cache = false; - $cached = false; - $wp_cache_get = false; - if ( preg_match( '/cache\s+(?:ok|pass|clear|whitelist)/i', $endOfLineComment ) ) { - $whitelisted_cache = true; - } - if ( ! $whitelisted_cache && ! empty( $this->tokens[ $stackPtr ]['conditions'] ) ) { - $scope_function = $this->phpcsFile->getCondition( $stackPtr, \T_FUNCTION ); - - if ( false === $scope_function ) { - $scope_function = $this->phpcsFile->getCondition( $stackPtr, \T_CLOSURE ); - } + $cached = false; + $wp_cache_get = false; - if ( false !== $scope_function ) { - $scopeStart = $this->tokens[ $scope_function ]['scope_opener']; - $scopeEnd = $this->tokens[ $scope_function ]['scope_closer']; + $scope_function = Conditions::getLastCondition( $this->phpcsFile, $stackPtr, Collections::functionDeclarationTokens() ); + if ( false !== $scope_function ) { + $scopeStart = $this->tokens[ $scope_function ]['scope_opener']; + $scopeEnd = $this->tokens[ $scope_function ]['scope_closer']; - for ( $i = ( $scopeStart + 1 ); $i < $scopeEnd; $i++ ) { - if ( \T_STRING === $this->tokens[ $i ]['code'] ) { + for ( $i = ( $scopeStart + 1 ); $i < $scopeEnd; $i++ ) { + if ( \T_STRING === $this->tokens[ $i ]['code'] ) { - if ( isset( $this->cacheDeleteFunctions[ $this->tokens[ $i ]['content'] ] ) ) { + if ( isset( $this->cacheDeleteFunctions[ $this->tokens[ $i ]['content'] ] ) ) { - if ( \in_array( $method, array( 'query', 'update', 'replace', 'delete' ), true ) ) { - $cached = true; - break; - } - } elseif ( isset( $this->cacheGetFunctions[ $this->tokens[ $i ]['content'] ] ) ) { + if ( \in_array( $method, array( 'query', 'update', 'replace', 'delete' ), true ) ) { + $cached = true; + break; + } + } elseif ( isset( $this->cacheGetFunctions[ $this->tokens[ $i ]['content'] ] ) ) { - $wp_cache_get = true; + $wp_cache_get = true; - } elseif ( isset( $this->cacheSetFunctions[ $this->tokens[ $i ]['content'] ] ) ) { + } elseif ( isset( $this->cacheSetFunctions[ $this->tokens[ $i ]['content'] ] ) ) { - if ( $wp_cache_get ) { - $cached = true; - break; - } + if ( $wp_cache_get ) { + $cached = true; + break; } } } } } - if ( ! $cached && ! $whitelisted_cache ) { + if ( ! $cached ) { $message = 'Direct database call without caching detected. Consider using wp_cache_get() / wp_cache_set() or wp_cache_delete().'; $this->phpcsFile->addWarning( $message, $stackPtr, 'NoCaching' ); } @@ -234,7 +272,7 @@ protected function mergeFunctionLists() { } if ( $this->customCacheGetFunctions !== $this->addedCustomFunctions['cacheget'] ) { - $this->cacheGetFunctions = $this->merge_custom_array( + $this->cacheGetFunctions = RulesetPropertyHelper::merge_custom_array( $this->customCacheGetFunctions, $this->cacheGetFunctions ); @@ -243,7 +281,7 @@ protected function mergeFunctionLists() { } if ( $this->customCacheSetFunctions !== $this->addedCustomFunctions['cacheset'] ) { - $this->cacheSetFunctions = $this->merge_custom_array( + $this->cacheSetFunctions = RulesetPropertyHelper::merge_custom_array( $this->customCacheSetFunctions, $this->cacheSetFunctions ); @@ -252,7 +290,7 @@ protected function mergeFunctionLists() { } if ( $this->customCacheDeleteFunctions !== $this->addedCustomFunctions['cachedelete'] ) { - $this->cacheDeleteFunctions = $this->merge_custom_array( + $this->cacheDeleteFunctions = RulesetPropertyHelper::merge_custom_array( $this->customCacheDeleteFunctions, $this->cacheDeleteFunctions ); @@ -260,5 +298,4 @@ protected function mergeFunctionLists() { $this->addedCustomFunctions['cachedelete'] = $this->customCacheDeleteFunctions; } } - } diff --git a/WordPress/Sniffs/DB/PreparedSQLPlaceholdersSniff.php b/WordPress/Sniffs/DB/PreparedSQLPlaceholdersSniff.php index b4dafe6a17..043175d8fb 100644 --- a/WordPress/Sniffs/DB/PreparedSQLPlaceholdersSniff.php +++ b/WordPress/Sniffs/DB/PreparedSQLPlaceholdersSniff.php @@ -3,25 +3,33 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\DB; +namespace WordPressCS\WordPress\Sniffs\DB; -use WordPress\Sniff; -use PHP_CodeSniffer_Tokens as Tokens; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\Arrays; +use PHPCSUtils\Utils\PassedParameters; +use PHPCSUtils\Utils\TextStrings; +use WordPressCS\WordPress\Helpers\MinimumWPVersionTrait; +use WordPressCS\WordPress\Helpers\WPDBTrait; +use WordPressCS\WordPress\Sniff; /** - * Check for incorrect use of the $wpdb->prepare method. + * Checks for incorrect use of the $wpdb->prepare method. * - * Check the following issues: - * - The only placeholders supported are: %d, %f (%F) and %s and their variations. + * Checks the following issues: + * - The only placeholders supported are: %d, %f (%F), %s, %i, and their variations. * - Literal % signs need to be properly escaped as `%%`. - * - Simple placeholders (%d, %f, %F, %s) should be left unquoted in the query string. + * - Simple placeholders (%d, %f, %F, %s, %i) should be left unquoted in the query string. * - Complex placeholders - numbered and formatted variants - will not be quoted * automagically by $wpdb->prepare(), so if used for values, should be quoted in * the query string. + * The only exception to this is complex placeholders for %i. In that case, the + * replacement *will* still be backtick-quoted. * - Either an array of replacements should be passed matching the number of * placeholders found or individual parameters for each placeholder should * be passed. @@ -31,22 +39,23 @@ * created using code along the lines of: * `sprintf( 'query .... IN (%s) ...', implode( ',', array_fill( 0, count( $something ), '%s' ) ) )`. * - * A "PreparedSQLPlaceholders replacement count" whitelist comment is supported - * specifically to silence the `ReplacementsWrongNumber` and `UnfinishedPrepare` - * error codes. The other error codes are not affected by it. - * * @link https://developer.wordpress.org/reference/classes/wpdb/prepare/ * @link https://core.trac.wordpress.org/changeset/41496 * @link https://core.trac.wordpress.org/changeset/41471 + * @link https://core.trac.wordpress.org/changeset/55151 * - * @package WPCS\WordPressCodingStandards + * @since 0.14.0 + * @since 3.0.0 Support for the %i placeholder has been added * - * @since 0.14.0 + * @uses \WordPressCS\WordPress\Helpers\MinimumWPVersionTrait::$minimum_wp_version */ -class PreparedSQLPlaceholdersSniff extends Sniff { +final class PreparedSQLPlaceholdersSniff extends Sniff { + + use MinimumWPVersionTrait; + use WPDBTrait; /** - * These regexes copied from http://php.net/manual/en/function.sprintf.php#93552 + * These regexes were originally copied from https://www.php.net/function.sprintf#93552 * and adjusted for limitations in `$wpdb->prepare()`. * * Near duplicate of the one used in the WP.I18n sniff, but with fewer types allowed. @@ -75,7 +84,7 @@ class PreparedSQLPlaceholdersSniff extends Sniff { [0-9]+ # Width specifier. (?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character. ) - [dfFs] # Type specifier. + [dfFsi] # Type specifier. ) )'; @@ -96,7 +105,7 @@ class PreparedSQLPlaceholdersSniff extends Sniff { (?! # Negative look ahead. %[^%] # Not a correct literal % (%%). | - %%[dfFs] # Nor a correct literal % (%%), followed by a simple placeholder. + %%[dfFsi] # Nor a correct literal % (%%), followed by a simple placeholder. ) (?:[0-9]+\\\\??\$)?+ # Optional ordering of the placeholders. [+-]?+ # Optional sign specifier. @@ -111,7 +120,7 @@ class PreparedSQLPlaceholdersSniff extends Sniff { [0-9]++ # Width specifier. (?:\.(?:[ 0]|\'.)?[0-9]+)?+ # Optional precision specifier with optional padding character. ) - (?![dfFs]) # Negative look ahead: not one of the supported placeholders. + (?![dfFsi]) # Negative look ahead: not one of the supported placeholders. (?:[^ \'"]*|$) # but something else instead. ) )`x'; @@ -170,16 +179,22 @@ public function register() { */ public function process_token( $stackPtr ) { - if ( ! $this->is_wpdb_method_call( $stackPtr, $this->target_methods ) ) { + $this->set_minimum_wp_version(); + + if ( ! $this->is_wpdb_method_call( $this->phpcsFile, $stackPtr, $this->target_methods ) ) { return; } - $parameters = $this->get_function_call_parameters( $this->methodPtr ); + $parameters = PassedParameters::getParameters( $this->phpcsFile, $this->methodPtr ); if ( empty( $parameters ) ) { return; } - $query = $parameters[1]; + $query = PassedParameters::getParameterFromStack( $parameters, 1, 'query' ); + if ( false === $query ) { + return; + } + $text_string_tokens_found = false; $variable_found = false; $sql_wildcard_found = false; @@ -190,10 +205,12 @@ public function process_token( $stackPtr ) { 'implode_fill' => 0, 'adjustment_count' => 0, ); + $skip_from = null; + $skip_to = null; for ( $i = $query['start']; $i <= $query['end']; $i++ ) { // Skip over groups of tokens if they are part of an inline function call. - if ( isset( $skip_from, $skip_to ) && $i >= $skip_from && $i < $skip_to ) { + if ( isset( $skip_from, $skip_to ) && $i >= $skip_from && $i <= $skip_to ) { $i = $skip_to; continue; } @@ -210,9 +227,35 @@ public function process_token( $stackPtr ) { if ( \T_STRING === $this->tokens[ $i ]['code'] ) { if ( 'sprintf' === strtolower( $this->tokens[ $i ]['content'] ) ) { - $sprintf_parameters = $this->get_function_call_parameters( $i ); + $sprintf_parameters = PassedParameters::getParameters( $this->phpcsFile, $i ); if ( ! empty( $sprintf_parameters ) ) { + /* + * Check for named params. sprintf() does not support this due to its variadic nature, + * and we cannot analyse the code correctly if it is used, so skip the whole sprintf() + * in that case. + */ + $valid_sprintf = true; + foreach ( $sprintf_parameters as $param ) { + if ( isset( $param['name'] ) ) { + $valid_sprintf = false; + break; + } + } + + if ( false === $valid_sprintf ) { + $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true ); + if ( \T_OPEN_PARENTHESIS === $this->tokens[ $next ]['code'] + && isset( $this->tokens[ $next ]['parenthesis_closer'] ) + ) { + $skip_from = ( $i + 1 ); + $skip_to = $this->tokens[ $next ]['parenthesis_closer']; + } + + continue; + } + + // We know for sure this sprintf() uses positional parameters, so this will be fine. $skip_from = ( $sprintf_parameters[1]['end'] + 1 ); $last_param = end( $sprintf_parameters ); $skip_to = ( $last_param['end'] + 1 ); @@ -220,43 +263,47 @@ public function process_token( $stackPtr ) { $valid_in_clauses['implode_fill'] += $this->analyse_sprintf( $sprintf_parameters ); $valid_in_clauses['adjustment_count'] += ( \count( $sprintf_parameters ) - 1 ); } - unset( $sprintf_parameters, $last_param ); + unset( $sprintf_parameters, $valid_sprintf, $last_param ); } elseif ( 'implode' === strtolower( $this->tokens[ $i ]['content'] ) ) { $prev = $this->phpcsFile->findPrevious( - Tokens::$textStringTokens, + Tokens::$emptyTokens + array( \T_STRING_CONCAT => \T_STRING_CONCAT ), ( $i - 1 ), - $query['start'] + $query['start'], + true ); - $prev_content = $this->strip_quotes( $this->tokens[ $prev ]['content'] ); - $regex_quote = $this->get_regex_quote_snippet( $prev_content, $this->tokens[ $prev ]['content'] ); - - // Only examine the implode if preceded by an ` IN (`. - if ( preg_match( '`\s+IN\s*\(\s*(' . $regex_quote . ')?$`i', $prev_content, $match ) > 0 ) { + if ( isset( Tokens::$textStringTokens[ $this->tokens[ $prev ]['code'] ] ) ) { + $prev_content = TextStrings::stripQuotes( $this->tokens[ $prev ]['content'] ); + $regex_quote = $this->get_regex_quote_snippet( $prev_content, $this->tokens[ $prev ]['content'] ); - if ( isset( $match[1] ) && $regex_quote !== $this->regex_quote ) { - $this->phpcsFile->addError( - 'Dynamic placeholder generation should not have surrounding quotes.', - $i, - 'QuotedDynamicPlaceholderGeneration' - ); - } + // Only examine the implode if preceded by an ` IN (`. + if ( preg_match( '`\s+IN\s*\(\s*(' . $regex_quote . ')?$`i', $prev_content, $match ) > 0 ) { - if ( $this->analyse_implode( $i ) === true ) { - ++$valid_in_clauses['uses_in']; - ++$valid_in_clauses['implode_fill']; + if ( isset( $match[1] ) && $regex_quote !== $this->regex_quote ) { + $this->phpcsFile->addError( + 'Dynamic placeholder generation should not have surrounding quotes.', + $prev, + 'QuotedDynamicPlaceholderGeneration' + ); + } - $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true ); - if ( \T_OPEN_PARENTHESIS === $this->tokens[ $next ]['code'] - && isset( $this->tokens[ $next ]['parenthesis_closer'] ) - ) { - $skip_from = ( $i + 1 ); - $skip_to = ( $this->tokens[ $next ]['parenthesis_closer'] + 1 ); + if ( $this->analyse_implode( $i ) === true ) { + ++$valid_in_clauses['uses_in']; + ++$valid_in_clauses['implode_fill']; + + $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true ); + if ( \T_OPEN_PARENTHESIS === $this->tokens[ $next ]['code'] + && isset( $this->tokens[ $next ]['parenthesis_closer'] ) + ) { + $skip_from = ( $i + 1 ); + $skip_to = $this->tokens[ $next ]['parenthesis_closer']; + } } } + unset( $next, $prev_content, $regex_quote, $match ); } - unset( $prev, $next, $prev_content, $regex_quote, $match ); + unset( $prev ); } } @@ -268,7 +315,7 @@ public function process_token( $stackPtr ) { $regex_quote = $this->regex_quote; if ( isset( Tokens::$stringTokens[ $this->tokens[ $i ]['code'] ] ) ) { - $content = $this->strip_quotes( $content ); + $content = TextStrings::stripQuotes( $content ); $regex_quote = $this->get_regex_quote_snippet( $content, $this->tokens[ $i ]['content'] ); } @@ -276,17 +323,22 @@ public function process_token( $stackPtr ) { || \T_HEREDOC === $this->tokens[ $i ]['code'] ) { // Only interested in actual query text, so strip out variables. - $stripped_content = $this->strip_interpolated_variables( $content ); + $stripped_content = TextStrings::stripEmbeds( $content ); if ( $stripped_content !== $content ) { - $interpolated_vars = $this->get_interpolated_variables( $content ); - $vars_without_wpdb = array_diff( $interpolated_vars, array( 'wpdb' ) ); - $content = $stripped_content; + $vars_without_wpdb = array_filter( + TextStrings::getEmbeds( $content ), + static function ( $symbol ) { + return preg_match( '`^\{?\$\{?wpdb\??->`', $symbol ) !== 1; + } + ); + + $content = $stripped_content; if ( ! empty( $vars_without_wpdb ) ) { $variable_found = true; } } - unset( $stripped_content, $interpolated_vars, $vars_without_wpdb ); + unset( $stripped_content, $vars_without_wpdb ); } $placeholders = preg_match_all( '`' . self::PREPARE_PLACEHOLDER_REGEX . '`x', $content, $matches ); @@ -392,8 +444,27 @@ public function process_token( $stackPtr ) { unset( $match, $matches ); } + if ( $this->wp_version_compare( $this->minimum_wp_version, '6.2', '<' ) ) { + if ( preg_match_all( '`' . self::PREPARE_PLACEHOLDER_REGEX . '`x', $content, $matches ) > 0 ) { + if ( ! empty( $matches[0] ) ) { + foreach ( $matches[0] as $match ) { + if ( 'i' === substr( $match, -1 ) ) { + $this->phpcsFile->addError( + 'The %%i modifier is only supported in WP 6.2 or higher. Found: "%s".', + $i, + 'UnsupportedIdentifierPlaceholder', + array( $match ) + ); + } + } + } + } + unset( $match, $matches ); + } + /* - * Analyse the query for quoted placeholders. + * Analyse the query for single/double quoted simple value placeholders + * Identifiers are checked separately. */ $regex = '`(' . $regex_quote . ')%[dfFs]\1`'; if ( preg_match_all( $regex, $content, $matches ) > 0 ) { @@ -410,6 +481,26 @@ public function process_token( $stackPtr ) { unset( $match, $matches ); } + /* + * Analyse the query for quoted identifier placeholders. + */ + $regex = '/(' . $regex_quote . '|`)(?' . self::PREPARE_PLACEHOLDER_REGEX . ')\1/x'; + if ( preg_match_all( $regex, $content, $matches ) > 0 ) { + if ( ! empty( $matches ) ) { + foreach ( $matches['placeholder'] as $index => $match ) { + if ( 'i' === substr( $match, -1 ) ) { + $this->phpcsFile->addError( + 'Placeholders used for identifiers (%%i) in the query string in $wpdb->prepare() are always quoted automagically. Please remove the surrounding quotes. Found: %s', + $i, + 'QuotedIdentifierPlaceholder', + array( $matches[0][ $index ] ) + ); + } + } + } + unset( $index, $match, $matches ); + } + /* * Analyse the query for unquoted complex placeholders. */ @@ -417,7 +508,7 @@ public function process_token( $stackPtr ) { if ( preg_match_all( $regex, $content, $matches ) > 0 ) { if ( ! empty( $matches[0] ) ) { foreach ( $matches[0] as $match ) { - if ( preg_match( '`%[dfFs]`', $match ) !== 1 ) { + if ( substr( $match, -1 ) !== 'i' && preg_match( '`^%[dfFsi]$`', $match ) !== 1 ) { // Identifiers must always be unquoted. $this->phpcsFile->addWarning( 'Complex placeholders used for values in the query string in $wpdb->prepare() will NOT be quoted automagically. Found: %s.', $i, @@ -445,11 +536,6 @@ public function process_token( $stackPtr ) { return; } - $count_diff_whitelisted = $this->has_whitelist_comment( - 'PreparedSQLPlaceholders replacement count', - $stackPtr - ); - if ( 0 === $total_placeholders ) { if ( 1 === $total_parameters ) { if ( false === $variable_found && false === $sql_wildcard_found ) { @@ -465,7 +551,7 @@ public function process_token( $stackPtr ) { 'UnnecessaryPrepare' ); } - } elseif ( false === $count_diff_whitelisted && 0 === $valid_in_clauses['uses_in'] ) { + } elseif ( 0 === $valid_in_clauses['uses_in'] ) { $this->phpcsFile->addWarning( 'Replacement variables found, but no valid placeholders found in the query.', $i, @@ -486,27 +572,25 @@ public function process_token( $stackPtr ) { return; } - if ( true === $count_diff_whitelisted ) { - return; - } - $replacements = $parameters; - array_shift( $replacements ); // Remove the query. + unset( $replacements['query'], $replacements[1] ); // Remove the query param, whether passed positionally or named. - // The parameters may have been passed as an array in parameter 2. - if ( isset( $parameters[2] ) && 2 === $total_parameters ) { + // The parameters may have been passed as an array in the variadic $args parameter. + $args_param = PassedParameters::getParameterFromStack( $parameters, 2, 'args' ); + if ( false !== $args_param && 2 === $total_parameters ) { $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, - $parameters[2]['start'], - ( $parameters[2]['end'] + 1 ), + $args_param['start'], + ( $args_param['end'] + 1 ), true ); if ( false !== $next && ( \T_ARRAY === $this->tokens[ $next ]['code'] - || \T_OPEN_SHORT_ARRAY === $this->tokens[ $next ]['code'] ) + || ( isset( Collections::shortArrayListOpenTokensBC()[ $this->tokens[ $next ]['code'] ] ) + && Arrays::isShortArray( $this->phpcsFile, $next ) === true ) ) ) { - $replacements = $this->get_function_call_parameters( $next ); + $replacements = PassedParameters::getParameters( $this->phpcsFile, $next ); } } @@ -581,15 +665,11 @@ protected function get_regex_quote_snippet( $stripped_content, $original_content protected function analyse_sprintf( $sprintf_params ) { $found = 0; - unset( $sprintf_params[1] ); + unset( $sprintf_params[1] ); // Remove the positionally passed $format param. foreach ( $sprintf_params as $sprintf_param ) { - if ( strpos( strtolower( $sprintf_param['raw'] ), 'implode' ) === false ) { - continue; - } - $implode = $this->phpcsFile->findNext( - Tokens::$emptyTokens, + Tokens::$emptyTokens + array( \T_NS_SEPARATOR => \T_NS_SEPARATOR ), $sprintf_param['start'], $sprintf_param['end'], true @@ -615,6 +695,12 @@ protected function analyse_sprintf( $sprintf_params ) { * * This pattern presumes unquoted placeholders! * + * Identifiers (%i) are not supported, as this function is designed to work + * with `IN()`, which contains a list of values. In the future, it should + * be possible to simplify code using the implode/array_fill pattern to + * use a variable number of identifiers, e.g. `CONCAT(%...i)`, + * https://core.trac.wordpress.org/ticket/54042 + * * @since 0.14.0 * * @param int $implode_token The stackPtr to the implode function call. @@ -622,24 +708,27 @@ protected function analyse_sprintf( $sprintf_params ) { * @return bool True if the pattern is found, false otherwise. */ protected function analyse_implode( $implode_token ) { - $implode_params = $this->get_function_call_parameters( $implode_token ); - + $implode_params = PassedParameters::getParameters( $this->phpcsFile, $implode_token ); if ( empty( $implode_params ) || \count( $implode_params ) !== 2 ) { return false; } - if ( preg_match( '`^(["\']), ?\1$`', $implode_params[1]['raw'] ) !== 1 ) { + $implode_separator_param = PassedParameters::getParameterFromStack( $implode_params, 1, 'separator' ); + if ( false === $implode_separator_param + || preg_match( '`^(["\']), ?\1$`', $implode_separator_param['clean'] ) !== 1 + ) { return false; } - if ( strpos( strtolower( $implode_params[2]['raw'] ), 'array_fill' ) === false ) { + $implode_array_param = PassedParameters::getParameterFromStack( $implode_params, 2, 'array' ); + if ( false === $implode_array_param ) { return false; } $array_fill = $this->phpcsFile->findNext( - Tokens::$emptyTokens, - $implode_params[2]['start'], - $implode_params[2]['end'], + Tokens::$emptyTokens + array( \T_NS_SEPARATOR => \T_NS_SEPARATOR ), + $implode_array_param['start'], + $implode_array_param['end'], true ); @@ -649,13 +738,24 @@ protected function analyse_implode( $implode_token ) { return false; } - $array_fill_params = $this->get_function_call_parameters( $array_fill ); + $array_fill_value_param = PassedParameters::getParameter( $this->phpcsFile, $array_fill, 3, 'value' ); + if ( false === $array_fill_value_param ) { + return false; + } - if ( empty( $array_fill_params ) || \count( $array_fill_params ) !== 3 ) { + if ( "'%i'" === $array_fill_value_param['clean'] + || '"%i"' === $array_fill_value_param['clean'] + ) { + $firstNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, $array_fill_value_param['start'], $array_fill_value_param['end'], true ); + + $this->phpcsFile->addError( + 'The %i placeholder cannot be used within SQL `IN()` clauses.', + $firstNonEmpty, + 'IdentifierWithinIN' + ); return false; } - return (bool) preg_match( '`^(["\'])%[dfFs]\1$`', $array_fill_params[3]['raw'] ); + return (bool) preg_match( '`^(["\'])%[dfFs]\1$`', $array_fill_value_param['clean'] ); } - } diff --git a/WordPress/Sniffs/DB/PreparedSQLSniff.php b/WordPress/Sniffs/DB/PreparedSQLSniff.php index 38f1e012fa..7b6529ef90 100644 --- a/WordPress/Sniffs/DB/PreparedSQLSniff.php +++ b/WordPress/Sniffs/DB/PreparedSQLSniff.php @@ -3,29 +3,34 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\DB; +namespace WordPressCS\WordPress\Sniffs\DB; -use WordPress\Sniff; -use PHP_CodeSniffer_Tokens as Tokens; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\TextStrings; +use WordPressCS\WordPress\Helpers\ContextHelper; +use WordPressCS\WordPress\Helpers\FormattingFunctionsHelper; +use WordPressCS\WordPress\Helpers\WPDBTrait; +use WordPressCS\WordPress\Sniff; /** * Sniff for prepared SQL. * * Makes sure that variables aren't directly interpolated into SQL statements. * - * @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#formatting-sql-statements + * @link https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#formatting-sql-statements * - * @package WPCS\WordPressCodingStandards - * - * @since 0.8.0 - * @since 0.13.0 Class name changed: this class is now namespaced. - * @since 1.0.0 This sniff has been moved from the `WP` category to the `DB` category. + * @since 0.8.0 + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 1.0.0 This sniff has been moved from the `WP` category to the `DB` category. */ -class PreparedSQLSniff extends Sniff { +final class PreparedSQLSniff extends Sniff { + + use WPDBTrait; /** * The lists of $wpdb methods. @@ -33,7 +38,7 @@ class PreparedSQLSniff extends Sniff { * @since 0.8.0 * @since 0.11.0 Changed from static to non-static. * - * @var array + * @var array */ protected $methods = array( 'get_var' => true, @@ -44,31 +49,55 @@ class PreparedSQLSniff extends Sniff { 'query' => true, ); + /** + * Functions that escape values for use in SQL queries. + * + * @since 0.9.0 + * @since 0.11.0 Changed from public static to protected non-static. + * @since 3.0.0 - Moved from the Sniff class to this class. + * - The property visibility has changed from `protected` to `private`. + * + * @var array + */ + private $SQLEscapingFunctions = array( + 'absint' => true, + 'esc_sql' => true, + 'floatval' => true, + 'intval' => true, + 'like_escape' => true, + ); + + /** + * Functions whose output is automatically escaped for use in SQL queries. + * + * @since 0.9.0 + * @since 0.11.0 Changed from public static to protected non-static. + * @since 3.0.0 - Moved from the Sniff class to this class. + * - The property visibility has changed from `protected` to `private`. + * + * @var array + */ + private $SQLAutoEscapedFunctions = array( + 'count' => true, + ); + /** * Tokens that we don't flag when they are found in a $wpdb method call. * + * This token array is augmented from within the register() method. + * * @since 0.9.0 + * @since 3.0.0 The property visibility has changed from `protected` to `private`. * * @var array */ - protected $ignored_tokens = array( - \T_OBJECT_OPERATOR => true, - \T_OPEN_PARENTHESIS => true, - \T_CLOSE_PARENTHESIS => true, + private $ignored_tokens = array( \T_STRING_CONCAT => true, \T_CONSTANT_ENCAPSED_STRING => true, - \T_OPEN_SQUARE_BRACKET => true, - \T_CLOSE_SQUARE_BRACKET => true, \T_COMMA => true, \T_LNUMBER => true, - \T_START_HEREDOC => true, - \T_END_HEREDOC => true, - \T_START_NOWDOC => true, - \T_NOWDOC => true, - \T_END_NOWDOC => true, - \T_INT_CAST => true, - \T_DOUBLE_CAST => true, - \T_BOOL_CAST => true, + \T_DNUMBER => true, + \T_NS_SEPARATOR => true, ); /** @@ -101,8 +130,17 @@ class PreparedSQLSniff extends Sniff { * @return array */ public function register() { - - $this->ignored_tokens = $this->ignored_tokens + Tokens::$emptyTokens; + // Enrich the array of tokens which can be safely ignored. + $this->ignored_tokens += Tokens::$bracketTokens; + $this->ignored_tokens += Tokens::$heredocTokens; + $this->ignored_tokens += Tokens::$castTokens; + $this->ignored_tokens += Tokens::$arithmeticTokens; + $this->ignored_tokens += Collections::incrementDecrementOperators(); + $this->ignored_tokens += Collections::objectOperators(); + $this->ignored_tokens += Tokens::$emptyTokens; + + // The contents of heredoc tokens needs to be examined. + unset( $this->ignored_tokens[ \T_HEREDOC ] ); return array( \T_VARIABLE, @@ -122,11 +160,7 @@ public function register() { */ public function process_token( $stackPtr ) { - if ( ! $this->is_wpdb_method_call( $stackPtr, $this->methods ) ) { - return; - } - - if ( $this->has_whitelist_comment( 'unprepared SQL', $stackPtr ) ) { + if ( ! $this->is_wpdb_method_call( $this->phpcsFile, $stackPtr, $this->methods ) ) { return; } @@ -141,17 +175,17 @@ public function process_token( $stackPtr ) { ) { $bad_variables = array_filter( - $this->get_interpolated_variables( $this->tokens[ $this->i ]['content'] ), - function ( $symbol ) { - return ( 'wpdb' !== $symbol ); + TextStrings::getEmbeds( $this->tokens[ $this->i ]['content'] ), + static function ( $symbol ) { + return preg_match( '`^\{?\$\{?wpdb\??->`', $symbol ) !== 1; } ); foreach ( $bad_variables as $bad_variable ) { $this->phpcsFile->addError( - 'Use placeholders and $wpdb->prepare(); found interpolated variable $%s at %s', + 'Use placeholders and $wpdb->prepare(); found interpolated variable %s at %s', $this->i, - 'NotPrepared', + 'InterpolatedNotPrepared', array( $bad_variable, $this->tokens[ $this->i ]['content'], @@ -163,11 +197,11 @@ function ( $symbol ) { if ( \T_VARIABLE === $this->tokens[ $this->i ]['code'] ) { if ( '$wpdb' === $this->tokens[ $this->i ]['content'] ) { - $this->is_wpdb_method_call( $this->i, $this->methods ); + $this->is_wpdb_method_call( $this->phpcsFile, $this->i, $this->methods ); continue; } - if ( $this->is_safe_casted( $this->i ) ) { + if ( ContextHelper::is_safe_casted( $this->phpcsFile, $this->i ) ) { continue; } } @@ -180,17 +214,17 @@ function ( $symbol ) { ) { // Find the opening parenthesis. - $opening_paren = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $this->i + 1 ), null, true, null, true ); + $opening_paren = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $this->i + 1 ), null, true ); if ( false !== $opening_paren && \T_OPEN_PARENTHESIS === $this->tokens[ $opening_paren ]['code'] && isset( $this->tokens[ $opening_paren ]['parenthesis_closer'] ) ) { - // Skip past the end of the function. + // Skip past to the end of the function call. $this->i = $this->tokens[ $opening_paren ]['parenthesis_closer']; continue; } - } elseif ( isset( $this->formattingFunctions[ $this->tokens[ $this->i ]['content'] ] ) ) { + } elseif ( FormattingFunctionsHelper::is_formatting_function( $this->tokens[ $this->i ]['content'] ) ) { continue; } } @@ -205,5 +239,4 @@ function ( $symbol ) { return $this->end; } - } diff --git a/WordPress/Sniffs/DB/RestrictedClassesSniff.php b/WordPress/Sniffs/DB/RestrictedClassesSniff.php index 3b6828cd7d..4262889a43 100644 --- a/WordPress/Sniffs/DB/RestrictedClassesSniff.php +++ b/WordPress/Sniffs/DB/RestrictedClassesSniff.php @@ -3,13 +3,13 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\DB; +namespace WordPressCS\WordPress\Sniffs\DB; -use WordPress\AbstractClassRestrictionsSniff; +use WordPressCS\WordPress\AbstractClassRestrictionsSniff; /** * Verifies that no database related PHP classes are used. @@ -19,14 +19,12 @@ * helps keep your code forward-compatible and, in cases where results are cached in memory, * it can be many times faster." * - * @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#database-queries + * @link https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#database-queries * - * @package WPCS\WordPressCodingStandards - * - * @since 0.10.0 - * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.10.0 + * @since 0.13.0 Class name changed: this class is now namespaced. */ -class RestrictedClassesSniff extends AbstractClassRestrictionsSniff { +final class RestrictedClassesSniff extends AbstractClassRestrictionsSniff { /** * Groups of classes to restrict. @@ -56,5 +54,4 @@ public function getGroups() { ); } - } diff --git a/WordPress/Sniffs/DB/RestrictedFunctionsSniff.php b/WordPress/Sniffs/DB/RestrictedFunctionsSniff.php index b80883b76f..7d23adaaab 100644 --- a/WordPress/Sniffs/DB/RestrictedFunctionsSniff.php +++ b/WordPress/Sniffs/DB/RestrictedFunctionsSniff.php @@ -3,13 +3,13 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\DB; +namespace WordPressCS\WordPress\Sniffs\DB; -use WordPress\AbstractFunctionRestrictionsSniff; +use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff; /** * Verifies that no database related PHP functions are used. @@ -19,14 +19,12 @@ * helps keep your code forward-compatible and, in cases where results are cached in memory, * it can be many times faster." * - * @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#database-queries + * @link https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#database-queries * - * @package WPCS\WordPressCodingStandards - * - * @since 0.10.0 - * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.10.0 + * @since 0.13.0 Class name changed: this class is now namespaced. */ -class RestrictedFunctionsSniff extends AbstractFunctionRestrictionsSniff { +final class RestrictedFunctionsSniff extends AbstractFunctionRestrictionsSniff { /** * Groups of functions to restrict. @@ -56,11 +54,10 @@ public function getGroups() { 'mysqlnd_memcache_*', 'maxdb_*', ), - 'whitelist' => array( + 'allow' => array( 'mysql_to_rfc3339' => true, ), ), ); } - } diff --git a/WordPress/Sniffs/DB/SlowDBQuerySniff.php b/WordPress/Sniffs/DB/SlowDBQuerySniff.php index e7592df1af..5cbd99ac61 100644 --- a/WordPress/Sniffs/DB/SlowDBQuerySniff.php +++ b/WordPress/Sniffs/DB/SlowDBQuerySniff.php @@ -3,29 +3,24 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\DB; +namespace WordPressCS\WordPress\Sniffs\DB; -use WordPress\AbstractArrayAssignmentRestrictionsSniff; +use WordPressCS\WordPress\AbstractArrayAssignmentRestrictionsSniff; /** * Flag potentially slow queries. * - * @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#uncached-pageload + * @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#uncached-pageload * - * @package WPCS\WordPressCodingStandards - * - * @since 0.3.0 - * @since 0.12.0 Introduced new and more intuitively named 'slow query' whitelist - * comment, replacing the 'tax_query' whitelist comment which is now - * deprecated. - * @since 0.13.0 Class name changed: this class is now namespaced. - * @since 1.0.0 This sniff has been moved from the `VIP` category to the `DB` category. + * @since 0.3.0 + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 1.0.0 This sniff has been moved from the `VIP` category to the `DB` category. */ -class SlowDBQuerySniff extends AbstractArrayAssignmentRestrictionsSniff { +final class SlowDBQuerySniff extends AbstractArrayAssignmentRestrictionsSniff { /** * Groups of variables to restrict. @@ -47,54 +42,17 @@ public function getGroups() { ); } - /** - * Processes this test, when one of its tokens is encountered. - * - * @since 0.10.0 - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return int|void Integer stack pointer to skip forward or void to continue - * normal file processing. - */ - public function process_token( $stackPtr ) { - - if ( $this->has_whitelist_comment( 'slow query', $stackPtr ) ) { - return; - } - - if ( $this->has_whitelist_comment( 'tax_query', $stackPtr ) ) { - /* - * Only throw the warning about a deprecated comment when the sniff would otherwise - * have been triggered on the array key. - */ - if ( \in_array( $this->tokens[ $stackPtr ]['code'], array( \T_CONSTANT_ENCAPSED_STRING, \T_DOUBLE_QUOTED_STRING ), true ) ) { - $this->phpcsFile->addWarning( - 'The "tax_query" whitelist comment is deprecated in favor of the "slow query" whitelist comment.', - $stackPtr, - 'DeprecatedWhitelistFlagFound' - ); - } - - return; - } - - return parent::process_token( $stackPtr ); - } - /** * Callback to process each confirmed key, to check value. - * This must be extended to add the logic to check assignment value. * * @param string $key Array index / key. * @param mixed $val Assigned value. * @param int $line Token line. * @param array $group Group definition. - * @return mixed FALSE if no match, TRUE if matches, STRING if matches - * with custom error message passed to ->process(). + * + * @return bool Always returns TRUE as the value is irrelevant. */ public function callback( $key, $val, $line, $group ) { return true; } - } diff --git a/WordPress/Sniffs/DateTime/CurrentTimeTimestampSniff.php b/WordPress/Sniffs/DateTime/CurrentTimeTimestampSniff.php new file mode 100644 index 0000000000..34bfe514f7 --- /dev/null +++ b/WordPress/Sniffs/DateTime/CurrentTimeTimestampSniff.php @@ -0,0 +1,168 @@ + Key is function name, value irrelevant. + */ + protected $target_functions = array( + 'current_time' => true, + ); + + /** + * Process the parameters of a matched function. + * + * @since 2.2.0 + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * @param array $parameters Array with information about the parameters. + * + * @return void + */ + public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { + /* + * We already know there will be valid open & close parentheses as otherwise the parameter + * retrieval function call would have returned an empty array, so no additional checks needed. + */ + $open_parens = $this->phpcsFile->findNext( \T_OPEN_PARENTHESIS, $stackPtr ); + $close_parens = $this->tokens[ $open_parens ]['parenthesis_closer']; + + /* + * Check whether the first parameter is a timestamp format. + */ + $type_param = PassedParameters::getParameterFromStack( $parameters, 1, 'type' ); + if ( false === $type_param ) { + // Type parameter not found. Bow out. + return; + } + + $content_type = ''; + for ( $i = $type_param['start']; $i <= $type_param['end']; $i++ ) { + if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) { + continue; + } + + if ( isset( Tokens::$textStringTokens[ $this->tokens[ $i ]['code'] ] ) ) { + $content_type = trim( TextStrings::stripQuotes( $this->tokens[ $i ]['content'] ) ); + if ( 'U' !== $content_type && 'timestamp' !== $content_type ) { + // Most likely valid use of current_time(). + return; + } + + continue; + } + + if ( isset( Tokens::$heredocTokens[ $this->tokens[ $i ]['code'] ] ) ) { + continue; + } + + /* + * If we're still here, we've encountered an unexpected token, like a variable or + * function call. Bow out as we can't determine the runtime value. + */ + return; + } + + $gmt_true = false; + + /* + * Check whether the second parameter, $gmt, is a set to `true` or `1`. + */ + $gmt_param = PassedParameters::getParameterFromStack( $parameters, 2, 'gmt' ); + if ( is_array( $gmt_param ) ) { + $content_gmt = ''; + if ( 'true' === $gmt_param['clean'] || '1' === $gmt_param['clean'] ) { + $content_gmt = $gmt_param['clean']; + $gmt_true = true; + } + } + + /* + * Non-UTC timestamp requested. + */ + if ( false === $gmt_true ) { + $this->phpcsFile->addWarning( + 'Calling current_time() with a $type of "timestamp" or "U" is strongly discouraged as it will not return a Unix (UTC) timestamp. Please consider using a non-timestamp format or otherwise refactoring this code.', + $stackPtr, + 'Requested' + ); + + return; + } + + /* + * UTC timestamp requested. Should use time() instead. + */ + $has_comment = $this->phpcsFile->findNext( Tokens::$commentTokens, ( $stackPtr + 1 ), ( $close_parens + 1 ) ); + $error = 'Don\'t use current_time() for retrieving a Unix (UTC) timestamp. Use time() instead. Found: %s'; + $error_code = 'RequestedUTC'; + + $code_snippet = "current_time( '" . $content_type . "'"; + if ( isset( $content_gmt ) ) { + $code_snippet .= ', ' . $content_gmt; + } + $code_snippet .= ' )'; + + if ( false !== $has_comment ) { + // If there are comments, we don't auto-fix as it would remove those comments. + $this->phpcsFile->addError( $error, $stackPtr, $error_code, array( $code_snippet ) ); + return; + } + + $fix = $this->phpcsFile->addFixableError( $error, $stackPtr, $error_code, array( $code_snippet ) ); + if ( true === $fix ) { + $this->phpcsFile->fixer->beginChangeset(); + + for ( $i = ( $stackPtr + 1 ); $i < $close_parens; $i++ ) { + $this->phpcsFile->fixer->replaceToken( $i, '' ); + } + + $this->phpcsFile->fixer->replaceToken( $stackPtr, 'time(' ); + $this->phpcsFile->fixer->endChangeset(); + } + } +} diff --git a/WordPress/Sniffs/DateTime/RestrictedFunctionsSniff.php b/WordPress/Sniffs/DateTime/RestrictedFunctionsSniff.php new file mode 100644 index 0000000000..279da5735c --- /dev/null +++ b/WordPress/Sniffs/DateTime/RestrictedFunctionsSniff.php @@ -0,0 +1,59 @@ + array( + 'type' => 'error', + 'message' => 'Using %s() and similar isn\'t allowed, instead use WP internal timezone support.', + 'functions' => array( + 'date_default_timezone_set', + ), + ), + + /* + * Use gmdate(), not date(). + * Don't rely on the current PHP time zone as it might have been changed by third party code. + * + * @link https://make.wordpress.org/core/2019/09/23/date-time-improvements-wp-5-3/ + * @link https://core.trac.wordpress.org/ticket/46438 + * @link https://github.com/WordPress/WordPress-Coding-Standards/issues/1713 + */ + 'date' => array( + 'type' => 'error', + 'message' => '%s() is affected by runtime timezone changes which can cause date/time to be incorrectly displayed. Use gmdate() instead.', + 'functions' => array( + 'date', + ), + ), + ); + } +} diff --git a/WordPress/Sniffs/Files/FileNameSniff.php b/WordPress/Sniffs/Files/FileNameSniff.php index 057dc2bb6b..63fc917440 100644 --- a/WordPress/Sniffs/Files/FileNameSniff.php +++ b/WordPress/Sniffs/Files/FileNameSniff.php @@ -3,34 +3,39 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\Files; +namespace WordPressCS\WordPress\Sniffs\Files; -use WordPress\Sniff; +use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\ObjectDeclarations; +use PHPCSUtils\Utils\TextStrings; +use WordPressCS\WordPress\Helpers\IsUnitTestTrait; +use WordPressCS\WordPress\Sniff; /** - * Ensures filenames do not contain underscores. + * Ensures filenames do not contain underscores and where applicable are prefixed with `class-`. * - * @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions + * @link https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#naming-conventions * - * @package WPCS\WordPressCodingStandards - * - * @since 0.1.0 - * @since 0.11.0 - This sniff will now also check for all lowercase file names. - * - This sniff will now also verify that files containing a class start with `class-`. - * - This sniff will now also verify that files in `wp-includes` containing - * template tags end in `-template`. Based on @subpackage file DocBlock tag. - * - This sniff will now allow for underscores in file names for certain theme - * specific exceptions if the `$is_theme` property is set to `true`. - * @since 0.12.0 Now extends the `WordPress_Sniff` class. - * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.1.0 + * @since 0.11.0 - This sniff will now also check for all lowercase file names. + * - This sniff will now also verify that files containing a class start with `class-`. + * - This sniff will now also verify that files in `wp-includes` containing + * template tags end in `-template`. Based on @subpackage file DocBlock tag. + * - This sniff will now allow for underscores in file names for certain theme + * specific exceptions if the `$is_theme` property is set to `true`. + * @since 0.12.0 Now extends the WordPressCS native `Sniff` class. + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 3.0.0 Test class files are now completely exempt from this rule. * - * @uses \WordPress\Sniff::$custom_test_class_whitelist + * @uses \WordPressCS\WordPress\Helpers\IsUnitTestTrait::$custom_test_classes */ -class FileNameSniff extends Sniff { +final class FileNameSniff extends Sniff { + + use IsUnitTestTrait; /** * Regex for the theme specific exceptions. @@ -89,27 +94,39 @@ class FileNameSniff extends Sniff { /** * Historical exceptions in WP core to the class name rule. * + * Note: these files were renamed to comply with the naming conventions in + * WP 6.1.0. + * This means we no longer need to make an exception for them in the + * `check_filename_has_class_prefix()` check, however, we do still need to + * make an exception in the `check_filename_is_hyphenated()` check. + * * @since 0.11.0 + * @since 3.0.0 Property has been renamed from `$class_exceptions` to `$hyphenation_exceptions`, * * @var array */ - private $class_exceptions = array( + private $hyphenation_exceptions = array( 'class.wp-dependencies.php' => true, 'class.wp-scripts.php' => true, 'class.wp-styles.php' => true, + 'functions.wp-scripts.php' => true, + 'functions.wp-styles.php' => true, ); /** * Unit test version of the historical exceptions in WP core. * * @since 0.11.0 + * @since 3.0.0 Property has been renamed from `$unittest_class_exceptions` to `$unittest_hyphenation_exceptions`, * * @var array */ - private $unittest_class_exceptions = array( + private $unittest_hyphenation_exceptions = array( 'class.wp-dependencies.inc' => true, 'class.wp-scripts.inc' => true, 'class.wp-styles.inc' => true, + 'functions.wp-scripts.inc' => true, + 'functions.wp-styles.inc' => true, ); /** @@ -119,13 +136,10 @@ class FileNameSniff extends Sniff { */ public function register() { if ( \defined( '\PHP_CODESNIFFER_IN_TESTS' ) ) { - $this->class_exceptions = array_merge( $this->class_exceptions, $this->unittest_class_exceptions ); + $this->hyphenation_exceptions += $this->unittest_hyphenation_exceptions; } - return array( - \T_OPEN_TAG, - \T_OPEN_TAG_WITH_ECHO, - ); + return Collections::phpOpenTags(); } /** @@ -137,112 +151,163 @@ public function register() { * normal file processing. */ public function process_token( $stackPtr ) { - - // Usage of `strip_quotes` is to ensure `stdin_path` passed by IDEs does not include quotes. - $file = $this->strip_quotes( $this->phpcsFile->getFileName() ); + // Usage of `stripQuotes` is to ensure `stdin_path` passed by IDEs does not include quotes. + $file = TextStrings::stripQuotes( $this->phpcsFile->getFileName() ); if ( 'STDIN' === $file ) { return; } - // Respect phpcs:disable comments as long as they are not accompanied by an enable (PHPCS 3.2+). - if ( \defined( '\T_PHPCS_DISABLE' ) && \defined( '\T_PHPCS_ENABLE' ) ) { - $i = -1; - while ( $i = $this->phpcsFile->findNext( \T_PHPCS_DISABLE, ( $i + 1 ) ) ) { - if ( empty( $this->tokens[ $i ]['sniffCodes'] ) - || isset( $this->tokens[ $i ]['sniffCodes']['WordPress'] ) - || isset( $this->tokens[ $i ]['sniffCodes']['WordPress.Files'] ) - || isset( $this->tokens[ $i ]['sniffCodes']['WordPress.Files.FileName'] ) - ) { - do { - $i = $this->phpcsFile->findNext( \T_PHPCS_ENABLE, ( $i + 1 ) ); - } while ( false !== $i - && ! empty( $this->tokens[ $i ]['sniffCodes'] ) - && ! isset( $this->tokens[ $i ]['sniffCodes']['WordPress'] ) - && ! isset( $this->tokens[ $i ]['sniffCodes']['WordPress.Files'] ) - && ! isset( $this->tokens[ $i ]['sniffCodes']['WordPress.Files.FileName'] ) ); - - if ( false === $i ) { - // The entire (rest of the) file is disabled. - return; - } - } - } + $class_ptr = $this->phpcsFile->findNext( \T_CLASS, $stackPtr ); + if ( false !== $class_ptr && $this->is_test_class( $this->phpcsFile, $class_ptr ) ) { + /* + * This rule should not be applied to test classes (at all). + * @link https://github.com/WordPress/WordPress-Coding-Standards/issues/1995 + */ + return; } - $fileName = basename( $file ); - $expected = strtolower( str_replace( '_', '-', $fileName ) ); + // Respect phpcs:disable comments as long as they are not accompanied by an enable. + $i = -1; + while ( $i = $this->phpcsFile->findNext( \T_PHPCS_DISABLE, ( $i + 1 ) ) ) { + if ( empty( $this->tokens[ $i ]['sniffCodes'] ) + || isset( $this->tokens[ $i ]['sniffCodes']['WordPress'] ) + || isset( $this->tokens[ $i ]['sniffCodes']['WordPress.Files'] ) + || isset( $this->tokens[ $i ]['sniffCodes']['WordPress.Files.FileName'] ) + ) { + do { + $i = $this->phpcsFile->findNext( \T_PHPCS_ENABLE, ( $i + 1 ) ); + } while ( false !== $i + && ! empty( $this->tokens[ $i ]['sniffCodes'] ) + && ! isset( $this->tokens[ $i ]['sniffCodes']['WordPress'] ) + && ! isset( $this->tokens[ $i ]['sniffCodes']['WordPress.Files'] ) + && ! isset( $this->tokens[ $i ]['sniffCodes']['WordPress.Files.FileName'] ) ); - /* - * Generic check for lowercase hyphenated file names. - */ - if ( $fileName !== $expected && ( false === $this->is_theme || 1 !== preg_match( self::THEME_EXCEPTIONS_REGEX, $fileName ) ) ) { - $this->phpcsFile->addError( - 'Filenames should be all lowercase with hyphens as word separators. Expected %s, but found %s.', - 0, - 'NotHyphenatedLowercase', - array( $expected, $fileName ) - ); - } - unset( $expected ); - - /* - * Check files containing a class for the "class-" prefix and that the rest of - * the file name reflects the class name. - */ - if ( true === $this->strict_class_file_names ) { - $has_class = $this->phpcsFile->findNext( \T_CLASS, $stackPtr ); - if ( false !== $has_class && false === $this->is_test_class( $has_class ) ) { - $class_name = $this->phpcsFile->getDeclarationName( $has_class ); - $expected = 'class-' . strtolower( str_replace( '_', '-', $class_name ) ); - - if ( substr( $fileName, 0, -4 ) !== $expected && ! isset( $this->class_exceptions[ $fileName ] ) ) { - $this->phpcsFile->addError( - 'Class file names should be based on the class name with "class-" prepended. Expected %s, but found %s.', - 0, - 'InvalidClassFileName', - array( - $expected . '.php', - $fileName, - ) - ); + if ( false === $i ) { + // The entire (rest of the) file is disabled. + return; } - unset( $expected ); } } - /* - * Check non-class files in "wp-includes" with a "@subpackage Template" tag for a "-template" suffix. - */ - if ( false !== strpos( $file, \DIRECTORY_SEPARATOR . 'wp-includes' . \DIRECTORY_SEPARATOR ) ) { - $subpackage_tag = $this->phpcsFile->findNext( \T_DOC_COMMENT_TAG, $stackPtr, null, false, '@subpackage' ); - if ( false !== $subpackage_tag ) { - $subpackage = $this->phpcsFile->findNext( \T_DOC_COMMENT_STRING, $subpackage_tag ); - if ( false !== $subpackage ) { - $fileName_end = substr( $fileName, -13 ); - $has_class = $this->phpcsFile->findNext( \T_CLASS, $stackPtr ); - - if ( ( 'Template' === trim( $this->tokens[ $subpackage ]['content'] ) - && $this->tokens[ $subpackage_tag ]['line'] === $this->tokens[ $subpackage ]['line'] ) - && ( ( ! \defined( '\PHP_CODESNIFFER_IN_TESTS' ) && '-template.php' !== $fileName_end ) - || ( \defined( '\PHP_CODESNIFFER_IN_TESTS' ) && '-template.inc' !== $fileName_end ) ) - && false === $has_class - ) { - $this->phpcsFile->addError( - 'Files containing template tags should have "-template" appended to the end of the file name. Expected %s, but found %s.', - 0, - 'InvalidTemplateTagFileName', - array( - substr( $fileName, 0, -4 ) . '-template.php', - $fileName, - ) - ); - } - } - } + $file_name = basename( $file ); + + $this->check_filename_is_hyphenated( $file_name ); + + if ( true === $this->strict_class_file_names && false !== $class_ptr ) { + $this->check_filename_has_class_prefix( $class_ptr, $file_name ); + } + + if ( false !== strpos( $file, \DIRECTORY_SEPARATOR . 'wp-includes' . \DIRECTORY_SEPARATOR ) + && false === $class_ptr + ) { + $this->check_filename_for_template_suffix( $stackPtr, $file_name ); } // Only run this sniff once per file, no need to run it again. return ( $this->phpcsFile->numTokens + 1 ); } + /** + * Generic check for lowercase hyphenated file names. + * + * @since 3.0.0 + * + * @param string $file_name The name of the current file. + * + * @return void + */ + protected function check_filename_is_hyphenated( $file_name ) { + $extension = strrchr( $file_name, '.' ); + $name = substr( $file_name, 0, ( strlen( $file_name ) - strlen( $extension ) ) ); + + $expected = strtolower( preg_replace( '`[[:punct:]]`', '-', $name ) ) . $extension; + if ( $file_name === $expected + || isset( $this->hyphenation_exceptions[ $file_name ] ) + ) { + return; + } + + if ( true === $this->is_theme && 1 === preg_match( self::THEME_EXCEPTIONS_REGEX, $file_name ) ) { + return; + } + + $this->phpcsFile->addError( + 'Filenames should be all lowercase with hyphens as word separators. Expected %s, but found %s.', + 0, + 'NotHyphenatedLowercase', + array( $expected, $file_name ) + ); + } + + + /** + * Check files containing a class for the "class-" prefix and that the rest of + * the file name reflects the class name. + * + * @since 3.0.0 + * + * @param int $class_ptr Stack pointer to the first T_CLASS in the file. + * @param string $file_name The name of the current file. + * + * @return void + */ + protected function check_filename_has_class_prefix( $class_ptr, $file_name ) { + $extension = strrchr( $file_name, '.' ); + $class_name = ObjectDeclarations::getName( $this->phpcsFile, $class_ptr ); + $expected = 'class-' . strtolower( str_replace( '_', '-', $class_name ) ) . $extension; + + if ( $file_name === $expected ) { + return; + } + + $this->phpcsFile->addError( + 'Class file names should be based on the class name with "class-" prepended. Expected %s, but found %s.', + 0, + 'InvalidClassFileName', + array( + $expected, + $file_name, + ) + ); + } + + /** + * Check non-class files in "wp-includes" with a "@subpackage Template" tag for a "-template" suffix. + * + * @since 3.0.0 + * + * @param int $stackPtr Stack pointer to the first PHP open tag in the file. + * @param string $file_name The name of the current file. + * + * @return void + */ + protected function check_filename_for_template_suffix( $stackPtr, $file_name ) { + $subpackage_tag = $this->phpcsFile->findNext( \T_DOC_COMMENT_TAG, $stackPtr, null, false, '@subpackage' ); + if ( false === $subpackage_tag ) { + return; + } + + $subpackage = $this->phpcsFile->findNext( \T_DOC_COMMENT_STRING, $subpackage_tag ); + if ( false === $subpackage ) { + return; + } + + $fileName_end = substr( $file_name, -13 ); + + if ( ( 'Template' === trim( $this->tokens[ $subpackage ]['content'] ) + && $this->tokens[ $subpackage_tag ]['line'] === $this->tokens[ $subpackage ]['line'] ) + && ( ( ! \defined( '\PHP_CODESNIFFER_IN_TESTS' ) && '-template.php' !== $fileName_end ) + || ( \defined( '\PHP_CODESNIFFER_IN_TESTS' ) && '-template.inc' !== $fileName_end ) ) + ) { + $this->phpcsFile->addError( + 'Files containing template tags should have "-template" appended to the end of the file name. Expected %s, but found %s.', + 0, + 'InvalidTemplateTagFileName', + array( + substr( $file_name, 0, -4 ) . '-template.php', + $file_name, + ) + ); + } + } } diff --git a/WordPress/Sniffs/Functions/DontExtractSniff.php b/WordPress/Sniffs/Functions/DontExtractSniff.php deleted file mode 100644 index d2766e1629..0000000000 --- a/WordPress/Sniffs/Functions/DontExtractSniff.php +++ /dev/null @@ -1,73 +0,0 @@ - false, - 'FoundPropertyForDeprecatedSniff' => false, - ); - - /** - * Don't use. - * - * @deprecated 1.0.0 - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return void|int - */ - public function process_token( $stackPtr ) { - if ( false === $this->thrown['DeprecatedSniff'] ) { - $this->thrown['DeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.Functions.DontExtract" sniff has been renamed to "WordPress.PHP.DontExtract". Please update your custom ruleset.', - 0, - 'DeprecatedSniff' - ); - } - - if ( ! empty( $this->exclude ) - && false === $this->thrown['FoundPropertyForDeprecatedSniff'] - ) { - $this->thrown['FoundPropertyForDeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.Functions.DontExtract" sniff has been renamed to "WordPress.PHP.DontExtract". Please update your custom ruleset.', - 0, - 'FoundPropertyForDeprecatedSniff' - ); - } - - return parent::process_token( $stackPtr ); - } - -} diff --git a/WordPress/Sniffs/Functions/FunctionCallSignatureNoParamsSniff.php b/WordPress/Sniffs/Functions/FunctionCallSignatureNoParamsSniff.php deleted file mode 100644 index 820abc09b5..0000000000 --- a/WordPress/Sniffs/Functions/FunctionCallSignatureNoParamsSniff.php +++ /dev/null @@ -1,91 +0,0 @@ -phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); - - if ( \T_OPEN_PARENTHESIS !== $this->tokens[ $openParenthesis ]['code'] ) { - // Not a function call. - return; - } - - if ( ! isset( $this->tokens[ $openParenthesis ]['parenthesis_closer'] ) ) { - // Not a function call. - return; - } - - // Find the previous non-empty token. - $search = Tokens::$emptyTokens; - $search[] = \T_BITWISE_AND; - $previous = $this->phpcsFile->findPrevious( $search, ( $stackPtr - 1 ), null, true ); - if ( \T_FUNCTION === $this->tokens[ $previous ]['code'] ) { - // It's a function definition, not a function call. - return; - } - - $closer = $this->tokens[ $openParenthesis ]['parenthesis_closer']; - - if ( ( $closer - 1 ) === $openParenthesis ) { - return; - } - - $nextNonWhitespace = $this->phpcsFile->findNext( \T_WHITESPACE, ( $openParenthesis + 1 ), null, true ); - - if ( $nextNonWhitespace !== $closer ) { - // Function has params or comment between parenthesis. - return; - } - - $fix = $this->phpcsFile->addFixableError( - 'Function calls without parameters should have no spaces between the parenthesis.', - ( $openParenthesis + 1 ), - 'WhitespaceFound' - ); - if ( true === $fix ) { - // If there is only whitespace between the parenthesis, it will just be the one token. - $this->phpcsFile->fixer->replaceToken( ( $openParenthesis + 1 ), '' ); - } - } - -} diff --git a/WordPress/Sniffs/Functions/FunctionRestrictionsSniff.php b/WordPress/Sniffs/Functions/FunctionRestrictionsSniff.php deleted file mode 100644 index 0b60a51402..0000000000 --- a/WordPress/Sniffs/Functions/FunctionRestrictionsSniff.php +++ /dev/null @@ -1,48 +0,0 @@ - array( - * 'lambda' => array( - * 'type' => 'error' | 'warning', - * 'message' => 'Use anonymous functions instead please!', - * 'functions' => array( 'file_get_contents', 'create_function' ), - * ) - * ) - * - * @return array - */ - public function getGroups() { - return array(); - } - -} diff --git a/WordPress/Sniffs/NamingConventions/PrefixAllGlobalsSniff.php b/WordPress/Sniffs/NamingConventions/PrefixAllGlobalsSniff.php index 43d5a2b9a7..b04a652f89 100644 --- a/WordPress/Sniffs/NamingConventions/PrefixAllGlobalsSniff.php +++ b/WordPress/Sniffs/NamingConventions/PrefixAllGlobalsSniff.php @@ -3,28 +3,50 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\NamingConventions; - -use WordPress\AbstractFunctionParameterSniff; -use WordPress\PHPCSHelper; -use PHP_CodeSniffer_Tokens as Tokens; +namespace WordPressCS\WordPress\Sniffs\NamingConventions; + +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\BackCompat\Helper; +use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\Conditions; +use PHPCSUtils\Utils\Context; +use PHPCSUtils\Utils\FunctionDeclarations; +use PHPCSUtils\Utils\Lists; +use PHPCSUtils\Utils\MessageHelper; +use PHPCSUtils\Utils\Namespaces; +use PHPCSUtils\Utils\ObjectDeclarations; +use PHPCSUtils\Utils\Parentheses; +use PHPCSUtils\Utils\PassedParameters; +use PHPCSUtils\Utils\Scopes; +use PHPCSUtils\Utils\TextStrings; +use PHPCSUtils\Utils\Variables; +use WordPressCS\WordPress\AbstractFunctionParameterSniff; +use WordPressCS\WordPress\Helpers\DeprecationHelper; +use WordPressCS\WordPress\Helpers\IsUnitTestTrait; +use WordPressCS\WordPress\Helpers\ListHelper; +use WordPressCS\WordPress\Helpers\RulesetPropertyHelper; +use WordPressCS\WordPress\Helpers\VariableHelper; +use WordPressCS\WordPress\Helpers\WPGlobalVariablesHelper; +use WordPressCS\WordPress\Helpers\WPHookHelper; /** * Verify that everything defined in the global namespace is prefixed with a theme/plugin specific prefix. * - * @package WPCS\WordPressCodingStandards - * - * @since 0.12.0 - * @since 0.13.0 Class name changed: this class is now namespaced. - * @since 1.2.0 Now also checks whether namespaces are prefixed. + * @since 0.12.0 + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 1.2.0 Now also checks whether namespaces are prefixed. + * @since 2.2.0 - Now also checks variables assigned via the list() construct. + * - Now also ignores global functions which are marked as @deprecated. * - * @uses \WordPress\Sniff::$custom_test_class_whitelist + * @uses \WordPressCS\WordPress\Helpers\IsUnitTestTrait::$custom_test_classes */ -class PrefixAllGlobalsSniff extends AbstractFunctionParameterSniff { +final class PrefixAllGlobalsSniff extends AbstractFunctionParameterSniff { + + use IsUnitTestTrait; /** * Error message template. @@ -33,26 +55,39 @@ class PrefixAllGlobalsSniff extends AbstractFunctionParameterSniff { */ const ERROR_MSG = '%s by a theme/plugin should start with the theme/plugin prefix. Found: "%s".'; + /** + * Minimal number of characters the prefix needs in order to be valid. + * + * @since 2.2.0 + * + * @link https://github.com/WordPress/WordPress-Coding-Standards/issues/1733 Issue 1733. + * + * @var int + */ + const MIN_PREFIX_LENGTH = 3; + /** * Target prefixes. * * @since 0.12.0 * - * @var string[]|string + * @var string[] */ - public $prefixes = ''; + public $prefixes = array(); /** - * Prefix blacklist. + * Prefix blocklist. * * @since 0.12.0 + * @since 3.0.0 Renamed from `$prefix_blacklist` to `$prefix_blocklist`. * - * @var string[] + * @var array Key is prefix, value irrelevant. */ - protected $prefix_blacklist = array( + protected $prefix_blocklist = array( 'wordpress' => true, 'wp' => true, '_' => true, + 'php' => true, // See #1728, the 'php' prefix is reserved by PHP itself. ); /** @@ -62,7 +97,7 @@ class PrefixAllGlobalsSniff extends AbstractFunctionParameterSniff { * * @since 0.12.0 * - * @var string[] + * @var array */ private $validated_prefixes = array(); @@ -76,7 +111,7 @@ class PrefixAllGlobalsSniff extends AbstractFunctionParameterSniff { * * @since 1.2.0 * - * @var array + * @var array> */ private $validated_namespace_prefixes = array(); @@ -91,32 +126,15 @@ class PrefixAllGlobalsSniff extends AbstractFunctionParameterSniff { */ private $previous_prefixes = array(); - /** - * A list of all PHP superglobals with the exception of $GLOBALS which is handled separately. - * - * @since 0.12.0 - * - * @var array - */ - protected $superglobals = array( - '_COOKIE' => true, - '_ENV' => true, - '_GET' => true, - '_FILES' => true, - '_POST' => true, - '_REQUEST' => true, - '_SERVER' => true, - '_SESSION' => true, - ); - /** * A list of core hooks that are allowed to be called by plugins and themes. * * @since 0.14.0 + * @since 3.0.0 Renamed from `$whitelisted_core_hooks` to `$allowed_core_hooks`. * - * @var array + * @var array Key is hook name, value irrelevant. */ - protected $whitelisted_core_hooks = array( + protected $allowed_core_hooks = array( 'widget_title' => true, 'add_meta_boxes' => true, ); @@ -124,55 +142,267 @@ class PrefixAllGlobalsSniff extends AbstractFunctionParameterSniff { /** * A list of core constants that are allowed to be defined by plugins and themes. * - * @since 1.0.0 - * * Source: {@link https://core.trac.wordpress.org/browser/trunk/src/wp-includes/default-constants.php#L0} - * The constants are listed in the order they are found in the source file - * to make life easier for future updates. + * The constants are listed in alphabetic order. * Only overrulable constants are listed, i.e. those defined within core within * a `if ( ! defined() ) {}` wrapper. * - * @var array + * Last update: July 2023 for WP 6.3 at https://github.com/WordPress/wordpress-develop/commit/6281ce432c50345a57768bf53854d9b65b6cdd52 + * + * @since 1.0.0 + * @since 3.0.0 Renamed from `$whitelisted_core_constants` to `$allowed_core_constants`. + * + * @var array Key is constant name, value irrelevant. */ - protected $whitelisted_core_constants = array( - 'WP_MEMORY_LIMIT' => true, - 'WP_MAX_MEMORY_LIMIT' => true, + protected $allowed_core_constants = array( + 'ADMIN_COOKIE_PATH' => true, + 'AUTH_COOKIE' => true, + 'AUTOSAVE_INTERVAL' => true, + 'COOKIEHASH' => true, + 'COOKIEPATH' => true, + 'COOKIE_DOMAIN' => true, + 'EMPTY_TRASH_DAYS' => true, + 'FORCE_SSL_ADMIN' => true, + 'FORCE_SSL_LOGIN' => true, // Deprecated. + 'LOGGED_IN_COOKIE' => true, + 'MEDIA_TRASH' => true, + 'MUPLUGINDIR' => true, // Deprecated. + 'PASS_COOKIE' => true, + 'PLUGINDIR' => true, // Deprecated. + 'PLUGINS_COOKIE_PATH' => true, + 'RECOVERY_MODE_COOKIE' => true, + 'SCRIPT_DEBUG' => true, + 'SECURE_AUTH_COOKIE' => true, + 'SHORTINIT' => true, + 'SITECOOKIEPATH' => true, + 'TEST_COOKIE' => true, + 'USER_COOKIE' => true, + 'WPMU_PLUGIN_DIR' => true, + 'WPMU_PLUGIN_URL' => true, + 'WP_CACHE' => true, 'WP_CONTENT_DIR' => true, + 'WP_CONTENT_URL' => true, + 'WP_CRON_LOCK_TIMEOUT' => true, 'WP_DEBUG' => true, 'WP_DEBUG_DISPLAY' => true, 'WP_DEBUG_LOG' => true, - 'WP_CACHE' => true, - 'SCRIPT_DEBUG' => true, - 'MEDIA_TRASH' => true, - 'SHORTINIT' => true, - 'WP_CONTENT_URL' => true, + 'WP_DEFAULT_THEME' => true, + 'WP_DEVELOPMENT_MODE' => true, + 'WP_MAX_MEMORY_LIMIT' => true, + 'WP_MEMORY_LIMIT' => true, 'WP_PLUGIN_DIR' => true, 'WP_PLUGIN_URL' => true, - 'PLUGINDIR' => true, - 'WPMU_PLUGIN_DIR' => true, - 'WPMU_PLUGIN_URL' => true, - 'MUPLUGINDIR' => true, - 'COOKIEHASH' => true, - 'USER_COOKIE' => true, - 'PASS_COOKIE' => true, - 'AUTH_COOKIE' => true, - 'SECURE_AUTH_COOKIE' => true, - 'LOGGED_IN_COOKIE' => true, - 'TEST_COOKIE' => true, - 'COOKIEPATH' => true, - 'SITECOOKIEPATH' => true, - 'ADMIN_COOKIE_PATH' => true, - 'PLUGINS_COOKIE_PATH' => true, - 'COOKIE_DOMAIN' => true, - 'FORCE_SSL_ADMIN' => true, - 'FORCE_SSL_LOGIN' => true, - 'AUTOSAVE_INTERVAL' => true, - 'EMPTY_TRASH_DAYS' => true, 'WP_POST_REVISIONS' => true, - 'WP_CRON_LOCK_TIMEOUT' => true, - 'WP_DEFAULT_THEME' => true, + 'WP_START_TIMESTAMP' => true, ); + /** + * A list of functions declared in WP core as "Pluggable", i.e. overloadable from a plugin. + * + * Note: deprecated functions should still be included in this list as plugins may support older WP versions. + * + * @since 3.0.0. + * + * @var array Key is function name, value irrelevant. + */ + protected $pluggable_functions = array( + 'auth_redirect' => true, + 'cache_users' => true, + 'check_admin_referer' => true, + 'check_ajax_referer' => true, + 'get_avatar' => true, + 'get_currentuserinfo' => true, // Deprecated. + 'get_user_by' => true, + 'get_user_by_email' => true, // Deprecated. + 'get_userdata' => true, + 'get_userdatabylogin' => true, // Deprecated. + 'graceful_fail' => true, + 'install_global_terms' => true, + 'install_network' => true, + 'is_user_logged_in' => true, + // 'lowercase_octets' => true, => unclear if this function is meant to be publicly pluggable. + 'maybe_add_column' => true, + 'maybe_create_table' => true, + 'set_current_user' => true, // Deprecated. + 'twenty_twenty_one_entry_meta_footer' => true, + 'twenty_twenty_one_post_thumbnail' => true, + 'twenty_twenty_one_post_title' => true, + 'twenty_twenty_one_posted_by' => true, + 'twenty_twenty_one_posted_on' => true, + 'twenty_twenty_one_setup' => true, + 'twenty_twenty_one_the_posts_navigation' => true, + 'twentyeleven_admin_header_image' => true, + 'twentyeleven_admin_header_style' => true, + 'twentyeleven_comment' => true, + 'twentyeleven_content_nav' => true, + 'twentyeleven_continue_reading_link' => true, + 'twentyeleven_header_style' => true, + 'twentyeleven_posted_on' => true, + 'twentyeleven_setup' => true, + 'twentyfifteen_comment_nav' => true, + 'twentyfifteen_entry_meta' => true, + 'twentyfifteen_excerpt_more' => true, + 'twentyfifteen_fonts_url' => true, + 'twentyfifteen_get_color_scheme' => true, + 'twentyfifteen_get_color_scheme_choices' => true, + 'twentyfifteen_get_link_url' => true, + 'twentyfifteen_header_style' => true, + 'twentyfifteen_post_thumbnail' => true, + 'twentyfifteen_sanitize_color_scheme' => true, + 'twentyfifteen_setup' => true, + 'twentyfifteen_the_custom_logo' => true, + 'twentyfourteen_admin_header_image' => true, + 'twentyfourteen_admin_header_style' => true, + 'twentyfourteen_excerpt_more' => true, + 'twentyfourteen_font_url' => true, + 'twentyfourteen_header_style' => true, + 'twentyfourteen_list_authors' => true, + 'twentyfourteen_paging_nav' => true, + 'twentyfourteen_post_nav' => true, + 'twentyfourteen_post_thumbnail' => true, + 'twentyfourteen_posted_on' => true, + 'twentyfourteen_setup' => true, + 'twentyfourteen_the_attached_image' => true, + 'twentynineteen_comment_count' => true, + 'twentynineteen_comment_form' => true, + 'twentynineteen_discussion_avatars_list' => true, + 'twentynineteen_entry_footer' => true, + 'twentynineteen_get_user_avatar_markup' => true, + 'twentynineteen_post_thumbnail' => true, + 'twentynineteen_posted_by' => true, + 'twentynineteen_posted_on' => true, + 'twentynineteen_setup' => true, + 'twentynineteen_the_posts_navigation' => true, + 'twentyseventeen_edit_link' => true, + 'twentyseventeen_entry_footer' => true, + 'twentyseventeen_fonts_url' => true, + 'twentyseventeen_header_style' => true, + 'twentyseventeen_posted_on' => true, + 'twentyseventeen_time_link' => true, + 'twentysixteen_categorized_blog' => true, + 'twentysixteen_entry_date' => true, + 'twentysixteen_entry_meta' => true, + 'twentysixteen_entry_taxonomies' => true, + 'twentysixteen_excerpt' => true, + 'twentysixteen_excerpt_more' => true, + 'twentysixteen_fonts_url' => true, + 'twentysixteen_get_color_scheme' => true, + 'twentysixteen_get_color_scheme_choices' => true, + 'twentysixteen_header_style' => true, + 'twentysixteen_post_thumbnail' => true, + 'twentysixteen_sanitize_color_scheme' => true, + 'twentysixteen_setup' => true, + 'twentysixteen_the_custom_logo' => true, + 'twentyten_admin_header_style' => true, + 'twentyten_comment' => true, + 'twentyten_continue_reading_link' => true, + 'twentyten_posted_in' => true, + 'twentyten_posted_on' => true, + 'twentyten_setup' => true, + 'twentythirteen_entry_date' => true, + 'twentythirteen_entry_meta' => true, + 'twentythirteen_excerpt_more' => true, + 'twentythirteen_fonts_url' => true, + 'twentythirteen_paging_nav' => true, + 'twentythirteen_post_nav' => true, + 'twentythirteen_the_attached_image' => true, + 'twentytwelve_comment' => true, + 'twentytwelve_content_nav' => true, + 'twentytwelve_entry_meta' => true, + 'twentytwelve_get_font_url' => true, + 'twentytwenty_customize_partial_blogdescription' => true, + 'twentytwenty_customize_partial_blogname' => true, + 'twentytwenty_customize_partial_site_logo' => true, + 'twentytwenty_generate_css' => true, + 'twentytwenty_get_customizer_css' => true, + 'twentytwenty_get_theme_svg' => true, + 'twentytwenty_the_theme_svg' => true, + 'twentytwentytwo_styles' => true, + 'twentytwentytwo_support' => true, + 'wp_authenticate' => true, + 'wp_cache_add_multiple' => true, + 'wp_cache_delete_multiple' => true, + 'wp_cache_flush_group' => true, + 'wp_cache_flush_runtime' => true, + 'wp_cache_get_multiple' => true, + 'wp_cache_set_multiple' => true, + 'wp_cache_supports' => true, + 'wp_check_password' => true, + 'wp_clear_auth_cookie' => true, + 'wp_clearcookie' => true, // Deprecated. + 'wp_create_nonce' => true, + 'wp_generate_auth_cookie' => true, + 'wp_generate_password' => true, + 'wp_get_cookie_login' => true, // Deprecated. + 'wp_get_current_user' => true, + // 'wp_handle_upload_error' => true, => unclear if this function is meant to be publicly pluggable. + 'wp_hash' => true, + 'wp_hash_password' => true, + 'wp_install' => true, + 'wp_install_defaults' => true, + 'wp_login' => true, // Deprecated. + 'wp_logout' => true, + 'wp_mail' => true, + 'wp_new_blog_notification' => true, + 'wp_new_user_notification' => true, + 'wp_nonce_tick' => true, + 'wp_notify_moderator' => true, + 'wp_notify_postauthor' => true, + 'wp_parse_auth_cookie' => true, + 'wp_password_change_notification' => true, + 'wp_rand' => true, + 'wp_redirect' => true, + 'wp_safe_redirect' => true, + 'wp_salt' => true, + 'wp_sanitize_redirect' => true, + 'wp_set_auth_cookie' => true, + 'wp_set_current_user' => true, + 'wp_set_password' => true, + 'wp_setcookie' => true, // Deprecated. + 'wp_text_diff' => true, + 'wp_upgrade' => true, + 'wp_validate_auth_cookie' => true, + 'wp_validate_redirect' => true, + 'wp_verify_nonce' => true, + ); + + /** + * A list of classes declared in WP core as "Pluggable", i.e. overloadable from a plugin. + * + * Source: {@link https://core.trac.wordpress.org/browser/trunk/src/wp-includes/pluggable.php} + * and {@link https://core.trac.wordpress.org/browser/trunk/src/wp-includes/pluggable-deprecated.php} + * + * Note: deprecated classes should still be included in this list as plugins may support older WP versions. + * + * @since 3.0.0. + * + * @var array Key is class name, value irrelevant. + */ + protected $pluggable_classes = array( + 'TwentyTwenty_Customize' => true, + 'TwentyTwenty_Non_Latin_Languages' => true, + 'TwentyTwenty_SVG_Icons' => true, + 'TwentyTwenty_Script_Loader' => true, + 'TwentyTwenty_Separator_Control' => true, + 'TwentyTwenty_Walker_Comment' => true, + 'TwentyTwenty_Walker_Page' => true, + 'Twenty_Twenty_One_Customize' => true, + 'WP_User_Search' => true, + 'wp_atom_server' => true, // Deprecated. + ); + + /** + * List of all PHP native functions. + * + * Using this list rather than a call to `function_exists()` prevents + * false negatives from user-defined functions when those would be + * autoloaded via a Composer autoload files directives. + * + * @var array + */ + private $built_in_functions; + + /** * Returns an array of tokens this test wants to listen for. * @@ -181,17 +411,26 @@ class PrefixAllGlobalsSniff extends AbstractFunctionParameterSniff { * @return array */ public function register() { - $targets = array( - \T_NAMESPACE => \T_NAMESPACE, - \T_FUNCTION => \T_FUNCTION, - \T_CLASS => \T_CLASS, - \T_INTERFACE => \T_INTERFACE, - \T_TRAIT => \T_TRAIT, - \T_CONST => \T_CONST, - \T_VARIABLE => \T_VARIABLE, - \T_DOLLAR => \T_DOLLAR, // Variable variables. - \T_ANON_CLASS => \T_ANON_CLASS, // Only used for skipping over test classes. + // Get a list of all PHP native functions. + $all_functions = get_defined_functions(); + $this->built_in_functions = array_flip( $all_functions['internal'] ); + $this->built_in_functions = array_change_key_case( $this->built_in_functions, \CASE_LOWER ); + + // Make sure the pluggable functions and classes list can be easily compared. + $this->pluggable_functions = array_change_key_case( $this->pluggable_functions, \CASE_LOWER ); + $this->pluggable_classes = array_change_key_case( $this->pluggable_classes, \CASE_LOWER ); + + // Set the sniff targets. + $targets = array( + \T_NAMESPACE => \T_NAMESPACE, + \T_FUNCTION => \T_FUNCTION, + \T_CONST => \T_CONST, + \T_VARIABLE => \T_VARIABLE, + \T_DOLLAR => \T_DOLLAR, // Variable variables. + \T_FN_ARROW => \T_FN_ARROW, // T_FN_ARROW is only used for skipping over (for now). ); + $targets += Tokens::$ooScopeTokens; // T_ANON_CLASS is only used for skipping over test classes. + $targets += Collections::listOpenTokensBC(); // Add function call target for hook names and constants defined using define(). $parent = parent::register(); @@ -210,7 +449,8 @@ public function register() { * @return array */ public function getGroups() { - $this->target_functions = $this->hookInvokeFunctions; + // Only retrieve functions which are not used for deprecated hooks. + $this->target_functions = WPHookHelper::get_functions( false ); $this->target_functions['define'] = true; return parent::getGroups(); @@ -227,26 +467,17 @@ public function getGroups() { * normal file processing. */ public function process_token( $stackPtr ) { - /* - * Allow for whitelisting. - * - * Generally speaking a theme/plugin should *only* execute their own hooks, but there may be a - * good reason to execute a core hook. - * - * Similarly, newer PHP or WP functions or constants may need to be emulated for continued support - * of older PHP and WP versions. - */ - if ( $this->has_whitelist_comment( 'prefix', $stackPtr ) ) { - return; - } // Allow overruling the prefixes set in a ruleset via the command line. - $cl_prefixes = trim( PHPCSHelper::get_config_data( 'prefixes' ) ); + $cl_prefixes = Helper::getConfigData( 'prefixes' ); if ( ! empty( $cl_prefixes ) ) { - $this->prefixes = $cl_prefixes; + $cl_prefixes = trim( $cl_prefixes ); + if ( '' !== $cl_prefixes ) { + $this->prefixes = array_filter( array_map( 'trim', explode( ',', $cl_prefixes ) ) ); + } } - $this->prefixes = $this->merge_custom_array( $this->prefixes, array(), false ); + $this->prefixes = RulesetPropertyHelper::merge_custom_array( $this->prefixes, array(), false ); if ( empty( $this->prefixes ) ) { // No prefixes passed, nothing to do. return; @@ -259,10 +490,8 @@ public function process_token( $stackPtr ) { } // Ignore test classes. - if ( ( \T_CLASS === $this->tokens[ $stackPtr ]['code'] - || \T_TRAIT === $this->tokens[ $stackPtr ]['code'] - || \T_ANON_CLASS === $this->tokens[ $stackPtr ]['code'] ) - && true === $this->is_test_class( $stackPtr ) + if ( isset( Tokens::$ooScopeTokens[ $this->tokens[ $stackPtr ]['code'] ] ) + && true === $this->is_test_class( $this->phpcsFile, $stackPtr ) ) { if ( $this->tokens[ $stackPtr ]['scope_condition'] === $stackPtr && isset( $this->tokens[ $stackPtr ]['scope_closer'] ) ) { // Skip forward to end of test class. @@ -276,6 +505,29 @@ public function process_token( $stackPtr ) { return; } + /* + * Ignore the contents of arrow functions which do not declare closures. + * + * - Parameters declared by arrow functions do not need to be prefixed (handled elsewhere). + * - New variables declared within an arrow function are local to the arrow function, so can be ignored. + * - A `global` statement is not allowed within an arrow function. + * + * Note: this does mean some convoluted code may get ignored (false negatives), but this is currently + * not reliably solvable as PHPCS does not add arrow functions to the 'conditions' array. + */ + if ( \T_FN_ARROW === $this->tokens[ $stackPtr ]['code'] + && isset( $this->tokens[ $stackPtr ]['scope_closer'] ) + ) { + $has_closure = $this->phpcsFile->findNext( \T_CLOSURE, ( $stackPtr + 1 ), $this->tokens[ $stackPtr ]['scope_closer'] ); + if ( false !== $has_closure ) { + // Skip to the start of the closure. + return $has_closure; + } + + // Skip the arrow function completely. + return $this->tokens[ $stackPtr ]['scope_closer']; + } + if ( \T_STRING === $this->tokens[ $stackPtr ]['code'] ) { // Disallow excluding function groups for this sniff. $this->exclude = array(); @@ -290,8 +542,11 @@ public function process_token( $stackPtr ) { return $this->process_variable_assignment( $stackPtr ); + } elseif ( isset( Collections::listOpenTokensBC()[ $this->tokens[ $stackPtr ]['code'] ] ) ) { + return $this->process_list_assignment( $stackPtr ); + } elseif ( \T_NAMESPACE === $this->tokens[ $stackPtr ]['code'] ) { - $namespace_name = $this->get_declared_namespace_name( $stackPtr ); + $namespace_name = Namespaces::getDeclaredName( $this->phpcsFile, $stackPtr ); if ( false === $namespace_name || '' === $namespace_name || '\\' === $namespace_name ) { return; @@ -333,7 +588,7 @@ public function process_token( $stackPtr ) { } else { // Namespaced methods, classes and constants do not need to be prefixed. - $namespace = $this->determine_namespace( $stackPtr ); + $namespace = Namespaces::determineNamespace( $this->phpcsFile, $stackPtr ); if ( '' !== $namespace && '\\' !== $namespace ) { return; } @@ -342,39 +597,60 @@ public function process_token( $stackPtr ) { $error_text = 'Unknown syntax used'; $error_code = 'NonPrefixedSyntaxFound'; - switch ( $this->tokens[ $stackPtr ]['type'] ) { - case 'T_FUNCTION': + switch ( $this->tokens[ $stackPtr ]['code'] ) { + case \T_FUNCTION: // Methods in a class do not need to be prefixed. - if ( $this->phpcsFile->hasCondition( $stackPtr, array( \T_CLASS, \T_ANON_CLASS, \T_INTERFACE, \T_TRAIT ) ) === true ) { + if ( Scopes::isOOMethod( $this->phpcsFile, $stackPtr ) === true ) { return; } - $item_name = $this->phpcsFile->getDeclarationName( $stackPtr ); - if ( function_exists( '\\' . $item_name ) ) { + if ( DeprecationHelper::is_function_deprecated( $this->phpcsFile, $stackPtr ) === true ) { + /* + * Deprecated functions don't have to comply with the naming conventions, + * otherwise functions deprecated in favour of a function with a compliant + * name would still trigger an error. + */ + return; + } + + $item_name = FunctionDeclarations::getName( $this->phpcsFile, $stackPtr ); + $item_lc = strtolower( $item_name ); + if ( isset( $this->built_in_functions[ $item_lc ] ) ) { // Backfill for PHP native function. return; } - $error_text = 'Functions declared'; + if ( isset( $this->pluggable_functions[ $item_lc ] ) ) { + // Pluggable function should not be prefixed. + return; + } + + $error_text = 'Functions declared in the global namespace'; $error_code = 'NonPrefixedFunctionFound'; break; - case 'T_CLASS': - case 'T_INTERFACE': - case 'T_TRAIT': - $item_name = $this->phpcsFile->getDeclarationName( $stackPtr ); + case \T_CLASS: + case \T_INTERFACE: + case \T_TRAIT: + case \T_ENUM: + $item_name = ObjectDeclarations::getName( $this->phpcsFile, $stackPtr ); $error_text = 'Classes declared'; $error_code = 'NonPrefixedClassFound'; - switch ( $this->tokens[ $stackPtr ]['type'] ) { - case 'T_CLASS': + switch ( $this->tokens[ $stackPtr ]['code'] ) { + case \T_CLASS: + if ( isset( $this->pluggable_classes[ strtolower( $item_name ) ] ) ) { + // Pluggable class should not be prefixed. + return; + } + if ( class_exists( '\\' . $item_name, false ) ) { // Backfill for PHP native class. return; } break; - case 'T_INTERFACE': + case \T_INTERFACE: if ( interface_exists( '\\' . $item_name, false ) ) { // Backfill for PHP native interface. return; @@ -384,7 +660,7 @@ public function process_token( $stackPtr ) { $error_code = 'NonPrefixedInterfaceFound'; break; - case 'T_TRAIT': + case \T_TRAIT: // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.trait_existsFound if ( function_exists( '\trait_exists' ) && trait_exists( '\\' . $item_name, false ) ) { // Backfill for PHP native trait. @@ -395,16 +671,23 @@ public function process_token( $stackPtr ) { $error_code = 'NonPrefixedTraitFound'; break; - default: - // Left empty on purpose. + case \T_ENUM: + // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.enum_existsFound + if ( function_exists( '\enum_exists' ) && enum_exists( '\\' . $item_name, false ) ) { + // Backfill for PHP native enum. + return; + } + + $error_text = 'Enums declared'; + $error_code = 'NonPrefixedEnumFound'; break; } break; - case 'T_CONST': - // Constants in a class do not need to be prefixed. - if ( true === $this->is_class_constant( $stackPtr ) ) { + case \T_CONST: + // Constants in an OO construct do not need to be prefixed. + if ( true === Scopes::isOOConstant( $this->phpcsFile, $stackPtr ) ) { return; } @@ -420,7 +703,7 @@ public function process_token( $stackPtr ) { return; } - if ( isset( $this->whitelisted_core_constants[ $item_name ] ) ) { + if ( isset( $this->allowed_core_constants[ $item_name ] ) ) { // Defining a WP Core constant intended for overruling. return; } @@ -520,13 +803,9 @@ protected function process_variable_variable( $stackPtr ) { * forbidden since PHP 7.0. Presuming cross-version code and if not, that * is for the PHPCompatibility standard to detect. */ - if ( $this->phpcsFile->hasCondition( $stackPtr, array( \T_FUNCTION, \T_CLOSURE ) ) === true ) { - $condition = $this->phpcsFile->getCondition( $stackPtr, \T_FUNCTION ); - if ( false === $condition ) { - $condition = $this->phpcsFile->getCondition( $stackPtr, \T_CLOSURE ); - } - - $has_global = $this->phpcsFile->findPrevious( \T_GLOBAL, ( $stackPtr - 1 ), $this->tokens[ $condition ]['scope_opener'] ); + $functionPtr = Conditions::getLastCondition( $this->phpcsFile, $stackPtr, Collections::functionDeclarationTokens() ); + if ( false !== $functionPtr ) { + $has_global = $this->phpcsFile->findPrevious( \T_GLOBAL, ( $stackPtr - 1 ), $this->tokens[ $functionPtr ]['scope_opener'] ); if ( false === $has_global ) { // No variable import happening. return; @@ -543,7 +822,7 @@ protected function process_variable_variable( $stackPtr ) { $stackPtr, 'NonPrefixedVariableFound', array( - 'Variables defined', + 'Global variables defined', $variable_name, ) ); @@ -560,21 +839,25 @@ protected function process_variable_variable( $stackPtr ) { * Check that defined global variables are prefixed. * * @since 0.12.0 + * @since 2.2.0 Added $in_list parameter. * - * @param int $stackPtr The position of the current token in the stack. + * @param int $stackPtr The position of the current token in the stack. + * @param bool $in_list Whether or not this is a variable in a list assignment. + * Defaults to false. * * @return int|void Integer stack pointer to skip forward or void to continue * normal file processing. */ - protected function process_variable_assignment( $stackPtr ) { + protected function process_variable_assignment( $stackPtr, $in_list = false ) { /* * We're only concerned with variables which are being defined. * `is_assigment()` will not recognize property assignments, which is good in this case. * However it will also not recognize $b in `foreach( $a as $b )` as an assignment, so * we need a separate check for that. */ - if ( false === $this->is_assignment( $stackPtr ) - && false === $this->is_foreach_as( $stackPtr ) + if ( false === $in_list + && false === VariableHelper::is_assignment( $this->phpcsFile, $stackPtr ) + && Context::inForeachCondition( $this->phpcsFile, $stackPtr ) !== 'afterAs' ) { return; } @@ -583,7 +866,9 @@ protected function process_variable_assignment( $stackPtr ) { $variable_name = substr( $this->tokens[ $stackPtr ]['content'], 1 ); // Strip the dollar sign. // Bow out early if we know for certain no prefix is needed. - if ( $this->variable_prefixed_or_whitelisted( $stackPtr, $variable_name ) === true ) { + if ( 'GLOBALS' !== $variable_name + && $this->variable_prefixed_or_allowed( $stackPtr, $variable_name ) === true + ) { return; } @@ -601,11 +886,11 @@ protected function process_variable_assignment( $stackPtr ) { } $stackPtr = $array_key; - $variable_name = $this->strip_quotes( $this->tokens[ $array_key ]['content'] ); + $variable_name = TextStrings::stripQuotes( $this->tokens[ $array_key ]['content'] ); // Check whether a prefix is needed. if ( isset( Tokens::$stringTokens[ $this->tokens[ $array_key ]['code'] ] ) - && $this->variable_prefixed_or_whitelisted( $stackPtr, $variable_name ) === true + && $this->variable_prefixed_or_allowed( $stackPtr, $variable_name ) === true ) { return; } @@ -616,7 +901,7 @@ protected function process_variable_assignment( $stackPtr ) { $exploded = explode( '$', $variable_name ); $first = rtrim( $exploded[0], '{' ); if ( '' !== $first ) { - if ( $this->variable_prefixed_or_whitelisted( $array_key, $first ) === true ) { + if ( $this->variable_prefixed_or_allowed( $array_key, $first ) === true ) { return; } } else { @@ -629,38 +914,32 @@ protected function process_variable_assignment( $stackPtr ) { } } else { // Function parameters do not need to be prefixed. - if ( isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) { - foreach ( $this->tokens[ $stackPtr ]['nested_parenthesis'] as $opener => $closer ) { - if ( isset( $this->tokens[ $opener ]['parenthesis_owner'] ) - && ( \T_FUNCTION === $this->tokens[ $this->tokens[ $opener ]['parenthesis_owner'] ]['code'] - || \T_CLOSURE === $this->tokens[ $this->tokens[ $opener ]['parenthesis_owner'] ]['code'] ) - ) { - return; - } + if ( false === $in_list ) { + $functionPtr = Parentheses::getLastOwner( $this->phpcsFile, $stackPtr, Collections::functionDeclarationTokens() ); + if ( false !== $functionPtr ) { + return; } - unset( $opener, $closer ); + unset( $functionPtr ); } // Properties in a class do not need to be prefixed. - if ( true === $this->is_class_property( $stackPtr ) ) { + if ( false === $in_list && true === Scopes::isOOProperty( $this->phpcsFile, $stackPtr ) ) { return; } // Local variables in a function do not need to be prefixed unless they are being imported. - if ( $this->phpcsFile->hasCondition( $stackPtr, array( \T_FUNCTION, \T_CLOSURE ) ) === true ) { - $condition = $this->phpcsFile->getCondition( $stackPtr, \T_FUNCTION ); - if ( false === $condition ) { - $condition = $this->phpcsFile->getCondition( $stackPtr, \T_CLOSURE ); - } - - $has_global = $this->phpcsFile->findPrevious( \T_GLOBAL, ( $stackPtr - 1 ), $this->tokens[ $condition ]['scope_opener'] ); - if ( false === $has_global ) { - // No variable import happening. + $functionPtr = Conditions::getLastCondition( $this->phpcsFile, $stackPtr, Collections::functionDeclarationTokens() ); + if ( false !== $functionPtr ) { + $has_global = $this->phpcsFile->findPrevious( \T_GLOBAL, ( $stackPtr - 1 ), $this->tokens[ $functionPtr ]['scope_opener'] ); + if ( false === $has_global + || Conditions::getLastCondition( $this->phpcsFile, $has_global, Collections::functionDeclarationTokens() ) !== $functionPtr + ) { + // No variable import happening in the current scope. return; } // Ok, this may be an imported global variable. - $end_of_statement = $this->phpcsFile->findNext( \T_SEMICOLON, ( $has_global + 1 ) ); + $end_of_statement = $this->phpcsFile->findNext( array( \T_SEMICOLON, \T_CLOSE_TAG ), ( $has_global + 1 ) ); if ( false === $end_of_statement ) { // No semi-colon - live coding. return; @@ -681,19 +960,19 @@ protected function process_variable_assignment( $stackPtr ) { } } - unset( $condition, $has_global, $end_of_statement, $ptr, $imported ); - + unset( $has_global, $end_of_statement, $ptr ); } } // Still here ? In that case, the variable name should be prefixed. - $recorded = $this->addMessage( + $recorded = MessageHelper::addMessage( + $this->phpcsFile, self::ERROR_MSG, $stackPtr, $is_error, 'NonPrefixedVariableFound', array( - 'Variables defined', + 'Global variables defined', '$' . $variable_name, ) ); @@ -703,49 +982,79 @@ protected function process_variable_assignment( $stackPtr ) { } } + /** + * Check that global variables declared via a list construct are prefixed. + * + * {@internal No need to take special measures for nested lists. Nested or not, + * each list part can only contain one variable being written to.} + * + * @since 2.2.0 + * + * @param int $stackPtr The position of the current token in the stack. + * + * @return int|void Integer stack pointer to skip forward or void to continue + * normal file processing. + */ + protected function process_list_assignment( $stackPtr ) { + $list_open_close = Lists::getOpenClose( $this->phpcsFile, $stackPtr ); + if ( false === $list_open_close ) { + // Short array, not short list. + return; + } + + $var_pointers = ListHelper::get_list_variables( $this->phpcsFile, $stackPtr ); + foreach ( $var_pointers as $ptr ) { + $this->process_variable_assignment( $ptr, true ); + } + + // No need to re-examine these variables. + return $list_open_close['closer']; + } + /** * Process the parameters of a matched function. * * @since 0.12.0 * * @param int $stackPtr The position of the current token in the stack. - * @param array $group_name The name of the group which was matched. - * @param string $matched_content The token content (function name) which was matched. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. * @param array $parameters Array with information about the parameters. * * @return void */ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { + if ( 'define' === $matched_content ) { + $target_param = PassedParameters::getParameterFromStack( $parameters, 1, 'constant_name' ); - // Ignore deprecated hook names. - if ( strpos( $matched_content, '_deprecated' ) > 0 ) { - return; + } else { + $target_param = WPHookHelper::get_hook_name_param( $matched_content, $parameters ); } - // No matter whether it is a constant definition or a hook call, both use the first parameter. - if ( ! isset( $parameters[1] ) ) { + if ( false === $target_param ) { return; } - $is_error = true; - $raw_content = $this->strip_quotes( $parameters[1]['raw'] ); + $is_error = true; + $clean_content = TextStrings::stripQuotes( $target_param['clean'] ); if ( ( 'define' !== $matched_content - && isset( $this->whitelisted_core_hooks[ $raw_content ] ) ) + && isset( $this->allowed_core_hooks[ $clean_content ] ) ) || ( 'define' === $matched_content - && isset( $this->whitelisted_core_constants[ $raw_content ] ) ) + && isset( $this->allowed_core_constants[ $clean_content ] ) ) ) { return; } - if ( $this->is_prefixed( $parameters[1]['start'], $raw_content ) === true ) { + if ( $this->is_prefixed( $target_param['start'], $clean_content ) === true ) { return; } else { // This may be a dynamic hook/constant name. $first_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, - $parameters[1]['start'], - ( $parameters[1]['end'] + 1 ), + $target_param['start'], + ( $target_param['end'] + 1 ), true ); @@ -753,11 +1062,11 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p return; } - $first_non_empty_content = $this->strip_quotes( $this->tokens[ $first_non_empty ]['content'] ); + $first_non_empty_content = TextStrings::stripQuotes( $this->tokens[ $first_non_empty ]['content'] ); // Try again with just the first token if it's a text string. if ( isset( Tokens::$stringTokens[ $this->tokens[ $first_non_empty ]['code'] ] ) - && $this->is_prefixed( $parameters[1]['start'], $first_non_empty_content ) === true + && $this->is_prefixed( $target_param['start'], $first_non_empty_content ) === true ) { return; } @@ -768,7 +1077,7 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p $exploded = explode( '$', $first_non_empty_content ); $first = rtrim( $exploded[0], '{' ); if ( '' !== $first ) { - if ( $this->is_prefixed( $parameters[1]['start'], $first ) === true ) { + if ( $this->is_prefixed( $target_param['start'], $first ) === true ) { return; } } else { @@ -782,29 +1091,35 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p } if ( 'define' === $matched_content ) { - if ( \defined( '\\' . $raw_content ) ) { + if ( \defined( '\\' . $clean_content ) ) { // Backfill for PHP native constant. return; } - if ( strpos( $raw_content, '\\' ) !== false ) { + if ( strpos( $clean_content, '\\' ) !== false ) { // Namespaced or unreachable constant. return; } $data = array( 'Global constants defined' ); $error_code = 'NonPrefixedConstantFound'; + if ( false === $is_error ) { + $error_code = 'VariableConstantNameFound'; + } } else { $data = array( 'Hook names invoked' ); $error_code = 'NonPrefixedHooknameFound'; + if ( false === $is_error ) { + $error_code = 'DynamicHooknameFound'; + } } - $data[] = $raw_content; + $data[] = $clean_content; - $recorded = $this->addMessage( self::ERROR_MSG, $first_non_empty, $is_error, $error_code, $data ); + $recorded = MessageHelper::addMessage( $this->phpcsFile, self::ERROR_MSG, $first_non_empty, $is_error, $error_code, $data ); if ( true === $recorded ) { - $this->record_potential_prefix_metric( $stackPtr, $raw_content ); + $this->record_potential_prefix_metric( $stackPtr, $clean_content ); } } @@ -846,16 +1161,17 @@ private function is_prefixed( $stackPtr, $name ) { * * @since 0.12.0 * @since 1.0.1 Added $stackPtr parameter. + * @since 3.0.0 Renamed from `variable_prefixed_or_whitelisted()` to `variable_prefixed_or_allowed()`. * * @param int $stackPtr The position of the token to record the metric against. * @param string $name Variable name without the dollar sign. * - * @return bool True if the variable name is whitelisted or already prefixed. + * @return bool True if the variable name is allowed or already prefixed. * False otherwise. */ - private function variable_prefixed_or_whitelisted( $stackPtr, $name ) { + private function variable_prefixed_or_allowed( $stackPtr, $name ) { // Ignore superglobals and WP global variables. - if ( isset( $this->superglobals[ $name ] ) || isset( $this->wp_globals[ $name ] ) ) { + if ( Variables::isSuperglobalName( $name ) || WPGlobalVariablesHelper::is_wp_global( $name ) ) { return true; } @@ -866,10 +1182,12 @@ private function variable_prefixed_or_whitelisted( $stackPtr, $name ) { * Validate an array of prefixes as passed through a custom property or via the command line. * * Checks that the prefix: - * - is not one of the blacklisted ones. + * - is not one of the blocked ones. * - complies with the PHP rules for valid function, class, variable, constant names. * * @since 0.12.0 + * + * @return void */ private function validate_prefixes() { if ( $this->previous_prefixes === $this->prefixes ) { @@ -885,7 +1203,7 @@ private function validate_prefixes() { foreach ( $this->prefixes as $key => $prefix ) { $prefixLC = strtolower( $prefix ); - if ( isset( $this->prefix_blacklist[ $prefixLC ] ) ) { + if ( isset( $this->prefix_blocklist[ $prefixLC ] ) ) { $this->phpcsFile->addError( 'The "%s" prefix is not allowed.', 0, @@ -895,8 +1213,28 @@ private function validate_prefixes() { continue; } - // Validate the prefix against characters allowed for function, class, constant names etc. - if ( preg_match( '`^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff\\\\]*$`', $prefix ) !== 1 ) { + $prefix_length = strlen( $prefix ); + if ( function_exists( 'iconv_strlen' ) ) { + $prefix_length = iconv_strlen( $prefix, Helper::getEncoding( $this->phpcsFile ) ); + } + + if ( $prefix_length < self::MIN_PREFIX_LENGTH ) { + $this->phpcsFile->addError( + 'The "%s" prefix is too short. Short prefixes are not unique enough and may cause name collisions with other code.', + 0, + 'ShortPrefixPassed', + array( $prefix ) + ); + continue; + } + + /* + * Validate the prefix against characters allowed for function, class, constant names etc. + * Note: this does not use the PHPCSUtils `NamingConventions::isValidIdentifierName()` method + * as we want to allow namespace separators in the prefixes. + */ + if ( preg_match( '`^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff\\\\]*$`', $prefix ) !== 1 ) { + $this->phpcsFile->addWarning( 'The "%s" prefix is not a valid namespace/function/class/variable/constant prefix in PHP.', 0, diff --git a/WordPress/Sniffs/NamingConventions/ValidFunctionNameSniff.php b/WordPress/Sniffs/NamingConventions/ValidFunctionNameSniff.php index dc72792185..a925de6fde 100644 --- a/WordPress/Sniffs/NamingConventions/ValidFunctionNameSniff.php +++ b/WordPress/Sniffs/NamingConventions/ValidFunctionNameSniff.php @@ -3,180 +3,186 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\NamingConventions; +namespace WordPressCS\WordPress\Sniffs\NamingConventions; -use PEAR_Sniffs_NamingConventions_ValidFunctionNameSniff as PHPCS_PEAR_ValidFunctionNameSniff; -use PHP_CodeSniffer_File as File; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Utils\FunctionDeclarations; +use PHPCSUtils\Utils\NamingConventions; +use PHPCSUtils\Utils\ObjectDeclarations; +use PHPCSUtils\Utils\Scopes; +use WordPressCS\WordPress\Helpers\DeprecationHelper; +use WordPressCS\WordPress\Helpers\SnakeCaseHelper; +use WordPressCS\WordPress\Sniff; /** * Enforces WordPress function name and method name format, based upon Squiz code. * - * @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions + * @link https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#naming-conventions * - * @package WPCS\WordPressCodingStandards - * - * @since 0.1.0 - * @since 0.13.0 Class name changed: this class is now namespaced. - * - * Last synced with parent class July 2016 up to commit 4fea2e651109e41066a81e22e004d851fb1287f6. - * @link https://github.com/squizlabs/PHP_CodeSniffer/blob/master/CodeSniffer/Standards/PEAR/Sniffs/NamingConventions/ValidFunctionNameSniff.php - * - * {@internal While this class extends the PEAR parent, it does not actually use the checks - * contained in the parent. It only uses the properties and the token registration from the parent.}} + * @since 0.1.0 + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 2.0.0 The `get_name_suggestion()` method has been moved to the + * WordPress native `Sniff` base class as `get_snake_case_name_suggestion()`. + * @since 2.2.0 Will now ignore functions and methods which are marked as @deprecated. + * @since 3.0.0 This sniff has been refactored and no longer extends the upstream + * PEAR.NamingConventions.ValidFunctionName sniff. */ -class ValidFunctionNameSniff extends PHPCS_PEAR_ValidFunctionNameSniff { +final class ValidFunctionNameSniff extends Sniff { /** - * Additional double underscore prefixed methods specific to certain PHP native extensions. - * - * Currently only handles the SoapClient Extension. + * Returns an array of tokens this test wants to listen for. * - * @link http://php.net/manual/en/class.soapclient.php + * @since 3.0.0 * - * @var array => + * @return array */ - private $methodsDoubleUnderscore = array( - 'doRequest' => 'SoapClient', - 'getFunctions' => 'SoapClient', - 'getLastRequest' => 'SoapClient', - 'getLastRequestHeaders' => 'SoapClient', - 'getLastResponse' => 'SoapClient', - 'getLastResponseHeaders' => 'SoapClient', - 'getTypes' => 'SoapClient', - 'setCookie' => 'SoapClient', - 'setLocation' => 'SoapClient', - 'setSoapHeaders' => 'SoapClient', - 'soapCall' => 'SoapClient', - ); + public function register() { + return array( \T_FUNCTION ); + } /** - * Processes the tokens outside the scope. + * Processes this test, when one of its tokens is encountered. * - * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being processed. - * @param int $stackPtr The position where this token was - * found. + * @since 3.0.0 * - * @return void + * @param int $stackPtr The position of the current token in the stack. + * + * @return int|void Integer stack pointer to skip forward or void to continue + * normal file processing. */ - protected function processTokenOutsideScope( File $phpcsFile, $stackPtr ) { - $functionName = $phpcsFile->getDeclarationName( $stackPtr ); + public function process_token( $stackPtr ) { + + if ( DeprecationHelper::is_function_deprecated( $this->phpcsFile, $stackPtr ) === true ) { + /* + * Deprecated functions don't have to comply with the naming conventions, + * otherwise functions deprecated in favour of a function with a compliant + * name would still trigger an error. + */ + return; + } - if ( ! isset( $functionName ) ) { - // Ignore closures. + $name = FunctionDeclarations::getName( $this->phpcsFile, $stackPtr ); + if ( empty( $name ) === true ) { + // Live coding or parse error. return; } - if ( '' === ltrim( $functionName, '_' ) ) { - // Ignore special functions. + if ( '' === ltrim( $name, '_' ) ) { + // Ignore special functions, like __(). return; } - // Is this a magic function ? I.e., it is prefixed with "__" ? - // Outside class scope this basically just means __autoload(). - if ( 0 === strpos( $functionName, '__' ) ) { - $magicPart = strtolower( substr( $functionName, 2 ) ); - if ( isset( $this->magicFunctions[ $magicPart ] ) ) { - return; - } + $ooPtr = Scopes::validDirectScope( $this->phpcsFile, $stackPtr, Tokens::$ooScopeTokens ); + if ( false === $ooPtr ) { + $this->process_function_declaration( $stackPtr, $name ); + } else { + $this->process_method_declaration( $stackPtr, $name, $ooPtr ); + } + } + /** + * Processes a function declaration for a function in the global namespace. + * + * @since 0.1.0 + * @since 3.0.0 Renamed from `processTokenOutsideScope()` to `process_function_declaration()`. + * Method signature has been changed as well as this method no longer overloads + * a method from the PEAR sniff which was previously the sniff parent. + * + * @param int $stackPtr The position where this token was found. + * @param string $functionName The name of the function. + * + * @return void + */ + protected function process_function_declaration( $stackPtr, $functionName ) { + // PHP magic functions are exempt from our rules. + if ( FunctionDeclarations::isMagicFunctionName( $functionName ) === true ) { + return; + } + + // Is the function name prefixed with "__" ? + if ( preg_match( '`^__[^_]`', $functionName ) === 1 ) { $error = 'Function name "%s" is invalid; only PHP magic methods should be prefixed with a double underscore'; $errorData = array( $functionName ); - $phpcsFile->addError( $error, $stackPtr, 'FunctionDoubleUnderscore', $errorData ); + $this->phpcsFile->addError( $error, $stackPtr, 'FunctionDoubleUnderscore', $errorData ); } - if ( strtolower( $functionName ) !== $functionName ) { + $suggested_name = SnakeCaseHelper::get_suggestion( $functionName ); + if ( $suggested_name !== $functionName ) { $error = 'Function name "%s" is not in snake case format, try "%s"'; $errorData = array( $functionName, - $this->get_name_suggestion( $functionName ), + $suggested_name, ); - $phpcsFile->addError( $error, $stackPtr, 'FunctionNameInvalid', $errorData ); + $this->phpcsFile->addError( $error, $stackPtr, 'FunctionNameInvalid', $errorData ); } } /** - * Processes the tokens within the scope. + * Processes a method declaration. + * + * @since 0.1.0 + * @since 3.0.0 Renamed from `processTokenWithinScope()` to `process_method_declaration()`. + * Method signature has been changed as well, as this method no longer overloads + * a method from the PEAR sniff which was previously the sniff parent. * - * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being processed. - * @param int $stackPtr The position where this token was - * found. - * @param int $currScope The position of the current scope. + * @param int $stackPtr The position where this token was found. + * @param string $methodName The name of the method. + * @param int $currScope The position of the current scope. * * @return void */ - protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currScope ) { - $methodName = $phpcsFile->getDeclarationName( $stackPtr ); - - if ( ! isset( $methodName ) ) { - // Ignore closures. - return; - } + protected function process_method_declaration( $stackPtr, $methodName, $currScope ) { - $className = $phpcsFile->getDeclarationName( $currScope ); + if ( \T_ANON_CLASS === $this->tokens[ $currScope ]['code'] ) { + $className = '[Anonymous Class]'; + } else { + $className = ObjectDeclarations::getName( $this->phpcsFile, $currScope ); - // Ignore special functions. - if ( '' === ltrim( $methodName, '_' ) ) { - return; - } + // PHP4 constructors are allowed to break our rules. + if ( NamingConventions::isEqual( $methodName, $className ) === true ) { + return; + } - // PHP4 constructors are allowed to break our rules. - if ( $methodName === $className ) { - return; + // PHP4 destructors are allowed to break our rules. + if ( NamingConventions::isEqual( $methodName, '_' . $className ) === true ) { + return; + } } - // PHP4 destructors are allowed to break our rules. - if ( '_' . $className === $methodName ) { + // PHP magic methods are exempt from our rules. + if ( FunctionDeclarations::isMagicMethodName( $methodName ) === true ) { return; } - $extended = $phpcsFile->findExtendedClassName( $currScope ); - $interfaces = $phpcsFile->findImplementedInterfaceNames( $currScope ); + $extended = ObjectDeclarations::findExtendedClassName( $this->phpcsFile, $currScope ); + $interfaces = ObjectDeclarations::findImplementedInterfaceNames( $this->phpcsFile, $currScope ); // If this is a child class or interface implementation, it may have to use camelCase or double underscores. if ( ! empty( $extended ) || ! empty( $interfaces ) ) { return; } - // Is this a magic method ? I.e. is it prefixed with "__" ? - if ( 0 === strpos( $methodName, '__' ) ) { - $magicPart = strtolower( substr( $methodName, 2 ) ); - if ( isset( $this->magicMethods[ $magicPart ] ) || isset( $this->methodsDoubleUnderscore[ $magicPart ] ) ) { - return; - } - + // Is the method name prefixed with "__" ? + if ( preg_match( '`^__[^_]`', $methodName ) === 1 ) { $error = 'Method name "%s" is invalid; only PHP magic methods should be prefixed with a double underscore'; $errorData = array( $className . '::' . $methodName ); - $phpcsFile->addError( $error, $stackPtr, 'MethodDoubleUnderscore', $errorData ); + $this->phpcsFile->addError( $error, $stackPtr, 'MethodDoubleUnderscore', $errorData ); } // Check for all lowercase. - if ( strtolower( $methodName ) !== $methodName ) { + $suggested_name = SnakeCaseHelper::get_suggestion( $methodName ); + if ( $suggested_name !== $methodName ) { $error = 'Method name "%s" in class %s is not in snake case format, try "%s"'; $errorData = array( $methodName, $className, - $this->get_name_suggestion( $methodName ), + $suggested_name, ); - $phpcsFile->addError( $error, $stackPtr, 'MethodNameInvalid', $errorData ); + $this->phpcsFile->addError( $error, $stackPtr, 'MethodNameInvalid', $errorData ); } } - - /** - * Transform the existing function/method name to one which complies with the naming conventions. - * - * @param string $name The function/method name. - * @return string - */ - protected function get_name_suggestion( $name ) { - $suggested = preg_replace( '/([A-Z])/', '_$1', $name ); - $suggested = strtolower( $suggested ); - $suggested = str_replace( '__', '_', $suggested ); - $suggested = trim( $suggested, '_' ); - return $suggested; - } - } diff --git a/WordPress/Sniffs/NamingConventions/ValidHookNameSniff.php b/WordPress/Sniffs/NamingConventions/ValidHookNameSniff.php index 7d7dcccff7..57ce70464a 100644 --- a/WordPress/Sniffs/NamingConventions/ValidHookNameSniff.php +++ b/WordPress/Sniffs/NamingConventions/ValidHookNameSniff.php @@ -3,13 +3,16 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\NamingConventions; +namespace WordPressCS\WordPress\Sniffs\NamingConventions; -use WordPress\AbstractFunctionParameterSniff; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Utils\TextStrings; +use WordPressCS\WordPress\AbstractFunctionParameterSniff; +use WordPressCS\WordPress\Helpers\WPHookHelper; /** * Use lowercase letters in action and filter names. Separate words via underscores. @@ -19,13 +22,11 @@ * * Hook names invoked with `do_action_deprecated()` and `apply_filters_deprecated()` are ignored. * - * @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions + * @link https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#naming-conventions * - * @package WPCS\WordPressCodingStandards - * - * @since 0.10.0 - * @since 0.11.0 Extends the WordPress_AbstractFunctionParameterSniff class. - * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.10.0 + * @since 0.11.0 Extends the WordPressCS native `AbstractFunctionParameterSniff` class. + * @since 0.13.0 Class name changed: this class is now namespaced. */ class ValidHookNameSniff extends AbstractFunctionParameterSniff { @@ -64,14 +65,16 @@ class ValidHookNameSniff extends AbstractFunctionParameterSniff { protected $punctuation_regex = '`[^\w%s]`'; /** - * Groups of function to restrict. + * Groups of functions to restrict. * * @since 0.11.0 * * @return array */ public function getGroups() { - $this->target_functions = $this->hookInvokeFunctions; + // Only retrieve functions which are not used for deprecated hooks. + $this->target_functions = WPHookHelper::get_functions( false ); + return parent::getGroups(); } @@ -81,81 +84,129 @@ public function getGroups() { * @since 0.11.0 * * @param int $stackPtr The position of the current token in the stack. - * @param array $group_name The name of the group which was matched. - * @param string $matched_content The token content (function name) which was matched. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. * @param array $parameters Array with information about the parameters. * * @return void */ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { - // Ignore deprecated hook names. - if ( strpos( $matched_content, '_deprecated' ) > 0 ) { - return; - } - if ( ! isset( $parameters[1] ) ) { + $hook_name_param = WPHookHelper::get_hook_name_param( $matched_content, $parameters ); + if ( false === $hook_name_param ) { return; } $regex = $this->prepare_regex(); - $case_errors = 0; - $underscores = 0; - $content = array(); - $expected = array(); + $case_errors = 0; + $underscores = 0; + $content = array(); + $expected = array(); + $last_non_empty = null; + + for ( $i = $hook_name_param['start']; $i <= $hook_name_param['end']; $i++ ) { + // Skip past comment tokens. + if ( isset( Tokens::$commentTokens[ $this->tokens[ $i ]['code'] ] ) ) { + continue; + } - for ( $i = $parameters[1]['start']; $i <= $parameters[1]['end']; $i++ ) { $content[ $i ] = $this->tokens[ $i ]['content']; $expected[ $i ] = $this->tokens[ $i ]['content']; - if ( \in_array( $this->tokens[ $i ]['code'], array( \T_CONSTANT_ENCAPSED_STRING, \T_DOUBLE_QUOTED_STRING ), true ) ) { - $string = $this->strip_quotes( $this->tokens[ $i ]['content'] ); - - /* - * Here be dragons - a double quoted string can contain extrapolated variables - * which don't have to comply with these rules. - */ - if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $i ]['code'] ) { - $transform = $this->transform_complex_string( $string, $regex ); - $case_transform = $this->transform_complex_string( $string, $regex, 'case' ); - $punct_transform = $this->transform_complex_string( $string, $regex, 'punctuation' ); - } else { - $transform = $this->transform( $string, $regex ); - $case_transform = $this->transform( $string, $regex, 'case' ); - $punct_transform = $this->transform( $string, $regex, 'punctuation' ); - } - - if ( $string === $transform ) { - continue; - } - - if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $i ]['code'] ) { - $expected[ $i ] = '"' . $transform . '"'; - } else { - $expected[ $i ] = '\'' . $transform . '\''; - } - - if ( $string !== $case_transform ) { - $case_errors++; - } - if ( $string !== $punct_transform ) { - $underscores++; - } + // Skip past potential variable array access: `$var['key']`. + if ( \T_VARIABLE === $this->tokens[ $i ]['code'] ) { + do { + $open_bracket = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true ); + if ( false === $open_bracket + || \T_OPEN_SQUARE_BRACKET !== $this->tokens[ $open_bracket ]['code'] + || ! isset( $this->tokens[ $open_bracket ]['bracket_closer'] ) + ) { + $last_non_empty = $i; + continue 2; + } + + $i = $this->tokens[ $open_bracket ]['bracket_closer']; + + } while ( isset( $this->tokens[ $i ] ) && $i <= $hook_name_param['end'] ); + + $last_non_empty = $i; + continue; + } + + // Skip over parameters passed to function calls. + if ( \T_OPEN_PARENTHESIS === $this->tokens[ $i ]['code'] + && ( \T_STRING === $this->tokens[ $last_non_empty ]['code'] + || \T_VARIABLE === $this->tokens[ $last_non_empty ]['code'] ) + && isset( $this->tokens[ $i ]['parenthesis_closer'] ) + ) { + $i = $this->tokens[ $i ]['parenthesis_closer']; + $last_non_empty = $i; + continue; + } + + // Skip past non text string tokens. + if ( isset( Tokens::$stringTokens[ $this->tokens[ $i ]['code'] ] ) === false ) { + $last_non_empty = $i; + continue; + } + + $last_non_empty = $i; + $string = TextStrings::stripQuotes( $this->tokens[ $i ]['content'] ); + + /* + * Here be dragons - a double quoted string can contain extrapolated variables + * which don't have to comply with these rules. + */ + if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $i ]['code'] ) { + $transform = $this->transform_complex_string( $string, $regex ); + $case_transform = $this->transform_complex_string( $string, $regex, 'case' ); + $punct_transform = $this->transform_complex_string( $string, $regex, 'punctuation' ); + } else { + $transform = $this->transform( $string, $regex ); + $case_transform = $this->transform( $string, $regex, 'case' ); + $punct_transform = $this->transform( $string, $regex, 'punctuation' ); + } + + if ( $string === $transform ) { + continue; + } + + if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $i ]['code'] ) { + $expected[ $i ] = '"' . $transform . '"'; + } else { + $expected[ $i ] = '\'' . $transform . '\''; + } + + if ( $string !== $case_transform ) { + ++$case_errors; + } + if ( $string !== $punct_transform ) { + ++$underscores; } } + $first_non_empty = $this->phpcsFile->findNext( + Tokens::$emptyTokens, + $hook_name_param['start'], + ( $hook_name_param['end'] + 1 ), + true + ); + $data = array( - implode( '', $expected ), - implode( '', $content ), + trim( implode( '', $expected ) ), + trim( implode( '', $content ) ), ); if ( $case_errors > 0 ) { $error = 'Hook names should be lowercase. Expected: %s, but found: %s.'; - $this->phpcsFile->addError( $error, $stackPtr, 'NotLowercase', $data ); + $this->phpcsFile->addError( $error, $first_non_empty, 'NotLowercase', $data ); } + if ( $underscores > 0 ) { $error = 'Words in hook names should be separated using underscores. Expected: %s, but found: %s.'; - $this->phpcsFile->addWarning( $error, $stackPtr, 'UseUnderscores', $data ); + $this->phpcsFile->addWarning( $error, $first_non_empty, 'UseUnderscores', $data ); } } @@ -163,8 +214,8 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p * Prepare the punctuation regular expression. * * Merges the existing regular expression with potentially provided extra word delimiters to allow. - * This is done 'late' and for each found token as otherwise inline `@codingStandardsChangeSetting` - * directives would be ignored. + * This is done 'late' and for each found token as otherwise inline `phpcs:set` directives + * would be ignored. * * @return string */ @@ -180,77 +231,47 @@ protected function prepare_regex() { /** * Transform an arbitrary string to lowercase and replace punctuation and spaces with underscores. * - * @param string $string The target string. + * @param string $text_string The target string. * @param string $regex The punctuation regular expression to use. - * @param string $transform_type Whether to a partial or complete transform. + * @param string $transform_type Whether to do a partial or complete transform. * Valid values are: 'full', 'case', 'punctuation'. * @return string */ - protected function transform( $string, $regex, $transform_type = 'full' ) { + protected function transform( $text_string, $regex, $transform_type = 'full' ) { switch ( $transform_type ) { case 'case': - return strtolower( $string ); + return strtolower( $text_string ); case 'punctuation': - return preg_replace( $regex, '_', $string ); + return preg_replace( $regex, '_', $text_string ); case 'full': default: - return preg_replace( $regex, '_', strtolower( $string ) ); + return preg_replace( $regex, '_', strtolower( $text_string ) ); } } /** * Transform a complex string which may contain variable extrapolation. * - * @param string $string The target string. + * @param string $text_string The target string. * @param string $regex The punctuation regular expression to use. - * @param string $transform_type Whether to a partial or complete transform. + * @param string $transform_type Whether to do a partial or complete transform. * Valid values are: 'full', 'case', 'punctuation'. * @return string */ - protected function transform_complex_string( $string, $regex, $transform_type = 'full' ) { - $output = preg_split( '`([\{\}\$\[\] ])`', $string, -1, \PREG_SPLIT_DELIM_CAPTURE ); - - $is_variable = false; - $has_braces = false; - $braces = 0; - - foreach ( $output as $i => $part ) { - if ( \in_array( $part, array( '$', '{' ), true ) ) { - $is_variable = true; - if ( '{' === $part ) { - $has_braces = true; - $braces++; - } - continue; - } + protected function transform_complex_string( $text_string, $regex, $transform_type = 'full' ) { + $plain_text = TextStrings::stripEmbeds( $text_string ); + $embeds = TextStrings::getEmbeds( $text_string ); - if ( true === $is_variable ) { - if ( '[' === $part ) { - $has_braces = true; - $braces++; - } - if ( \in_array( $part, array( '}', ']' ), true ) ) { - $braces--; - } - if ( false === $has_braces && ' ' === $part ) { - $is_variable = false; - $output[ $i ] = $this->transform( $part, $regex, $transform_type ); - } - - if ( ( true === $has_braces && 0 === $braces ) && false === \in_array( $output[ ( $i + 1 ) ], array( '{', '[' ), true ) ) { - $has_braces = false; - $is_variable = false; - } - continue; - } + $transformed_text = $this->transform( $plain_text, $regex, $transform_type ); - $output[ $i ] = $this->transform( $part, $regex, $transform_type ); + // Inject the embeds back into the text string. + foreach ( $embeds as $offset => $embed ) { + $transformed_text = substr_replace( $transformed_text, $embed, $offset, 0 ); } - return implode( '', $output ); + return $transformed_text; } - } diff --git a/WordPress/Sniffs/NamingConventions/ValidPostTypeSlugSniff.php b/WordPress/Sniffs/NamingConventions/ValidPostTypeSlugSniff.php new file mode 100644 index 0000000000..2e01da8abf --- /dev/null +++ b/WordPress/Sniffs/NamingConventions/ValidPostTypeSlugSniff.php @@ -0,0 +1,228 @@ + Key is function name, value irrelevant. + */ + protected $target_functions = array( + 'register_post_type' => true, + ); + + /** + * Array of reserved post type names which can not be used by themes and plugins. + * + * Source: {@link https://developer.wordpress.org/reference/functions/register_post_type/#reserved-post-types} + * + * Last update: July 2023 for WP 6.3 at https://github.com/WordPress/wordpress-develop/commit/6281ce432c50345a57768bf53854d9b65b6cdd52 + * + * @since 2.2.0 + * + * @var array Key is reserved post type name, value irrelevant. + */ + protected $reserved_names = array( + 'action' => true, // Not a WP post type, but prevents other problems. + 'attachment' => true, + 'author' => true, // Not a WP post type, but prevents other problems. + 'custom_css' => true, + 'customize_changeset' => true, + 'nav_menu_item' => true, + 'oembed_cache' => true, + 'order' => true, // Not a WP post type, but prevents other problems. + 'page' => true, + 'post' => true, + 'revision' => true, + 'theme' => true, // Not a WP post type, but prevents other problems. + 'user_request' => true, + 'wp_block' => true, + 'wp_global_styles' => true, + 'wp_navigation' => true, + 'wp_template' => true, + 'wp_template_part' => true, + ); + + /** + * All valid tokens for in the first parameter of register_post_type(). + * + * Set in `register()`. + * + * @since 2.2.0 + * + * @var array + */ + private $valid_tokens = array(); + + /** + * Returns an array of tokens this test wants to listen for. + * + * @since 2.2.0 + * + * @return array + */ + public function register() { + $this->valid_tokens = Tokens::$textStringTokens + Tokens::$heredocTokens + Tokens::$emptyTokens; + return parent::register(); + } + + /** + * Process the parameter of a matched function. + * + * Errors on invalid post type names when reserved keywords are used, + * the post type is too long, or contains invalid characters. + * + * @since 2.2.0 + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * @param array $parameters Array with information about the parameters. + * + * @return void + */ + public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { + $post_type_param = PassedParameters::getParameterFromStack( $parameters, 1, 'post_type' ); + if ( false === $post_type_param || '' === $post_type_param['clean'] ) { + // Error for using empty slug. + $this->phpcsFile->addError( + 'register_post_type() called without a post type slug. The slug must be a non-empty string.', + false === $post_type_param ? $stackPtr : $post_type_param['start'], + 'Empty' + ); + return; + } + + $string_start = $this->phpcsFile->findNext( Collections::textStringStartTokens(), $post_type_param['start'], ( $post_type_param['end'] + 1 ) ); + $string_pos = $this->phpcsFile->findNext( Tokens::$textStringTokens, $post_type_param['start'], ( $post_type_param['end'] + 1 ) ); + + $has_invalid_tokens = $this->phpcsFile->findNext( $this->valid_tokens, $post_type_param['start'], ( $post_type_param['end'] + 1 ), true ); + if ( false !== $has_invalid_tokens || false === $string_pos ) { + // Check for non string based slug parameter (we cannot determine if this is valid). + $this->phpcsFile->addWarning( + 'The post type slug is not a string literal. It is not possible to automatically determine the validity of this slug. Found: %s.', + $stackPtr, + 'NotStringLiteral', + array( + $post_type_param['clean'], + ), + 3 + ); + return; + } + + $post_type = TextStrings::getCompleteTextString( $this->phpcsFile, $string_start ); + if ( isset( Tokens::$heredocTokens[ $this->tokens[ $string_start ]['code'] ] ) ) { + // Trim off potential indentation from PHP 7.3 flexible heredoc/nowdoc content. + $post_type = ltrim( $post_type ); + } + + $data = array( + $post_type, + ); + + // Warn for dynamic parts in the slug parameter. + if ( 'T_DOUBLE_QUOTED_STRING' === $this->tokens[ $string_pos ]['type'] + || ( 'T_HEREDOC' === $this->tokens[ $string_pos ]['type'] + && strpos( $this->tokens[ $string_pos ]['content'], '$' ) !== false ) + ) { + $this->phpcsFile->addWarning( + 'The post type slug may, or may not, get too long with dynamic contents and could contain invalid characters. Found: "%s".', + $string_pos, + 'PartiallyDynamic', + $data + ); + $post_type = TextStrings::stripEmbeds( $post_type ); + } + + if ( preg_match( self::VALID_POST_TYPE_CHARACTERS, $post_type ) === 0 ) { + // Error for invalid characters. + $this->phpcsFile->addError( + 'register_post_type() called with invalid post type "%s". Post type contains invalid characters. Only lowercase alphanumeric characters, dashes, and underscores are allowed.', + $string_pos, + 'InvalidCharacters', + $data + ); + } + + if ( isset( $this->reserved_names[ $post_type ] ) ) { + // Error for using reserved slug names. + $this->phpcsFile->addError( + 'register_post_type() called with reserved post type "%s". Reserved post types should not be used as they interfere with the functioning of WordPress itself.', + $string_pos, + 'Reserved', + $data + ); + } elseif ( stripos( $post_type, 'wp_' ) === 0 ) { + // Error for using reserved slug prefix. + $this->phpcsFile->addError( + 'The post type passed to register_post_type() uses a prefix reserved for WordPress itself. Found: "%s".', + $string_pos, + 'ReservedPrefix', + $data + ); + } + + // Error for slugs that are too long. + if ( strlen( $post_type ) > self::POST_TYPE_MAX_LENGTH ) { + $this->phpcsFile->addError( + 'A post type slug must not exceed %d characters. Found: "%s" (%d characters).', + $string_pos, + 'TooLong', + array( + self::POST_TYPE_MAX_LENGTH, + $post_type, + strlen( $post_type ), + ) + ); + } + } +} diff --git a/WordPress/Sniffs/NamingConventions/ValidVariableNameSniff.php b/WordPress/Sniffs/NamingConventions/ValidVariableNameSniff.php index 2325514ad2..37171540cd 100644 --- a/WordPress/Sniffs/NamingConventions/ValidVariableNameSniff.php +++ b/WordPress/Sniffs/NamingConventions/ValidVariableNameSniff.php @@ -3,56 +3,35 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\NamingConventions; +namespace WordPressCS\WordPress\Sniffs\NamingConventions; -use PHP_CodeSniffer_Standards_AbstractVariableSniff as PHPCS_AbstractVariableSniff; -use PHP_CodeSniffer_File as File; -use PHP_CodeSniffer_Tokens as Tokens; -use WordPress\Sniff; +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\AbstractVariableSniff as PHPCS_AbstractVariableSniff; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Utils\Scopes; +use PHPCSUtils\Utils\TextStrings; +use PHPCSUtils\Utils\Variables; +use WordPressCS\WordPress\Helpers\ContextHelper; +use WordPressCS\WordPress\Helpers\RulesetPropertyHelper; +use WordPressCS\WordPress\Helpers\SnakeCaseHelper; /** * Checks the naming of variables and member variables. * - * @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#naming-conventions + * @link https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#naming-conventions * - * @package WPCS\WordPressCodingStandards - * - * @since 0.9.0 - * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.9.0 + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 2.0.0 Now offers name suggestions for variables in violation. * - * Last synced with base class June 2018 at commit 78ddbae97cac078f09928bf89e3ab9e53ad2ace0. - * @link https://github.com/squizlabs/PHP_CodeSniffer/blob/master/src/Standards/Squiz/Sniffs/NamingConventions/ValidVariableNameSniff.php - * One change from upstream deferred till later (PHPCS 3.3.0+): - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/issues/1048#issuecomment-364282100 + * Last synced with base class January 2022 at commit 4b49a952bf0e2c3863d0a113256bae0d7fe63d52. + * @link https://github.com/squizlabs/PHP_CodeSniffer/blob/master/src/Standards/Squiz/Sniffs/NamingConventions/ValidVariableNameSniff.php */ -class ValidVariableNameSniff extends PHPCS_AbstractVariableSniff { - - /** - * PHP Reserved Vars. - * - * @since 0.9.0 - * @since 0.11.0 Changed visibility from public to protected. - * - * @var array - */ - protected $php_reserved_vars = array( - '_SERVER' => true, - '_GET' => true, - '_POST' => true, - '_REQUEST' => true, - '_SESSION' => true, - '_ENV' => true, - '_COOKIE' => true, - '_FILES' => true, - 'GLOBALS' => true, - 'http_response_header' => true, - 'HTTP_RAW_POST_DATA' => true, - 'php_errormsg' => true, - ); +final class ValidVariableNameSniff extends PHPCS_AbstractVariableSniff { /** * Mixed-case variables used by WordPress. @@ -71,6 +50,7 @@ class ValidVariableNameSniff extends PHPCS_AbstractVariableSniff { 'is_winIE' => true, 'PHP_SELF' => true, 'post_ID' => true, + 'tag_ID' => true, 'user_ID' => true, ); @@ -79,26 +59,28 @@ class ValidVariableNameSniff extends PHPCS_AbstractVariableSniff { * * @since 0.9.0 * @since 0.11.0 Changed from public to protected. + * @since 3.0.0 Renamed from `$whitelisted_mixed_case_member_var_names` to `$allowed_mixed_case_member_var_names`. * * @var array */ - protected $whitelisted_mixed_case_member_var_names = array( - 'ID' => true, + protected $allowed_mixed_case_member_var_names = array( + 'cat_ID' => true, 'comment_ID' => true, + 'comment_author_IP' => true, 'comment_post_ID' => true, + 'ID' => true, 'post_ID' => true, - 'comment_author_IP' => true, - 'cat_ID' => true, ); /** * Custom list of properties which can have mixed case. * * @since 0.11.0 + * @since 3.0.0 Renamed from `$customPropertiesWhitelist` to `$allowed_custom_properties`. * - * @var string|string[] + * @var string[] */ - public $customPropertiesWhitelist = array(); + public $allowed_custom_properties = array(); /** * Cache of previously added custom functions. @@ -113,100 +95,86 @@ class ValidVariableNameSniff extends PHPCS_AbstractVariableSniff { */ protected $addedCustomProperties = array( 'properties' => null, - 'variables' => null, ); - /** - * Custom list of properties which can have mixed case. - * - * @since 0.10.0 - * @deprecated 0.11.0 Use $customPropertiesWhitelist instead. - * - * @var string|string[] - */ - public $customVariablesWhitelist = array(); - /** * Processes this test, when one of its tokens is encountered. * - * @param \PHP_CodeSniffer\Files\File $phpcs_file The file being scanned. - * @param int $stack_ptr The position of the current token in the - * stack passed in $tokens. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the + * stack passed in $tokens. * * @return void */ - protected function processVariable( File $phpcs_file, $stack_ptr ) { - - $tokens = $phpcs_file->getTokens(); - $var_name = ltrim( $tokens[ $stack_ptr ]['content'], '$' ); + protected function processVariable( File $phpcsFile, $stackPtr ) { + $tokens = $phpcsFile->getTokens(); // If it's a php reserved var, then its ok. - if ( isset( $this->php_reserved_vars[ $var_name ] ) ) { + if ( Variables::isPHPReservedVarName( $tokens[ $stackPtr ]['content'] ) ) { return; } // Merge any custom variables with the defaults. - $this->mergeWhiteList( $phpcs_file ); + $this->merge_allow_lists(); + + $var_name = ltrim( $tokens[ $stackPtr ]['content'], '$' ); // Likewise if it is a mixed-case var used by WordPress core. if ( isset( $this->wordpress_mixed_case_vars[ $var_name ] ) ) { return; } - $obj_operator = $phpcs_file->findNext( Tokens::$emptyTokens, ( $stack_ptr + 1 ), null, true ); - if ( \T_OBJECT_OPERATOR === $tokens[ $obj_operator ]['code'] ) { + $obj_operator = $phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); + if ( \T_OBJECT_OPERATOR === $tokens[ $obj_operator ]['code'] + || \T_NULLSAFE_OBJECT_OPERATOR === $tokens[ $obj_operator ]['code'] + ) { // Check to see if we are using a variable from an object. - $var = $phpcs_file->findNext( Tokens::$emptyTokens, ( $obj_operator + 1 ), null, true ); + $var = $phpcsFile->findNext( Tokens::$emptyTokens, ( $obj_operator + 1 ), null, true ); if ( \T_STRING === $tokens[ $var ]['code'] ) { - $bracket = $phpcs_file->findNext( Tokens::$emptyTokens, ( $var + 1 ), null, true ); + $bracket = $phpcsFile->findNext( Tokens::$emptyTokens, ( $var + 1 ), null, true ); if ( \T_OPEN_PARENTHESIS !== $tokens[ $bracket ]['code'] ) { $obj_var_name = $tokens[ $var ]['content']; - // There is no way for us to know if the var is public or - // private, so we have to ignore a leading underscore if there is - // one and just check the main part of the variable name. - $original_var_name = $obj_var_name; - if ( '_' === substr( $obj_var_name, 0, 1 ) ) { - $obj_var_name = substr( $obj_var_name, 1 ); + if ( isset( $this->allowed_mixed_case_member_var_names[ $obj_var_name ] ) ) { + return; } - if ( ! isset( $this->whitelisted_mixed_case_member_var_names[ $obj_var_name ] ) && self::isSnakeCase( $obj_var_name ) === false ) { - $error = 'Object property "%s" is not in valid snake_case format'; - $data = array( $original_var_name ); - $phpcs_file->addError( $error, $var, 'NotSnakeCaseMemberVar', $data ); + $suggested_name = SnakeCaseHelper::get_suggestion( $obj_var_name ); + if ( $suggested_name !== $obj_var_name ) { + $error = 'Object property "$%s" is not in valid snake_case format, try "$%s"'; + $data = array( + $obj_var_name, + $suggested_name, + ); + $phpcsFile->addError( $error, $var, 'UsedPropertyNotSnakeCase', $data ); } } } } - $in_class = false; - $obj_operator = $phpcs_file->findPrevious( Tokens::$emptyTokens, ( $stack_ptr - 1 ), null, true ); - if ( \T_DOUBLE_COLON === $tokens[ $obj_operator ]['code'] || \T_OBJECT_OPERATOR === $tokens[ $obj_operator ]['code'] ) { + $in_class = false; + if ( ContextHelper::has_object_operator_before( $phpcsFile, $stackPtr ) === true ) { // The variable lives within a class, and is referenced like // this: MyClass::$_variable or $class->variable. $in_class = true; } - // There is no way for us to know if the var is public or private, - // so we have to ignore a leading underscore if there is one and just - // check the main part of the variable name. - $original_var_name = $var_name; - if ( '_' === substr( $var_name, 0, 1 ) && true === $in_class ) { - $var_name = substr( $var_name, 1 ); - } - - if ( self::isSnakeCase( $var_name ) === false ) { - if ( $in_class && ! isset( $this->whitelisted_mixed_case_member_var_names[ $var_name ] ) ) { - $error = 'Object property "%s" is not in valid snake_case format'; - $error_name = 'NotSnakeCaseMemberVar'; + $suggested_name = SnakeCaseHelper::get_suggestion( $var_name ); + if ( $suggested_name !== $var_name ) { + if ( $in_class && ! isset( $this->allowed_mixed_case_member_var_names[ $var_name ] ) ) { + $error = 'Object property "$%s" is not in valid snake_case format, try "$%s"'; + $error_name = 'UsedPropertyNotSnakeCase'; } elseif ( ! $in_class ) { - $error = 'Variable "%s" is not in valid snake_case format'; - $error_name = 'NotSnakeCase'; + $error = 'Variable "$%s" is not in valid snake_case format, try "$%s"'; + $error_name = 'VariableNotSnakeCase'; } if ( isset( $error, $error_name ) ) { - $data = array( $original_var_name ); - $phpcs_file->addError( $error, $stack_ptr, $error_name, $data ); + $data = array( + $var_name, + $suggested_name, + ); + $phpcsFile->addError( $error, $stackPtr, $error_name, $data ); } } } @@ -214,123 +182,108 @@ protected function processVariable( File $phpcs_file, $stack_ptr ) { /** * Processes class member variables. * - * @param \PHP_CodeSniffer\Files\File $phpcs_file The file being scanned. - * @param int $stack_ptr The position of the current token in the - * stack passed in $tokens. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the + * stack passed in $tokens. * * @return void */ - protected function processMemberVar( File $phpcs_file, $stack_ptr ) { - - $tokens = $phpcs_file->getTokens(); - - $var_name = ltrim( $tokens[ $stack_ptr ]['content'], '$' ); - $member_props = $phpcs_file->getMemberProperties( $stack_ptr ); - if ( empty( $member_props ) ) { - // Couldn't get any info about this variable, which - // generally means it is invalid or possibly has a parse - // error. Any errors will be reported by the core, so - // we can ignore it. + protected function processMemberVar( File $phpcsFile, $stackPtr ) { + // Make sure this is actually an OO property and not an OO method parameter or illegal property declaration. + if ( Scopes::isOOProperty( $phpcsFile, $stackPtr ) === false ) { return; } // Merge any custom variables with the defaults. - $this->mergeWhiteList( $phpcs_file ); + $this->merge_allow_lists(); + + $tokens = $phpcsFile->getTokens(); + $var_name = ltrim( $tokens[ $stackPtr ]['content'], '$' ); - $error_data = array( $var_name ); - if ( ! isset( $this->whitelisted_mixed_case_member_var_names[ $var_name ] ) && false === self::isSnakeCase( $var_name ) ) { - $error = 'Member variable "%s" is not in valid snake_case format.'; - $phpcs_file->addError( $error, $stack_ptr, 'MemberNotSnakeCase', $error_data ); + if ( isset( $this->allowed_mixed_case_member_var_names[ $var_name ] ) ) { + return; + } + + $suggested_name = SnakeCaseHelper::get_suggestion( $var_name ); + if ( $suggested_name !== $var_name ) { + $error = 'Member variable "$%s" is not in valid snake_case format, try "$%s"'; + $data = array( + $var_name, + $suggested_name, + ); + $phpcsFile->addError( $error, $stackPtr, 'PropertyNotSnakeCase', $data ); } } /** - * Processes the variable found within a double quoted string. + * Processes the variables found within a double quoted string. * - * @param \PHP_CodeSniffer\Files\File $phpcs_file The file being scanned. - * @param int $stack_ptr The position of the double quoted - * string. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the double quoted + * string. * * @return void */ - protected function processVariableInString( File $phpcs_file, $stack_ptr ) { + protected function processVariableInString( File $phpcsFile, $stackPtr ) { + $tokens = $phpcsFile->getTokens(); - $tokens = $phpcs_file->getTokens(); + // There will always be embeds if the processVariableInString() was called. + $embeds = TextStrings::getEmbeds( $tokens[ $stackPtr ]['content'] ); - if ( preg_match_all( '|[^\\\]\${?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)|', $tokens[ $stack_ptr ]['content'], $matches ) > 0 ) { + // Merge any custom variables with the defaults. + $this->merge_allow_lists(); - // Merge any custom variables with the defaults. - $this->mergeWhiteList( $phpcs_file ); + foreach ( $embeds as $embed ) { + // Grab any variables contained in the embed. + if ( preg_match_all( '`\$(\{)?(?[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)(?(1)\})`', $embed, $matches ) === 0 ) { + continue; + } - foreach ( $matches[1] as $var_name ) { + foreach ( $matches['name'] as $var_name ) { // If it's a php reserved var, then its ok. - if ( isset( $this->php_reserved_vars[ $var_name ] ) ) { + if ( Variables::isPHPReservedVarName( $var_name ) ) { continue; } // Likewise if it is a mixed-case var used by WordPress core. if ( isset( $this->wordpress_mixed_case_vars[ $var_name ] ) ) { - return; + continue; } - if ( false === self::isSnakeCase( $var_name ) ) { - $error = 'Variable "%s" is not in valid snake_case format'; - $data = array( $var_name ); - $phpcs_file->addError( $error, $stack_ptr, 'StringNotSnakeCase', $data ); + $suggested_name = SnakeCaseHelper::get_suggestion( $var_name ); + if ( $suggested_name !== $var_name ) { + $error = 'Variable "$%s" is not in valid snake_case format, try "$%s"'; + $data = array( + $var_name, + $suggested_name, + ); + $phpcsFile->addError( $error, $stackPtr, 'InterpolatedVariableNotSnakeCase', $data ); } } } } /** - * Return whether the variable is in snake_case. - * - * @param string $var_name Variable name. - * @return bool - */ - public static function isSnakeCase( $var_name ) { - return (bool) preg_match( '/^[a-z0-9_]+$/', $var_name ); - } - - /** - * Merge a custom whitelist provided via a custom ruleset with the predefined whitelist, + * Merge a custom allow list provided via a custom ruleset with the predefined allow list, * if we haven't already. * * @since 0.10.0 - * - * @param \PHP_CodeSniffer\Files\File $phpcs_file The file being scanned. + * @since 2.0.0 Removed unused $phpcs_file parameter. + * @since 3.0.0 Renamed from `mergeWhiteList()` to `merge_allow_lists()`. * * @return void */ - protected function mergeWhiteList( File $phpcs_file ) { - if ( $this->customPropertiesWhitelist !== $this->addedCustomProperties['properties'] - || $this->customVariablesWhitelist !== $this->addedCustomProperties['variables'] - ) { + protected function merge_allow_lists() { + if ( $this->allowed_custom_properties !== $this->addedCustomProperties['properties'] ) { // Fix property potentially passed as comma-delimited string. - $customProperties = Sniff::merge_custom_array( $this->customPropertiesWhitelist, array(), false ); + $customProperties = RulesetPropertyHelper::merge_custom_array( $this->allowed_custom_properties, array(), false ); - if ( ! empty( $this->customVariablesWhitelist ) ) { - $customProperties = Sniff::merge_custom_array( - $this->customVariablesWhitelist, - $customProperties, - false - ); - - $phpcs_file->addWarning( - 'The customVariablesWhitelist property is deprecated in favor of customPropertiesWhitelist.', - 0, - 'DeprecatedCustomVariablesWhitelist' - ); - } - - $this->whitelisted_mixed_case_member_var_names = Sniff::merge_custom_array( + $this->allowed_mixed_case_member_var_names = RulesetPropertyHelper::merge_custom_array( $customProperties, - $this->whitelisted_mixed_case_member_var_names + $this->allowed_mixed_case_member_var_names ); - $this->addedCustomProperties['properties'] = $this->customPropertiesWhitelist; - $this->addedCustomProperties['variables'] = $this->customVariablesWhitelist; + $this->addedCustomProperties['properties'] = $this->allowed_custom_properties; } } - } diff --git a/WordPress/Sniffs/PHP/DevelopmentFunctionsSniff.php b/WordPress/Sniffs/PHP/DevelopmentFunctionsSniff.php index 0b910ce30e..af1c2ebf47 100644 --- a/WordPress/Sniffs/PHP/DevelopmentFunctionsSniff.php +++ b/WordPress/Sniffs/PHP/DevelopmentFunctionsSniff.php @@ -3,23 +3,21 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\PHP; +namespace WordPressCS\WordPress\Sniffs\PHP; -use WordPress\AbstractFunctionRestrictionsSniff; +use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff; /** * Restrict the use of various development functions. * - * @package WPCS\WordPressCodingStandards - * - * @since 0.11.0 - * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.11.0 + * @since 0.13.0 Class name changed: this class is now namespaced. */ -class DevelopmentFunctionsSniff extends AbstractFunctionRestrictionsSniff { +final class DevelopmentFunctionsSniff extends AbstractFunctionRestrictionsSniff { /** * Groups of functions to restrict. @@ -62,5 +60,4 @@ public function getGroups() { ), ); } - } diff --git a/WordPress/Sniffs/PHP/DiscourageGotoSniff.php b/WordPress/Sniffs/PHP/DiscourageGotoSniff.php deleted file mode 100644 index f41433961e..0000000000 --- a/WordPress/Sniffs/PHP/DiscourageGotoSniff.php +++ /dev/null @@ -1,50 +0,0 @@ -phpcsFile->addWarning( 'Using the "goto" language construct is discouraged', $stackPtr, 'Found' ); - } - -} diff --git a/WordPress/Sniffs/PHP/DiscouragedFunctionsSniff.php b/WordPress/Sniffs/PHP/DiscouragedFunctionsSniff.php deleted file mode 100644 index 763ab86b24..0000000000 --- a/WordPress/Sniffs/PHP/DiscouragedFunctionsSniff.php +++ /dev/null @@ -1,63 +0,0 @@ - array( 'type' => 'warning', - 'message' => '%s() should only be used when dealing with legacy applications rawurlencode() should now be used instead. See http://php.net/manual/en/function.rawurlencode.php and http://www.faqs.org/rfcs/rfc3986.html', + 'message' => '%s() should only be used when dealing with legacy applications rawurlencode() should now be used instead. See https://www.php.net/function.rawurlencode and http://www.faqs.org/rfcs/rfc3986.html', 'functions' => array( 'urlencode', ), @@ -56,12 +54,10 @@ public function getGroups() { 'runtime_configuration' => array( 'type' => 'warning', - 'message' => '%s() found. Changing configuration at runtime is rarely necessary.', + 'message' => '%s() found. Changing configuration values at runtime is strongly discouraged.', 'functions' => array( 'error_reporting', - 'ini_alter', 'ini_restore', - 'ini_set', 'apache_setenv', 'putenv', 'set_include_path', @@ -101,5 +97,4 @@ public function getGroups() { ), ); } - } diff --git a/WordPress/Sniffs/PHP/DontExtractSniff.php b/WordPress/Sniffs/PHP/DontExtractSniff.php index be4f823bdc..0d9425273e 100644 --- a/WordPress/Sniffs/PHP/DontExtractSniff.php +++ b/WordPress/Sniffs/PHP/DontExtractSniff.php @@ -3,26 +3,25 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\PHP; +namespace WordPressCS\WordPress\Sniffs\PHP; -use WordPress\AbstractFunctionRestrictionsSniff; +use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff; /** * Restricts the usage of extract(). * - * @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#dont-extract + * @link https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#dont-extract * - * @package WPCS\WordPressCodingStandards - * - * @since 0.10.0 Previously this check was contained within WordPress_Sniffs_VIP_RestrictedFunctionsSniff. - * @since 0.13.0 Class name changed: this class is now namespaced. - * @since 1.0.0 This sniff has been moved from the `Functions` category to the `PHP` category. + * @since 0.10.0 Previously this check was contained within the + * `WordPress.VIP.RestrictedFunctions` sniff. + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 1.0.0 This sniff has been moved from the `Functions` category to the `PHP` category. */ -class DontExtractSniff extends AbstractFunctionRestrictionsSniff { +final class DontExtractSniff extends AbstractFunctionRestrictionsSniff { /** * Groups of functions to restrict. @@ -50,5 +49,4 @@ public function getGroups() { ); } - } diff --git a/WordPress/Sniffs/PHP/IniSetSniff.php b/WordPress/Sniffs/PHP/IniSetSniff.php new file mode 100644 index 0000000000..b393fbc1ce --- /dev/null +++ b/WordPress/Sniffs/PHP/IniSetSniff.php @@ -0,0 +1,193 @@ + true, + 'ini_alter' => true, // Alias function name. + ); + + /** + * Array of PHP configuration options that are safe to be manipulated, as changing + * the value of these, won't cause interoperability issues between WP/plugins/themes. + * + * @since 2.1.0 + * @since 3.0.0 Renamed from `$whitelisted_options` to `$safe_options`. + * + * @var array Multidimensional array with parameter details. + * $safe_options = array( + * (string) option name. = array( + * (string[]) 'valid_values' = array() + * ) + * ); + */ + protected $safe_options = array( + 'auto_detect_line_endings' => array(), + 'highlight.bg' => array(), + 'highlight.comment' => array(), + 'highlight.default' => array(), + 'highlight.html' => array(), + 'highlight.keyword' => array(), + 'highlight.string' => array(), + 'short_open_tag' => array( + 'valid_values' => array( 'true', '1', 'on' ), + ), + ); + + /** + * Array of PHP configuration options that are not allowed to be manipulated, as changing + * the value of these, will be problematic for interoperability between WP/plugins/themes. + * + * @since 2.1.0 + * @since 3.0.0 Renamed from `$blacklisted_options` to `$disallowed_options`. + * + * @var array Multidimensional array with parameter details. + * $disallowed_options = array( + * (string) option name. = array( + * (string[]) 'invalid_values' = array() + * (string) 'message' + * ) + * ); + */ + protected $disallowed_options = array( + 'bcmath.scale' => array( + 'message' => 'Use `bcscale()` instead.', + ), + 'display_errors' => array( + 'message' => 'Use `WP_DEBUG_DISPLAY` instead.', + ), + 'error_reporting' => array( + 'message' => 'Use `WP_DEBUG` instead.', + ), + 'filter.default' => array( + 'message' => 'Changing the option value can break other plugins. Use the filter flag constants when calling the Filter functions instead.', + ), + 'filter.default_flags' => array( + 'message' => 'Changing the option value can break other plugins. Use the filter flag constants when calling the Filter functions instead.', + ), + 'iconv.input_encoding' => array( + 'message' => 'This option is not supported since PHP 5.6 - use `iconv_set_encoding()` instead.', + ), + 'iconv.internal_encoding' => array( + 'message' => 'This option is not supported since PHP 5.6 - use `iconv_set_encoding()` instead.', + ), + 'iconv.output_encoding' => array( + 'message' => 'This option is not supported since PHP 5.6 - use `iconv_set_encoding()` instead.', + ), + 'ignore_user_abort' => array( + 'message' => 'Use `ignore_user_abort()` instead.', + ), + 'log_errors' => array( + 'message' => 'Use `WP_DEBUG_LOG` instead.', + ), + 'max_execution_time' => array( + 'message' => 'Use `set_time_limit()` instead.', + ), + 'memory_limit' => array( + 'message' => 'Use `wp_raise_memory_limit()` or hook into the filters in that function.', + ), + 'short_open_tag' => array( + 'invalid_values' => array( 'false', '0', 'off' ), + 'message' => 'Turning off short_open_tag is prohibited as it can break other plugins.', + ), + ); + + /** + * Process the parameter of a matched function. + * + * Errors if an option is found in the disallow-list. Warns as + * 'risky' when the option is not found in the safe-list. + * + * @since 2.1.0 + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * @param array $parameters Array with information about the parameters. + * + * @return void + */ + public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { + $option_param = PassedParameters::getParameterFromStack( $parameters, 1, 'option' ); + $value_param = PassedParameters::getParameterFromStack( $parameters, 2, 'value' ); + + if ( false === $option_param || false === $value_param ) { + // Missing required param. Not the concern of this sniff. Bow out. + return; + } + + $option_name = TextStrings::stripQuotes( $option_param['clean'] ); + $option_value = TextStrings::stripQuotes( $value_param['clean'] ); + if ( isset( $this->safe_options[ $option_name ] ) ) { + $safe_option = $this->safe_options[ $option_name ]; + if ( empty( $safe_option['valid_values'] ) || in_array( strtolower( $option_value ), $safe_option['valid_values'], true ) ) { + return; + } + } + + if ( isset( $this->disallowed_options[ $option_name ] ) ) { + $disallowed_option = $this->disallowed_options[ $option_name ]; + if ( empty( $disallowed_option['invalid_values'] ) + || in_array( strtolower( $option_value ), $disallowed_option['invalid_values'], true ) + ) { + $this->phpcsFile->addError( + 'Found: %s(%s, %s). %s', + $stackPtr, + MessageHelper::stringToErrorcode( $option_name . '_Disallowed' ), + array( + $matched_content, + $option_param['clean'], + $value_param['clean'], + $disallowed_option['message'], + ) + ); + return; + } + } + + $this->phpcsFile->addWarning( + 'Changing configuration values at runtime is strongly discouraged. Found: %s(%s, %s)', + $stackPtr, + 'Risky', + array( + $matched_content, + $option_param['clean'], + $value_param['clean'], + ) + ); + } +} diff --git a/WordPress/Sniffs/PHP/NoSilencedErrorsSniff.php b/WordPress/Sniffs/PHP/NoSilencedErrorsSniff.php index fca1f28fd5..4817f82652 100644 --- a/WordPress/Sniffs/PHP/NoSilencedErrorsSniff.php +++ b/WordPress/Sniffs/PHP/NoSilencedErrorsSniff.php @@ -3,27 +3,28 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\PHP; +namespace WordPressCS\WordPress\Sniffs\PHP; -use WordPress\Sniff; -use PHP_CodeSniffer_Tokens as Tokens; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\BackCompat\BCFile; +use PHPCSUtils\Utils\GetTokensAsString; +use WordPressCS\WordPress\Helpers\RulesetPropertyHelper; +use WordPressCS\WordPress\Sniff; /** * Discourage the use of the PHP error silencing operator. * * This sniff allows the error operator to be used with a select list - * of whitelisted functions, as no amount of error checking can prevent + * of functions, as no amount of error checking can prevent * PHP from throwing errors when those functions are used. * - * @package WPCS\WordPressCodingStandards - * - * @since 1.1.0 + * @since 1.1.0 */ -class NoSilencedErrorsSniff extends Sniff { +final class NoSilencedErrorsSniff extends Sniff { /** * Number of tokens to display in the error message to show @@ -36,33 +37,35 @@ class NoSilencedErrorsSniff extends Sniff { public $context_length = 6; /** - * Whether or not the `$function_whitelist` should be used. + * Whether or not the `$allowedFunctionsList` should be used. * * Defaults to true. * - * This property only affects whether the standard function whitelist is - * used. The custom whitelist, if set, will always be respected. + * This property only affects whether the standard function list is used. + * The custom allowed functions list, if set, will always be respected. * * @since 1.1.0 + * @since 3.0.0 Renamed from `$use_default_whitelist` to `$usePHPFunctionsList`. * * @var bool */ - public $use_default_whitelist = true; + public $usePHPFunctionsList = true; /** - * User defined whitelist. + * User defined function list. * - * Allows users to pass a list of additional functions to whitelist - * from their custom ruleset. + * Allows users to pass a list of additional functions for which to allow + * the use of the silence operator. This list can be set in a custom ruleset. * * @since 1.1.0 + * @since 3.0.0 Renamed from `$custom_whitelist` to `$customAllowedFunctionsList`. * - * @var array + * @var string[] */ - public $custom_whitelist = array(); + public $customAllowedFunctionsList = array(); /** - * PHP native function whitelist. + * PHP native functions allow list. * * Errors caused by calls to any of these native PHP functions * are allowed to be silenced as file system permissions and such @@ -76,66 +79,71 @@ class NoSilencedErrorsSniff extends Sniff { * error will be thrown on failure are accepted into this list. * * @since 1.1.0 + * @since 3.0.0 Renamed from `$function_whitelist` to `$allowedFunctionsList`. * - * @var array => + * @var array Key is function name, value irrelevant. */ - protected $function_whitelist = array( + protected $allowedFunctionsList = array( // Directory extension. - 'chdir' => true, - 'opendir' => true, - 'scandir' => true, + 'chdir' => true, + 'opendir' => true, + 'scandir' => true, // File extension. - 'file_exists' => true, - 'file_get_contents' => true, - 'file' => true, - 'fileatime' => true, - 'filectime' => true, - 'filegroup' => true, - 'fileinode' => true, - 'filemtime' => true, - 'fileowner' => true, - 'fileperms' => true, - 'filesize' => true, - 'filetype' => true, - 'fopen' => true, - 'is_dir' => true, - 'is_executable' => true, - 'is_file' => true, - 'is_link' => true, - 'is_readable' => true, - 'is_writable' => true, - 'is_writeable' => true, - 'lstat' => true, - 'mkdir' => true, - 'move_uploaded_file' => true, - 'readfile' => true, - 'readlink' => true, - 'rename' => true, - 'rmdir' => true, - 'stat' => true, - 'unlink' => true, + 'file_exists' => true, + 'file_get_contents' => true, + 'file' => true, + 'fileatime' => true, + 'filectime' => true, + 'filegroup' => true, + 'fileinode' => true, + 'filemtime' => true, + 'fileowner' => true, + 'fileperms' => true, + 'filesize' => true, + 'filetype' => true, + 'fopen' => true, + 'is_dir' => true, + 'is_executable' => true, + 'is_file' => true, + 'is_link' => true, + 'is_readable' => true, + 'is_writable' => true, + 'is_writeable' => true, + 'lstat' => true, + 'mkdir' => true, + 'move_uploaded_file' => true, + 'readfile' => true, + 'readlink' => true, + 'rename' => true, + 'rmdir' => true, + 'stat' => true, + 'unlink' => true, // FTP extension. - 'ftp_chdir' => true, - 'ftp_login' => true, - 'ftp_rename' => true, + 'ftp_chdir' => true, + 'ftp_login' => true, + 'ftp_rename' => true, // Stream extension. - 'stream_select' => true, - 'stream_set_chunk_size' => true, + 'stream_select' => true, + 'stream_set_chunk_size' => true, // Zlib extension. - 'deflate_add' => true, - 'deflate_init' => true, - 'inflate_add' => true, - 'inflate_init' => true, - 'readgzfile' => true, + 'deflate_add' => true, + 'deflate_init' => true, + 'inflate_add' => true, + 'inflate_init' => true, + 'readgzfile' => true, + + // LibXML extension. + 'libxml_disable_entity_loader' => true, // PHP 8.0 deprecation warning, but function call still needed in select cases. // Miscellaneous other functions. - 'imagecreatefromstring' => true, - 'parse_url' => true, // Pre-PHP 5.3.3 an E_WARNING was thrown when URL parsing failed. - 'unserialize' => true, + 'imagecreatefromstring' => true, + 'imagecreatefromwebp' => true, + 'parse_url' => true, // Pre-PHP 5.3.3 an E_WARNING was thrown when URL parsing failed. + 'unserialize' => true, ); /** @@ -146,7 +154,7 @@ class NoSilencedErrorsSniff extends Sniff { * * @since 1.1.0 * - * @var array + * @var array */ private $empty_tokens = array(); @@ -175,26 +183,28 @@ public function register() { * @param int $stackPtr The position of the current token in the stack. */ public function process_token( $stackPtr ) { - // Handle the user-defined custom function whitelist. - $this->custom_whitelist = $this->merge_custom_array( $this->custom_whitelist, array(), false ); - $this->custom_whitelist = array_map( 'strtolower', $this->custom_whitelist ); - - if ( true === $this->use_default_whitelist || ! empty( $this->custom_whitelist ) ) { - /* - * Check if the error silencing is done for one of the whitelisted functions. - */ - $next_non_empty = $this->phpcsFile->findNext( $this->empty_tokens, ( $stackPtr + 1 ), null, true, null, true ); - if ( false !== $next_non_empty && \T_STRING === $this->tokens[ $next_non_empty ]['code'] ) { - $has_parenthesis = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next_non_empty + 1 ), null, true, null, true ); - if ( false !== $has_parenthesis && \T_OPEN_PARENTHESIS === $this->tokens[ $has_parenthesis ]['code'] ) { - $function_name = strtolower( $this->tokens[ $next_non_empty ]['content'] ); - if ( ( true === $this->use_default_whitelist - && isset( $this->function_whitelist[ $function_name ] ) === true ) - || in_array( $function_name, $this->custom_whitelist, true ) === true - ) { - $this->phpcsFile->recordMetric( $stackPtr, 'Error silencing', 'whitelisted function call: ' . $function_name ); - return; - } + // Handle the user-defined custom function list. + $this->customAllowedFunctionsList = RulesetPropertyHelper::merge_custom_array( $this->customAllowedFunctionsList, array(), false ); + $this->customAllowedFunctionsList = array_map( 'strtolower', $this->customAllowedFunctionsList ); + + /* + * Check if the error silencing is done for one of the allowed functions. + * + * @internal The function call name determination is done even when there is no allow list active + * to allow the metrics to be more informative. + */ + $next_non_empty = $this->phpcsFile->findNext( $this->empty_tokens, ( $stackPtr + 1 ), null, true, null, true ); + if ( false !== $next_non_empty && \T_STRING === $this->tokens[ $next_non_empty ]['code'] ) { + $has_parenthesis = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next_non_empty + 1 ), null, true, null, true ); + if ( false !== $has_parenthesis && \T_OPEN_PARENTHESIS === $this->tokens[ $has_parenthesis ]['code'] ) { + $function_name = strtolower( $this->tokens[ $next_non_empty ]['content'] ); + if ( ( true === $this->usePHPFunctionsList + && isset( $this->allowedFunctionsList[ $function_name ] ) === true ) + || ( ! empty( $this->customAllowedFunctionsList ) + && in_array( $function_name, $this->customAllowedFunctionsList, true ) === true ) + ) { + $this->phpcsFile->recordMetric( $stackPtr, 'Error silencing', 'silencing allowed function call: ' . $function_name ); + return; } } } @@ -206,13 +216,12 @@ public function process_token( $stackPtr ) { } // Prepare the "Found" string to display. - $end_of_statement = $this->phpcsFile->findEndOfStatement( $stackPtr, \T_COMMA ); + $end_of_statement = BCFile::findEndOfStatement( $this->phpcsFile, $stackPtr, \T_COMMA ); if ( ( $end_of_statement - $stackPtr ) < $context_length ) { $context_length = ( $end_of_statement - $stackPtr ); } - $found = $this->phpcsFile->getTokensAsString( $stackPtr, $context_length ); - $found = str_replace( array( "\t", "\n", "\r" ), ' ', $found ) . '...'; + $found = GetTokensAsString::compact( $this->phpcsFile, $stackPtr, ( $stackPtr + $context_length - 1 ), true ) . '...'; $error_msg = 'Silencing errors is strongly discouraged. Use proper error checking instead.'; $data = array(); if ( $this->context_length > 0 ) { @@ -228,10 +237,9 @@ public function process_token( $stackPtr ) { ); if ( isset( $function_name ) ) { - $this->phpcsFile->recordMetric( $stackPtr, 'Error silencing', $function_name ); + $this->phpcsFile->recordMetric( $stackPtr, 'Error silencing', '@' . $function_name ); } else { $this->phpcsFile->recordMetric( $stackPtr, 'Error silencing', $found ); } } - } diff --git a/WordPress/Sniffs/PHP/POSIXFunctionsSniff.php b/WordPress/Sniffs/PHP/POSIXFunctionsSniff.php index b1631c7153..72f77ad551 100644 --- a/WordPress/Sniffs/PHP/POSIXFunctionsSniff.php +++ b/WordPress/Sniffs/PHP/POSIXFunctionsSniff.php @@ -3,28 +3,27 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\PHP; +namespace WordPressCS\WordPress\Sniffs\PHP; -use WordPress\AbstractFunctionRestrictionsSniff; +use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff; /** * Perl compatible regular expressions (PCRE, preg_ functions) should be used in preference * to their POSIX counterparts. * - * @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#regular-expressions - * @link http://php.net/manual/en/ref.regex.php + * @link https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#regular-expressions + * @link https://php-legacy-docs.zend.com/manual/php5/en/ref.regex * - * @package WPCS\WordPressCodingStandards - * - * @since 0.10.0 Previously this check was contained within WordPress_Sniffs_VIP_RestrictedFunctionsSniff - * and the WordPress_Sniffs_PHP_DiscouragedPHPFunctionsSniff. - * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.10.0 Previously this check was contained within the + * `WordPress.VIP.RestrictedFunctions` and the + * `WordPress.PHP.DiscouragedPHPFunctions` sniffs. + * @since 0.13.0 Class name changed: this class is now namespaced. */ -class POSIXFunctionsSniff extends AbstractFunctionRestrictionsSniff { +final class POSIXFunctionsSniff extends AbstractFunctionRestrictionsSniff { /** * Groups of functions to restrict. @@ -71,5 +70,4 @@ public function getGroups() { ); } - } diff --git a/WordPress/Sniffs/PHP/PregQuoteDelimiterSniff.php b/WordPress/Sniffs/PHP/PregQuoteDelimiterSniff.php index 87a668e086..3a1ab1eaf7 100644 --- a/WordPress/Sniffs/PHP/PregQuoteDelimiterSniff.php +++ b/WordPress/Sniffs/PHP/PregQuoteDelimiterSniff.php @@ -3,22 +3,21 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\PHP; +namespace WordPressCS\WordPress\Sniffs\PHP; -use WordPress\AbstractFunctionParameterSniff; +use PHPCSUtils\Utils\PassedParameters; +use WordPressCS\WordPress\AbstractFunctionParameterSniff; /** * Flag calling preg_quote() without the second ($delimiter) parameter. * - * @package WPCS\WordPressCodingStandards - * - * @since 1.0.0 + * @since 1.0.0 */ -class PregQuoteDelimiterSniff extends AbstractFunctionParameterSniff { +final class PregQuoteDelimiterSniff extends AbstractFunctionParameterSniff { /** * The group name for this group of functions. @@ -32,11 +31,11 @@ class PregQuoteDelimiterSniff extends AbstractFunctionParameterSniff { /** * List of functions this sniff should examine. * - * @link http://php.net/preg_quote + * @link https://www.php.net/preg_quote * * @since 1.0.0 * - * @var array => + * @var array Key is function name, value irrelevant. */ protected $target_functions = array( 'preg_quote' => true, @@ -48,22 +47,24 @@ class PregQuoteDelimiterSniff extends AbstractFunctionParameterSniff { * @since 1.0.0 * * @param int $stackPtr The position of the current token in the stack. - * @param array $group_name The name of the group which was matched. - * @param string $matched_content The token content (function name) which was matched. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. * @param array $parameters Array with information about the parameters. * * @return void */ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { - if ( \count( $parameters ) > 1 ) { + + $delimiter = PassedParameters::getParameterFromStack( $parameters, 2, 'delimiter' ); + if ( false !== $delimiter ) { return; } $this->phpcsFile->addWarning( - 'Passing the $delimiter as the second parameter to preg_quote() is strongly recommended.', + 'Passing the $delimiter parameter to preg_quote() is strongly recommended.', $stackPtr, 'Missing' ); } - } diff --git a/WordPress/Sniffs/PHP/RestrictedPHPFunctionsSniff.php b/WordPress/Sniffs/PHP/RestrictedPHPFunctionsSniff.php index 235db95e51..f3102e33d5 100644 --- a/WordPress/Sniffs/PHP/RestrictedPHPFunctionsSniff.php +++ b/WordPress/Sniffs/PHP/RestrictedPHPFunctionsSniff.php @@ -3,22 +3,20 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\PHP; +namespace WordPressCS\WordPress\Sniffs\PHP; -use WordPress\AbstractFunctionRestrictionsSniff; +use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff; /** * Forbids the use of various native PHP functions and suggests alternatives. * - * @package WPCS\WordPressCodingStandards - * - * @since 0.14.0 + * @since 0.14.0 */ -class RestrictedPHPFunctionsSniff extends AbstractFunctionRestrictionsSniff { +final class RestrictedPHPFunctionsSniff extends AbstractFunctionRestrictionsSniff { /** * Groups of functions to forbid. @@ -37,12 +35,11 @@ public function getGroups() { return array( 'create_function' => array( 'type' => 'error', - 'message' => '%s() is deprecated as of PHP 7.2, please use full fledged functions or anonymous functions instead.', + 'message' => '%s() is deprecated as of PHP 7.2 and removed in PHP 8.0. Please use declared named or anonymous functions instead.', 'functions' => array( 'create_function', ), ), ); } - } diff --git a/WordPress/Sniffs/PHP/StrictComparisonsSniff.php b/WordPress/Sniffs/PHP/StrictComparisonsSniff.php deleted file mode 100644 index ec43f504cd..0000000000 --- a/WordPress/Sniffs/PHP/StrictComparisonsSniff.php +++ /dev/null @@ -1,56 +0,0 @@ -has_whitelist_comment( 'loose comparison', $stackPtr ) ) { - $error = 'Found: ' . $this->tokens[ $stackPtr ]['content'] . '. Use strict comparisons (=== or !==).'; - $this->phpcsFile->addWarning( $error, $stackPtr, 'LooseComparison' ); - } - } - -} diff --git a/WordPress/Sniffs/PHP/StrictInArraySniff.php b/WordPress/Sniffs/PHP/StrictInArraySniff.php index c5ac94c9ed..dc7aa7aace 100644 --- a/WordPress/Sniffs/PHP/StrictInArraySniff.php +++ b/WordPress/Sniffs/PHP/StrictInArraySniff.php @@ -3,29 +3,27 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\PHP; +namespace WordPressCS\WordPress\Sniffs\PHP; -use WordPress\AbstractFunctionParameterSniff; +use PHPCSUtils\Utils\PassedParameters; +use WordPressCS\WordPress\AbstractFunctionParameterSniff; /** * Flag calling in_array(), array_search() and array_keys() without true as the third parameter. * - * @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#using-in_array-without-strict-parameter - * - * @package WPCS\WordPressCodingStandards - * - * @since 0.9.0 - * @since 0.10.0 This sniff not only checks for `in_array()`, but also `array_search()` and `array_keys()`. - * The sniff no longer needlessly extends the WordPress_Sniffs_Arrays_ArrayAssignmentRestrictionsSniff - * which it didn't use. - * @since 0.11.0 Refactored to extend the new WordPress_AbstractFunctionParameterSniff. - * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.9.0 + * @since 0.10.0 - This sniff not only checks for `in_array()`, but also `array_search()` + * and `array_keys()`. + * - The sniff no longer needlessly extends the `ArrayAssignmentRestrictionsSniff` + * class which it didn't use. + * @since 0.11.0 Refactored to extend the new WordPressCS native `AbstractFunctionParameterSniff` class. + * @since 0.13.0 Class name changed: this class is now namespaced. */ -class StrictInArraySniff extends AbstractFunctionParameterSniff { +final class StrictInArraySniff extends AbstractFunctionParameterSniff { /** * The group name for this group of functions. @@ -39,24 +37,35 @@ class StrictInArraySniff extends AbstractFunctionParameterSniff { /** * List of array functions to which a $strict parameter can be passed. * - * The $strict parameter is the third and last parameter for each of these functions. - * * The array_keys() function only requires the $strict parameter when the optional - * second parameter $search has been set. + * second parameter $filter_value has been set. * - * @link http://php.net/in-array - * @link http://php.net/array-search - * @link http://php.net/array-keys + * @link https://www.php.net/in-array + * @link https://www.php.net/array-search + * @link https://www.php.net/array-keys * * @since 0.10.0 * @since 0.11.0 Renamed from $array_functions to $target_functions. + * @since 3.0.0 The format of the array value has changed from boolean to array. * - * @var array => + * @var array Key is the function name. */ protected $target_functions = array( - 'in_array' => true, - 'array_search' => true, - 'array_keys' => false, + 'in_array' => array( + 'param_position' => 3, + 'param_name' => 'strict', + 'always_needed' => true, + ), + 'array_search' => array( + 'param_position' => 3, + 'param_name' => 'strict', + 'always_needed' => true, + ), + 'array_keys' => array( + 'param_position' => 3, + 'param_name' => 'strict', + 'always_needed' => false, + ), ); /** @@ -65,40 +74,49 @@ class StrictInArraySniff extends AbstractFunctionParameterSniff { * @since 0.11.0 * * @param int $stackPtr The position of the current token in the stack. - * @param array $group_name The name of the group which was matched. - * @param string $matched_content The token content (function name) which was matched. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. * @param array $parameters Array with information about the parameters. * * @return void */ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { - // Check if the strict check is actually needed. - if ( false === $this->target_functions[ $matched_content ] ) { - if ( \count( $parameters ) === 1 ) { + $param_info = $this->target_functions[ $matched_content ]; + + /* + * Check if the strict check is actually needed. + * + * Important! This check only applies to array_keys() in the current form of the sniff + * and has been written to be specific to that function. + * If more functions would be added with 'always_needed' set to `false`, + * this code will need to be adjusted to handle those. + */ + if ( false === $param_info['always_needed'] ) { + $has_filter_value = PassedParameters::getParameterFromStack( $parameters, 2, 'filter_value' ); + if ( false === $has_filter_value ) { return; } } - // We're only interested in the third parameter. - if ( false === isset( $parameters[3] ) || 'true' !== strtolower( $parameters[3]['raw'] ) ) { + $found_parameter = PassedParameters::getParameterFromStack( $parameters, $param_info['param_position'], $param_info['param_name'] ); + if ( false === $found_parameter || 'true' !== strtolower( $found_parameter['clean'] ) ) { $errorcode = 'MissingTrueStrict'; /* * Use a different error code when `false` is found to allow for excluding * the warning as this will be a conscious choice made by the dev. */ - if ( isset( $parameters[3] ) && 'false' === strtolower( $parameters[3]['raw'] ) ) { + if ( is_array( $found_parameter ) && 'false' === strtolower( $found_parameter['clean'] ) ) { $errorcode = 'FoundNonStrictFalse'; } $this->phpcsFile->addWarning( - 'Not using strict comparison for %s; supply true for third argument.', - ( isset( $parameters[3]['start'] ) ? $parameters[3]['start'] : $parameters[1]['start'] ), + 'Not using strict comparison for %s; supply true for $%s argument.', + ( isset( $found_parameter['start'] ) ? $found_parameter['start'] : $stackPtr ), $errorcode, - array( $matched_content ) + array( $matched_content, $param_info['param_name'] ) ); - return; } } - } diff --git a/WordPress/Sniffs/PHP/TypeCastsSniff.php b/WordPress/Sniffs/PHP/TypeCastsSniff.php index c35afac4df..3e07c7fb51 100644 --- a/WordPress/Sniffs/PHP/TypeCastsSniff.php +++ b/WordPress/Sniffs/PHP/TypeCastsSniff.php @@ -3,32 +3,29 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\PHP; +namespace WordPressCS\WordPress\Sniffs\PHP; -use WordPress\Sniff; -use PHP_CodeSniffer_Tokens as Tokens; +use WordPressCS\WordPress\Sniff; /** * Verifies the correct usage of type cast keywords. * * Type casts should be: - * - lowercase; - * - short form, i.e. (bool) not (boolean); * - normalized, i.e. (float) not (real). * * Additionally, the use of the (unset) and (binary) casts is discouraged. * - * @link https://make.wordpress.org/core/handbook/best-practices/.... + * @link https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#space-usage * - * @package WPCS\WordPressCodingStandards - * - * @since 1.2.0 + * @since 1.2.0 + * @since 2.0.0 No longer checks that type casts are lowercase or short form. + * Relevant PHPCS native sniffs have been included in the rulesets instead. */ -class TypeCastsSniff extends Sniff { +final class TypeCastsSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. @@ -36,7 +33,11 @@ class TypeCastsSniff extends Sniff { * @return array */ public function register() { - return Tokens::$castTokens; + return array( + \T_DOUBLE_CAST, + \T_UNSET_CAST, + \T_BINARY_CAST, + ); } /** @@ -52,39 +53,7 @@ public function process_token( $stackPtr ) { $typecast = str_replace( ' ', '', $this->tokens[ $stackPtr ]['content'] ); $typecast_lc = strtolower( $typecast ); - $this->phpcsFile->recordMetric( $stackPtr, 'Typecast encountered', $typecast ); - switch ( $token_code ) { - case \T_BOOL_CAST: - if ( '(bool)' !== $typecast_lc ) { - $fix = $this->phpcsFile->addFixableError( - 'Short form type keywords must be used; expected "(bool)" but found "%s"', - $stackPtr, - 'LongBoolFound', - array( $typecast ) - ); - - if ( true === $fix ) { - $this->phpcsFile->fixer->replaceToken( $stackPtr, '(bool)' ); - } - } - break; - - case \T_INT_CAST: - if ( '(int)' !== $typecast_lc ) { - $fix = $this->phpcsFile->addFixableError( - 'Short form type keywords must be used; expected "(int)" but found "%s"', - $stackPtr, - 'LongIntFound', - array( $typecast ) - ); - - if ( true === $fix ) { - $this->phpcsFile->fixer->replaceToken( $stackPtr, '(int)' ); - } - } - break; - case \T_DOUBLE_CAST: if ( '(float)' !== $typecast_lc ) { $fix = $this->phpcsFile->addFixableError( @@ -101,19 +70,14 @@ public function process_token( $stackPtr ) { break; case \T_UNSET_CAST: - $this->phpcsFile->addWarning( - 'Using the "(unset)" cast is strongly discouraged. Use the "unset()" language construct or assign "null" as the value to the variable instead.', + $this->phpcsFile->addError( + 'Using the "(unset)" cast is forbidden as the type cast is removed in PHP 8.0. Use the "unset()" language construct instead.', $stackPtr, 'UnsetFound' ); break; - case \T_STRING_CAST: case \T_BINARY_CAST: - if ( \T_STRING_CAST === $token_code && '(binary)' !== $typecast_lc ) { - break; - } - $this->phpcsFile->addWarning( 'Using binary casting is strongly discouraged. Found: "%s"', $stackPtr, @@ -122,33 +86,5 @@ public function process_token( $stackPtr ) { ); break; } - - /* - * {@internal Once the minimum PHPCS version has gone up to PHPCS 3.3.0+, the lowercase - * check below can be removed in favour of adding the `Generic.PHP.LowerCaseType` sniff - * to the ruleset. - * Note: the `register()` function also needs adjusting in that case to only register the - * targetted type casts above and the metrics recording should probably be adjusted as well. - * The above mentioned Generic sniff records metrics about the case of typecasts, so we - * don't need to worry about those no longer being recorded. They will be, just slightly - * differently.}} - */ - if ( $typecast_lc !== $typecast ) { - $data = array( - $typecast_lc, - $typecast, - ); - - $fix = $this->phpcsFile->addFixableError( - 'PHP type casts must be lowercase; expected "%s" but found "%s"', - $stackPtr, - 'NonLowercaseFound', - $data - ); - if ( true === $fix ) { - $this->phpcsFile->fixer->replaceToken( $stackPtr, $typecast_lc ); - } - } } - } diff --git a/WordPress/Sniffs/PHP/YodaConditionsSniff.php b/WordPress/Sniffs/PHP/YodaConditionsSniff.php index cd1f2997db..150993c6a4 100644 --- a/WordPress/Sniffs/PHP/YodaConditionsSniff.php +++ b/WordPress/Sniffs/PHP/YodaConditionsSniff.php @@ -3,36 +3,36 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\PHP; +namespace WordPressCS\WordPress\Sniffs\PHP; -use WordPress\Sniff; -use PHP_CodeSniffer_Tokens as Tokens; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Tokens\Collections; +use WordPressCS\WordPress\Sniff; /** * Enforces Yoda conditional statements. * - * @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#yoda-conditions + * @link https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#yoda-conditions * - * @package WPCS\WordPressCodingStandards - * - * @since 0.3.0 - * @since 0.12.0 This class now extends WordPress_Sniff. - * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.3.0 + * @since 0.12.0 This class now extends the WordPressCS native `Sniff` class. + * @since 0.13.0 Class name changed: this class is now namespaced. */ -class YodaConditionsSniff extends Sniff { +final class YodaConditionsSniff extends Sniff { /** * The tokens that indicate the start of a condition. * * @since 0.12.0 + * @since 3.0.0 This property is now `private`. * * @var array */ - protected $condition_start_tokens; + private $condition_start_tokens; /** * Returns an array of tokens this test wants to listen for. @@ -81,7 +81,7 @@ public function process_token( $stackPtr ) { continue; } - // If this is a variable or array, we've seen all we need to see. + // If this is a variable or array assignment, we've seen all we need to see. if ( \T_VARIABLE === $this->tokens[ $i ]['code'] || \T_CLOSE_SQUARE_BRACKET === $this->tokens[ $i ]['code'] ) { @@ -106,9 +106,9 @@ public function process_token( $stackPtr ) { $next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next_non_empty + 1 ), null, true ); } - if ( \in_array( $this->tokens[ $next_non_empty ]['code'], array( \T_SELF, \T_PARENT, \T_STATIC ), true ) ) { + if ( isset( Collections::ooHierarchyKeywords()[ $this->tokens[ $next_non_empty ]['code'] ] ) === true ) { $next_non_empty = $this->phpcsFile->findNext( - array_merge( Tokens::$emptyTokens, array( \T_DOUBLE_COLON ) ), + ( Tokens::$emptyTokens + array( \T_DOUBLE_COLON => \T_DOUBLE_COLON ) ), ( $next_non_empty + 1 ), null, true @@ -121,5 +121,4 @@ public function process_token( $stackPtr ) { $this->phpcsFile->addError( 'Use Yoda Condition checks, you must.', $stackPtr, 'NotYoda' ); } - } diff --git a/WordPress/Sniffs/Security/EscapeOutputSniff.php b/WordPress/Sniffs/Security/EscapeOutputSniff.php index abb66540d0..e356c46ff5 100644 --- a/WordPress/Sniffs/Security/EscapeOutputSniff.php +++ b/WordPress/Sniffs/Security/EscapeOutputSniff.php @@ -3,118 +3,77 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\Security; - -use WordPress\Sniff; -use PHP_CodeSniffer_Tokens as Tokens; +namespace WordPressCS\WordPress\Sniffs\Security; + +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\BackCompat\BCFile; +use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\Arrays; +use PHPCSUtils\Utils\Conditions; +use PHPCSUtils\Utils\Operators; +use PHPCSUtils\Utils\PassedParameters; +use PHPCSUtils\Utils\TextStrings; +use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff; +use WordPressCS\WordPress\Helpers\ArrayWalkingFunctionsHelper; +use WordPressCS\WordPress\Helpers\ConstantsHelper; +use WordPressCS\WordPress\Helpers\ContextHelper; +use WordPressCS\WordPress\Helpers\EscapingFunctionsTrait; +use WordPressCS\WordPress\Helpers\FormattingFunctionsHelper; +use WordPressCS\WordPress\Helpers\PrintingFunctionsTrait; +use WordPressCS\WordPress\Helpers\VariableHelper; /** * Verifies that all outputted strings are escaped. * - * @link http://codex.wordpress.org/Data_Validation Data Validation on WordPress Codex + * @link https://developer.wordpress.org/apis/security/data-validation/ WordPress Developer Docs on Data Validation. * - * @package WPCS\WordPressCodingStandards + * @since 2013-06-11 + * @since 0.4.0 This class now extends the WordPressCS native `Sniff` class. + * @since 0.5.0 The various function list properties which used to be contained in this class + * have been moved to the WordPressCS native `Sniff` parent class. + * @since 0.12.0 This sniff will now also check for output escaping when using shorthand + * echo tags ` */ protected $unsafePrintingFunctions = array( - '_e' => 'esc_html_e() or esc_attr_e()', - '_ex' => 'echo esc_html_x() or echo esc_attr_x()', - ); - - /** - * Cache of previously added custom functions. - * - * Prevents having to do the same merges over and over again. - * - * @since 0.4.0 - * @since 0.11.0 - Changed from public static to protected non-static. - * - Changed the format from simple bool to array. - * - * @var array - */ - protected $addedCustomFunctions = array( - 'escape' => array(), - 'autoescape' => array(), - 'sanitize' => array(), - 'print' => array(), - ); - - /** - * List of names of the tokens representing PHP magic constants. - * - * @since 0.10.0 - * - * @var array - */ - private $magic_constant_tokens = array( - 'T_CLASS_C' => true, // __CLASS__ - 'T_DIR' => true, // __DIR__ - 'T_FILE' => true, // __FILE__ - 'T_FUNC_C' => true, // __FUNCTION__ - 'T_LINE' => true, // __LINE__ - 'T_METHOD_C' => true, // __METHOD__ - 'T_NS_C' => true, // __NAMESPACE__ - 'T_TRAIT_C' => true, // __TRAIT__ + '_e' => array( + 'alternative' => 'esc_html_e() or esc_attr_e()', + 'params' => array( + 1 => 'text', + ), + ), + '_ex' => array( + 'alternative' => 'echo esc_html_x() or echo esc_attr_x()', + 'params' => array( + 1 => 'text', + ), + ), ); /** @@ -122,7 +81,7 @@ class EscapeOutputSniff extends Sniff { * * @since 1.0.0 * - * @var array + * @var array */ private $safe_php_constants = array( 'PHP_EOL' => true, // String. @@ -136,244 +95,476 @@ class EscapeOutputSniff extends Sniff { ); /** - * List of names of the cast tokens which can be considered as a safe escaping method. + * List of tokens which can be considered as safe when directly part of the output. + * + * This list is enhanced with additional tokens in the `register()` method. * * @since 0.12.0 * - * @var array + * @var array */ - private $safe_cast_tokens = array( - 'T_INT_CAST' => true, // (int) - 'T_DOUBLE_CAST' => true, // (float) - 'T_BOOL_CAST' => true, // (bool) - 'T_UNSET_CAST' => true, // (unset) + private $safe_components = array( + \T_LNUMBER => \T_LNUMBER, + \T_DNUMBER => \T_DNUMBER, + \T_TRUE => \T_TRUE, + \T_FALSE => \T_FALSE, + \T_NULL => \T_NULL, + \T_CONSTANT_ENCAPSED_STRING => \T_CONSTANT_ENCAPSED_STRING, + \T_START_NOWDOC => \T_START_NOWDOC, + \T_NOWDOC => \T_NOWDOC, + \T_END_NOWDOC => \T_END_NOWDOC, + \T_BOOLEAN_NOT => \T_BOOLEAN_NOT, ); /** - * List of tokens which can be considered as a safe when directly part of the output. + * List of keyword tokens this sniff listens for, which can also be used as an inline expression. * - * @since 0.12.0 + * @since 3.0.0 * - * @var array + * @var array */ - private $safe_components = array( - 'T_CONSTANT_ENCAPSED_STRING' => true, - 'T_LNUMBER' => true, - 'T_MINUS' => true, - 'T_PLUS' => true, - 'T_MULTIPLY' => true, - 'T_DIVIDE' => true, - 'T_MODULUS' => true, - 'T_TRUE' => true, - 'T_FALSE' => true, - 'T_NULL' => true, - 'T_DNUMBER' => true, - 'T_START_NOWDOC' => true, - 'T_NOWDOC' => true, - 'T_END_NOWDOC' => true, + private $target_keywords = array( + \T_EXIT => \T_EXIT, + \T_PRINT => \T_PRINT, + \T_THROW => \T_THROW, ); /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return string|int[] */ public function register() { + // Enrich the list of "safe components" tokens. + $this->safe_components += Tokens::$comparisonTokens; + $this->safe_components += Tokens::$operators; + $this->safe_components += Tokens::$booleanOperators; + $this->safe_components += Collections::incrementDecrementOperators(); + + // Set up the tokens the sniff should listen too. + $targets = array_merge( parent::register(), $this->target_keywords ); + $targets[] = \T_ECHO; + $targets[] = \T_OPEN_TAG_WITH_ECHO; + + return $targets; + } - $tokens = array( - \T_ECHO, - \T_PRINT, - \T_EXIT, - \T_STRING, - \T_OPEN_TAG_WITH_ECHO, + /** + * Groups of functions this sniff is looking for. + * + * @since 3.0.0 + * + * @return array + */ + public function getGroups() { + // Make sure all array keys are lowercase (could contain user provided function names). + $printing_functions = array_change_key_case( $this->get_printing_functions(), \CASE_LOWER ); + + // Remove the unsafe printing functions to prevent duplicate notices. + $printing_functions = array_diff_key( $printing_functions, $this->unsafePrintingFunctions ); + + return array( + 'unsafe_printing_functions' => array( + 'functions' => array_keys( $this->unsafePrintingFunctions ), + ), + 'printing_functions' => array( + 'functions' => array_keys( $printing_functions ), + ), ); - - /* - * Check whether short open echo tags are disabled and if so, register the - * T_INLINE_HTML token which is how short open tags are being handled in that case. - * - * In PHP < 5.4, support for short open echo tags depended on whether the - * `short_open_tag` ini directive was set to `true`. - * For PHP >= 5.4, the `short_open_tag` no longer affects the short open - * echo tags and these are now always enabled. - */ - if ( \PHP_VERSION_ID < 50400 && false === (bool) ini_get( 'short_open_tag' ) ) { - $tokens[] = \T_INLINE_HTML; - } - return $tokens; } /** * Processes this test, when one of its tokens is encountered. * + * @since 3.0.0 This method has been split up. + * * @param int $stackPtr The position of the current token in the stack. * * @return int|void Integer stack pointer to skip forward or void to continue * normal file processing. */ public function process_token( $stackPtr ) { + $start = ( $stackPtr + 1 ); + $end = $start; - $this->mergeFunctionLists(); + switch ( $this->tokens[ $stackPtr ]['code'] ) { + case \T_STRING: + // Prevent exclusion of any of the function groups. + $this->exclude = array(); - $function = $this->tokens[ $stackPtr ]['content']; + // In the tests, custom printing functions may be added/removed on the fly. + if ( defined( 'PHP_CODESNIFFER_IN_TESTS' ) ) { + $this->setup_groups( 'functions' ); + } - // Find the opening parenthesis (if present; T_ECHO might not have it). - $open_paren = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); + // Let the abstract parent class handle the initial function call check. + return parent::process_token( $stackPtr ); - // If function, not T_ECHO nor T_PRINT. - if ( \T_STRING === $this->tokens[ $stackPtr ]['code'] ) { - // Skip if it is a function but is not one of the printing functions. - if ( ! isset( $this->printingFunctions[ $this->tokens[ $stackPtr ]['content'] ] ) ) { - return; - } + case \T_EXIT: + $next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); + if ( false === $next_non_empty + || \T_OPEN_PARENTHESIS !== $this->tokens[ $next_non_empty ]['code'] + || isset( $this->tokens[ $next_non_empty ]['parenthesis_closer'] ) === false + ) { + // Live coding/parse error or an exit/die which doesn't pass a status code. Ignore. + return; + } - if ( isset( $this->tokens[ $open_paren ]['parenthesis_closer'] ) ) { - $end_of_statement = $this->tokens[ $open_paren ]['parenthesis_closer']; - } + // $end is not examined, so make sure the parentheses are balanced. + $start = $next_non_empty; + $end = ( $this->tokens[ $next_non_empty ]['parenthesis_closer'] + 1 ); + break; + + case \T_THROW: + // Find the open parentheses, while stepping over the exception creation tokens. + $ignore = Tokens::$emptyTokens; + $ignore += Collections::namespacedNameTokens(); + $ignore += Collections::functionCallTokens(); + $ignore += Collections::objectOperators(); + + $next_relevant = $this->phpcsFile->findNext( $ignore, ( $stackPtr + 1 ), null, true ); + if ( false === $next_relevant ) { + return; + } - // These functions only need to have the first argument escaped. - if ( \in_array( $function, array( 'trigger_error', 'user_error' ), true ) ) { - $first_param = $this->get_function_call_parameter( $stackPtr, 1 ); - $end_of_statement = ( $first_param['end'] + 1 ); - unset( $first_param ); - } - } elseif ( \T_INLINE_HTML === $this->tokens[ $stackPtr ]['code'] ) { - // Skip if no PHP short_open_tag is found in the string. - if ( false === strpos( $this->tokens[ $stackPtr ]['content'], 'tokens[ $next_relevant ]['code'] ) { + $next_relevant = $this->phpcsFile->findNext( $ignore, ( $next_relevant + 1 ), null, true ); + if ( false === $next_relevant ) { + return; + } + } - // Report on what is very likely a PHP short open echo tag outputting a variable. - if ( preg_match( '`\<\?\=[\s]*(\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*(?:(?:->\S+|\[[^\]]+\]))*)[\s]*;?[\s]*\?\>`', $this->tokens[ $stackPtr ]['content'], $matches ) > 0 ) { - $this->phpcsFile->addError( - "All output should be run through an escaping function (see the Security sections in the WordPress Developer Handbooks), found '%s'.", - $stackPtr, - 'OutputNotEscapedShortEcho', - array( $matches[1] ) - ); - return; - } + if ( \T_OPEN_PARENTHESIS !== $this->tokens[ $next_relevant ]['code'] + || isset( $this->tokens[ $next_relevant ]['parenthesis_closer'] ) === false + ) { + // Live codind/parse error or a pre-created exception. Nothing to do for us. + return; + } - return; + $end = $this->tokens[ $next_relevant ]['parenthesis_closer']; + + // Check if the throw is within a `try-catch`. + // Doing this here (instead of earlier) to allow skipping to the end of the statement. + $search_for = Collections::closedScopes(); + $search_for[ \T_TRY ] = \T_TRY; + + $last_condition = Conditions::getLastCondition( $this->phpcsFile, $stackPtr, $search_for ); + if ( false !== $last_condition && \T_TRY === $this->tokens[ $last_condition ]['code'] ) { + // This exception will (probably) be caught, so ignore it. + return $end; + } + + $call_token = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $next_relevant - 1 ), null, true ); + $params = PassedParameters::getParameters( $this->phpcsFile, $call_token ); + if ( empty( $params ) ) { + // No parameters passed, nothing to do. + return $end; + } + + // Examine each parameter individually. + foreach ( $params as $param ) { + $this->check_code_is_escaped( $param['start'], ( $param['end'] + 1 ) ); + } + + return $end; + + case \T_PRINT: + $end = BCFile::findEndOfStatement( $this->phpcsFile, $stackPtr ); + if ( \T_COMMA !== $this->tokens[ $end ]['code'] + && \T_SEMICOLON !== $this->tokens[ $end ]['code'] + && \T_COLON !== $this->tokens[ $end ]['code'] + && \T_DOUBLE_ARROW !== $this->tokens[ $end ]['code'] + && isset( $this->tokens[ ( $end + 1 ) ] ) + ) { + /* + * FindEndOfStatement includes a comma/(semi-)colon/double arrow if that's the end of + * the statement, but for everything else, it returns the last non-empty token _before_ + * the end, which would mean the last non-empty token in the statement would not + * be examined. Let's fix that. + */ + ++$end; + } + + // Note: no need to check for close tag as close tag will have the token before the tag as the $end. + if ( $end >= ( $this->phpcsFile->numTokens - 1 ) ) { + $last_non_empty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, $end, null, true ); + if ( \T_SEMICOLON !== $this->tokens[ $last_non_empty ]['code'] ) { + // Live coding/parse error at end of file. Ignore. + return; + } + } + + // Special case for a print statement *within* a ternary, where we need to find the "inline else" as the end token. + $prev_non_empty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); + if ( \T_INLINE_THEN === $this->tokens[ $prev_non_empty ]['code'] ) { + $target_nesting_level = 0; + if ( empty( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) === false ) { + $target_nesting_level = \count( $this->tokens[ $stackPtr ]['nested_parenthesis'] ); + } + + $inline_else = false; + for ( $i = ( $stackPtr + 1 ); $i < $end; $i++ ) { + if ( \T_INLINE_ELSE !== $this->tokens[ $i ]['code'] ) { + continue; + } + + if ( empty( $this->tokens[ $i ]['nested_parenthesis'] ) + && 0 === $target_nesting_level + ) { + $inline_else = $i; + break; + } + + if ( empty( $this->tokens[ $i ]['nested_parenthesis'] ) === false + && \count( $this->tokens[ $i ]['nested_parenthesis'] ) === $target_nesting_level + ) { + $inline_else = $i; + break; + } + } + + if ( false === $inline_else ) { + // Live coding/parse error. Bow out. + return; + } + + $end = $inline_else; + } + + break; + + // Echo, open tag with echo. + default: + $end = $this->phpcsFile->findNext( array( \T_SEMICOLON, \T_CLOSE_TAG ), $stackPtr ); + if ( false === $end ) { + // Live coding/parse error. Bow out. + return; + } + + break; } - // Checking for the ignore comment, ex: //xss ok. - if ( $this->has_whitelist_comment( 'xss', $stackPtr ) ) { + return $this->check_code_is_escaped( $start, $end ); + } + + /** + * Process a matched function call token. + * + * @since 3.0.0 Split off from the process_token() method. + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * + * @return int|void Integer stack pointer to skip forward or void to continue + * normal file processing. + */ + public function process_matched_token( $stackPtr, $group_name, $matched_content ) { + // Make sure we only deal with actual function calls, not function import use statements. + $next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); + if ( false === $next_non_empty + || \T_OPEN_PARENTHESIS !== $this->tokens[ $next_non_empty ]['code'] + || isset( $this->tokens[ $next_non_empty ]['parenthesis_closer'] ) === false + ) { + // Live coding, parse error or not a function _call_. return; } - if ( isset( $this->unsafePrintingFunctions[ $function ] ) ) { + $end = $this->tokens[ $next_non_empty ]['parenthesis_closer']; + + if ( 'unsafe_printing_functions' === $group_name ) { $error = $this->phpcsFile->addError( "All output should be run through an escaping function (like %s), found '%s'.", $stackPtr, 'UnsafePrintingFunction', - array( $this->unsafePrintingFunctions[ $function ], $function ) + array( $this->unsafePrintingFunctions[ $matched_content ]['alternative'], $matched_content ) ); // If the error was reported, don't bother checking the function's arguments. - if ( $error ) { - return isset( $end_of_statement ) ? $end_of_statement : null; + if ( $error || empty( $this->unsafePrintingFunctions[ $matched_content ]['params'] ) ) { + return $end; } - } - $ternary = false; + // If the function was not reported for being unsafe, examine the relevant parameters. + $params = PassedParameters::getParameters( $this->phpcsFile, $stackPtr ); + foreach ( $this->unsafePrintingFunctions[ $matched_content ]['params'] as $position => $name ) { + $param = PassedParameters::getParameterFromStack( $params, $position, $name ); + if ( false === $param ) { + // Parameter doesn't exist. Nothing to do. + continue; + } - // This is already determined if this is a function and not T_ECHO. - if ( ! isset( $end_of_statement ) ) { + $this->check_code_is_escaped( $param['start'], ( $param['end'] + 1 ) ); + } - $end_of_statement = $this->phpcsFile->findNext( array( \T_SEMICOLON, \T_CLOSE_TAG ), $stackPtr ); - $last_token = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $end_of_statement - 1 ), null, true ); + return $end; + } - // Check for the ternary operator. We only need to do this here if this - // echo is lacking parenthesis. Otherwise it will be handled below. - if ( \T_OPEN_PARENTHESIS !== $this->tokens[ $open_paren ]['code'] || \T_CLOSE_PARENTHESIS !== $this->tokens[ $last_token ]['code'] ) { + $params = PassedParameters::getParameters( $this->phpcsFile, $stackPtr ); - $ternary = $this->phpcsFile->findNext( \T_INLINE_THEN, $stackPtr, $end_of_statement ); + /* + * These functions only need to have their first argument - `$message` - escaped. + * Note: user_error() is an alias for trigger_error(), so the param names are the same. + */ + if ( 'trigger_error' === $matched_content || 'user_error' === $matched_content ) { + $message_param = PassedParameters::getParameterFromStack( $params, 1, 'message' ); + if ( false === $message_param ) { + // Message parameter doesn't exist. Nothing to do. + return $end; + } + + return $this->check_code_is_escaped( $message_param['start'], ( $message_param['end'] + 1 ) ); + } - // If there is a ternary skip over the part before the ?. However, if - // the ternary is within parentheses, it will be handled in the loop. - if ( false !== $ternary && empty( $this->tokens[ $ternary ]['nested_parenthesis'] ) ) { - $stackPtr = $ternary; + /* + * If the first param to `_deprecated_file()` - `$file` - follows the typical `basename( __FILE__ )` + * pattern, it doesn't need to be escaped. + */ + if ( '_deprecated_file' === $matched_content ) { + $file_param = PassedParameters::getParameterFromStack( $params, 1, 'file' ); + + if ( false !== $file_param ) { + // Check for a particular code pattern which can safely be ignored. + if ( preg_match( '`^[\\\\]?basename\s*\(\s*__FILE__\s*\)$`', $file_param['clean'] ) === 1 ) { + unset( $params[1], $params['file'] ); // Remove the param, whether passed positionally or named. } } + unset( $file_param ); } - // Ignore the function itself. - $stackPtr++; + // Examine each parameter individually. + foreach ( $params as $param ) { + $this->check_code_is_escaped( $param['start'], ( $param['end'] + 1 ) ); + } + + return $end; + } + + /** + * Check whether each relevant part of an arbitrary group of token is output escaped. + * + * @since 3.0.0 Split off from the process_token() method. + * + * @param int $start The position to start checking from. + * @param int $end The position to stop the check at. + * + * @return int Integer stack pointer to skip forward. + */ + protected function check_code_is_escaped( $start, $end ) { + /* + * Check for a ternary operator. + * We only need to do this here if this statement is lacking parenthesis. + * Otherwise it will be handled in the below loop. + */ + $ternary = false; + $next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $start + 1 ), null, true ); + $last_non_empty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $end - 1 ), null, true ); + + if ( \T_OPEN_PARENTHESIS !== $this->tokens[ $next_non_empty ]['code'] + || \T_CLOSE_PARENTHESIS !== $this->tokens[ $last_non_empty ]['code'] + || ( \T_OPEN_PARENTHESIS === $this->tokens[ $next_non_empty ]['code'] + && \T_CLOSE_PARENTHESIS === $this->tokens[ $last_non_empty ]['code'] + && isset( $this->tokens[ $next_non_empty ]['parenthesis_closer'] ) + && $this->tokens[ $next_non_empty ]['parenthesis_closer'] !== $last_non_empty + ) + ) { + // If there is a (long) ternary skip over the part before the ?. + $ternary = $this->find_long_ternary( $start, $end ); + if ( false !== $ternary ) { + $start = ( $ternary + 1 ); + } + } $in_cast = false; + $watch = true; // Looping through echo'd components. - $watch = true; - for ( $i = $stackPtr; $i < $end_of_statement; $i++ ) { - + for ( $i = $start; $i < $end; $i++ ) { // Ignore whitespaces and comments. if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) { continue; } - // Ignore namespace separators. - if ( \T_NS_SEPARATOR === $this->tokens[ $i ]['code'] ) { + // Skip over irrelevant tokens. + if ( isset( Tokens::$magicConstants[ $this->tokens[ $i ]['code'] ] ) // Magic constants for debug functions. + || \T_NS_SEPARATOR === $this->tokens[ $i ]['code'] + || \T_DOUBLE_ARROW === $this->tokens[ $i ]['code'] + || \T_CLOSE_PARENTHESIS === $this->tokens[ $i ]['code'] + ) { continue; } if ( \T_OPEN_PARENTHESIS === $this->tokens[ $i ]['code'] ) { - if ( ! isset( $this->tokens[ $i ]['parenthesis_closer'] ) ) { // Live coding or parse error. break; } if ( $in_cast ) { - // Skip to the end of a function call if it has been casted to a safe value. $i = $this->tokens[ $i ]['parenthesis_closer']; $in_cast = false; } else { - - // Skip over the condition part of a ternary (i.e., to after the ?). - $ternary = $this->phpcsFile->findNext( \T_INLINE_THEN, $i, $this->tokens[ $i ]['parenthesis_closer'] ); - + // Skip over the condition part of a (long) ternary (i.e., to after the ?). + $ternary = $this->find_long_ternary( ( $i + 1 ), $this->tokens[ $i ]['parenthesis_closer'] ); if ( false !== $ternary ) { - - $next_paren = $this->phpcsFile->findNext( \T_OPEN_PARENTHESIS, ( $i + 1 ), $this->tokens[ $i ]['parenthesis_closer'] ); - - // We only do it if the ternary isn't within a subset of parentheses. - if ( false === $next_paren || ( isset( $this->tokens[ $next_paren ]['parenthesis_closer'] ) && $ternary > $this->tokens[ $next_paren ]['parenthesis_closer'] ) ) { - $i = $ternary; - } + $i = $ternary; } } continue; } - // Handle arrays for those functions that accept them. - if ( \T_ARRAY === $this->tokens[ $i ]['code'] ) { - $i++; // Skip the opening parenthesis. + /* + * If a keyword is encountered in an inline expression and the keyword is one + * this sniff listens to, recurse into the sniff, handle the expression + * based on the keyword and skip over the code examined. + */ + if ( isset( $this->target_keywords[ $this->tokens[ $i ]['code'] ] ) ) { + $return_value = $this->process_token( $i ); + if ( isset( $return_value ) ) { + $i = $return_value; + } continue; } - if ( \T_OPEN_SHORT_ARRAY === $this->tokens[ $i ]['code'] - || \T_CLOSE_SHORT_ARRAY === $this->tokens[ $i ]['code'] - ) { - continue; - } + // Handle PHP 8.0+ match expressions. + if ( \T_MATCH === $this->tokens[ $i ]['code'] ) { + $match_valid = $this->walk_match_expression( $i ); + if ( false === $match_valid ) { + // Live coding or parse error. Shouldn't be possible as PHP[CS] will tokenize the keyword as `T_STRING` in that case. + break; // @codeCoverageIgnore + } - if ( \in_array( $this->tokens[ $i ]['code'], array( \T_DOUBLE_ARROW, \T_CLOSE_PARENTHESIS ), true ) ) { + $i = $match_valid; continue; } - // Handle magic constants for debug functions. - if ( isset( $this->magic_constant_tokens[ $this->tokens[ $i ]['type'] ] ) ) { + // Examine the items in an array individually for array parameters. + if ( isset( Collections::arrayOpenTokensBC()[ $this->tokens[ $i ]['code'] ] ) ) { + $array_open_close = Arrays::getOpenClose( $this->phpcsFile, $i ); + if ( false === $array_open_close ) { + // Short list or misidentified short array token. + continue; + } + + $array_items = PassedParameters::getParameters( $this->phpcsFile, $i, 0, true ); + if ( ! empty( $array_items ) ) { + foreach ( $array_items as $array_item ) { + $this->check_code_is_escaped( $array_item['start'], ( $array_item['end'] + 1 ) ); + } + } + + $i = $array_open_close['closer']; continue; } - // Handle safe PHP native constants. + // Ignore safe PHP native constants. if ( \T_STRING === $this->tokens[ $i ]['code'] && isset( $this->safe_php_constants[ $this->tokens[ $i ]['content'] ] ) - && $this->is_use_of_global_constant( $i ) + && ConstantsHelper::is_use_of_global_constant( $this->phpcsFile, $i ) ) { continue; } @@ -403,67 +594,144 @@ public function process_token( $stackPtr ) { // Allow T_CONSTANT_ENCAPSED_STRING eg: echo 'Some String'; // Also T_LNUMBER, e.g.: echo 45; exit -1; and booleans. - if ( isset( $this->safe_components[ $this->tokens[ $i ]['type'] ] ) ) { + if ( isset( $this->safe_components[ $this->tokens[ $i ]['code'] ] ) ) { continue; } + // Check for use of *::class. + if ( \T_STRING === $this->tokens[ $i ]['code'] + || \T_VARIABLE === $this->tokens[ $i ]['code'] + || isset( Collections::ooHierarchyKeywords()[ $this->tokens[ $i ]['code'] ] ) + ) { + $double_colon = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), $end, true ); + if ( false !== $double_colon + && \T_DOUBLE_COLON === $this->tokens[ $double_colon ]['code'] + ) { + $class_keyword = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $double_colon + 1 ), $end, true ); + if ( false !== $class_keyword + && \T_STRING === $this->tokens[ $class_keyword ]['code'] + && 'class' === strtolower( $this->tokens[ $class_keyword ]['content'] ) + ) { + $i = $class_keyword; + continue; + } + } + } + $watch = false; // Allow int/double/bool casted variables. - if ( isset( $this->safe_cast_tokens[ $this->tokens[ $i ]['type'] ] ) ) { + if ( isset( ContextHelper::get_safe_cast_tokens()[ $this->tokens[ $i ]['code'] ] ) ) { + /* + * If the next thing is a match expression, skip over it as whatever is + * being returned will be safe casted. + * Do not set `$in_cast` to `true`. + */ + $next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), $end, true ); + if ( false !== $next_non_empty + && \T_MATCH === $this->tokens[ $next_non_empty ]['code'] + && isset( $this->tokens[ $next_non_empty ]['scope_closer'] ) + ) { + $i = $this->tokens[ $next_non_empty ]['scope_closer']; + continue; + } + $in_cast = true; continue; } + // Handle heredocs separately as they only need escaping when interpolation is used. + if ( \T_START_HEREDOC === $this->tokens[ $i ]['code'] ) { + $current = ( $i + 1 ); + while ( isset( $this->tokens[ $current ] ) && \T_HEREDOC === $this->tokens[ $current ]['code'] ) { + $embeds = TextStrings::getEmbeds( $this->tokens[ $current ]['content'] ); + if ( ! empty( $embeds ) ) { + $this->phpcsFile->addError( + 'All output should be run through an escaping function (see the Security sections in the WordPress Developer Handbooks), found interpolation in unescaped heredoc.', + $current, + 'HeredocOutputNotEscaped' + ); + } + ++$current; + } + + $i = $current; + continue; + } + // Now check that next token is a function call. if ( \T_STRING === $this->tokens[ $i ]['code'] ) { - $ptr = $i; $functionName = $this->tokens[ $i ]['content']; - $function_opener = $this->phpcsFile->findNext( \T_OPEN_PARENTHESIS, ( $i + 1 ), null, false, null, true ); - $is_formatting_function = isset( $this->formattingFunctions[ $functionName ] ); - - if ( false !== $function_opener ) { + $function_opener = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true ); + $is_formatting_function = FormattingFunctionsHelper::is_formatting_function( $functionName ); - if ( 'array_map' === $functionName ) { - - // Get the first parameter (name of function being used on the array). - $mapped_function = $this->phpcsFile->findNext( - Tokens::$emptyTokens, - ( $function_opener + 1 ), - $this->tokens[ $function_opener ]['parenthesis_closer'], - true - ); - - // If we're able to resolve the function name, do so. - if ( $mapped_function && \T_CONSTANT_ENCAPSED_STRING === $this->tokens[ $mapped_function ]['code'] ) { - $functionName = $this->strip_quotes( $this->tokens[ $mapped_function ]['content'] ); - $ptr = $mapped_function; + if ( false !== $function_opener + && \T_OPEN_PARENTHESIS === $this->tokens[ $function_opener ]['code'] + ) { + if ( ArrayWalkingFunctionsHelper::is_array_walking_function( $functionName ) ) { + // Get the callback parameter. + $callback = ArrayWalkingFunctionsHelper::get_callback_parameter( $this->phpcsFile, $ptr ); + + if ( ! empty( $callback ) ) { + /* + * If this is a function callback (not a method callback array) and we're able + * to resolve the function name, do so. + */ + $mapped_function = $this->phpcsFile->findNext( + Tokens::$emptyTokens, + $callback['start'], + ( $callback['end'] + 1 ), + true + ); + + if ( false !== $mapped_function + && \T_CONSTANT_ENCAPSED_STRING === $this->tokens[ $mapped_function ]['code'] + ) { + $functionName = TextStrings::stripQuotes( $this->tokens[ $mapped_function ]['content'] ); + $ptr = $mapped_function; + } } } - // Skip pointer to after the function. - // If this is a formatting function we just skip over the opening - // parenthesis. Otherwise we skip all the way to the closing. + // If this is a formatting function, we examine the parameters individually. if ( $is_formatting_function ) { - $i = ( $function_opener + 1 ); + $formatting_params = PassedParameters::getParameters( $this->phpcsFile, $i ); + if ( ! empty( $formatting_params ) ) { + foreach ( $formatting_params as $format_param ) { + $this->check_code_is_escaped( $format_param['start'], ( $format_param['end'] + 1 ) ); + } + } + $watch = true; + } + + // Skip pointer to after the function. + if ( isset( $this->tokens[ $function_opener ]['parenthesis_closer'] ) ) { + $i = $this->tokens[ $function_opener ]['parenthesis_closer']; } else { - if ( isset( $this->tokens[ $function_opener ]['parenthesis_closer'] ) ) { - $i = $this->tokens[ $function_opener ]['parenthesis_closer']; - } else { - // Live coding or parse error. - break; - } + // Live coding or parse error. + break; } } // If this is a safe function, we don't flag it. - if ( - $is_formatting_function - || isset( $this->autoEscapedFunctions[ $functionName ] ) - || isset( $this->escapingFunctions[ $functionName ] ) + if ( $is_formatting_function + || $this->is_escaping_function( $functionName ) + || $this->is_auto_escaped_function( $functionName ) ) { + // Special case get_search_query() which is unsafe if $escaped = false. + if ( 'get_search_query' === strtolower( $functionName ) ) { + $escaped_param = PassedParameters::getParameter( $this->phpcsFile, $ptr, 1, 'escaped' ); + if ( false !== $escaped_param && 'true' !== $escaped_param['clean'] ) { + $this->phpcsFile->addError( + 'Output from get_search_query() is unsafe due to $escaped parameter being set to "false".', + $ptr, + 'UnsafeSearchQuery' + ); + } + } + continue; } @@ -474,71 +742,160 @@ public function process_token( $stackPtr ) { $ptr = $i; } + // Make the error message a little more informative for array access variables. + if ( \T_VARIABLE === $this->tokens[ $ptr ]['code'] ) { + $array_keys = VariableHelper::get_array_access_keys( $this->phpcsFile, $ptr ); + + if ( ! empty( $array_keys ) ) { + $content .= '[' . implode( '][', $array_keys ) . ']'; + } + } + $this->phpcsFile->addError( "All output should be run through an escaping function (see the Security sections in the WordPress Developer Handbooks), found '%s'.", $ptr, 'OutputNotEscaped', - $content + array( $content ) ); } - return $end_of_statement; + return $end; } /** - * Merge custom functions provided via a custom ruleset with the defaults, if we haven't already. + * Check whether there is a ternary token at the right nesting level in an arbitrary set of tokens. * - * @since 0.11.0 Split out from the `process()` method. + * @since 3.0.0 Split off from the process_token() method. * - * @return void + * @param int $start The position to start checking from. + * @param int $end The position to stop the check at. + * + * @return int|false Stack pointer to the ternary or FALSE if no ternary was found or + * if this is a short ternary. */ - protected function mergeFunctionLists() { - if ( $this->customEscapingFunctions !== $this->addedCustomFunctions['escape'] - || $this->customSanitizingFunctions !== $this->addedCustomFunctions['sanitize'] - ) { - $customEscapeFunctions = $this->merge_custom_array( $this->customEscapingFunctions, array(), false ); - - if ( ! empty( $this->customSanitizingFunctions ) ) { - $customEscapeFunctions = $this->merge_custom_array( - $this->customSanitizingFunctions, - $customEscapeFunctions, - false - ); - - $this->phpcsFile->addWarning( - 'The customSanitizingFunctions property is deprecated in favor of customEscapingFunctions.', - 0, - 'DeprecatedCustomSanitizingFunctions' - ); + private function find_long_ternary( $start, $end ) { + for ( $i = $start; $i < $end; $i++ ) { + // Ignore anything within square brackets. + if ( isset( $this->tokens[ $i ]['bracket_opener'], $this->tokens[ $i ]['bracket_closer'] ) + && $i === $this->tokens[ $i ]['bracket_opener'] + ) { + $i = $this->tokens[ $i ]['bracket_closer']; + continue; } - $this->escapingFunctions = $this->merge_custom_array( - $customEscapeFunctions, - $this->escapingFunctions - ); + // Skip past nested arrays, function calls and arbitrary groupings. + if ( \T_OPEN_PARENTHESIS === $this->tokens[ $i ]['code'] + && isset( $this->tokens[ $i ]['parenthesis_closer'] ) + ) { + $i = $this->tokens[ $i ]['parenthesis_closer']; + continue; + } + + // Skip past closures, anonymous classes and anything else scope related. + if ( isset( $this->tokens[ $i ]['scope_condition'], $this->tokens[ $i ]['scope_closer'] ) + && $this->tokens[ $i ]['scope_condition'] === $i + ) { + $i = $this->tokens[ $i ]['scope_closer']; + continue; + } - $this->addedCustomFunctions['escape'] = $this->customEscapingFunctions; - $this->addedCustomFunctions['sanitize'] = $this->customSanitizingFunctions; + if ( \T_INLINE_THEN !== $this->tokens[ $i ]['code'] ) { + continue; + } + + /* + * Okay, we found a ternary and it should be at the correct nesting level. + * If this is a short ternary, it shouldn't be ignored though. + */ + if ( Operators::isShortTernary( $this->phpcsFile, $i ) === true ) { + return false; + } + + return $i; } - if ( $this->customAutoEscapedFunctions !== $this->addedCustomFunctions['autoescape'] ) { - $this->autoEscapedFunctions = $this->merge_custom_array( - $this->customAutoEscapedFunctions, - $this->autoEscapedFunctions - ); + return false; + } - $this->addedCustomFunctions['autoescape'] = $this->customAutoEscapedFunctions; + /** + * Examine a match expression and only check for escaping in the "returned" parts of the match expression. + * + * {@internal PHPCSUtils will likely contain a utility for parsing match expressions in the future. + * Ref: https://github.com/PHPCSStandards/PHPCSUtils/issues/497} + * + * @since 3.0.0 + * + * @param int $stackPtr Pointer to a T_MATCH token. + * + * @return int|false Stack pointer to skip to or FALSE if the match expression contained a parse error. + */ + private function walk_match_expression( $stackPtr ) { + if ( ! isset( $this->tokens[ $stackPtr ]['scope_opener'], $this->tokens[ $stackPtr ]['scope_closer'] ) ) { + // Parse error/live coding. Shouldn't be possible as PHP[CS] will tokenize the keyword as `T_STRING` in that case. + return false; // @codeCoverageIgnore } - if ( $this->customPrintingFunctions !== $this->addedCustomFunctions['print'] ) { + $current = $this->tokens[ $stackPtr ]['scope_opener']; + $end = $this->tokens[ $stackPtr ]['scope_closer']; + do { + $current = $this->phpcsFile->findNext( \T_MATCH_ARROW, ( $current + 1 ), $end ); + if ( false === $current ) { + // We must have reached the last match item (or there is a parse error). + break; + } - $this->printingFunctions = $this->merge_custom_array( - $this->customPrintingFunctions, - $this->printingFunctions - ); + $item_start = ( $current + 1 ); + $item_end = false; - $this->addedCustomFunctions['print'] = $this->customPrintingFunctions; - } - } + // Find the first comma at the same level. + for ( $i = $item_start; $i <= $end; $i++ ) { + // Ignore anything within square brackets. + if ( isset( $this->tokens[ $i ]['bracket_opener'], $this->tokens[ $i ]['bracket_closer'] ) + && $i === $this->tokens[ $i ]['bracket_opener'] + ) { + $i = $this->tokens[ $i ]['bracket_closer']; + continue; + } + + // Skip past nested arrays, function calls and arbitrary groupings. + if ( \T_OPEN_PARENTHESIS === $this->tokens[ $i ]['code'] + && isset( $this->tokens[ $i ]['parenthesis_closer'] ) + ) { + $i = $this->tokens[ $i ]['parenthesis_closer']; + continue; + } + // Skip past closures, anonymous classes and anything else scope related. + if ( isset( $this->tokens[ $i ]['scope_condition'], $this->tokens[ $i ]['scope_closer'] ) + && $this->tokens[ $i ]['scope_condition'] === $i + ) { + $i = $this->tokens[ $i ]['scope_closer']; + continue; + } + + if ( \T_COMMA !== $this->tokens[ $i ]['code'] + && $i !== $end + ) { + continue; + } + + $item_end = $i; + break; + } + + if ( false === $item_end ) { + // Parse error/live coding. Shouldn't be possible. + return false; // @codeCoverageIgnore + } + + // Now check that the value returned by this match "leaf" is correctly escaped. + $this->check_code_is_escaped( $item_start, $item_end ); + + // Independently of whether or not the check was succesfull or ran into (parse error) problems, + // always skip to the identified end of the item. + $current = $item_end; + } while ( $current < $end ); + + return $end; + } } diff --git a/WordPress/Sniffs/Security/NonceVerificationSniff.php b/WordPress/Sniffs/Security/NonceVerificationSniff.php index 4909a01f99..0241147327 100644 --- a/WordPress/Sniffs/Security/NonceVerificationSniff.php +++ b/WordPress/Sniffs/Security/NonceVerificationSniff.php @@ -3,27 +3,42 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\Security; +namespace WordPressCS\WordPress\Sniffs\Security; -use WordPress\Sniff; +use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\Conditions; +use PHPCSUtils\Utils\Context; +use PHPCSUtils\Utils\Lists; +use PHPCSUtils\Utils\MessageHelper; +use PHPCSUtils\Utils\Scopes; +use WordPressCS\WordPress\Helpers\ContextHelper; +use WordPressCS\WordPress\Helpers\RulesetPropertyHelper; +use WordPressCS\WordPress\Helpers\SanitizationHelperTrait; +use WordPressCS\WordPress\Helpers\UnslashingFunctionsHelper; +use WordPressCS\WordPress\Helpers\VariableHelper; +use WordPressCS\WordPress\Sniff; /** * Checks that nonce verification accompanies form processing. * - * @link https://developer.wordpress.org/plugins/security/nonces/ Nonces on Plugin Developer Handbook + * @link https://developer.wordpress.org/plugins/security/nonces/ Nonces on Plugin Developer Handbook * - * @package WPCS\WordPressCodingStandards + * @since 0.5.0 + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 1.0.0 This sniff has been moved from the `CSRF` category to the `Security` category. + * @since 3.0.0 This sniff has received significant updates to its logic and structure. * - * @since 0.5.0 - * @since 0.13.0 Class name changed: this class is now namespaced. - * @since 1.0.0 This sniff has been moved from the `CSRF` category to the `Security` category. + * @uses \WordPressCS\WordPress\Helpers\SanitizationHelperTrait::$customSanitizingFunctions + * @uses \WordPressCS\WordPress\Helpers\SanitizationHelperTrait::$customUnslashingSanitizingFunctions */ class NonceVerificationSniff extends Sniff { + use SanitizationHelperTrait; + /** * Superglobals to notify about when not accompanied by an nonce check. * @@ -35,63 +50,35 @@ class NonceVerificationSniff extends Sniff { */ protected $superglobals = array( '$_POST' => true, - '$_FILE' => true, + '$_FILES' => true, '$_GET' => false, '$_REQUEST' => false, ); - /** - * Superglobals to give an error for when not accompanied by an nonce check. - * - * @since 0.5.0 - * @since 0.11.0 Changed visibility from public to protected. - * - * @deprecated 0.12.0 Replaced by $superglobals property. - * - * @var array - */ - protected $errorForSuperGlobals = array(); - - /** - * Superglobals to give a warning for when not accompanied by an nonce check. - * - * If the variable is also in the error list, that takes precedence. - * - * @since 0.5.0 - * @since 0.11.0 Changed visibility from public to protected. - * - * @deprecated 0.12.0 Replaced by $superglobals property. - * - * @var array - */ - protected $warnForSuperGlobals = array(); - /** * Custom list of functions which verify nonces. * * @since 0.5.0 * - * @var string|string[] + * @var string[] */ public $customNonceVerificationFunctions = array(); /** - * Custom list of functions that sanitize the values passed to them. - * - * @since 0.11.0 - * - * @var string|string[] - */ - public $customSanitizingFunctions = array(); - - /** - * Custom sanitizing functions that implicitly unslash the values passed to them. + * List of the functions which verify nonces. * - * @since 0.11.0 + * @since 0.5.0 + * @since 0.11.0 Changed from public static to protected non-static. + * @since 3.0.0 - Moved from the generic `Sniff` class to this class. + * - Visibility changed from `protected` to `private. * - * @var string|string[] + * @var array */ - public $customUnslashingSanitizingFunctions = array(); + private $nonceVerificationFunctions = array( + 'wp_verify_nonce' => true, + 'check_admin_referer' => true, + 'check_ajax_referer' => true, + ); /** * Cache of previously added custom functions. @@ -101,14 +88,36 @@ class NonceVerificationSniff extends Sniff { * @since 0.5.0 * @since 0.11.0 - Changed from public static to protected non-static. * - Changed the format from simple bool to array. + * @since 3.0.0 - Property rename from `$addedCustomFunctions` to `$addedCustomNonceFunctions`. + * - Visibility changed from `protected` to `private. + * - Format changed from a multi-dimensional array to a single-dimensional array. * * @var array */ - protected $addedCustomFunctions = array( - 'nonce' => array(), - 'sanitize' => array(), - 'unslashsanitize' => array(), - ); + private $addedCustomNonceFunctions = array(); + + /** + * Information on the all scopes that were checked to find a nonce verification in a particular file. + * + * The array will be in the following format: + * ``` + * array( + * 'file' => (string) The name of the file. + * 'cache' => (array) array( + * # => array( The key is the token pointer to the "start" position. + * 'end' => (int) The token pointer to the "end" position. + * 'nonce' => (int|bool) The token pointer where n nonce check + * was found, or false if none was found. + * ) + * ) + * ) + * ``` + * + * @since 3.0.0 + * + * @var array + */ + private $cached_results; /** * Returns an array of tokens this test wants to listen for. @@ -116,10 +125,10 @@ class NonceVerificationSniff extends Sniff { * @return array */ public function register() { + $targets = array( \T_VARIABLE => \T_VARIABLE ); + $targets += Collections::listOpenTokensBC(); // We need to skip over lists. - return array( - \T_VARIABLE, - ); + return $targets; } /** @@ -127,77 +136,287 @@ public function register() { * * @param int $stackPtr The position of the current token in the stack. * - * @return void + * @return int|void Integer stack pointer to skip forward or void to continue + * normal file processing. */ public function process_token( $stackPtr ) { + // Skip over lists as whatever is in those will always be assignments. + if ( isset( Collections::listOpenTokensBC()[ $this->tokens[ $stackPtr ]['code'] ] ) ) { + $open_close = Lists::getOpenClose( $this->phpcsFile, $stackPtr ); + $skip_to = $stackPtr; + if ( false !== $open_close ) { + $skip_to = $open_close['closer']; + } - $instance = $this->tokens[ $stackPtr ]; + return $skip_to; + } - if ( ! isset( $this->superglobals[ $instance['content'] ] ) ) { + if ( ! isset( $this->superglobals[ $this->tokens[ $stackPtr ]['content'] ] ) ) { return; } - if ( $this->has_whitelist_comment( 'CSRF', $stackPtr ) ) { + if ( Scopes::isOOProperty( $this->phpcsFile, $stackPtr ) ) { + // Property with the same name as a superglobal. Not our target. return; } - if ( $this->is_assignment( $stackPtr ) ) { - return; + // Determine the cache keys for this item. + $cache_keys = array( + 'file' => $this->phpcsFile->getFilename(), + 'start' => 0, + 'end' => $stackPtr, + ); + + // If we're in a function, only look inside of it. + // This doesn't take arrow functions into account as those are "open". + $functionPtr = Conditions::getLastCondition( $this->phpcsFile, $stackPtr, array( \T_FUNCTION, \T_CLOSURE ) ); + if ( false !== $functionPtr ) { + $cache_keys['start'] = $this->tokens[ $functionPtr ]['scope_opener']; } $this->mergeFunctionLists(); - if ( $this->is_only_sanitized( $stackPtr ) ) { + $needs_nonce = $this->needs_nonce_check( $stackPtr, $cache_keys ); + if ( false === $needs_nonce ) { return; } - if ( $this->has_nonce_check( $stackPtr ) ) { + if ( $this->has_nonce_check( $stackPtr, $cache_keys, ( 'after' === $needs_nonce ) ) ) { return; } // If we're still here, no nonce-verification function was found. - $this->addMessage( + $error_code = 'Missing'; + if ( false === $this->superglobals[ $this->tokens[ $stackPtr ]['content'] ] ) { + $error_code = 'Recommended'; + } + + MessageHelper::addMessage( + $this->phpcsFile, 'Processing form data without nonce verification.', $stackPtr, - $this->superglobals[ $instance['content'] ], - 'NoNonceVerification' + $this->superglobals[ $this->tokens[ $stackPtr ]['content'] ], + $error_code ); } /** - * Merge custom functions provided via a custom ruleset with the defaults, if we haven't already. + * Determine whether or not a nonce check is needed for the current superglobal. * - * @since 0.11.0 Split out from the `process()` method. + * @since 3.0.0 + * + * @param int $stackPtr The position of the current token in the stack of tokens. + * @param array $cache_keys The keys for the applicable cache (to potentially set). + * + * @return string|false String "before" or "after" if a nonce check is needed. + * FALSE when no nonce check is needed. + */ + protected function needs_nonce_check( $stackPtr, array $cache_keys ) { + $in_nonce_check = ContextHelper::is_in_function_call( $this->phpcsFile, $stackPtr, $this->nonceVerificationFunctions ); + if ( false !== $in_nonce_check ) { + // This *is* the nonce check, so bow out, but do store to cache. + // @todo Change to use arg unpacking once PHP < 5.6 has been dropped. + $this->set_cache( $cache_keys['file'], $cache_keys['start'], $cache_keys['end'], $in_nonce_check ); + return false; + } + + if ( Context::inUnset( $this->phpcsFile, $stackPtr ) ) { + // Variable is only being unset, no nonce check needed. + return false; + } + + if ( VariableHelper::is_assignment( $this->phpcsFile, $stackPtr, false ) ) { + // Overwriting the value of a superglobal. + return false; + } + + $needs_nonce = 'before'; + if ( ContextHelper::is_in_isset_or_empty( $this->phpcsFile, $stackPtr ) + || ContextHelper::is_in_type_test( $this->phpcsFile, $stackPtr ) + || VariableHelper::is_comparison( $this->phpcsFile, $stackPtr ) + || VariableHelper::is_assignment( $this->phpcsFile, $stackPtr, true ) + || ContextHelper::is_in_array_comparison( $this->phpcsFile, $stackPtr ) + || ContextHelper::is_in_function_call( $this->phpcsFile, $stackPtr, UnslashingFunctionsHelper::get_functions() ) !== false + || $this->is_only_sanitized( $this->phpcsFile, $stackPtr ) + ) { + $needs_nonce = 'after'; + } + + return $needs_nonce; + } + + /** + * Check if this token has an associated nonce check. + * + * @since 0.5.0 + * @since 3.0.0 - Moved from the generic `Sniff` class to this class. + * - Visibility changed from `protected` to `private. + * - New `$cache_keys` parameter. + * - New `$allow_nonce_after` parameter. + * + * @param int $stackPtr The position of the current token in the stack of tokens. + * @param array $cache_keys The keys for the applicable cache. + * @param bool $allow_nonce_after Whether the nonce check _must_ be before the $stackPtr or + * is allowed _after_ the $stackPtr. + * + * @return bool + */ + private function has_nonce_check( $stackPtr, array $cache_keys, $allow_nonce_after = false ) { + $start = $cache_keys['start']; + $end = $cache_keys['end']; + + // We allow for certain actions, such as an isset() check to come before the nonce check. + // If this superglobal is inside such a check, look for the nonce after it as well, + // all the way to the end of the scope. + if ( true === $allow_nonce_after ) { + $end = ( 0 === $start ) ? $this->phpcsFile->numTokens : $this->tokens[ $start ]['scope_closer']; + } + + // Check against the cache. + $current_cache = $this->get_cache( $cache_keys['file'], $start ); + if ( false !== $current_cache['nonce'] ) { + // If we have already found a nonce check in this scope, we just + // need to check whether it comes before this token. It is OK if the + // check is after the token though, if this was only an isset() check. + return ( true === $allow_nonce_after || $current_cache['nonce'] < $stackPtr ); + } elseif ( $end <= $current_cache['end'] ) { + // If not, we can still go ahead and return false if we've already + // checked to the end of the search area. + return false; + } + + $search_start = $start; + if ( $current_cache['end'] > $start ) { + // We haven't checked this far yet, but we can still save work by + // skipping over the part we've already checked. + $search_start = $this->cached_results['cache'][ $start ]['end']; + } + + // Loop through the tokens looking for nonce verification functions. + for ( $i = $search_start; $i < $end; $i++ ) { + // Skip over nested closed scope constructs. + if ( isset( Collections::closedScopes()[ $this->tokens[ $i ]['code'] ] ) + || \T_FN === $this->tokens[ $i ]['code'] + ) { + if ( isset( $this->tokens[ $i ]['scope_closer'] ) ) { + $i = $this->tokens[ $i ]['scope_closer']; + } + continue; + } + + // If this isn't a function name, skip it. + if ( \T_STRING !== $this->tokens[ $i ]['code'] ) { + continue; + } + + // If this is one of the nonce verification functions, we can bail out. + if ( isset( $this->nonceVerificationFunctions[ $this->tokens[ $i ]['content'] ] ) ) { + /* + * Now, make sure it is a call to a global function. + */ + if ( ContextHelper::has_object_operator_before( $this->phpcsFile, $i ) === true ) { + continue; + } + + if ( ContextHelper::is_token_namespaced( $this->phpcsFile, $i ) === true ) { + continue; + } + + $this->set_cache( $cache_keys['file'], $start, $end, $i ); + return true; + } + } + + // We're still here, so no luck. + $this->set_cache( $cache_keys['file'], $start, $end, false ); + + return false; + } + + /** + * Helper function to retrieve results from the cache. + * + * @since 3.0.0 + * + * @param string $filename The name of the current file. + * @param int $start The stack pointer searches started from. + * + * @return array + */ + private function get_cache( $filename, $start ) { + if ( is_array( $this->cached_results ) + && $filename === $this->cached_results['file'] + && isset( $this->cached_results['cache'][ $start ] ) + ) { + return $this->cached_results['cache'][ $start ]; + } + + return array( + 'end' => 0, + 'nonce' => false, + ); + } + + /** + * Helper function to store results to the cache. + * + * @since 3.0.0 + * + * @param string $filename The name of the current file. + * @param int $start The stack pointer searches started from. + * @param int $end The stack pointer searched stopped at. + * @param int|bool $nonce Stack pointer to the nonce verification function call or false if none was found. * * @return void */ - protected function mergeFunctionLists() { - if ( $this->customNonceVerificationFunctions !== $this->addedCustomFunctions['nonce'] ) { - $this->nonceVerificationFunctions = $this->merge_custom_array( - $this->customNonceVerificationFunctions, - $this->nonceVerificationFunctions + private function set_cache( $filename, $start, $end, $nonce ) { + if ( is_array( $this->cached_results ) === false + || $filename !== $this->cached_results['file'] + ) { + $this->cached_results = array( + 'file' => $filename, + 'cache' => array( + $start => array( + 'end' => $end, + 'nonce' => $nonce, + ), + ), ); - - $this->addedCustomFunctions['nonce'] = $this->customNonceVerificationFunctions; + return; } - if ( $this->customSanitizingFunctions !== $this->addedCustomFunctions['sanitize'] ) { - $this->sanitizingFunctions = $this->merge_custom_array( - $this->customSanitizingFunctions, - $this->sanitizingFunctions + // Okay, so we know the current cache is for the current file. Check if we've seen this start pointer before. + if ( isset( $this->cached_results['cache'][ $start ] ) === false ) { + $this->cached_results['cache'][ $start ] = array( + 'end' => $end, + 'nonce' => $nonce, ); + return; + } - $this->addedCustomFunctions['sanitize'] = $this->customSanitizingFunctions; + // Update existing entry. + if ( $end > $this->cached_results['cache'][ $start ]['end'] ) { + $this->cached_results['cache'][ $start ]['end'] = $end; } - if ( $this->customUnslashingSanitizingFunctions !== $this->addedCustomFunctions['unslashsanitize'] ) { - $this->unslashingSanitizingFunctions = $this->merge_custom_array( - $this->customUnslashingSanitizingFunctions, - $this->unslashingSanitizingFunctions + $this->cached_results['cache'][ $start ]['nonce'] = $nonce; + } + + /** + * Merge custom functions provided via a custom ruleset with the defaults, if we haven't already. + * + * @since 0.11.0 Split out from the `process()` method. + * + * @return void + */ + protected function mergeFunctionLists() { + if ( $this->customNonceVerificationFunctions !== $this->addedCustomNonceFunctions ) { + $this->nonceVerificationFunctions = RulesetPropertyHelper::merge_custom_array( + $this->customNonceVerificationFunctions, + $this->nonceVerificationFunctions ); - $this->addedCustomFunctions['unslashsanitize'] = $this->customUnslashingSanitizingFunctions; + $this->addedCustomNonceFunctions = $this->customNonceVerificationFunctions; } } - } diff --git a/WordPress/Sniffs/Security/PluginMenuSlugSniff.php b/WordPress/Sniffs/Security/PluginMenuSlugSniff.php index 31b6447edb..64711da29c 100644 --- a/WordPress/Sniffs/Security/PluginMenuSlugSniff.php +++ b/WordPress/Sniffs/Security/PluginMenuSlugSniff.php @@ -3,27 +3,27 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\Security; +namespace WordPressCS\WordPress\Sniffs\Security; -use WordPress\AbstractFunctionParameterSniff; +use PHPCSUtils\Utils\PassedParameters; +use WordPressCS\WordPress\AbstractFunctionParameterSniff; /** * Warn about __FILE__ for page registration. * - * @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#using-__file__-for-page-registration + * @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#using-__file__-for-page-registration * - * @package WPCS\WordPressCodingStandards - * - * @since 0.3.0 - * @since 0.11.0 Refactored to extend the new WordPress_AbstractFunctionParameterSniff. - * @since 0.13.0 Class name changed: this class is now namespaced. - * @since 1.0.0 This sniff has been moved from the `VIP` category to the `Security` category. + * @since 0.3.0 + * @since 0.11.0 Refactored to extend the new WordPressCS native + * `AbstractFunctionParameterSniff` class. + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 1.0.0 This sniff has been moved from the `VIP` category to the `Security` category. */ -class PluginMenuSlugSniff extends AbstractFunctionParameterSniff { +final class PluginMenuSlugSniff extends AbstractFunctionParameterSniff { /** * The group name for this group of functions. @@ -40,25 +40,61 @@ class PluginMenuSlugSniff extends AbstractFunctionParameterSniff { * @since 0.3.0 * @since 0.11.0 Renamed from $add_menu_functions to $target_functions * and changed visibility to protected. + * @since 3.0.0 The format of the value has changed from a numerically indexed + * array containing parameter positions to an array with the parameter + * position as the index and the parameter name as value. * - * @var array => + * @var array> Key is the name of the functions being targetted. + * Value is an array with parameter positions as the + * keys and parameter names as the values */ protected $target_functions = array( - 'add_menu_page' => array( 4 ), - 'add_object_page' => array( 4 ), - 'add_utility_page' => array( 4 ), - 'add_submenu_page' => array( 1, 5 ), - 'add_dashboard_page' => array( 4 ), - 'add_posts_page' => array( 4 ), - 'add_media_page' => array( 4 ), - 'add_links_page' => array( 4 ), - 'add_pages_page' => array( 4 ), - 'add_comments_page' => array( 4 ), - 'add_theme_page' => array( 4 ), - 'add_plugins_page' => array( 4 ), - 'add_users_page' => array( 4 ), - 'add_management_page' => array( 4 ), - 'add_options_page' => array( 4 ), + 'add_comments_page' => array( + 4 => 'menu_slug', + ), + 'add_dashboard_page' => array( + 4 => 'menu_slug', + ), + 'add_links_page' => array( + 4 => 'menu_slug', + ), + 'add_management_page' => array( + 4 => 'menu_slug', + ), + 'add_media_page' => array( + 4 => 'menu_slug', + ), + 'add_menu_page' => array( + 4 => 'menu_slug', + ), + 'add_object_page' => array( + 4 => 'menu_slug', + ), + 'add_options_page' => array( + 4 => 'menu_slug', + ), + 'add_pages_page' => array( + 4 => 'menu_slug', + ), + 'add_plugins_page' => array( + 4 => 'menu_slug', + ), + 'add_posts_page' => array( + 4 => 'menu_slug', + ), + 'add_submenu_page' => array( + 1 => 'parent_slug', + 5 => 'menu_slug', + ), + 'add_theme_page' => array( + 4 => 'menu_slug', + ), + 'add_users_page' => array( + 4 => 'menu_slug', + ), + 'add_utility_page' => array( + 4 => 'menu_slug', + ), ); /** @@ -67,22 +103,24 @@ class PluginMenuSlugSniff extends AbstractFunctionParameterSniff { * @since 0.11.0 * * @param int $stackPtr The position of the current token in the stack. - * @param array $group_name The name of the group which was matched. - * @param string $matched_content The token content (function name) which was matched. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. * @param array $parameters Array with information about the parameters. * * @return void */ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { - foreach ( $this->target_functions[ $matched_content ] as $position ) { - if ( isset( $parameters[ $position ] ) ) { - $file_constant = $this->phpcsFile->findNext( \T_FILE, $parameters[ $position ]['start'], ( $parameters[ $position ]['end'] + 1 ) ); + foreach ( $this->target_functions[ $matched_content ] as $position => $param_name ) { + $found_param = PassedParameters::getParameterFromStack( $parameters, $position, $param_name ); + if ( false === $found_param ) { + continue; + } - if ( false !== $file_constant ) { - $this->phpcsFile->addWarning( 'Using __FILE__ for menu slugs risks exposing filesystem structure.', $stackPtr, 'Using__FILE__' ); - } + $file_constant = $this->phpcsFile->findNext( \T_FILE, $found_param['start'], ( $found_param['end'] + 1 ) ); + if ( false !== $file_constant ) { + $this->phpcsFile->addWarning( 'Using __FILE__ for menu slugs risks exposing filesystem structure.', $file_constant, 'Using__FILE__' ); } } } - } diff --git a/WordPress/Sniffs/Security/SafeRedirectSniff.php b/WordPress/Sniffs/Security/SafeRedirectSniff.php index d4cebdf567..88a08313a7 100644 --- a/WordPress/Sniffs/Security/SafeRedirectSniff.php +++ b/WordPress/Sniffs/Security/SafeRedirectSniff.php @@ -3,22 +3,20 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\Security; +namespace WordPressCS\WordPress\Sniffs\Security; -use WordPress\AbstractFunctionRestrictionsSniff; +use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff; /** * Encourages use of wp_safe_redirect() to avoid open redirect vulnerabilities. * - * @package WPCS\WordPressCodingStandards - * - * @since 1.0.0 + * @since 1.0.0 */ -class SafeRedirectSniff extends AbstractFunctionRestrictionsSniff { +final class SafeRedirectSniff extends AbstractFunctionRestrictionsSniff { /** * Groups of functions to restrict. @@ -37,12 +35,11 @@ public function getGroups() { return array( 'wp_redirect' => array( 'type' => 'warning', - 'message' => '%s() found. Using wp_safe_redirect(), along with the allowed_redirect_hosts filter if needed, can help avoid any chances of malicious redirects within code. It is also important to remember to call exit() after a redirect so that no other unwanted code is executed.', + 'message' => '%s() found. Using wp_safe_redirect(), along with the "allowed_redirect_hosts" filter if needed, can help avoid any chances of malicious redirects within code. It is also important to remember to call exit() after a redirect so that no other unwanted code is executed.', 'functions' => array( 'wp_redirect', ), ), ); } - } diff --git a/WordPress/Sniffs/Security/ValidatedSanitizedInputSniff.php b/WordPress/Sniffs/Security/ValidatedSanitizedInputSniff.php index 25756ce294..6f46261b56 100644 --- a/WordPress/Sniffs/Security/ValidatedSanitizedInputSniff.php +++ b/WordPress/Sniffs/Security/ValidatedSanitizedInputSniff.php @@ -3,29 +3,41 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\Security; +namespace WordPressCS\WordPress\Sniffs\Security; -use WordPress\Sniff; +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Utils\Context; +use PHPCSUtils\Utils\TextStrings; +use PHPCSUtils\Utils\Variables; +use WordPressCS\WordPress\Helpers\ContextHelper; +use WordPressCS\WordPress\Helpers\SanitizationHelperTrait; +use WordPressCS\WordPress\Helpers\ValidationHelper; +use WordPressCS\WordPress\Helpers\VariableHelper; +use WordPressCS\WordPress\Sniff; /** * Flag any non-validated/sanitized input ( _GET / _POST / etc. ). * - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/issues/69 + * @link https://github.com/WordPress/WordPress-Coding-Standards/issues/69 * - * @package WPCS\WordPressCodingStandards + * @since 0.3.0 + * @since 0.4.0 This class now extends the WordPressCS native `Sniff` class. + * @since 0.5.0 Method getArrayIndexKey() has been moved to the WordPressCS native `Sniff` class. + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 1.0.0 This sniff has been moved from the `VIP` category to the `Security` category. * - * @since 0.3.0 - * @since 0.4.0 This class now extends WordPress_Sniff. - * @since 0.5.0 Method getArrayIndexKey() has been moved to WordPress_Sniff. - * @since 0.13.0 Class name changed: this class is now namespaced. - * @since 1.0.0 This sniff has been moved from the `VIP` category to the `Security` category. + * @uses \WordPressCS\WordPress\Helpers\SanitizationHelperTrait::$customSanitizingFunctions + * @uses \WordPressCS\WordPress\Helpers\SanitizationHelperTrait::$customUnslashingSanitizingFunctions */ class ValidatedSanitizedInputSniff extends Sniff { + use SanitizationHelperTrait; + /** * Check for validation functions for a variable within its own parenthesis only. * @@ -34,37 +46,20 @@ class ValidatedSanitizedInputSniff extends Sniff { public $check_validation_in_scope_only = false; /** - * Custom list of functions that sanitize the values passed to them. - * - * @since 0.5.0 - * - * @var string|string[] - */ - public $customSanitizingFunctions = array(); - - /** - * Custom sanitizing functions that implicitly unslash the values passed to them. - * - * @since 0.5.0 - * - * @var string|string[] - */ - public $customUnslashingSanitizingFunctions = array(); - - /** - * Cache of previously added custom functions. + * Superglobals for which the values will be slashed by WP. * - * Prevents having to do the same merges over and over again. + * @link https://developer.wordpress.org/reference/functions/wp_magic_quotes/ * - * @since 0.5.0 - * @since 0.11.0 - Changed from static to non-static. - * - Changed the format from simple bool to array. + * @since 3.0.0 * - * @var array + * @var array */ - protected $addedCustomFunctions = array( - 'sanitize' => array(), - 'unslashsanitize' => array(), + private $slashed_superglobals = array( + '$_COOKIE' => true, + '$_GET' => true, + '$_POST' => true, + '$_REQUEST' => true, + '$_SERVER' => true, ); /** @@ -89,96 +84,161 @@ public function register() { */ public function process_token( $stackPtr ) { - $superglobals = $this->input_superglobals; - // Handling string interpolation. if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $stackPtr ]['code'] || \T_HEREDOC === $this->tokens[ $stackPtr ]['code'] ) { + // Retrieve all embeds, but use only the initial variable name part. $interpolated_variables = array_map( - function ( $symbol ) { - return '$' . $symbol; + static function ( $embed ) { + return preg_replace( '`^(\{?\$\{?\(?)([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)(.*)$`', '$2', $embed ); }, - $this->get_interpolated_variables( $this->tokens[ $stackPtr ]['content'] ) + TextStrings::getEmbeds( $this->tokens[ $stackPtr ]['content'] ) + ); + + // Filter the embeds down to superglobals only. + $interpolated_superglobals = array_filter( + $interpolated_variables, + static function ( $var_name ) { + return ( 'GLOBALS' !== $var_name && Variables::isSuperglobalName( $var_name ) ); + } ); - foreach ( array_intersect( $interpolated_variables, $superglobals ) as $bad_variable ) { + + foreach ( $interpolated_superglobals as $bad_variable ) { $this->phpcsFile->addError( 'Detected usage of a non-sanitized, non-validated input variable %s: %s', $stackPtr, 'InputNotValidatedNotSanitized', array( $bad_variable, $this->tokens[ $stackPtr ]['content'] ) ); } return; } - // Check if this is a superglobal. - if ( ! \in_array( $this->tokens[ $stackPtr ]['content'], $superglobals, true ) ) { + /* Handle variables */ + + // Check if this is a superglobal we want to examine. + if ( '$GLOBALS' === $this->tokens[ $stackPtr ]['content'] + || Variables::isSuperglobalName( $this->tokens[ $stackPtr ]['content'] ) === false + ) { + return; + } + + // If the variable is being unset, we don't care about it. + if ( Context::inUnset( $this->phpcsFile, $stackPtr ) ) { return; } // If we're overriding a superglobal with an assignment, no need to test. - if ( $this->is_assignment( $stackPtr ) ) { + if ( VariableHelper::is_assignment( $this->phpcsFile, $stackPtr ) ) { return; } // This superglobal is being validated. - if ( $this->is_in_isset_or_empty( $stackPtr ) ) { + if ( ContextHelper::is_in_isset_or_empty( $this->phpcsFile, $stackPtr ) ) { return; } - $array_key = $this->get_array_access_key( $stackPtr ); + $array_keys = VariableHelper::get_array_access_keys( $this->phpcsFile, $stackPtr ); - if ( empty( $array_key ) ) { + if ( empty( $array_keys ) ) { return; } - $error_data = array( $this->tokens[ $stackPtr ]['content'] ); + $error_data = array( $this->tokens[ $stackPtr ]['content'] . '[' . implode( '][', $array_keys ) . ']' ); - // Check for validation first. - if ( ! $this->is_validated( $stackPtr, $array_key, $this->check_validation_in_scope_only ) ) { - $this->phpcsFile->addError( 'Detected usage of a non-validated input variable: %s', $stackPtr, 'InputNotValidated', $error_data ); - // return; // Should we just return and not look for sanitizing functions ? + /* + * Check for validation first. + */ + $validated = false; + + for ( $i = ( $stackPtr + 1 ); $i < $this->phpcsFile->numTokens; $i++ ) { + if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) { + continue; + } + + if ( \T_OPEN_SQUARE_BRACKET === $this->tokens[ $i ]['code'] + && isset( $this->tokens[ $i ]['bracket_closer'] ) + ) { + // Skip over array keys. + $i = $this->tokens[ $i ]['bracket_closer']; + continue; + } + + if ( \T_COALESCE === $this->tokens[ $i ]['code'] ) { + $validated = true; + } + + // Anything else means this is not a validation coalesce. + break; + } + + if ( false === $validated ) { + $validated = ValidationHelper::is_validated( $this->phpcsFile, $stackPtr, $array_keys, $this->check_validation_in_scope_only ); + } + + if ( false === $validated ) { + $this->phpcsFile->addError( + 'Detected usage of a possibly undefined superglobal array index: %s. Use isset() or empty() to check the index exists before using it', + $stackPtr, + 'InputNotValidated', + $error_data + ); } - if ( $this->has_whitelist_comment( 'sanitization', $stackPtr ) ) { + // If this variable is being tested with one of the `is_..()` functions, sanitization isn't needed. + if ( ContextHelper::is_in_type_test( $this->phpcsFile, $stackPtr ) ) { return; } // If this is a comparison ('a' == $_POST['foo']), sanitization isn't needed. - if ( $this->is_comparison( $stackPtr ) ) { + if ( VariableHelper::is_comparison( $this->phpcsFile, $stackPtr, false ) ) { return; } - $this->mergeFunctionLists(); + // If this is a comparison using the array comparison functions, sanitization isn't needed. + if ( ContextHelper::is_in_array_comparison( $this->phpcsFile, $stackPtr ) ) { + return; + } // Now look for sanitizing functions. - if ( ! $this->is_sanitized( $stackPtr, true ) ) { - $this->phpcsFile->addError( 'Detected usage of a non-sanitized input variable: %s', $stackPtr, 'InputNotSanitized', $error_data ); + if ( ! $this->is_sanitized( $this->phpcsFile, $stackPtr, array( $this, 'add_unslash_error' ) ) ) { + $this->phpcsFile->addError( + 'Detected usage of a non-sanitized input variable: %s', + $stackPtr, + 'InputNotSanitized', + $error_data + ); } } /** - * Merge custom functions provided via a custom ruleset with the defaults, if we haven't already. + * Add an error for missing use of unslashing. * - * @since 0.11.0 Split out from the `process()` method. + * @since 0.5.0 + * @since 3.0.0 - Moved from the `Sniff` class to this class. + * - The `$phpcsFile` parameter was added. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The index of the token in the stack + * which is missing unslashing. * * @return void */ - protected function mergeFunctionLists() { - if ( $this->customSanitizingFunctions !== $this->addedCustomFunctions['sanitize'] ) { - $this->sanitizingFunctions = $this->merge_custom_array( - $this->customSanitizingFunctions, - $this->sanitizingFunctions - ); + public function add_unslash_error( File $phpcsFile, $stackPtr ) { + $tokens = $phpcsFile->getTokens(); + $var_name = $tokens[ $stackPtr ]['content']; - $this->addedCustomFunctions['sanitize'] = $this->customSanitizingFunctions; + if ( isset( $this->slashed_superglobals[ $var_name ] ) === false ) { + // WP doesn't slash these, so they don't need unslashing. + return; } - if ( $this->customUnslashingSanitizingFunctions !== $this->addedCustomFunctions['unslashsanitize'] ) { - $this->unslashingSanitizingFunctions = $this->merge_custom_array( - $this->customUnslashingSanitizingFunctions, - $this->unslashingSanitizingFunctions - ); + // We know there will be array keys as that's checked in the process_token() method. + $array_keys = VariableHelper::get_array_access_keys( $phpcsFile, $stackPtr ); + $error_data = array( $var_name . '[' . implode( '][', $array_keys ) . ']' ); - $this->addedCustomFunctions['unslashsanitize'] = $this->customUnslashingSanitizingFunctions; - } + $phpcsFile->addError( + '%s not unslashed before sanitization. Use wp_unslash() or similar', + $stackPtr, + 'MissingUnslash', + $error_data + ); } - } diff --git a/WordPress/Sniffs/Utils/I18nTextDomainFixerSniff.php b/WordPress/Sniffs/Utils/I18nTextDomainFixerSniff.php index daf9c4a8d6..d87f338dd0 100644 --- a/WordPress/Sniffs/Utils/I18nTextDomainFixerSniff.php +++ b/WordPress/Sniffs/Utils/I18nTextDomainFixerSniff.php @@ -3,14 +3,19 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\Utils; +namespace WordPressCS\WordPress\Sniffs\Utils; -use WordPress\AbstractFunctionParameterSniff; -use PHP_CodeSniffer_Tokens as Tokens; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\BackCompat\Helper; +use PHPCSUtils\Utils\GetTokensAsString; +use PHPCSUtils\Utils\PassedParameters; +use PHPCSUtils\Utils\TextStrings; +use WordPressCS\WordPress\AbstractFunctionParameterSniff; +use WordPressCS\WordPress\Helpers\RulesetPropertyHelper; /** * Comprehensive I18n text domain fixer tool. @@ -21,11 +26,9 @@ * * Note: Without a user-defined configuration in a custom ruleset, this sniff will be ignored. * - * @package WPCS\WordPressCodingStandards - * - * @since 1.2.0 + * @since 1.2.0 */ -class I18nTextDomainFixerSniff extends AbstractFunctionParameterSniff { +final class I18nTextDomainFixerSniff extends AbstractFunctionParameterSniff { /** * A list of tokenizers this sniff supports. @@ -44,7 +47,7 @@ class I18nTextDomainFixerSniff extends AbstractFunctionParameterSniff { * * @since 1.2.0 * - * @var string[]|string + * @var string[] */ public $old_text_domain; @@ -70,50 +73,159 @@ class I18nTextDomainFixerSniff extends AbstractFunctionParameterSniff { * The WP Internationalization related functions to target for the replacements. * * @since 1.2.0 + * @since 3.0.0 The format of the value has changed from a numerically indexed + * array containing parameter positions to an array with the parameter + * position as the index and the parameter name as value. * - * @var array => + * @var array> Function name as key, array with target + * parameter and name as value. */ protected $target_functions = array( - 'load_textdomain' => 1, - 'load_plugin_textdomain' => 1, - 'load_muplugin_textdomain' => 1, - 'load_theme_textdomain' => 1, - 'load_child_theme_textdomain' => 1, - 'unload_textdomain' => 1, - - '__' => 2, - '_e' => 2, - '_x' => 3, - '_ex' => 3, - '_n' => 4, - '_nx' => 5, - '_n_noop' => 3, - '_nx_noop' => 4, - 'translate_nooped_plural' => 3, - '_c' => 2, // Deprecated. - '_nc' => 4, // Deprecated. - '__ngettext' => 4, // Deprecated. - '__ngettext_noop' => 3, // Deprecated. - 'translate_with_context' => 2, // Deprecated. - - 'esc_html__' => 2, - 'esc_html_e' => 2, - 'esc_html_x' => 3, - 'esc_attr__' => 2, - 'esc_attr_e' => 2, - 'esc_attr_x' => 3, - - 'is_textdomain_loaded' => 1, - 'get_translations_for_domain' => 1, + 'load_textdomain' => array( + 'position' => 1, + 'name' => 'domain', + ), + 'load_plugin_textdomain' => array( + 'position' => 1, + 'name' => 'domain', + ), + 'load_muplugin_textdomain' => array( + 'position' => 1, + 'name' => 'domain', + ), + 'load_theme_textdomain' => array( + 'position' => 1, + 'name' => 'domain', + ), + 'load_child_theme_textdomain' => array( + 'position' => 1, + 'name' => 'domain', + ), + 'load_script_textdomain' => array( + 'position' => 2, + 'name' => 'domain', + ), + 'unload_textdomain' => array( + 'position' => 1, + 'name' => 'domain', + ), + + '__' => array( + 'position' => 2, + 'name' => 'domain', + ), + '_e' => array( + 'position' => 2, + 'name' => 'domain', + ), + '_x' => array( + 'position' => 3, + 'name' => 'domain', + ), + '_ex' => array( + 'position' => 3, + 'name' => 'domain', + ), + '_n' => array( + 'position' => 4, + 'name' => 'domain', + ), + '_nx' => array( + 'position' => 5, + 'name' => 'domain', + ), + '_n_noop' => array( + 'position' => 3, + 'name' => 'domain', + ), + '_nx_noop' => array( + 'position' => 4, + 'name' => 'domain', + ), + 'translate_nooped_plural' => array( + 'position' => 3, + 'name' => 'domain', + ), + + 'esc_html__' => array( + 'position' => 2, + 'name' => 'domain', + ), + 'esc_html_e' => array( + 'position' => 2, + 'name' => 'domain', + ), + 'esc_html_x' => array( + 'position' => 3, + 'name' => 'domain', + ), + 'esc_attr__' => array( + 'position' => 2, + 'name' => 'domain', + ), + 'esc_attr_e' => array( + 'position' => 2, + 'name' => 'domain', + ), + 'esc_attr_x' => array( + 'position' => 3, + 'name' => 'domain', + ), + + 'is_textdomain_loaded' => array( + 'position' => 1, + 'name' => 'domain', + ), + 'get_translations_for_domain' => array( + 'position' => 1, + 'name' => 'domain', + ), + + // Deprecated functions. + '_c' => array( + 'position' => 2, + 'name' => 'domain', + ), + '_nc' => array( + 'position' => 4, + 'name' => 'domain', + ), + '__ngettext' => array( + 'position' => 4, + 'name' => 'domain', + ), + '__ngettext_noop' => array( + 'position' => 3, + 'name' => 'domain', + ), + 'translate_with_context' => array( + 'position' => 2, + 'name' => 'domain', + ), // Shouldn't be used by plugins/themes. - 'translate' => 2, - 'translate_with_gettext_context' => 3, + 'translate' => array( + 'position' => 2, + 'name' => 'domain', + ), + 'translate_with_gettext_context' => array( + 'position' => 3, + 'name' => 'domain', + ), // WP private functions. Shouldn't be used by plugins/themes. - '_load_textdomain_just_in_time' => 1, - '_get_path_to_translation_from_lang_dir' => 1, - '_get_path_to_translation' => 1, + '_load_textdomain_just_in_time' => array( + 'position' => 1, + 'name' => 'domain', + ), + '_get_path_to_translation_from_lang_dir' => array( + 'position' => 1, + 'name' => 'domain', + ), + '_get_path_to_translation' => array( + 'position' => 1, + 'name' => 'domain', + ), ); /** @@ -121,7 +233,7 @@ class I18nTextDomainFixerSniff extends AbstractFunctionParameterSniff { * * @since 1.2.0 * - * @var string + * @var bool */ private $is_valid = false; @@ -139,7 +251,7 @@ class I18nTextDomainFixerSniff extends AbstractFunctionParameterSniff { * * @since 1.2.0 * - * @var string + * @var bool */ private $header_found = false; @@ -150,45 +262,51 @@ class I18nTextDomainFixerSniff extends AbstractFunctionParameterSniff { * * @since 1.2.0 * - * @var array Array key is the header name, the value indicated whether it is a - * required (true) or optional (false) header. + * @var array Array key is the header name, the value indicated whether it is a + * required (true) or optional (false) header. */ private $theme_headers = array( - 'Theme Name' => true, - 'Theme URI' => false, - 'Author' => true, - 'Author URI' => false, - 'Description' => true, - 'Version' => true, - 'License' => true, - 'License URI' => true, - 'Tags' => false, - 'Text Domain' => true, - 'Domain Path' => false, + 'Theme Name' => true, + 'Theme URI' => false, + 'Author' => true, + 'Author URI' => false, + 'Description' => true, + 'Version' => true, + 'Requires at least' => true, + 'Tested up to' => true, + 'Requires PHP' => true, + 'License' => true, + 'License URI' => true, + 'Text Domain' => true, + 'Tags' => false, + 'Domain Path' => false, ); /** * Possible headers for a plugin. * - * @link https://developer.wordpress.org/plugins/the-basics/header-requirements/ + * @link https://developer.wordpress.org/plugins/plugin-basics/header-requirements/ * * @since 1.2.0 * - * @var array Array key is the header name, the value indicated whether it is a - * required (true) or optional (false) header. + * @var array Array key is the header name, the value indicated whether it is a + * required (true) or optional (false) header. */ private $plugin_headers = array( - 'Plugin Name' => true, - 'Plugin URI' => false, - 'Description' => false, - 'Version' => false, - 'Author' => false, - 'Author URI' => false, - 'License' => false, - 'License URI' => false, - 'Text Domain' => false, - 'Domain Path' => false, - 'Network' => false, + 'Plugin Name' => true, + 'Plugin URI' => false, + 'Description' => false, + 'Version' => false, + 'Requires at least' => false, + 'Requires PHP' => false, + 'Author' => false, + 'Author URI' => false, + 'License' => false, + 'License URI' => false, + 'Text Domain' => false, + 'Domain Path' => false, + 'Network' => false, + 'Update URI' => false, ); /** @@ -227,7 +345,7 @@ class I18nTextDomainFixerSniff extends AbstractFunctionParameterSniff { * * @since 1.2.0 * - * @var integer + * @var int */ private $tab_width = null; @@ -272,12 +390,6 @@ public function register() { * normal file processing. */ public function process_token( $stackPtr ) { - if ( \T_COMMENT === $this->tokens[ $stackPtr ]['code'] - && strpos( $this->tokens[ $stackPtr ]['content'], '@codingStandardsChangeSetting' ) !== false - ) { - return; - } - // Check if the old/new properties are correctly set. If not, bow out. if ( ! is_string( $this->new_text_domain ) || '' === $this->new_text_domain @@ -286,7 +398,7 @@ public function process_token( $stackPtr ) { } if ( isset( $this->old_text_domain ) ) { - $this->old_text_domain = $this->merge_custom_array( $this->old_text_domain, array(), false ); + $this->old_text_domain = RulesetPropertyHelper::merge_custom_array( $this->old_text_domain, array(), false ); if ( ! is_array( $this->old_text_domain ) || array() === $this->old_text_domain @@ -331,14 +443,7 @@ public function process_token( $stackPtr ) { } if ( isset( $this->tab_width ) === false ) { - if ( isset( $this->phpcsFile->config->tabWidth ) === false - || 0 === $this->phpcsFile->config->tabWidth - ) { - // We have no idea how wide tabs are, so assume 4 spaces for fixing. - $this->tab_width = 4; - } else { - $this->tab_width = $this->phpcsFile->config->tabWidth; - } + $this->tab_width = Helper::getTabWidth( $this->phpcsFile ); } if ( \T_DOC_COMMENT_OPEN_TAG === $this->tokens[ $stackPtr ]['code'] @@ -347,7 +452,7 @@ public function process_token( $stackPtr ) { // Examine for plugin/theme file header. return $this->process_comments( $stackPtr ); - } elseif ( 'CSS' !== $this->phpcsFile->tokenizerType ) { + } elseif ( isset( $this->phpcsFile->tokenizerType ) === false || 'CSS' !== $this->phpcsFile->tokenizerType ) { // Examine a T_STRING token in a PHP file as a function call. return parent::process_token( $stackPtr ); } @@ -360,25 +465,35 @@ public function process_token( $stackPtr ) { * @since 1.2.0 * * @param int $stackPtr The position of the current token in the stack. - * @param array $group_name The name of the group which was matched. - * @param string $matched_content The token content (function name) which was matched. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. * @param array $parameters Array with information about the parameters. * * @return void */ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { - $target_param = $this->target_functions[ $matched_content ]; + $target_param_specs = $this->target_functions[ $matched_content ]; + $found_param = PassedParameters::getParameterFromStack( $parameters, $target_param_specs['position'], $target_param_specs['name'] ); - if ( isset( $parameters[ $target_param ] ) === false && 1 !== $target_param ) { + if ( false === $found_param && 1 !== $target_param_specs['position'] ) { $error_msg = 'Missing $domain arg'; $error_code = 'MissingArgDomain'; - if ( isset( $parameters[ ( $target_param - 1 ) ] ) ) { + $has_named_params = false; + foreach ( $parameters as $param ) { + if ( isset( $param['name'] ) ) { + $has_named_params = true; + break; + } + } + + if ( false === $has_named_params && isset( $parameters[ ( $target_param_specs['position'] - 1 ) ] ) ) { $fix = $this->phpcsFile->addFixableError( $error_msg, $stackPtr, $error_code ); if ( true === $fix ) { - $start_previous = $parameters[ ( $target_param - 1 ) ]['start']; - $end_previous = $parameters[ ( $target_param - 1 ) ]['end']; + $start_previous = $parameters[ ( $target_param_specs['position'] - 1 ) ]['start']; + $end_previous = $parameters[ ( $target_param_specs['position'] - 1 ) ]['end']; if ( \T_WHITESPACE === $this->tokens[ $start_previous ]['code'] && $this->tokens[ $start_previous ]['content'] === $this->phpcsFile->eolChar ) { @@ -407,6 +522,16 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p $this->phpcsFile->fixer->addContent( $end_previous, $replacement ); } } + } elseif ( true === $has_named_params ) { + /* + * Function call using named arguments. For now, we will not auto-fix this. + * + * {@internal If we don't bother with indentation and such, this can be made + * auto-fixable by getting the 'end' of the last seen parameter and adding the + * domain parameter, with the 'domain: ' parameter label, after the last + * seen parameter.} + */ + $this->phpcsFile->addError( $error_msg, $stackPtr, $error_code ); } else { $error_msg .= ' and preceding argument(s)'; $error_code = 'MissingArgs'; @@ -419,8 +544,8 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p } // Target parameter found. Let's examine it. - $domain_param_start = $parameters[ $target_param ]['start']; - $domain_param_end = $parameters[ $target_param ]['end']; + $domain_param_start = $found_param['start']; + $domain_param_end = $found_param['end']; $domain_token = null; for ( $i = $domain_param_start; $i <= $domain_param_end; $i++ ) { @@ -442,7 +567,7 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p } // If we're still here, this means only one T_CONSTANT_ENCAPSED_STRING was found. - $old_domain = $this->strip_quotes( $this->tokens[ $domain_token ]['content'] ); + $old_domain = TextStrings::stripQuotes( $this->tokens[ $domain_token ]['content'] ); if ( ! \in_array( $old_domain, $this->old_text_domain, true ) ) { // Not a text domain targetted for replacement, ignore. @@ -468,16 +593,16 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p * @since 1.2.0 * * @param int $stackPtr The position of the current token in the stack. - * @param array $group_name The name of the group which was matched. - * @param string $matched_content The token content (function name) which was matched. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. * * @return void */ public function process_no_parameters( $stackPtr, $group_name, $matched_content ) { - $target_param = $this->target_functions[ $matched_content ]; - if ( 1 !== $target_param ) { + if ( 1 !== $target_param['position'] ) { // Only process the no param case as fixable if the text domain is expected to be the first parameter. $this->phpcsFile->addWarning( 'Missing $domain arg and preceding argument(s)', $stackPtr, 'MissingArgs' ); return; @@ -538,7 +663,8 @@ public function process_no_parameters( $stackPtr, $group_name, $matched_content * * @param int $stackPtr The position of the current token in the stack. * - * @return void + * @return int|void Integer stack pointer to skip forward or void to continue + * normal file processing. */ public function process_comments( $stackPtr ) { if ( true === $this->header_found && ! defined( 'PHP_CODESNIFFER_IN_TESTS' ) ) { @@ -550,13 +676,13 @@ public function process_comments( $stackPtr ) { $type = 'plugin'; $skip_to = $stackPtr; - $file = $this->strip_quotes( $this->phpcsFile->getFileName() ); + $file = TextStrings::stripQuotes( $this->phpcsFile->getFileName() ); if ( 'STDIN' === $file ) { return; } $file_name = basename( $file ); - if ( 'CSS' === $this->phpcsFile->tokenizerType ) { + if ( isset( $this->phpcsFile->tokenizerType ) && 'CSS' === $this->phpcsFile->tokenizerType ) { if ( 'style.css' !== $file_name && ! defined( 'PHP_CODESNIFFER_IN_TESTS' ) ) { // CSS files only need to be examined for the file header. return ( $this->phpcsFile->numTokens + 1 ); @@ -690,7 +816,7 @@ public function process_comments( $stackPtr ) { } $replacement = $this->phpcsFile->eolChar - . $this->phpcsFile->getTokensAsString( $i, ( $last_header_ptr - $i ), true ) + . GetTokensAsString::origContent( $this->phpcsFile, $i, ( $last_header_ptr - 1 ) ) . $replacement; } diff --git a/WordPress/Sniffs/VIP/AdminBarRemovalSniff.php b/WordPress/Sniffs/VIP/AdminBarRemovalSniff.php deleted file mode 100644 index ba8208c601..0000000000 --- a/WordPress/Sniffs/VIP/AdminBarRemovalSniff.php +++ /dev/null @@ -1,465 +0,0 @@ - false, - 'FoundPropertyForDeprecatedSniff' => false, - ); - - /** - * A list of tokenizers this sniff supports. - * - * @since 0.11.0 - * - * @var array - */ - public $supportedTokenizers = array( 'PHP', 'CSS' ); - - /** - * Whether or not the sniff only checks for removal of the admin bar - * or any manipulation to the visibility of the admin bar. - * - * Defaults to true: only check for removal of the admin bar. - * Set to false to check for any form of manipulation of the visibility - * of the admin bar. - * - * @since 0.11.0 - * - * @var bool - */ - public $remove_only = true; - - /** - * Functions this sniff is looking for. - * - * @since 0.11.0 - * - * @var array - */ - protected $target_functions = array( - 'show_admin_bar' => true, - 'add_filter' => true, - ); - - /** - * CSS properties this sniff is looking for. - * - * @since 0.11.0 - * - * @var array - */ - protected $target_css_properties = array( - 'visibility' => array( - 'type' => '!=', - 'value' => 'hidden', - ), - 'display' => array( - 'type' => '!=', - 'value' => 'none', - ), - 'opacity' => array( - 'type' => '>', - 'value' => 0.3, - ), - ); - - /** - * CSS selectors this sniff is looking for. - * - * @since 0.11.0 - * - * @var array - */ - protected $target_css_selectors = array( - '.show-admin-bar', - '#wpadminbar', - ); - - /** - * String tokens within PHP files we want to deal with. - * - * Set from the register() method. - * - * @since 0.11.0 - * - * @var array - */ - private $string_tokens = array(); - - /** - * Regex template for use with the CSS selectors in combination with PHP text strings. - * - * @since 0.11.0 - * - * @var string - */ - private $target_css_selectors_regex = '`(?:%s).*?\{(.*)$`'; - - /** - * Property to keep track of whether a ' ) ) { - // Make sure we check any content on this line before the closing style tag. - $this->in_style[ $file_name ] = false; - $content = trim( substr( $content, 0, strpos( $content, '' ) ) ); - } - } elseif ( true === $this->has_html_open_tag( 'style', $stackPtr, $content ) ) { - // Ok, found a ' ) ) { - // Make sure we check any content on this line after the opening style tag. - $this->in_style[ $file_name ] = true; - $content = trim( substr( $content, ( strpos( $content, '' ); - $content = trim( substr( $content, $start, ( $end - $start ) ) ); - unset( $start, $end ); - } - } else { - return; - } - - // Are we in one of the target selectors ? - if ( true === $this->in_target_selector[ $file_name ] ) { - if ( false !== strpos( $content, '}' ) ) { - // Make sure we check any content on this line before the selector closing brace. - $this->in_target_selector[ $file_name ] = false; - $content = trim( substr( $content, 0, strpos( $content, '}' ) ) ); - } - } elseif ( preg_match( $this->target_css_selectors_regex, $content, $matches ) > 0 ) { - // Ok, found a new target selector. - $content = ''; - - if ( isset( $matches[1] ) && '' !== $matches[1] ) { - if ( false === strpos( $matches[1], '}' ) ) { - // Make sure we check any content on this line before the closing brace. - $this->in_target_selector[ $file_name ] = true; - $content = trim( $matches[1] ); - } else { - // Ok, we have the selector open and close brace on the same line. - $content = trim( substr( $matches[1], 0, strpos( $matches[1], '}' ) ) ); - } - } else { - $this->in_target_selector[ $file_name ] = true; - } - } else { - return; - } - unset( $matches ); - - // Now let's do the check for the CSS properties. - if ( ! empty( $content ) ) { - foreach ( $this->target_css_properties as $property => $requirements ) { - if ( false !== strpos( $content, $property ) ) { - $error = true; - - if ( true === $this->remove_only ) { - // Check the value of the CSS property. - if ( preg_match( '`' . preg_quote( $property, '`' ) . '\s*:\s*(.+?)\s*(?:!important)?;`', $content, $matches ) > 0 ) { - $value = trim( $matches[1] ); - $valid = $this->validate_css_property_value( $value, $requirements['type'], $requirements['value'] ); - if ( true === $valid ) { - $error = false; - } - } - } - - if ( true === $error ) { - $this->phpcsFile->addError( 'Hiding of the admin bar is not allowed.', $stackPtr, 'HidingDetected' ); - } - } - } - } - } - - /** - * Processes this test for T_STYLE tokens in CSS files. - * - * @since 0.11.0 - * - * @param int $stackPtr The position of the current token in the stack passed in $tokens. - * - * @return void - */ - protected function process_css_style( $stackPtr ) { - if ( ! isset( $this->target_css_properties[ $this->tokens[ $stackPtr ]['content'] ] ) ) { - // Not one of the CSS properties we're interested in. - return; - } - - $css_property = $this->target_css_properties[ $this->tokens[ $stackPtr ]['content'] ]; - - // Check if the CSS selector matches. - $opener = $this->phpcsFile->findPrevious( \T_OPEN_CURLY_BRACKET, $stackPtr ); - if ( false !== $opener ) { - for ( $i = ( $opener - 1 ); $i >= 0; $i-- ) { - if ( isset( Tokens::$commentTokens[ $this->tokens[ $i ]['code'] ] ) - || \T_CLOSE_CURLY_BRACKET === $this->tokens[ $i ]['code'] - ) { - break; - } - } - $start = ( $i + 1 ); - $selector = trim( $this->phpcsFile->getTokensAsString( $start, ( $opener - $start ) ) ); - unset( $i ); - - foreach ( $this->target_css_selectors as $target_selector ) { - if ( false !== strpos( $selector, $target_selector ) ) { - $error = true; - - if ( true === $this->remove_only ) { - // Check the value of the CSS property. - $valuePtr = $this->phpcsFile->findNext( array( \T_COLON, \T_WHITESPACE ), ( $stackPtr + 1 ), null, true ); - $value = $this->tokens[ $valuePtr ]['content']; - $valid = $this->validate_css_property_value( $value, $css_property['type'], $css_property['value'] ); - if ( true === $valid ) { - $error = false; - } - } - - if ( true === $error ) { - $this->phpcsFile->addError( 'Hiding of the admin bar is not allowed.', $stackPtr, 'HidingDetected' ); - } - } - } - } - } - - /** - * Verify if a CSS property value complies with an expected value. - * - * {@internal This is a method stub, doing only what is needed for this sniff. - * If at some point in the future other sniff would need similar functionality, - * this method should be moved to the WordPress_Sniff class and expanded to cover - * all types of comparisons.}} - * - * @since 0.11.0 - * - * @param mixed $value The value of CSS property. - * @param string $compare_type The type of comparison to use for the validation. - * @param string $compare_value The value to compare against. - * - * @return bool True if the property value complies, false otherwise. - */ - protected function validate_css_property_value( $value, $compare_type, $compare_value ) { - switch ( $compare_type ) { - case '!=': - return $value !== $compare_value; - - case '>': - return $value > $compare_value; - - default: - return false; - } - } - -} diff --git a/WordPress/Sniffs/VIP/CronIntervalSniff.php b/WordPress/Sniffs/VIP/CronIntervalSniff.php deleted file mode 100644 index 0cf91d9486..0000000000 --- a/WordPress/Sniffs/VIP/CronIntervalSniff.php +++ /dev/null @@ -1,89 +0,0 @@ - false, - 'FoundPropertyForDeprecatedSniff' => false, - ); - - /** - * Don't use. - * - * @deprecated 1.0.0 - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return void|int - */ - public function process_token( $stackPtr ) { - if ( false === $this->thrown['DeprecatedSniff'] ) { - $this->thrown['DeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.CronInterval" sniff has been renamed to "WordPress.WP.CronInterval". Please update your custom ruleset.', - 0, - 'DeprecatedSniff' - ); - } - - if ( 900 !== (int) $this->min_interval - && false === $this->thrown['FoundPropertyForDeprecatedSniff'] - ) { - $this->thrown['FoundPropertyForDeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.CronInterval" sniff has been renamed to "WordPress.WP.CronInterval". Please update your custom ruleset.', - 0, - 'FoundPropertyForDeprecatedSniff' - ); - } - - return parent::process_token( $stackPtr ); - } - -} diff --git a/WordPress/Sniffs/VIP/DirectDatabaseQuerySniff.php b/WordPress/Sniffs/VIP/DirectDatabaseQuerySniff.php deleted file mode 100644 index a1abd4a683..0000000000 --- a/WordPress/Sniffs/VIP/DirectDatabaseQuerySniff.php +++ /dev/null @@ -1,78 +0,0 @@ - false, - 'FoundPropertyForDeprecatedSniff' => false, - ); - - /** - * Don't use. - * - * @deprecated 1.0.0 - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return void|int - */ - public function process_token( $stackPtr ) { - if ( false === $this->thrown['DeprecatedSniff'] ) { - $this->thrown['DeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.DirectDatabaseQuery" sniff has been renamed to "WordPress.DB.DirectDatabaseQuery". Please update your custom ruleset.', - 0, - 'DeprecatedSniff' - ); - } - - if ( false === $this->thrown['FoundPropertyForDeprecatedSniff'] - && ( ( array() !== $this->customCacheGetFunctions && $this->customCacheGetFunctions !== $this->addedCustomFunctions['cacheget'] ) - || ( array() !== $this->customCacheSetFunctions && $this->customCacheSetFunctions !== $this->addedCustomFunctions['cacheset'] ) - || ( array() !== $this->customCacheDeleteFunctions && $this->customCacheDeleteFunctions !== $this->addedCustomFunctions['cachedelete'] ) ) - ) { - $this->thrown['FoundPropertyForDeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.DirectDatabaseQuery" sniff has been renamed to "WordPress.DB.DirectDatabaseQuery". Please update your custom ruleset.', - 0, - 'FoundPropertyForDeprecatedSniff' - ); - } - - return parent::process_token( $stackPtr ); - } - -} diff --git a/WordPress/Sniffs/VIP/FileSystemWritesDisallowSniff.php b/WordPress/Sniffs/VIP/FileSystemWritesDisallowSniff.php deleted file mode 100644 index d95eea636b..0000000000 --- a/WordPress/Sniffs/VIP/FileSystemWritesDisallowSniff.php +++ /dev/null @@ -1,152 +0,0 @@ - false, - 'FoundPropertyForDeprecatedSniff' => false, - ); - - /** - * Groups of functions to restrict. - * - * Example: groups => array( - * 'lambda' => array( - * 'type' => 'error' | 'warning', - * 'message' => 'Use anonymous functions instead please!', - * 'functions' => array( 'file_get_contents', 'create_function' ), - * ) - * ) - * - * @return array - */ - public function getGroups() { - $groups = array( - 'file_ops' => array( - 'type' => 'error', - 'message' => 'Filesystem writes are forbidden, you should not be using %s()', - 'functions' => array( - 'delete', - 'file_put_contents', - 'flock', - 'fputcsv', - 'fputs', - 'fwrite', - 'ftruncate', - 'is_writable', - 'is_writeable', - 'link', - 'rename', - 'symlink', - 'tempnam', - 'touch', - 'unlink', - ), - ), - 'directory' => array( - 'type' => 'error', - 'message' => 'Filesystem writes are forbidden, you should not be using %s()', - 'functions' => array( - 'mkdir', - 'rmdir', - ), - ), - 'chmod' => array( - 'type' => 'error', - 'message' => 'Filesystem writes are forbidden, you should not be using %s()', - 'functions' => array( - 'chgrp', - 'chown', - 'chmod', - 'lchgrp', - 'lchown', - ), - ), - ); - - /* - * Maintain old behaviour - allow for changing the error type from the ruleset - * using the `error` property. - */ - if ( false === $this->error ) { - foreach ( $groups as $group_name => $details ) { - $groups[ $group_name ]['type'] = 'warning'; - } - } - - return $groups; - } - - /** - * Process the token and handle the deprecation notices. - * - * @since 1.0.0 Added to allow for throwing the deprecation notices. - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return void|int - */ - public function process_token( $stackPtr ) { - if ( false === $this->thrown['DeprecatedSniff'] ) { - $this->thrown['DeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.FileSystemWritesDisallow" sniff has been deprecated. Please update your custom ruleset.', - 0, - 'DeprecatedSniff' - ); - } - - if ( ( ! empty( $this->exclude ) || true !== $this->error ) - && false === $this->thrown['FoundPropertyForDeprecatedSniff'] - ) { - $this->thrown['FoundPropertyForDeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.FileSystemWritesDisallow" sniff has been deprecated. Please update your custom ruleset.', - 0, - 'FoundPropertyForDeprecatedSniff' - ); - } - - return parent::process_token( $stackPtr ); - } - -} diff --git a/WordPress/Sniffs/VIP/OrderByRandSniff.php b/WordPress/Sniffs/VIP/OrderByRandSniff.php deleted file mode 100644 index 5e33df9122..0000000000 --- a/WordPress/Sniffs/VIP/OrderByRandSniff.php +++ /dev/null @@ -1,107 +0,0 @@ - rand. - * - * @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#order-by-rand - * - * @package WPCS\WordPressCodingStandards - * - * @since 0.9.0 - * @since 0.13.0 Class name changed: this class is now namespaced. - * - * @deprecated 1.0.0 This sniff has been deprecated. - * This file remains for now to prevent BC breaks. - */ -class OrderByRandSniff extends AbstractArrayAssignmentRestrictionsSniff { - - /** - * Keep track of whether the warnings have been thrown to prevent - * the messages being thrown for every token triggering the sniff. - * - * @since 1.0.0 - * - * @var array - */ - private $thrown = array( - 'DeprecatedSniff' => false, - 'FoundPropertyForDeprecatedSniff' => false, - ); - - /** - * Groups of variables to restrict. - * - * @return array - */ - public function getGroups() { - return array( - 'orderby' => array( - 'type' => 'error', - 'keys' => array( - 'orderby', - ), - ), - ); - } - - /** - * Process the token and handle the deprecation notices. - * - * @since 1.0.0 Added to allow for throwing the deprecation notices. - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return void|int - */ - public function process_token( $stackPtr ) { - if ( false === $this->thrown['DeprecatedSniff'] ) { - $this->thrown['DeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.OrderByRand" sniff has been deprecated. Please update your custom ruleset.', - 0, - 'DeprecatedSniff' - ); - } - - if ( ! empty( $this->exclude ) - && false === $this->thrown['FoundPropertyForDeprecatedSniff'] - ) { - $this->thrown['FoundPropertyForDeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.OrderByRand" sniff has been deprecated. Please update your custom ruleset.', - 0, - 'FoundPropertyForDeprecatedSniff' - ); - } - - return parent::process_token( $stackPtr ); - } - - /** - * Callback to process each confirmed key, to check value - * This must be extended to add the logic to check assignment value - * - * @param string $key Array index / key. - * @param mixed $val Assigned value. - * @param int $line Token line. - * @param array $group Group definition. - * @return mixed FALSE if no match, TRUE if matches, STRING if matches with custom error message passed to ->process(). - */ - public function callback( $key, $val, $line, $group ) { - if ( 'rand' === strtolower( $val ) ) { - return 'Detected forbidden query_var "%s" of "%s". Use vip_get_random_posts() instead.'; - } else { - return false; - } - } - -} diff --git a/WordPress/Sniffs/VIP/PluginMenuSlugSniff.php b/WordPress/Sniffs/VIP/PluginMenuSlugSniff.php deleted file mode 100644 index 072da87462..0000000000 --- a/WordPress/Sniffs/VIP/PluginMenuSlugSniff.php +++ /dev/null @@ -1,65 +0,0 @@ - false, - ); - - /** - * Don't use. - * - * @deprecated 1.0.0 - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return void|int - */ - public function process_token( $stackPtr ) { - if ( false === $this->thrown['DeprecatedSniff'] ) { - $this->phpcsFile->addWarning( - 'The "WordPress.VIP.PluginMenuSlug" sniff has been renamed to "WordPress.Security.PluginMenuSlug". Please update your custom ruleset.', - 0, - 'DeprecatedSniff' - ); - - $this->thrown['DeprecatedSniff'] = true; - } - - return parent::process_token( $stackPtr ); - } - -} diff --git a/WordPress/Sniffs/VIP/PostsPerPageSniff.php b/WordPress/Sniffs/VIP/PostsPerPageSniff.php deleted file mode 100644 index 73588a8f13..0000000000 --- a/WordPress/Sniffs/VIP/PostsPerPageSniff.php +++ /dev/null @@ -1,105 +0,0 @@ - false, - ); - - /** - * Groups of variables to restrict. - * - * @return array - */ - public function getGroups() { - return array( - 'posts_per_page' => array( - 'type' => 'error', - 'keys' => array( - 'posts_per_page', - 'nopaging', - 'numberposts', - ), - ), - ); - } - - /** - * Callback to process each confirmed key, to check value. - * - * @param string $key Array index / key. - * @param mixed $val Assigned value. - * @param int $line Token line. - * @param array $group Group definition. - * @return mixed FALSE if no match, TRUE if matches, STRING if matches - * with custom error message passed to ->process(). - */ - public function callback( $key, $val, $line, $group ) { - if ( 100 !== (int) $this->posts_per_page - && false === $this->thrown['FoundDeprecatedProperty'] - ) { - $this->phpcsFile->addWarning( - 'The "posts_per_page" property for the "WordPress.VIP.PostsPerPage" sniff is deprecated. The detection of high pagination limits has been moved to the "WordPress.WP.PostsPerPage" sniff. Please update your custom ruleset.', - 0, - 'FoundDeprecatedProperty' - ); - - $this->thrown['FoundDeprecatedProperty'] = true; - } - - $key = strtolower( $key ); - - if ( ( 'nopaging' === $key && ( 'true' === $val || 1 === $val ) ) - || ( \in_array( $key, array( 'numberposts', 'posts_per_page' ), true ) && '-1' === $val ) - ) { - return 'Disabling pagination is prohibited in VIP context, do not set `%s` to `%s` ever.'; - } - - return false; - } - -} diff --git a/WordPress/Sniffs/VIP/RestrictedFunctionsSniff.php b/WordPress/Sniffs/VIP/RestrictedFunctionsSniff.php deleted file mode 100644 index 7d7bad1c18..0000000000 --- a/WordPress/Sniffs/VIP/RestrictedFunctionsSniff.php +++ /dev/null @@ -1,280 +0,0 @@ - false, - 'FoundPropertyForDeprecatedSniff' => false, - ); - - /** - * Groups of functions to restrict. - * - * Example: groups => array( - * 'lambda' => array( - * 'type' => 'error' | 'warning', - * 'message' => 'Use anonymous functions instead please!', - * 'functions' => array( 'file_get_contents', 'create_function' ), - * ) - * ) - * - * @return array - */ - public function getGroups() { - return array( - // @link WordPress.com: https://vip.wordpress.com/documentation/vip/code-review-what-we-look-for/#switch_to_blog - // @link VIP Go: https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#switch_to_blog - 'switch_to_blog' => array( - 'type' => 'error', - 'message' => '%s() is not something you should ever need to do in a VIP theme context. Instead use an API (XML-RPC, REST) to interact with other sites if needed.', - 'functions' => array( 'switch_to_blog' ), - ), - - 'file_get_contents' => array( - 'type' => 'warning', - 'message' => '%s() is highly discouraged, please use wpcom_vip_file_get_contents() instead.', - 'functions' => array( - 'file_get_contents', - 'vip_wp_file_get_contents', - ), - ), - - 'wpcom_vip_get_term_link' => array( - 'type' => 'error', - 'message' => '%s() is deprecated, please use get_term_link(), get_tag_link(), or get_category_link() instead.', - 'functions' => array( - 'wpcom_vip_get_term_link', - ), - ), - - 'get_page_by_path' => array( - 'type' => 'error', - 'message' => '%s() is prohibited, please use wpcom_vip_get_page_by_path() instead.', - 'functions' => array( - 'get_page_by_path', - ), - ), - - 'get_page_by_title' => array( - 'type' => 'error', - 'message' => '%s() is prohibited, please use wpcom_vip_get_page_by_title() instead.', - 'functions' => array( - 'get_page_by_title', - ), - ), - - 'wpcom_vip_get_term_by' => array( - 'type' => 'error', - 'message' => '%s() is deprecated, please use get_term_by() or get_cat_ID() instead.', - 'functions' => array( - 'wpcom_vip_get_term_by', - ), - ), - - 'wpcom_vip_get_category_by_slug' => array( - 'type' => 'error', - 'message' => '%s() is deprecated, please use get_category_by_slug() instead.', - 'functions' => array( - 'wpcom_vip_get_category_by_slug', - ), - ), - - 'url_to_postid' => array( - 'type' => 'error', - 'message' => '%s() is prohibited, please use wpcom_vip_url_to_postid() instead.', - 'functions' => array( - 'url_to_postid', - 'url_to_post_id', - ), - ), - - 'attachment_url_to_postid' => array( - 'type' => 'error', - 'message' => '%s() is prohibited, please use wpcom_vip_attachment_url_to_postid() instead.', - 'functions' => array( - 'attachment_url_to_postid', - ), - ), - - // @link WordPress.com: https://lobby.vip.wordpress.com/wordpress-com-documentation/code-review-what-we-look-for/#remote-calls - // @link VIP Go: https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#remote-calls - 'wp_remote_get' => array( - 'type' => 'warning', - 'message' => '%s() is highly discouraged, please use vip_safe_wp_remote_get() instead.', - 'functions' => array( - 'wp_remote_get', - ), - ), - - // @link WordPress.com: https://lobby.vip.wordpress.com/wordpress-com-documentation/code-review-what-we-look-for/#custom-roles - // @link VIP Go: https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#custom-roles - 'custom_role' => array( - 'type' => 'error', - 'message' => 'Use wpcom_vip_add_role() instead of %s()', - 'functions' => array( - 'add_role', - ), - ), - - // @link WordPress.com: https://lobby.vip.wordpress.com/wordpress-com-documentation/code-review-what-we-look-for/#custom-roles - // @link VIP Go: https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#cache-constraints - 'cookies' => array( - 'type' => 'warning', - 'message' => 'Due to using Batcache, server side based client related logic will not work, use JS instead.', - 'functions' => array( - 'setcookie', - ), - ), - - // @link WordPress.com: https://lobby.vip.wordpress.com/wordpress-com-documentation/code-review-what-we-look-for/#wp_users-and-user_meta - 'user_meta' => array( - 'type' => 'error', - 'message' => '%s() usage is highly discouraged, check VIP documentation on "Working with wp_users"', - 'functions' => array( - 'get_user_meta', - 'update_user_meta', - 'delete_user_meta', - 'add_user_meta', - ), - ), - - // @todo Introduce a sniff specific to get_posts() that checks for suppress_filters=>false being supplied. - 'get_posts' => array( - 'type' => 'warning', - 'message' => '%s() is discouraged in favor of creating a new WP_Query() so that Advanced Post Cache will cache the query, unless you explicitly supply suppress_filters => false.', - 'functions' => array( - 'get_posts', - 'wp_get_recent_posts', - 'get_children', - ), - ), - - 'term_exists' => array( - 'type' => 'error', - 'message' => '%s() is highly discouraged due to not being cached; please use wpcom_vip_term_exists() instead.', - 'functions' => array( - 'term_exists', - ), - ), - - 'count_user_posts' => array( - 'type' => 'error', - 'message' => '%s() is highly discouraged due to not being cached; please use wpcom_vip_count_user_posts() instead.', - 'functions' => array( - 'count_user_posts', - ), - ), - - 'wp_old_slug_redirect' => array( - 'type' => 'error', - 'message' => '%s() is highly discouraged due to not being cached; please use wpcom_vip_old_slug_redirect() instead.', - 'functions' => array( - 'wp_old_slug_redirect', - ), - ), - - 'get_adjacent_post' => array( - 'type' => 'error', - 'message' => '%s() is highly discouraged due to not being cached; please use wpcom_vip_get_adjacent_post() instead.', - 'functions' => array( - 'get_adjacent_post', - 'get_previous_post', - 'get_previous_post_link', - 'get_next_post', - 'get_next_post_link', - ), - ), - - 'get_intermediate_image_sizes' => array( - 'type' => 'error', - 'message' => 'Intermediate images do not exist on the VIP platform, and thus get_intermediate_image_sizes() returns an empty array() on the platform. This behavior is intentional to prevent WordPress from generating multiple thumbnails when images are uploaded.', - 'functions' => array( - 'get_intermediate_image_sizes', - ), - ), - - // @link WordPress.com: https://lobby.vip.wordpress.com/wordpress-com-documentation/code-review-what-we-look-for/#mobile-detection - // @link VIP Go: https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#mobile-detection - 'wp_is_mobile' => array( - 'type' => 'error', - 'message' => '%s() found. When targeting mobile visitors, jetpack_is_mobile() should be used instead of wp_is_mobile. It is more robust and works better with full page caching.', - 'functions' => array( - 'wp_is_mobile', - ), - ), - ); - } - - /** - * Process the token and handle the deprecation notices. - * - * @since 1.0.0 Added to allow for throwing the deprecation notices. - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return void|int - */ - public function process_token( $stackPtr ) { - if ( false === $this->thrown['DeprecatedSniff'] ) { - $this->thrown['DeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.RestrictedFunctions" sniff has been deprecated. Please update your custom ruleset.', - 0, - 'DeprecatedSniff' - ); - } - - if ( ! empty( $this->exclude ) - && false === $this->thrown['FoundPropertyForDeprecatedSniff'] - ) { - $this->thrown['FoundPropertyForDeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.RestrictedFunctions" sniff has been deprecated. Please update your custom ruleset.', - 0, - 'FoundPropertyForDeprecatedSniff' - ); - } - - return parent::process_token( $stackPtr ); - } - -} diff --git a/WordPress/Sniffs/VIP/RestrictedVariablesSniff.php b/WordPress/Sniffs/VIP/RestrictedVariablesSniff.php deleted file mode 100644 index 37a5326003..0000000000 --- a/WordPress/Sniffs/VIP/RestrictedVariablesSniff.php +++ /dev/null @@ -1,113 +0,0 @@ - false, - 'FoundPropertyForDeprecatedSniff' => false, - ); - - /** - * Groups of variables to restrict. - * - * Example: groups => array( - * 'wpdb' => array( - * 'type' => 'error' | 'warning', - * 'message' => 'Dont use this one please!', - * 'variables' => array( '$val', '$var' ), - * 'object_vars' => array( '$foo->bar', .. ), - * 'array_members' => array( '$foo['bar']', .. ), - * ) - * ) - * - * @return array - */ - public function getGroups() { - return array( - // @link https://lobby.vip.wordpress.com/wordpress-com-documentation/code-review-what-we-look-for/#wp_users-and-user_meta - 'user_meta' => array( - 'type' => 'error', - 'message' => 'Usage of users/usermeta tables is highly discouraged in VIP context, For storing user additional user metadata, you should look at User Attributes.', - 'object_vars' => array( - '$wpdb->users', - '$wpdb->usermeta', - ), - ), - - // @link https://lobby.vip.wordpress.com/wordpress-com-documentation/code-review-what-we-look-for/#caching-constraints - 'cache_constraints' => array( - 'type' => 'warning', - 'message' => 'Due to using Batcache, server side based client related logic will not work, use JS instead.', - 'variables' => array( - '$_COOKIE', - ), - 'array_members' => array( - '$_SERVER[\'HTTP_USER_AGENT\']', - '$_SERVER[\'REMOTE_ADDR\']', - ), - ), - ); - } - - /** - * Process the token and handle the deprecation notices. - * - * @since 1.0.0 Added to allow for throwing the deprecation notices. - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return void|int - */ - public function process_token( $stackPtr ) { - if ( false === $this->thrown['DeprecatedSniff'] ) { - $this->thrown['DeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.RestrictedVariables" sniff has been deprecated. Please update your custom ruleset.', - 0, - 'DeprecatedSniff' - ); - } - - if ( ! empty( $this->exclude ) - && false === $this->thrown['FoundPropertyForDeprecatedSniff'] - ) { - $this->thrown['FoundPropertyForDeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.RestrictedVariables" sniff has been deprecated. Please update your custom ruleset.', - 0, - 'FoundPropertyForDeprecatedSniff' - ); - } - - return parent::process_token( $stackPtr ); - } - -} diff --git a/WordPress/Sniffs/VIP/SessionFunctionsUsageSniff.php b/WordPress/Sniffs/VIP/SessionFunctionsUsageSniff.php deleted file mode 100644 index 3554e2bbde..0000000000 --- a/WordPress/Sniffs/VIP/SessionFunctionsUsageSniff.php +++ /dev/null @@ -1,126 +0,0 @@ - false, - 'FoundPropertyForDeprecatedSniff' => false, - ); - - /** - * Groups of functions to restrict. - * - * Example: groups => array( - * 'lambda' => array( - * 'type' => 'error' | 'warning', - * 'message' => 'Use anonymous functions instead please!', - * 'functions' => array( 'file_get_contents', 'create_function' ), - * ) - * ) - * - * @return array - */ - public function getGroups() { - return array( - 'session' => array( - 'type' => 'error', - 'message' => 'The use of PHP session function %s() is prohibited.', - 'functions' => array( - 'session_abort', - 'session_cache_expire', - 'session_cache_limiter', - 'session_commit', - 'session_create_id', - 'session_decode', - 'session_destroy', - 'session_encode', - 'session_gc', - 'session_get_cookie_params', - 'session_id', - 'session_is_registered', - 'session_module_name', - 'session_name', - 'session_regenerate_id', - 'session_register_shutdown', - 'session_register', - 'session_reset', - 'session_save_path', - 'session_set_cookie_params', - 'session_set_save_handler', - 'session_start', - 'session_status', - 'session_unregister', - 'session_unset', - 'session_write_close', - ), - ), - ); - } - - - /** - * Process the token and handle the deprecation notices. - * - * @since 1.0.0 Added to allow for throwing the deprecation notices. - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return void|int - */ - public function process_token( $stackPtr ) { - if ( false === $this->thrown['DeprecatedSniff'] ) { - $this->thrown['DeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.SessionFunctionsUsage" sniff has been deprecated. Please update your custom ruleset.', - 0, - 'DeprecatedSniff' - ); - } - - if ( ! empty( $this->exclude ) - && false === $this->thrown['FoundPropertyForDeprecatedSniff'] - ) { - $this->thrown['FoundPropertyForDeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.SessionFunctionsUsage" sniff has been deprecated. Please update your custom ruleset.', - 0, - 'FoundPropertyForDeprecatedSniff' - ); - } - - return parent::process_token( $stackPtr ); - } - -} diff --git a/WordPress/Sniffs/VIP/SessionVariableUsageSniff.php b/WordPress/Sniffs/VIP/SessionVariableUsageSniff.php deleted file mode 100644 index 945807b450..0000000000 --- a/WordPress/Sniffs/VIP/SessionVariableUsageSniff.php +++ /dev/null @@ -1,81 +0,0 @@ - false, - ); - - /** - * Returns an array of tokens this test wants to listen for. - * - * @return array - */ - public function register() { - return array( - \T_VARIABLE, - ); - } - - /** - * Process the token and handle the deprecation notice. - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return void - */ - public function process_token( $stackPtr ) { - if ( false === $this->thrown['DeprecatedSniff'] ) { - $this->thrown['DeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.SessionVariableUsage" sniff has been deprecated. Please update your custom ruleset.', - 0, - 'DeprecatedSniff' - ); - } - - if ( '$_SESSION' === $this->tokens[ $stackPtr ]['content'] ) { - $this->phpcsFile->addError( - 'Usage of $_SESSION variable is prohibited.', - $stackPtr, - 'SessionVarsProhibited' - ); - } - } - -} diff --git a/WordPress/Sniffs/VIP/SlowDBQuerySniff.php b/WordPress/Sniffs/VIP/SlowDBQuerySniff.php deleted file mode 100644 index 20c6eae42e..0000000000 --- a/WordPress/Sniffs/VIP/SlowDBQuerySniff.php +++ /dev/null @@ -1,76 +0,0 @@ - false, - 'FoundPropertyForDeprecatedSniff' => false, - ); - - /** - * Don't use. - * - * @deprecated 1.0.0 - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return void|int - */ - public function process_token( $stackPtr ) { - if ( false === $this->thrown['DeprecatedSniff'] ) { - $this->thrown['DeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.SlowDBQuery" sniff has been renamed to "WordPress.DB.SlowDBQuery". Please update your custom ruleset.', - 0, - 'DeprecatedSniff' - ); - } - - if ( ! empty( $this->exclude ) - && false === $this->thrown['FoundPropertyForDeprecatedSniff'] - ) { - $this->thrown['FoundPropertyForDeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.SlowDBQuery" sniff has been renamed to "WordPress.DB.SlowDBQuery". Please update your custom ruleset.', - 0, - 'FoundPropertyForDeprecatedSniff' - ); - } - - return parent::process_token( $stackPtr ); - } - -} diff --git a/WordPress/Sniffs/VIP/SuperGlobalInputUsageSniff.php b/WordPress/Sniffs/VIP/SuperGlobalInputUsageSniff.php deleted file mode 100644 index 403317f9da..0000000000 --- a/WordPress/Sniffs/VIP/SuperGlobalInputUsageSniff.php +++ /dev/null @@ -1,87 +0,0 @@ - false, - ); - - /** - * Returns an array of tokens this test wants to listen for. - * - * @return array - */ - public function register() { - return array( - \T_VARIABLE, - ); - } - - /** - * Process the token and handle the deprecation notice. - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return void - */ - public function process_token( $stackPtr ) { - if ( false === $this->thrown['DeprecatedSniff'] ) { - $this->thrown['DeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.SuperGlobalInputUsage" sniff has been deprecated. Please update your custom ruleset.', - 0, - 'DeprecatedSniff' - ); - } - - // Check for global input variable. - if ( ! \in_array( $this->tokens[ $stackPtr ]['content'], $this->input_superglobals, true ) ) { - return; - } - - $varName = $this->tokens[ $stackPtr ]['content']; - - // If we're overriding a superglobal with an assignment, no need to test. - if ( $this->is_assignment( $stackPtr ) ) { - return; - } - - // Check for whitelisting comment. - if ( ! $this->has_whitelist_comment( 'input var', $stackPtr ) ) { - $this->phpcsFile->addWarning( 'Detected access of super global var %s, probably needs manual inspection.', $stackPtr, 'AccessDetected', array( $varName ) ); - } - } - -} diff --git a/WordPress/Sniffs/VIP/TimezoneChangeSniff.php b/WordPress/Sniffs/VIP/TimezoneChangeSniff.php deleted file mode 100644 index 395008a7d3..0000000000 --- a/WordPress/Sniffs/VIP/TimezoneChangeSniff.php +++ /dev/null @@ -1,75 +0,0 @@ - false, - 'FoundPropertyForDeprecatedSniff' => false, - ); - - /** - * Don't use. - * - * @deprecated 1.0.0 - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return void|int - */ - public function process_token( $stackPtr ) { - if ( false === $this->thrown['DeprecatedSniff'] ) { - $this->thrown['DeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.TimezoneChange" sniff has been renamed to "WordPress.WP.TimezoneChange". Please update your custom ruleset.', - 0, - 'DeprecatedSniff' - ); - } - - if ( ! empty( $this->exclude ) - && false === $this->thrown['FoundPropertyForDeprecatedSniff'] - ) { - $this->thrown['FoundPropertyForDeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.TimezoneChange" sniff has been renamed to "WordPress.WP.TimezoneChange". Please update your custom ruleset.', - 0, - 'FoundPropertyForDeprecatedSniff' - ); - } - - return parent::process_token( $stackPtr ); - } - -} diff --git a/WordPress/Sniffs/VIP/ValidatedSanitizedInputSniff.php b/WordPress/Sniffs/VIP/ValidatedSanitizedInputSniff.php deleted file mode 100644 index 699c00463e..0000000000 --- a/WordPress/Sniffs/VIP/ValidatedSanitizedInputSniff.php +++ /dev/null @@ -1,77 +0,0 @@ - false, - 'FoundPropertyForDeprecatedSniff' => false, - ); - - /** - * Don't use. - * - * @deprecated 1.0.0 - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return void|int - */ - public function process_token( $stackPtr ) { - if ( false === $this->thrown['DeprecatedSniff'] ) { - $this->thrown['DeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.ValidatedSanitizedInput" sniff has been renamed to "WordPress.Security.ValidatedSanitizedInput". Please update your custom ruleset.', - 0, - 'DeprecatedSniff' - ); - } - - if ( false === $this->thrown['FoundPropertyForDeprecatedSniff'] - && ( ( array() !== $this->customSanitizingFunctions && $this->customSanitizingFunctions !== $this->addedCustomFunctions['sanitize'] ) - || ( array() !== $this->customUnslashingSanitizingFunctions && $this->customUnslashingSanitizingFunctions !== $this->addedCustomFunctions['unslashsanitize'] ) - || false !== $this->check_validation_in_scope_only ) - ) { - $this->thrown['FoundPropertyForDeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.VIP.ValidatedSanitizedInput" sniff has been renamed to "WordPress.Security.ValidatedSanitizedInput". Please update your custom ruleset.', - 0, - 'FoundPropertyForDeprecatedSniff' - ); - } - - return parent::process_token( $stackPtr ); - } - -} diff --git a/WordPress/Sniffs/Variables/GlobalVariablesSniff.php b/WordPress/Sniffs/Variables/GlobalVariablesSniff.php deleted file mode 100644 index 03c7949387..0000000000 --- a/WordPress/Sniffs/Variables/GlobalVariablesSniff.php +++ /dev/null @@ -1,76 +0,0 @@ - false, - 'FoundPropertyForDeprecatedSniff' => false, - ); - - /** - * Don't use. - * - * @deprecated 1.0.0 - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return void|int - */ - public function process_token( $stackPtr ) { - if ( false === $this->thrown['DeprecatedSniff'] ) { - $this->thrown['DeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.Variables.GlobalVariables" sniff has been renamed to "WordPress.WP.GlobalVariablesOverride". Please update your custom ruleset.', - 0, - 'DeprecatedSniff' - ); - } - - if ( ! empty( $this->custom_test_class_whitelist ) - && false === $this->thrown['FoundPropertyForDeprecatedSniff'] - ) { - $this->thrown['FoundPropertyForDeprecatedSniff'] = $this->phpcsFile->addWarning( - 'The "WordPress.Variables.GlobalVariables" sniff has been renamed to "WordPress.WP.GlobalVariablesOverride". Please update your custom ruleset.', - 0, - 'FoundPropertyForDeprecatedSniff' - ); - } - - return parent::process_token( $stackPtr ); - } - -} diff --git a/WordPress/Sniffs/Variables/VariableRestrictionsSniff.php b/WordPress/Sniffs/Variables/VariableRestrictionsSniff.php deleted file mode 100644 index e29cd1aa42..0000000000 --- a/WordPress/Sniffs/Variables/VariableRestrictionsSniff.php +++ /dev/null @@ -1,51 +0,0 @@ - array( - * 'wpdb' => array( - * 'type' => 'error' | 'warning', - * 'message' => 'Dont use this one please!', - * 'variables' => array( '$val', '$var' ), - * 'object_vars' => array( '$foo->bar', .. ), - * 'array_members' => array( '$foo['bar']', .. ), - * ) - * ) - * - * @return array - */ - public function getGroups() { - return array(); - } - -} diff --git a/WordPress/Sniffs/WP/AlternativeFunctionsSniff.php b/WordPress/Sniffs/WP/AlternativeFunctionsSniff.php index e5d700c506..afd98fd2ac 100644 --- a/WordPress/Sniffs/WP/AlternativeFunctionsSniff.php +++ b/WordPress/Sniffs/WP/AlternativeFunctionsSniff.php @@ -3,27 +3,80 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\WP; +namespace WordPressCS\WordPress\Sniffs\WP; -use WordPress\AbstractFunctionRestrictionsSniff; +use PHPCSUtils\Utils\PassedParameters; +use PHPCSUtils\Utils\TextStrings; +use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff; +use WordPressCS\WordPress\Helpers\MinimumWPVersionTrait; /** * Discourages the use of various functions and suggests (WordPress) alternatives. * - * @package WPCS\WordPressCodingStandards - * - * @since 0.11.0 - * @since 0.13.0 Class name changed: this class is now namespaced. - * @since 1.0.0 - Takes the minimum supported WP version into account. - * - Takes exceptions based on passed parameters into account. + * @since 0.11.0 + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 1.0.0 - Takes the minimum supported WP version into account. + * - Takes exceptions based on passed parameters into account. * - * @uses \WordPress\Sniff::$minimum_supported_version + * @uses \WordPressCS\WordPress\Helpers\MinimumWPVersionTrait::$minimum_wp_version */ -class AlternativeFunctionsSniff extends AbstractFunctionRestrictionsSniff { +final class AlternativeFunctionsSniff extends AbstractFunctionRestrictionsSniff { + + use MinimumWPVersionTrait; + + /** + * Local input streams which should not be flagged for the file system function checks. + * + * @link https://www.php.net/wrappers.php + * + * @since 2.1.0 + * @since 3.0.0 The visibility was changed from `protected` to `private`. + * + * @var array + */ + private $allowed_local_streams = array( + 'php://input' => true, + 'php://output' => true, + 'php://stdin' => true, + 'php://stdout' => true, + 'php://stderr' => true, + ); + + /** + * Local input streams which should not be flagged for the file system function checks if + * the $filename starts with them. + * + * @link https://www.php.net/wrappers.php + * + * @since 2.1.0 + * @since 3.0.0 The visibility was changed from `protected` to `private`. + * + * @var array + */ + private $allowed_local_stream_partials = array( + 'php://temp/', + 'php://fd/', + ); + + /** + * Local input stream constants which should not be flagged for the file system function checks. + * + * @link https://www.php.net/wrappers.php + * + * @since 2.1.0 + * @since 3.0.0 The visibility was changed from `protected` to `private`. + * + * @var array + */ + private $allowed_local_stream_constants = array( + 'STDIN' => true, + 'STDOUT' => true, + 'STDERR' => true, + ); /** * Groups of functions to restrict. @@ -48,6 +101,9 @@ public function getGroups() { 'functions' => array( 'curl_*', ), + 'allow' => array( + 'curl_version' => true, + ), ), 'parse_url' => array( @@ -77,19 +133,46 @@ public function getGroups() { ), ), - 'file_system_read' => array( + 'unlink' => array( + 'type' => 'warning', + 'message' => '%s() is discouraged. Use wp_delete_file() to delete a file.', + 'since' => '4.2.0', + 'functions' => array( + 'unlink', + ), + ), + + 'rename' => array( 'type' => 'warning', - 'message' => 'File operations should use WP_Filesystem methods instead of direct PHP filesystem calls. Found: %s()', + 'message' => '%s() is discouraged. Use WP_Filesystem::move() to rename a file.', 'since' => '2.5.0', 'functions' => array( - 'readfile', - 'fopen', - 'fsockopen', - 'pfsockopen', + 'rename', + ), + ), + + 'file_system_operations' => array( + 'type' => 'warning', + 'message' => 'File operations should use WP_Filesystem methods instead of direct PHP filesystem calls. Found: %s().', + 'since' => '2.5.0', + 'functions' => array( + 'chgrp', + 'chmod', + 'chown', 'fclose', + 'file_put_contents', + 'fopen', + 'fputs', 'fread', + 'fsockopen', 'fwrite', - 'file_put_contents', + 'is_writable', + 'is_writeable', + 'mkdir', + 'pfsockopen', + 'readfile', + 'rmdir', + 'touch', ), ), @@ -107,8 +190,8 @@ public function getGroups() { 'message' => '%s() is discouraged. Rand seeding is not necessary when using the wp_rand() function (as you should).', 'since' => '2.6.2', 'functions' => array( - 'srand', 'mt_srand', + 'srand', ), ), @@ -117,8 +200,8 @@ public function getGroups() { 'message' => '%s() is discouraged. Use the far less predictable wp_rand() instead.', 'since' => '2.6.2', 'functions' => array( - 'rand', 'mt_rand', + 'rand', ), ), ); @@ -129,14 +212,15 @@ public function getGroups() { * * @param int $stackPtr The position of the current token in the stack. * @param string $group_name The name of the group which was matched. - * @param string $matched_content The token content (function name) which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. * * @return int|void Integer stack pointer to skip forward or void to continue * normal file processing. */ public function process_matched_token( $stackPtr, $group_name, $matched_content ) { - $this->get_wp_version_from_cl(); + $this->set_minimum_wp_version(); /* * Deal with exceptions. @@ -145,27 +229,31 @@ public function process_matched_token( $stackPtr, $group_name, $matched_content case 'strip_tags': /* * The function `wp_strip_all_tags()` is only a valid alternative when - * only the first parameter is passed to `strip_tags()`. + * only the first parameter, `$string`, is passed to `strip_tags()`. */ - if ( $this->get_function_call_parameter_count( $stackPtr ) !== 1 ) { + $has_allowed_tags = PassedParameters::getParameter( $this->phpcsFile, $stackPtr, 2, 'allowed_tags' ); + if ( false !== $has_allowed_tags ) { return; } + unset( $has_allowed_tags ); break; - case 'wp_parse_url': + case 'parse_url': /* * Before WP 4.7.0, the function `wp_parse_url()` was only a valid alternative - * if no second param was passed to `parse_url()`. + * if the second param - `$component` - was not passed to `parse_url()`. * * @see https://developer.wordpress.org/reference/functions/wp_parse_url/#changelog */ - if ( $this->get_function_call_parameter_count( $stackPtr ) !== 1 - && version_compare( $this->minimum_supported_version, '4.7.0', '<' ) + $has_component = PassedParameters::getParameter( $this->phpcsFile, $stackPtr, 2, 'component' ); + if ( false !== $has_component + && $this->wp_version_compare( $this->minimum_wp_version, '4.7.0', '<' ) ) { return; } + unset( $has_component ); break; case 'file_get_contents': @@ -173,37 +261,75 @@ public function process_matched_token( $stackPtr, $group_name, $matched_content * Using `wp_remote_get()` will only work for remote URLs. * See if we can determine is this function call is for a local file and if so, bow out. */ - $params = $this->get_function_call_parameters( $stackPtr ); + $params = PassedParameters::getParameters( $this->phpcsFile, $stackPtr ); - if ( isset( $params[2] ) && 'true' === $params[2]['raw'] ) { + $use_include_path_param = PassedParameters::getParameterFromStack( $params, 2, 'use_include_path' ); + if ( false !== $use_include_path_param && 'true' === $use_include_path_param['clean'] ) { // Setting `$use_include_path` to `true` is only relevant for local files. return; } - if ( isset( $params[1] ) === false ) { + $filename_param = PassedParameters::getParameterFromStack( $params, 1, 'filename' ); + if ( false === $filename_param ) { // If the file to get is not set, this is a non-issue anyway. return; } - if ( strpos( $params[1]['raw'], 'http:' ) !== false - || strpos( $params[1]['raw'], 'https:' ) !== false + if ( strpos( $filename_param['clean'], 'http:' ) !== false + || strpos( $filename_param['clean'], 'https:' ) !== false ) { // Definitely a URL, throw notice. break; } - if ( preg_match( '`\b(?:ABSPATH|WP_(?:CONTENT|PLUGIN)_DIR|WPMU_PLUGIN_DIR|TEMPLATEPATH|STYLESHEETPATH|(?:MU)?PLUGINDIR)\b`', $params[1]['raw'] ) === 1 ) { + $contains_wp_path_constant = preg_match( + '`\b(?:ABSPATH|WP_(?:CONTENT|PLUGIN)_DIR|WPMU_PLUGIN_DIR|TEMPLATEPATH|STYLESHEETPATH|(?:MU)?PLUGINDIR)\b`', + $filename_param['clean'] + ); + if ( 1 === $contains_wp_path_constant ) { // Using any of the constants matched in this regex is an indicator of a local file. return; } - if ( preg_match( '`(?:get_home_path|plugin_dir_path|get_(?:stylesheet|template)_directory|wp_upload_dir)\s*\(`i', $params[1]['raw'] ) === 1 ) { + $contains_wp_path_function_call = preg_match( + '`(?:get_home_path|plugin_dir_path|get_(?:stylesheet|template)_directory|wp_upload_dir)\s*\(`i', + $filename_param['clean'] + ); + if ( 1 === $contains_wp_path_function_call ) { // Using any of the functions matched in the regex is an indicator of a local file. return; } - unset( $params ); + if ( $this->is_local_data_stream( $filename_param['clean'] ) === true ) { + // Local data stream. + return; + } + unset( $params, $use_include_path_param, $filename_param, $contains_wp_path_constant, $contains_wp_path_function_call ); + break; + + case 'file_put_contents': + case 'fopen': + case 'readfile': + /* + * Allow for handling raw data streams from the request body. + * + * Note: at this time (December 2022) these three functions use the same parameter name for their + * first parameter. If this would change at any point in the future, this code will need to + * be made more modular and will need to pass the parameter name based on the function call detected. + */ + $filename_param = PassedParameters::getParameter( $this->phpcsFile, $stackPtr, 1, 'filename' ); + if ( false === $filename_param ) { + // If the file to work with is not set, local data streams don't come into play. + break; + } + + if ( $this->is_local_data_stream( $filename_param['clean'] ) === true ) { + // Local data stream. + return; + } + + unset( $filename_param ); break; } @@ -212,9 +338,34 @@ public function process_matched_token( $stackPtr, $group_name, $matched_content } // Verify if the alternative is available in the minimum supported WP version. - if ( version_compare( $this->groups[ $group_name ]['since'], $this->minimum_supported_version, '<=' ) ) { + if ( $this->wp_version_compare( $this->groups[ $group_name ]['since'], $this->minimum_wp_version, '<=' ) ) { return parent::process_matched_token( $stackPtr, $group_name, $matched_content ); } } + /** + * Determine based on the "clean" parameter value, whether a file parameter points to + * a local data stream. + * + * @param string $clean_param_value Parameter value without comments. + * + * @return bool True if this is a local data stream. False otherwise. + */ + protected function is_local_data_stream( $clean_param_value ) { + + $stripped = TextStrings::stripQuotes( $clean_param_value ); + if ( isset( $this->allowed_local_streams[ $stripped ] ) + || isset( $this->allowed_local_stream_constants[ $clean_param_value ] ) + ) { + return true; + } + + foreach ( $this->allowed_local_stream_partials as $partial ) { + if ( strpos( $stripped, $partial ) === 0 ) { + return true; + } + } + + return false; + } } diff --git a/WordPress/Sniffs/WP/CapabilitiesSniff.php b/WordPress/Sniffs/WP/CapabilitiesSniff.php new file mode 100644 index 0000000000..3a86562dfa --- /dev/null +++ b/WordPress/Sniffs/WP/CapabilitiesSniff.php @@ -0,0 +1,478 @@ + The key is the name of a function we're targetting, + * the value is an array containing the 1-based parameter position + * of the "capability" parameter within the function, as well as + * the name of the parameter as declared in the function. + * If the parameter name has been renamed since the release of PHP 8.0, + * the parameter can be set as an array. + */ + protected $target_functions = array( + 'add_comments_page' => array( + 'position' => 3, + 'name' => 'capability', + ), + 'add_dashboard_page' => array( + 'position' => 3, + 'name' => 'capability', + ), + 'add_links_page' => array( + 'position' => 3, + 'name' => 'capability', + ), + 'add_management_page' => array( + 'position' => 3, + 'name' => 'capability', + ), + 'add_media_page' => array( + 'position' => 3, + 'name' => 'capability', + ), + 'add_menu_page' => array( + 'position' => 3, + 'name' => 'capability', + ), + 'add_object_page' => array( // Deprecated since WP 4.5.0. + 'position' => 3, + 'name' => 'capability', + ), + 'add_options_page' => array( + 'position' => 3, + 'name' => 'capability', + ), + 'add_pages_page' => array( + 'position' => 3, + 'name' => 'capability', + ), + 'add_plugins_page' => array( + 'position' => 3, + 'name' => 'capability', + ), + 'add_posts_page' => array( + 'position' => 3, + 'name' => 'capability', + ), + 'add_submenu_page' => array( + 'position' => 4, + 'name' => 'capability', + ), + 'add_theme_page' => array( + 'position' => 3, + 'name' => 'capability', + ), + 'add_users_page' => array( + 'position' => 3, + 'name' => 'capability', + ), + 'add_utility_page' => array( // Deprecated since WP 4.5.0. + 'position' => 3, + 'name' => 'capability', + ), + 'author_can' => array( + 'position' => 2, + 'name' => 'capability', + ), + 'current_user_can' => array( + 'position' => 1, + 'name' => 'capability', + ), + 'current_user_can_for_blog' => array( + 'position' => 2, + 'name' => 'capability', + ), + 'map_meta_cap' => array( + 'position' => 1, + 'name' => 'cap', + ), + 'user_can' => array( + 'position' => 2, + 'name' => 'capability', + ), + ); + + /** + * List of core roles which should not to be used directly. + * + * @since 3.0.0 + * + * @var array Key is role available in WP Core, value irrelevant. + */ + private $core_roles = array( + 'super_admin' => true, + 'administrator' => true, + 'editor' => true, + 'author' => true, + 'contributor' => true, + 'subscriber' => true, + ); + + /** + * List of known primitive and meta core capabilities. + * + * Sources: + * - {@link https://wordpress.org/support/article/roles-and-capabilities/ Roles and Capabilities handbook page} + * - The `map_meta_cap()` function in the `src/wp-includes/capabilities.php` file. + * - The tests in the `tests/phpunit/tests/user/capabilities.php` file. + * + * List is sorted alphabetically. + * + * {@internal To be updated after every major release. Last updated for WordPress 6.1.0.} + * + * @since 3.0.0 + * + * @var array All capabilities available in core. + */ + private $core_capabilities = array( + 'activate_plugin' => true, + 'activate_plugins' => true, + 'add_comment_meta' => true, + 'add_post_meta' => true, + 'add_term_meta' => true, + 'add_user_meta' => true, + 'add_users' => true, + 'assign_categories' => true, + 'assign_post_tags' => true, + 'assign_term' => true, + 'create_app_password' => true, + 'create_sites' => true, + 'create_users' => true, + 'customize' => true, + 'deactivate_plugin' => true, + 'deactivate_plugins' => true, + 'delete_app_password' => true, + 'delete_app_passwords' => true, + 'delete_block' => true, // Only seen in tests. + 'delete_blocks' => true, // Alias for 'delete_posts', but supported. + 'delete_categories' => true, + 'delete_comment_meta' => true, + 'delete_others_blocks' => true, // Alias for 'delete_others_posts', but supported. + 'delete_others_pages' => true, + 'delete_others_posts' => true, + 'delete_page' => true, // Alias, but supported. + 'delete_pages' => true, + 'delete_plugins' => true, + 'delete_post_tags' => true, + 'delete_post' => true, // Alias, but supported. + 'delete_post_meta' => true, + 'delete_posts' => true, + 'delete_private_blocks' => true, // Alias for 'delete_private_posts', but supported. + 'delete_private_pages' => true, + 'delete_private_posts' => true, + 'delete_published_blocks' => true, // Alias for 'delete_published_posts', but supported. + 'delete_published_pages' => true, + 'delete_published_posts' => true, + 'delete_site' => true, + 'delete_sites' => true, + 'delete_term' => true, + 'delete_term_meta' => true, + 'delete_themes' => true, + 'delete_user' => true, // Alias for 'delete_users', but supported. + 'delete_user_meta' => true, + 'delete_users' => true, + 'edit_app_password' => true, + 'edit_categories' => true, + 'edit_block' => true, // Only seen in tests. + 'edit_blocks' => true, // Alias for 'edit_posts', but supported. + 'edit_comment' => true, // Alias, but supported. + 'edit_comment_meta' => true, + 'edit_css' => true, + 'edit_dashboard' => true, + 'edit_files' => true, + 'edit_others_blocks' => true, // Alias for 'edit_others_posts', but supported. + 'edit_others_pages' => true, + 'edit_others_posts' => true, + 'edit_page' => true, // Alias, but supported. + 'edit_pages' => true, + 'edit_plugins' => true, + 'edit_post_tags' => true, + 'edit_post' => true, // Alias, but supported. + 'edit_post_meta' => true, + 'edit_posts' => true, + 'edit_private_blocks' => true, // Alias for 'edit_private_posts', but supported. + 'edit_private_pages' => true, + 'edit_private_posts' => true, + 'edit_published_blocks' => true, // Alias for 'edit_published_posts', but supported. + 'edit_published_pages' => true, + 'edit_published_posts' => true, + 'edit_term' => true, + 'edit_term_meta' => true, + 'edit_theme_options' => true, + 'edit_themes' => true, + 'edit_user' => true, // Alias for 'edit_users', but supported. + 'edit_user_meta' => true, + 'edit_users' => true, + 'erase_others_personal_data' => true, + 'export' => true, + 'export_others_personal_data' => true, + 'import' => true, + 'install_languages' => true, + 'install_plugins' => true, + 'install_themes' => true, + 'list_app_passwords' => true, + 'list_users' => true, + 'manage_categories' => true, + 'manage_links' => true, + 'manage_network' => true, + 'manage_network_options' => true, + 'manage_network_plugins' => true, + 'manage_network_themes' => true, + 'manage_network_users' => true, + 'manage_options' => true, + 'manage_post_tags' => true, + 'manage_privacy_options' => true, + 'manage_sites' => true, + 'moderate_comments' => true, + 'publish_blocks' => true, // Alias for 'publish_posts', but supported. + 'publish_pages' => true, + 'publish_post' => true, // Alias, but supported. + 'publish_posts' => true, + 'promote_user' => true, + 'promote_users' => true, + 'read' => true, + 'read_block' => true, // Only seen in tests. + 'read_post' => true, // Alias, but supported. + 'read_page' => true, // Alias, but supported. + 'read_app_password' => true, + 'read_private_blocks' => true, // Alias for 'read_private_posts', but supported. + 'read_private_pages' => true, + 'read_private_posts' => true, + 'remove_user' => true, // Alias for 'remove_users', but supported. + 'remove_users' => true, + 'resume_plugin' => true, // Alias for 'resume_plugins', but supported. + 'resume_plugins' => true, + 'resume_theme' => true, // Alias for 'resume_themes', but supported. + 'resume_themes' => true, + 'setup_network' => true, + 'switch_themes' => true, + 'unfiltered_html' => true, + 'unfiltered_upload' => true, + 'update_core' => true, + 'update_https' => true, + 'update_languages' => true, + 'update_plugins' => true, + 'update_php' => true, + 'update_themes' => true, + 'upgrade_network' => true, + 'upload_files' => true, + 'upload_plugins' => true, + 'upload_themes' => true, + 'view_site_health_checks' => true, + ); + + /** + * List of deprecated core capabilities. + * + * User Levels were deprecated in version 3.0. + * + * {@internal To be updated after every major release. Last updated for WordPress 6.1.0.} + * + * @link https://github.com/WordPress/wordpress-develop/blob/master/tests/phpunit/tests/user/capabilities.php + * + * @since 3.0.0 + * + * @var array All deprecated capabilities in core. + */ + private $deprecated_capabilities = array( + 'level_10' => '3.0.0', + 'level_9' => '3.0.0', + 'level_8' => '3.0.0', + 'level_7' => '3.0.0', + 'level_6' => '3.0.0', + 'level_5' => '3.0.0', + 'level_4' => '3.0.0', + 'level_3' => '3.0.0', + 'level_2' => '3.0.0', + 'level_1' => '3.0.0', + 'level_0' => '3.0.0', + ); + + /** + * Process the parameters of a matched function. + * + * @since 3.0.0 + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * @param array $parameters Array with information about the parameters. + * + * @return void + */ + public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { + $function_details = $this->target_functions[ $matched_content ]; + + $parameter = PassedParameters::getParameterFromStack( + $parameters, + $function_details['position'], + $function_details['name'] + ); + + if ( false === $parameter ) { + return; + } + + // If the parameter is anything other than T_CONSTANT_ENCAPSED_STRING throw a warning and bow out. + $first_non_empty = null; + for ( $i = $parameter['start']; $i <= $parameter['end']; $i++ ) { + if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) { + continue; + } + + if ( \T_CONSTANT_ENCAPSED_STRING !== $this->tokens[ $i ]['code'] + || null !== $first_non_empty + ) { + // Throw warning at low severity. + $this->phpcsFile->addWarning( + 'Couldn\'t determine the value passed to the $%s parameter in function call to %s(). Please check if it matches a valid capability. Found: %s', + $i, + 'Undetermined', + array( + $function_details['name'], + $matched_content, + $parameter['clean'], + ), + 3 // Message severity set to below default. + ); + return; + } + + $first_non_empty = $i; + } + + if ( null === $first_non_empty ) { + // Parse error. Bow out. + return; + } + + /* + * As of this point we know that the `$capabilities` parameter only contains the one token + * and that that token is a `T_CONSTANT_ENCAPSED_STRING`. + */ + $matched_parameter = TextStrings::stripQuotes( $this->tokens[ $first_non_empty ]['content'] ); + + if ( isset( $this->core_capabilities[ $matched_parameter ] ) ) { + return; + } + + if ( empty( $matched_parameter ) ) { + $this->phpcsFile->addError( + 'An empty string is not a valid capability. Empty string found as the $%s parameter in a function call to %s()"', + $first_non_empty, + 'Invalid', + array( + $function_details['name'], + $matched_content, + ) + ); + return; + } + + // Check if additional capabilities were registered via the ruleset and if the found capability matches any of those. + $custom_capabilities = RulesetPropertyHelper::merge_custom_array( $this->custom_capabilities, array() ); + if ( isset( $custom_capabilities[ $matched_parameter ] ) ) { + return; + } + + if ( isset( $this->deprecated_capabilities[ $matched_parameter ] ) ) { + $this->set_minimum_wp_version(); + $is_error = $this->wp_version_compare( $this->deprecated_capabilities[ $matched_parameter ], $this->minimum_wp_version, '<' ); + + $data = array( + $matched_parameter, + $matched_content, + $this->deprecated_capabilities[ $matched_parameter ], + ); + + MessageHelper::addMessage( + $this->phpcsFile, + 'The capability "%s", found in the function call to %s(), has been deprecated since WordPress version %s.', + $first_non_empty, + $is_error, + 'Deprecated', + $data + ); + return; + } + + if ( isset( $this->core_roles[ $matched_parameter ] ) ) { + $this->phpcsFile->addError( + 'Capabilities should be used instead of roles. Found "%s" in function call to %s()', + $first_non_empty, + 'RoleFound', + array( + $matched_parameter, + $matched_content, + ) + ); + return; + } + + $this->phpcsFile->addWarning( + 'Found unknown capability "%s" in function call to %s(). Please check the spelling of the capability. If this is a custom capability, please verify the capability is registered with WordPress via a call to WP_Role(s)->add_cap().' . \PHP_EOL . 'Custom capabilities can be made known to this sniff by setting the "custom_capabilities" property in the PHPCS ruleset.', + $first_non_empty, + 'Unknown', + array( + $matched_parameter, + $matched_content, + ) + ); + } +} diff --git a/WordPress/Sniffs/WP/CapitalPDangitSniff.php b/WordPress/Sniffs/WP/CapitalPDangitSniff.php index fbe621bded..c183f156a0 100644 --- a/WordPress/Sniffs/WP/CapitalPDangitSniff.php +++ b/WordPress/Sniffs/WP/CapitalPDangitSniff.php @@ -3,32 +3,35 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\WP; +namespace WordPressCS\WordPress\Sniffs\WP; -use WordPress\Sniff; -use PHP_CodeSniffer_Tokens as Tokens; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\Namespaces; +use PHPCSUtils\Utils\ObjectDeclarations; +use WordPressCS\WordPress\Helpers\ContextHelper; +use WordPressCS\WordPress\Sniff; /** * Capital P Dangit! * - * Verify the correct spelling of `WordPress` in text strings, comments and class names. + * Verify the correct spelling of `WordPress` in text strings, comments and OO and namespace names. * - * @package WPCS\WordPressCodingStandards - * - * @since 0.12.0 - * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.12.0 + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 3.0.0 Now also checks namespace names. */ -class CapitalPDangitSniff extends Sniff { +final class CapitalPDangitSniff extends Sniff { /** * Regex to match a large number or spelling variations of WordPress in text strings. * * Prevents matches on: - * - URLs for wordpress.org/com/net/tv. + * - URLs for wordpress.org/com/net/test/tv. * - `@...` usernames starting with `wordpress` * - email addresses with a domain starting with `wordpress` * - email addresses with a user name ending with `wordpress` @@ -42,7 +45,7 @@ class CapitalPDangitSniff extends Sniff { * * @var string */ - const WP_REGEX = '#(?\'"()]*?\.(?:php|js|css|png|j[e]?pg|gif|pot))#i'; + const WP_REGEX = '#(?\'"()]*?\.(?:php|js|css|png|j[e]?pg|gif|pot))#i'; /** * Regex to match a large number or spelling variations of WordPress in class names. @@ -51,19 +54,6 @@ class CapitalPDangitSniff extends Sniff { */ const WP_CLASSNAME_REGEX = '`(?:^|_)(Word[_]*Pres+)(?:_|$)`i'; - /** - * String tokens we want to listen for. - * - * @var array - */ - private $text_string_tokens = array( - \T_CONSTANT_ENCAPSED_STRING => \T_CONSTANT_ENCAPSED_STRING, - \T_DOUBLE_QUOTED_STRING => \T_DOUBLE_QUOTED_STRING, - \T_HEREDOC => \T_HEREDOC, - \T_NOWDOC => \T_NOWDOC, - \T_INLINE_HTML => \T_INLINE_HTML, - ); - /** * Comment tokens we want to listen for as they contain text strings. * @@ -75,19 +65,6 @@ class CapitalPDangitSniff extends Sniff { \T_COMMENT => \T_COMMENT, ); - /** - * Class-like structure tokens to listen for. - * - * Using proper spelling in class, interface and trait names does not conflict with the naming conventions. - * - * @var array - */ - private $class_tokens = array( - \T_CLASS => \T_CLASS, - \T_INTERFACE => \T_INTERFACE, - \T_TRAIT => \T_TRAIT, - ); - /** * Combined text string and comment tokens array. * @@ -106,13 +83,16 @@ class CapitalPDangitSniff extends Sniff { */ public function register() { // Union the arrays - keeps the array keys. - $this->text_and_comment_tokens = ( $this->text_string_tokens + $this->comment_text_tokens ); + $this->text_and_comment_tokens = ( Tokens::$textStringTokens + $this->comment_text_tokens ); - $targets = ( $this->text_and_comment_tokens + $this->class_tokens ); + $targets = $this->text_and_comment_tokens; + $targets += Tokens::$ooScopeTokens; + $targets[ \T_NAMESPACE ] = \T_NAMESPACE; // Also sniff for array tokens to make skipping anything within those more efficient. - $targets[ \T_ARRAY ] = \T_ARRAY; - $targets[ \T_OPEN_SHORT_ARRAY ] = \T_OPEN_SHORT_ARRAY; + $targets += Collections::arrayOpenTokensBC(); + $targets += Collections::listTokens(); + $targets[ \T_OPEN_SQUARE_BRACKET ] = \T_OPEN_SQUARE_BRACKET; return $targets; } @@ -128,42 +108,76 @@ public function register() { * normal file processing. */ public function process_token( $stackPtr ) { - - if ( $this->has_whitelist_comment( 'spelling', $stackPtr ) ) { - return; - } - /* - * Ignore tokens within an array definition as this is a false positive in 80% of all cases. + * Ignore tokens within array and list definitions as well as within + * array keys as this is a false positive in 80% of all cases. * * The return values skip to the end of the array. * This prevents the sniff "hanging" on very long configuration arrays. */ - if ( \T_OPEN_SHORT_ARRAY === $this->tokens[ $stackPtr ]['code'] && isset( $this->tokens[ $stackPtr ]['bracket_closer'] ) ) { - return $this->tokens[ $stackPtr ]['bracket_closer']; - } elseif ( \T_ARRAY === $this->tokens[ $stackPtr ]['code'] && isset( $this->tokens[ $stackPtr ]['parenthesis_closer'] ) ) { + if ( ( \T_ARRAY === $this->tokens[ $stackPtr ]['code'] + || \T_LIST === $this->tokens[ $stackPtr ]['code'] ) + && isset( $this->tokens[ $stackPtr ]['parenthesis_closer'] ) + ) { return $this->tokens[ $stackPtr ]['parenthesis_closer']; } + if ( ( \T_OPEN_SHORT_ARRAY === $this->tokens[ $stackPtr ]['code'] + || \T_OPEN_SQUARE_BRACKET === $this->tokens[ $stackPtr ]['code'] ) + && isset( $this->tokens[ $stackPtr ]['bracket_closer'] ) + ) { + return $this->tokens[ $stackPtr ]['bracket_closer']; + } + + /* + * Deal with misspellings in namespace names. + * These are not auto-fixable, but need the attention of a developer. + */ + if ( \T_NAMESPACE === $this->tokens[ $stackPtr ]['code'] ) { + $ns_name = Namespaces::getDeclaredName( $this->phpcsFile, $stackPtr ); + if ( empty( $ns_name ) ) { + // Namespace operator or declaration without name. + return; + } + + $levels = explode( '\\', $ns_name ); + foreach ( $levels as $level ) { + if ( preg_match_all( self::WP_CLASSNAME_REGEX, $level, $matches, \PREG_PATTERN_ORDER ) > 0 ) { + $misspelled = $this->retrieve_misspellings( $matches[1] ); + + if ( ! empty( $misspelled ) ) { + $this->phpcsFile->addWarning( + 'Please spell "WordPress" correctly. Found: "%s" as part of the namespace name.', + $stackPtr, + 'MisspelledNamespaceName', + array( implode( ', ', $misspelled ) ) + ); + } + } + } + + return; + } + /* - * Deal with misspellings in class/interface/trait names. + * Deal with misspellings in class/interface/trait/enum names. * These are not auto-fixable, but need the attention of a developer. */ - if ( isset( $this->class_tokens[ $this->tokens[ $stackPtr ]['code'] ] ) ) { - $classname = $this->phpcsFile->getDeclarationName( $stackPtr ); + if ( isset( Tokens::$ooScopeTokens[ $this->tokens[ $stackPtr ]['code'] ] ) ) { + $classname = ObjectDeclarations::getName( $this->phpcsFile, $stackPtr ); if ( empty( $classname ) ) { return; } if ( preg_match_all( self::WP_CLASSNAME_REGEX, $classname, $matches, \PREG_PATTERN_ORDER ) > 0 ) { - $mispelled = $this->retrieve_misspellings( $matches[1] ); + $misspelled = $this->retrieve_misspellings( $matches[1] ); - if ( ! empty( $mispelled ) ) { + if ( ! empty( $misspelled ) ) { $this->phpcsFile->addWarning( - 'Please spell "WordPress" correctly. Found: "%s" as part of the class/interface/trait name.', + 'Please spell "WordPress" correctly. Found: "%s" as part of the class/interface/trait/enum name.', $stackPtr, 'MisspelledClassName', - array( implode( ', ', $mispelled ) ) + array( implode( ', ', $misspelled ) ) ); } } @@ -180,25 +194,35 @@ public function process_token( $stackPtr ) { || \T_DOC_COMMENT === $this->tokens[ $stackPtr ]['code'] ) { - $comment_start = $this->phpcsFile->findPrevious( \T_DOC_COMMENT_OPEN_TAG, ( $stackPtr - 1 ) ); - if ( false !== $comment_start ) { - $comment_tag = $this->phpcsFile->findPrevious( \T_DOC_COMMENT_TAG, ( $stackPtr - 1 ), $comment_start ); - if ( false !== $comment_tag && '@link' === $this->tokens[ $comment_tag ]['content'] ) { - // @link tag, so ignore. - return; - } + $comment_tag = $this->phpcsFile->findPrevious( + array( \T_DOC_COMMENT_TAG, \T_DOC_COMMENT_OPEN_TAG ), + ( $stackPtr - 1 ) + ); + if ( false !== $comment_tag + && \T_DOC_COMMENT_TAG === $this->tokens[ $comment_tag ]['code'] + && '@link' === $this->tokens[ $comment_tag ]['content'] + ) { + // @link tag, so ignore. + return; } } - // Ignore any text strings which are array keys `$var['key']` as this is a false positive in 80% of all cases. - if ( \T_CONSTANT_ENCAPSED_STRING === $this->tokens[ $stackPtr ]['code'] ) { - $prevToken = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true, null, true ); - if ( false !== $prevToken && \T_OPEN_SQUARE_BRACKET === $this->tokens[ $prevToken ]['code'] ) { - $nextToken = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true, null, true ); - if ( false !== $nextToken && \T_CLOSE_SQUARE_BRACKET === $this->tokens[ $nextToken ]['code'] ) { - return; - } - } + // Ignore constant declarations via define(). + if ( ContextHelper::is_in_function_call( $this->phpcsFile, $stackPtr, array( 'define' => true ), true, true ) ) { + return; + } + + // Ignore constant declarations using the const keyword. + $stop_points = array( + \T_CONST, + \T_SEMICOLON, + \T_OPEN_TAG, + \T_CLOSE_TAG, + \T_OPEN_CURLY_BRACKET, + ); + $maybe_const = $this->phpcsFile->findPrevious( $stop_points, ( $stackPtr - 1 ) ); + if ( false !== $maybe_const && \T_CONST === $this->tokens[ $maybe_const ]['code'] ) { + return; } $content = $this->tokens[ $stackPtr ]['content']; @@ -234,19 +258,24 @@ public function process_token( $stackPtr ) { } } - $mispelled = $this->retrieve_misspellings( $matches[1] ); + $misspelled = $this->retrieve_misspellings( $matches[1] ); - if ( empty( $mispelled ) ) { + if ( empty( $misspelled ) ) { return; } + $code = 'MisspelledInText'; + if ( isset( Tokens::$commentTokens[ $this->tokens[ $stackPtr ]['code'] ] ) ) { + $code = 'MisspelledInComment'; + } + $fix = $this->phpcsFile->addFixableWarning( 'Please spell "WordPress" correctly. Found %s misspelling(s): %s', $stackPtr, - 'Misspelled', + $code, array( - \count( $mispelled ), - implode( ', ', $mispelled ), + \count( $misspelled ), + implode( ', ', $misspelled ), ) ); @@ -269,7 +298,7 @@ public function process_token( $stackPtr ) { * @return array Array containing only the misspelled variants. */ protected function retrieve_misspellings( $match_stack ) { - $mispelled = array(); + $misspelled = array(); foreach ( $match_stack as $match ) { // Deal with multi-dimensional arrays when capturing offset. if ( \is_array( $match ) ) { @@ -277,11 +306,10 @@ protected function retrieve_misspellings( $match_stack ) { } if ( 'WordPress' !== $match ) { - $mispelled[] = $match; + $misspelled[] = $match; } } - return $mispelled; + return $misspelled; } - } diff --git a/WordPress/Sniffs/WP/ClassNameCaseSniff.php b/WordPress/Sniffs/WP/ClassNameCaseSniff.php new file mode 100644 index 0000000000..e1abd225a6 --- /dev/null +++ b/WordPress/Sniffs/WP/ClassNameCaseSniff.php @@ -0,0 +1,835 @@ +class_groups as $name ) { + $name_lc = $name . '_lc'; + $this->$name_lc = array_map( 'strtolower', $this->$name ); + $this->$name = array_combine( $this->$name_lc, $this->$name ); + } + } + + /** + * Groups of classes to restrict. + * + * @since 3.0.0 + * + * @return array + */ + public function getGroups() { + $groups = array(); + foreach ( $this->class_groups as $name ) { + $name_lc = $name . '_lc'; + $groups[ $name ] = array( + 'classes' => $this->$name_lc, + ); + } + + return $groups; + } + + /** + * Process a matched token. + * + * @since 3.0.0 + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. Will + * always be 'wp_classes'. + * @param string $matched_content The token content (class name) which was matched. + * in its original case. + * + * @return void + */ + public function process_matched_token( $stackPtr, $group_name, $matched_content ) { + + $matched_unqualified = ltrim( $matched_content, '\\' ); + $matched_lowercase = strtolower( $matched_unqualified ); + $matched_proper_case = $this->get_proper_case( $matched_lowercase ); + + if ( $matched_unqualified === $matched_proper_case ) { + // Already using proper case, nothing to do. + return; + } + + $warning = 'It is strongly recommended to refer to classes by their properly cased name. Expected: %s Found: %s'; + $data = array( + $matched_proper_case, + $matched_unqualified, + ); + + $this->phpcsFile->addWarning( $warning, $stackPtr, 'Incorrect', $data ); + } + + /** + * Match a lowercase class name to its proper cased name. + * + * @since 3.0.0 + * + * @param string $matched_lc Lowercase class name. + * + * @return string + */ + private function get_proper_case( $matched_lc ) { + foreach ( $this->class_groups as $name ) { + $current = $this->$name; // Needed to prevent issues with PHP < 7.0. + if ( isset( $current[ $matched_lc ] ) ) { + return $current[ $matched_lc ]; + } + } + + // Shouldn't be possible. + return ''; // @codeCoverageIgnore + } +} diff --git a/WordPress/Sniffs/WP/CronIntervalSniff.php b/WordPress/Sniffs/WP/CronIntervalSniff.php index 7cf2dd868d..fd48e1a2f2 100644 --- a/WordPress/Sniffs/WP/CronIntervalSniff.php +++ b/WordPress/Sniffs/WP/CronIntervalSniff.php @@ -3,30 +3,35 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\WP; +namespace WordPressCS\WordPress\Sniffs\WP; -use WordPress\Sniff; -use PHP_CodeSniffer_Tokens as Tokens; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\Arrays; +use PHPCSUtils\Utils\FunctionDeclarations; +use PHPCSUtils\Utils\Numbers; +use PHPCSUtils\Utils\PassedParameters; +use PHPCSUtils\Utils\TextStrings; +use WordPressCS\WordPress\Helpers\ContextHelper; +use WordPressCS\WordPress\Sniff; /** * Flag cron schedules less than 15 minutes. * - * @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#cron-schedules-less-than-15-minutes-or-expensive-events + * @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#cron-schedules-less-than-15-minutes-or-expensive-events * - * @package WPCS\WordPressCodingStandards - * - * @since 0.3.0 - * @since 0.11.0 - Extends the WordPress_Sniff class. - * - Now deals correctly with WP time constants. - * @since 0.13.0 Class name changed: this class is now namespaced. - * @since 0.14.0 The minimum cron interval tested against is now configurable. - * @since 1.0.0 This sniff has been moved from the `VIP` category to the `WP` category. + * @since 0.3.0 + * @since 0.11.0 - Extends the WordPressCS native `Sniff` class. + * - Now deals correctly with WP time constants. + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.14.0 The minimum cron interval tested against is now configurable. + * @since 1.0.0 This sniff has been moved from the `VIP` category to the `WP` category. */ -class CronIntervalSniff extends Sniff { +final class CronIntervalSniff extends Sniff { /** * Minimum allowed cron interval in seconds. @@ -55,16 +60,22 @@ class CronIntervalSniff extends Sniff { 'YEAR_IN_SECONDS' => 31536000, ); + /** + * Function within which the hook should be found. + * + * @var array + */ + protected $valid_functions = array( + 'add_filter' => true, + ); + /** * Returns an array of tokens this test wants to listen for. * * @return array */ public function register() { - return array( - \T_CONSTANT_ENCAPSED_STRING, - \T_DOUBLE_QUOTED_STRING, - ); + return Tokens::$stringTokens; } /** @@ -77,30 +88,37 @@ public function register() { public function process_token( $stackPtr ) { $token = $this->tokens[ $stackPtr ]; - if ( 'cron_schedules' !== $this->strip_quotes( $token['content'] ) ) { + if ( 'cron_schedules' !== TextStrings::stripQuotes( $token['content'] ) ) { return; } - // If within add_filter. - $functionPtr = $this->phpcsFile->findPrevious( \T_STRING, key( $token['nested_parenthesis'] ) ); - if ( false === $functionPtr || 'add_filter' !== $this->tokens[ $functionPtr ]['content'] ) { + // Check if the text was found within a function call to add_filter(). + $functionPtr = ContextHelper::is_in_function_call( $this->phpcsFile, $stackPtr, $this->valid_functions ); + if ( false === $functionPtr ) { return; } - $callback = $this->get_function_call_parameter( $functionPtr, 2 ); + $callback = PassedParameters::getParameter( $this->phpcsFile, $functionPtr, 2, 'callback' ); if ( false === $callback ) { return; } + if ( $stackPtr >= $callback['start'] && $stackPtr <= $callback['end'] ) { + // "cron_schedules" found in the second parameter, not the first. + return; + } + // Detect callback function name. $callbackArrayPtr = $this->phpcsFile->findNext( Tokens::$emptyTokens, $callback['start'], ( $callback['end'] + 1 ), true ); // If callback is array, get second element. if ( false !== $callbackArrayPtr && ( \T_ARRAY === $this->tokens[ $callbackArrayPtr ]['code'] - || \T_OPEN_SHORT_ARRAY === $this->tokens[ $callbackArrayPtr ]['code'] ) + || ( isset( Collections::shortArrayListOpenTokensBC()[ $this->tokens[ $callbackArrayPtr ]['code'] ] ) + && Arrays::isShortArray( $this->phpcsFile, $callbackArrayPtr ) === true ) + ) ) { - $callback = $this->get_function_call_parameter( $callbackArrayPtr, 2 ); + $callback = PassedParameters::getParameter( $this->phpcsFile, $callbackArrayPtr, 2 ); if ( false === $callback ) { $this->confused( $stackPtr ); @@ -111,30 +129,44 @@ public function process_token( $stackPtr ) { unset( $functionPtr ); // Search for the function in tokens. - $callbackFunctionPtr = $this->phpcsFile->findNext( array( \T_CONSTANT_ENCAPSED_STRING, \T_DOUBLE_QUOTED_STRING, \T_CLOSURE ), $callback['start'], ( $callback['end'] + 1 ) ); + $search = Tokens::$stringTokens; + $search[ \T_CLOSURE ] = \T_CLOSURE; + $search[ \T_FN ] = \T_FN; + $search[ \T_ELLIPSIS ] = \T_ELLIPSIS; + $callbackFunctionPtr = $this->phpcsFile->findNext( $search, $callback['start'], ( $callback['end'] + 1 ) ); if ( false === $callbackFunctionPtr ) { $this->confused( $stackPtr ); return; } - if ( \T_CLOSURE === $this->tokens[ $callbackFunctionPtr ]['code'] ) { + if ( \T_CLOSURE === $this->tokens[ $callbackFunctionPtr ]['code'] + || \T_FN === $this->tokens[ $callbackFunctionPtr ]['code'] + ) { $functionPtr = $callbackFunctionPtr; - } else { - $functionName = $this->strip_quotes( $this->tokens[ $callbackFunctionPtr ]['content'] ); - - for ( $ptr = 0; $ptr < $this->phpcsFile->numTokens; $ptr++ ) { - if ( \T_FUNCTION === $this->tokens[ $ptr ]['code'] ) { - $foundName = $this->phpcsFile->getDeclarationName( $ptr ); - if ( $foundName === $functionName ) { - $functionPtr = $ptr; - break; - } elseif ( isset( $this->tokens[ $ptr ]['scope_closer'] ) ) { - // Skip to the end of the function definition. - $ptr = $this->tokens[ $ptr ]['scope_closer']; + } elseif ( \T_ELLIPSIS === $this->tokens[ $callbackFunctionPtr ]['code'] ) { + // Check if this is a PHP 8.1 first class callable. + $before = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $callbackFunctionPtr - 1 ), null, true ); + $after = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $callbackFunctionPtr + 1 ), null, true ); + if ( ( false !== $before && \T_OPEN_PARENTHESIS === $this->tokens[ $before ]['code'] ) + && ( false !== $after && \T_CLOSE_PARENTHESIS === $this->tokens[ $after ]['code'] ) + ) { + // Ok, now see if we can find the function name. + $beforeOpen = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $before - 1 ), null, true ); + if ( false !== $beforeOpen && \T_STRING === $this->tokens[ $beforeOpen ]['code'] ) { + $found_function = $this->find_function_by_name( $this->tokens[ $beforeOpen ]['content'] ); + if ( false !== $found_function ) { + $functionPtr = $found_function; } } } + unset( $before, $after, $beforeOpen ); + } else { + $functionName = TextStrings::stripQuotes( $this->tokens[ $callbackFunctionPtr ]['content'] ); + $found_function = $this->find_function_by_name( $functionName ); + if ( false !== $found_function ) { + $functionPtr = $found_function; + } } if ( ! isset( $functionPtr ) ) { @@ -150,24 +182,65 @@ public function process_token( $stackPtr ) { $closing = $this->tokens[ $functionPtr ]['scope_closer']; for ( $i = $opening; $i <= $closing; $i++ ) { - if ( \in_array( $this->tokens[ $i ]['code'], array( \T_CONSTANT_ENCAPSED_STRING, \T_DOUBLE_QUOTED_STRING ), true ) ) { - if ( 'interval' === $this->strip_quotes( $this->tokens[ $i ]['content'] ) ) { + if ( isset( Tokens::$stringTokens[ $this->tokens[ $i ]['code'] ] ) === true ) { + if ( 'interval' === TextStrings::stripQuotes( $this->tokens[ $i ]['content'] ) ) { $operator = $this->phpcsFile->findNext( \T_DOUBLE_ARROW, $i, null, false, null, true ); if ( false === $operator ) { $this->confused( $stackPtr ); return; } - $valueStart = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $operator + 1 ), null, true, null, true ); - $valueEnd = $this->phpcsFile->findNext( array( \T_COMMA, \T_CLOSE_PARENTHESIS ), ( $valueStart + 1 ) ); - $value = ''; - for ( $j = $valueStart; $j < $valueEnd; $j++ ) { + $valueStart = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $operator + 1 ), null, true, null, true ); + $valueEnd = $this->phpcsFile->findNext( array( \T_COMMA, \T_CLOSE_PARENTHESIS ), ( $valueStart + 1 ) ); + $value = ''; + $parentheses_count = 0; + for ( $j = $valueStart; $j <= $valueEnd; $j++ ) { if ( isset( Tokens::$emptyTokens[ $this->tokens[ $j ]['code'] ] ) ) { continue; } + + if ( \T_NS_SEPARATOR === $this->tokens[ $j ]['code'] ) { + $value .= ' '; + continue; + } + + if ( $j === $valueEnd && \T_COMMA === $this->tokens[ $j ]['code'] ) { + break; + } + + // Make sure that PHP 7.4 numeric literals and PHP 8.1 explicit octals don't cause problems. + if ( \T_LNUMBER === $this->tokens[ $j ]['code'] + || \T_DNUMBER === $this->tokens[ $j ]['code'] + ) { + $number_info = Numbers::getCompleteNumber( $this->phpcsFile, $j ); + $value .= $number_info['decimal']; + $j = $number_info['last_token']; + continue; + } + + if ( \T_OPEN_PARENTHESIS === $this->tokens[ $j ]['code'] ) { + $value .= $this->tokens[ $j ]['content']; + ++$parentheses_count; + continue; + } + + if ( \T_CLOSE_PARENTHESIS === $this->tokens[ $j ]['code'] ) { + // Only add a close parenthesis if there are open parentheses. + if ( $parentheses_count > 0 ) { + $value .= $this->tokens[ $j ]['content']; + --$parentheses_count; + } + continue; + } + $value .= $this->tokens[ $j ]['content']; } + if ( $parentheses_count > 0 ) { + // Make sure all open parenthesis are closed. + $value .= str_repeat( ')', $parentheses_count ); + } + if ( is_numeric( $value ) ) { $interval = $value; break; @@ -176,9 +249,9 @@ public function process_token( $stackPtr ) { // Deal correctly with WP time constants. $value = str_replace( array_keys( $this->wp_time_constants ), array_values( $this->wp_time_constants ), $value ); - // If all digits and operators, eval! - if ( preg_match( '#^[\s\d+*/-]+$#', $value ) > 0 ) { - $interval = eval( "return ( $value );" ); // @codingStandardsIgnoreLine - No harm here. + // If all parentheses, digits and operators, eval! + if ( preg_match( '#^[\s\d()+*/-]+$#', $value ) > 0 ) { + $interval = eval( "return ( $value );" ); // phpcs:ignore Squiz.PHP.Eval -- No harm here. break; } @@ -205,10 +278,38 @@ public function process_token( $stackPtr ) { } } + /** + * Find a declared function in a file based on the function name. + * + * @param string $functionName The name of the function to find. + * + * @return int|false Integer stack pointer to the function keyword token or + * false if not found. + */ + private function find_function_by_name( $functionName ) { + $functionPtr = false; + for ( $ptr = 0; $ptr < $this->phpcsFile->numTokens; $ptr++ ) { + if ( \T_FUNCTION === $this->tokens[ $ptr ]['code'] ) { + $foundName = FunctionDeclarations::getName( $this->phpcsFile, $ptr ); + if ( $foundName === $functionName ) { + $functionPtr = $ptr; + break; + } elseif ( isset( $this->tokens[ $ptr ]['scope_closer'] ) ) { + // Skip to the end of the function definition. + $ptr = $this->tokens[ $ptr ]['scope_closer']; + } + } + } + + return $functionPtr; + } + /** * Add warning about unclear cron schedule change. * * @param int $stackPtr The position of the current token in the stack. + * + * @return void */ public function confused( $stackPtr ) { $this->phpcsFile->addWarning( @@ -217,5 +318,4 @@ public function confused( $stackPtr ) { 'ChangeDetected' ); } - } diff --git a/WordPress/Sniffs/WP/DeprecatedClassesSniff.php b/WordPress/Sniffs/WP/DeprecatedClassesSniff.php index 9d46fddc70..4c37043468 100644 --- a/WordPress/Sniffs/WP/DeprecatedClassesSniff.php +++ b/WordPress/Sniffs/WP/DeprecatedClassesSniff.php @@ -3,13 +3,15 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\WP; +namespace WordPressCS\WordPress\Sniffs\WP; -use WordPress\AbstractClassRestrictionsSniff; +use PHPCSUtils\Utils\MessageHelper; +use WordPressCS\WordPress\AbstractClassRestrictionsSniff; +use WordPressCS\WordPress\Helpers\MinimumWPVersionTrait; /** * Restricts the use of deprecated WordPress classes and suggests alternatives. @@ -20,17 +22,17 @@ * By default, it is set to presume that a project will support the current * WP version and up to three releases before. * - * @package WPCS\WordPressCodingStandards - * - * @since 0.12.0 - * @since 0.13.0 Class name changed: this class is now namespaced. - * @since 0.14.0 Now has the ability to handle minimum supported WP version - * being provided via the command-line or as as value - * in a custom ruleset. + * @since 0.12.0 + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.14.0 Now has the ability to handle minimum supported WP version + * being provided via the command-line or as as value + * in a custom ruleset. * - * @uses \WordPress\Sniff::$minimum_supported_version + * @uses \WordPressCS\WordPress\Helpers\MinimumWPVersionTrait::$minimum_wp_version */ -class DeprecatedClassesSniff extends AbstractClassRestrictionsSniff { +final class DeprecatedClassesSniff extends AbstractClassRestrictionsSniff { + + use MinimumWPVersionTrait; /** * List of deprecated classes with alternative when available. @@ -39,6 +41,8 @@ class DeprecatedClassesSniff extends AbstractClassRestrictionsSniff { * * Version numbers should be fully qualified. * + * Last update: July 2023 for WP 6.3 at https://github.com/WordPress/wordpress-develop/commit/6281ce432c50345a57768bf53854d9b65b6cdd52 + * * @var array */ private $deprecated_classes = array( @@ -48,8 +52,39 @@ class DeprecatedClassesSniff extends AbstractClassRestrictionsSniff { 'alt' => 'WP_User_Query', 'version' => '3.1.0', ), - ); + // WP 3.7.0. + 'WP_HTTP_Fsockopen' => array( + 'alt' => 'WP_HTTP::request()', + 'version' => '3.7.0', + ), + + // WP 4.9.0. + 'WP_Customize_New_Menu_Section' => array( + 'version' => '4.9.0', + ), + 'WP_Customize_New_Menu_Control' => array( + 'version' => '4.9.0', + ), + + // WP 5.3.0. + 'WP_Privacy_Data_Export_Requests_Table' => array( + 'alt' => 'WP_Privacy_Data_Export_Requests_List_Table', + 'version' => '5.3.0', + ), + 'WP_Privacy_Data_Removal_Requests_Table' => array( + 'alt' => 'WP_Privacy_Data_Removal_Requests_List_Table', + 'version' => '5.3.0', + ), + 'Services_JSON' => array( + 'alt' => 'The PHP native JSON extension', + 'version' => '5.3.0', + ), + 'Services_JSON_Error' => array( + 'alt' => 'The PHP native JSON extension', + 'version' => '5.3.0', + ), + ); /** * Groups of classes to restrict. @@ -58,13 +93,11 @@ class DeprecatedClassesSniff extends AbstractClassRestrictionsSniff { */ public function getGroups() { // Make sure all array keys are lowercase. - $keys = array_keys( $this->deprecated_classes ); - $keys = array_map( 'strtolower', $keys ); - $this->deprecated_classes = array_combine( $keys, $this->deprecated_classes ); + $this->deprecated_classes = array_change_key_case( $this->deprecated_classes, \CASE_LOWER ); return array( 'deprecated_classes' => array( - 'classes' => $keys, + 'classes' => array_keys( $this->deprecated_classes ), ), ); } @@ -73,15 +106,16 @@ public function getGroups() { * Process a matched token. * * @param int $stackPtr The position of the current token in the stack. - * @param array $group_name The name of the group which was matched. Will - * always be 'deprecated_functions'. - * @param string $matched_content The token content (class name) which was matched. + * @param string $group_name The name of the group which was matched. Will + * always be 'deprecated_classes'. + * @param string $matched_content The token content (class name) which was matched + * in its original case. * * @return void */ public function process_matched_token( $stackPtr, $group_name, $matched_content ) { - $this->get_wp_version_from_cl(); + $this->set_minimum_wp_version(); $class_name = ltrim( strtolower( $matched_content ), '\\' ); @@ -96,13 +130,13 @@ public function process_matched_token( $stackPtr, $group_name, $matched_content $data[] = $this->deprecated_classes[ $class_name ]['alt']; } - $this->addMessage( + MessageHelper::addMessage( + $this->phpcsFile, $message, $stackPtr, - ( version_compare( $this->deprecated_classes[ $class_name ]['version'], $this->minimum_supported_version, '<' ) ), - $this->string_to_errorcode( $class_name . 'Found' ), + ( $this->wp_version_compare( $this->deprecated_classes[ $class_name ]['version'], $this->minimum_wp_version, '<' ) ), + MessageHelper::stringToErrorcode( $class_name . 'Found' ), $data ); } - } diff --git a/WordPress/Sniffs/WP/DeprecatedFunctionsSniff.php b/WordPress/Sniffs/WP/DeprecatedFunctionsSniff.php index df4ccdaeb4..fc7af255e7 100644 --- a/WordPress/Sniffs/WP/DeprecatedFunctionsSniff.php +++ b/WordPress/Sniffs/WP/DeprecatedFunctionsSniff.php @@ -3,13 +3,15 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\WP; +namespace WordPressCS\WordPress\Sniffs\WP; -use WordPress\AbstractFunctionRestrictionsSniff; +use PHPCSUtils\Utils\MessageHelper; +use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff; +use WordPressCS\WordPress\Helpers\MinimumWPVersionTrait; /** * Restricts the use of various deprecated WordPress functions and suggests alternatives. @@ -20,23 +22,23 @@ * By default, it is set to presume that a project will support the current * WP version and up to three releases before. * - * @package WPCS\WordPressCodingStandards - * - * @since 0.11.0 - * @since 0.13.0 Class name changed: this class is now namespaced. - * @since 0.14.0 Now has the ability to handle minimum supported WP version - * being provided via the command-line or as as value - * in a custom ruleset. + * @since 0.11.0 + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.14.0 Now has the ability to handle minimum supported WP version + * being provided via the command-line or as as value + * in a custom ruleset. * - * @uses \WordPress\Sniff::$minimum_supported_version + * @uses \WordPressCS\WordPress\Helpers\MinimumWPVersionTrait::$minimum_wp_version */ -class DeprecatedFunctionsSniff extends AbstractFunctionRestrictionsSniff { +final class DeprecatedFunctionsSniff extends AbstractFunctionRestrictionsSniff { + + use MinimumWPVersionTrait; /** * List of deprecated functions with alternative when available. * * To be updated after every major release. - * Last updated for WordPress 4.8. + * Last updated for WordPress 6.3. * * Version numbers should be fully qualified. * Replacement functions should have parentheses. @@ -47,7 +49,6 @@ class DeprecatedFunctionsSniff extends AbstractFunctionRestrictionsSniff { * @var array */ private $deprecated_functions = array( - // WP 0.71. 'the_category_head' => array( 'alt' => 'get_the_category_by_ID()', @@ -406,10 +407,6 @@ class DeprecatedFunctionsSniff extends AbstractFunctionRestrictionsSniff { 'alt' => 'wp_register_widget_control()', 'version' => '2.8.0', ), - 'sanitize_url' => array( - 'alt' => 'esc_url_raw()', - 'version' => '2.8.0', - ), 'the_author_aim' => array( 'alt' => 'the_author_meta(\'aim\')', 'version' => '2.8.0', @@ -579,6 +576,11 @@ class DeprecatedFunctionsSniff extends AbstractFunctionRestrictionsSniff { 'alt' => 'wp_die()', 'version' => '3.0.0', ), + // Verified version & alternative. + 'install_blog_defaults' => array( + 'alt' => 'wp_install_defaults', + 'version' => '3.0.0', + ), 'is_main_blog' => array( 'alt' => 'is_main_site()', 'version' => '3.0.0', @@ -979,7 +981,7 @@ class DeprecatedFunctionsSniff extends AbstractFunctionRestrictionsSniff { 'version' => '3.5.0', ), 'wp_cache_reset' => array( - 'alt' => 'WP_Object_Cache::reset()', + 'alt' => 'wp_cache_switch_to_blog()', 'version' => '3.5.0', ), 'wp_create_thumbnail' => array( @@ -1330,6 +1332,271 @@ class DeprecatedFunctionsSniff extends AbstractFunctionRestrictionsSniff { 'alt' => '', 'version' => '4.9.0', ), + + // WP 5.1.0. + 'insert_blog' => array( + 'alt' => 'wp_insert_site()', + 'version' => '5.1.0', + ), + 'install_blog' => array( + 'alt' => '', + 'version' => '5.1.0', + ), + + // WP 5.3.0. + '_wp_json_prepare_data' => array( + 'alt' => '', + 'version' => '5.3.0', + ), + '_wp_privacy_requests_screen_options' => array( + 'alt' => '', + 'version' => '5.3.0', + ), + 'update_user_status' => array( + 'alt' => 'wp_update_user()', + 'version' => '5.3.0', + ), + + // WP 5.4.0. + 'wp_get_user_request_data' => array( + 'alt' => 'wp_get_user_request()', + 'version' => '5.4.0', + ), + + // WP 5.5.0. + '_wp_register_meta_args_whitelist' => array( + 'alt' => '_wp_register_meta_args_allowed_list()', + 'version' => '5.5.0', + ), + 'add_option_whitelist' => array( + 'alt' => 'add_allowed_options()', + 'version' => '5.5.0', + ), + 'remove_option_whitelist' => array( + 'alt' => 'remove_allowed_options()', + 'version' => '5.5.0', + ), + 'wp_blacklist_check' => array( + 'alt' => 'wp_check_comment_disallowed_list()', + 'version' => '5.5.0', + ), + 'wp_make_content_images_responsive' => array( + 'alt' => 'wp_filter_content_tags()', + 'version' => '5.5.0', + ), + 'wp_unregister_GLOBALS' => array( + 'alt' => '', + 'version' => '5.5.0', + ), + + // WP 5.7.0. + 'noindex' => array( + 'alt' => 'wp_robots_noindex()', + 'version' => '5.7.0', + ), + 'wp_no_robots' => array( + 'alt' => 'wp_robots_no_robots()', + 'version' => '5.7.0', + ), + 'wp_sensitive_page_meta' => array( + 'alt' => 'wp_robots_sensitive_page()', + 'version' => '5.7.0', + ), + + // WP 5.8.0. + '_excerpt_render_inner_columns_blocks' => array( + 'alt' => '_excerpt_render_inner_blocks()', + 'version' => '5.8.0', + ), + + // WP 5.9.0. + 'readonly' => array( + 'alt' => 'wp_readonly()', + 'version' => '5.9.0', + ), + + // WP 5.9.1. + 'wp_render_duotone_filter_preset' => array( + 'alt' => 'wp_get_duotone_filter_property()', + 'version' => '5.9.1', + ), + + // WP 6.0.0. + 'image_attachment_fields_to_save' => array( + 'alt' => '', + 'version' => '6.0.0', + ), + 'wp_add_iframed_editor_assets_html' => array( + 'alt' => '', + 'version' => '6.0.0', + ), + 'wp_skip_border_serialization' => array( + 'alt' => 'wp_should_skip_block_supports_serialization()', + 'version' => '6.0.0', + ), + 'wp_skip_dimensions_serialization' => array( + 'alt' => 'wp_should_skip_block_supports_serialization()', + 'version' => '6.0.0', + ), + 'wp_skip_spacing_serialization' => array( + 'alt' => 'wp_should_skip_block_supports_serialization()', + 'version' => '6.0.0', + ), + + // WP 6.0.2. + 'the_meta' => array( + 'alt' => 'get_post_meta()', + 'version' => '6.0.2', + ), + + // WP 6.0.3. + // Verified; see https://core.trac.wordpress.org/ticket/56791#comment:10. + '_filter_query_attachment_filenames' => array( + 'alt' => 'add_filter( "wp_allow_query_attachment_by_filename", "__return_true" )', + 'version' => '6.0.3', + ), + + // WP 6.1.0. + '_get_path_to_translation' => array( + 'alt' => 'WP_Textdomain_Registry', + 'version' => '6.1.0', + ), + '_get_path_to_translation_from_lang_dir' => array( + 'alt' => 'WP_Textdomain_Registry', + 'version' => '6.1.0', + ), + '_wp_multiple_block_styles' => array( + 'alt' => '', + 'version' => '6.1.0', + ), + 'global_terms' => array( + 'alt' => '', + 'version' => '6.1.0', + ), + 'global_terms_enabled' => array( + 'alt' => '', + 'version' => '6.1.0', + ), + 'install_global_terms' => array( + 'alt' => '', + 'version' => '6.1.0', + ), + 'sync_category_tag_slugs' => array( + 'alt' => '', + 'version' => '6.1.0', + ), + 'wp_get_attachment_thumb_file' => array( + 'alt' => '', + 'version' => '6.1.0', + ), + 'wp_typography_get_css_variable_inline_style' => array( + 'alt' => 'wp_style_engine_get_styles()', + 'version' => '6.1.0', + ), + + // WP 6.2.0. + '_resolve_home_block_template' => array( + 'alt' => '', + 'version' => '6.2.0', + ), + 'get_page_by_title' => array( + 'alt' => 'WP_Query', + 'version' => '6.2.0', + ), + + // WP 6.3.0. + '_wp_tinycolor_bound_alpha' => array( + 'alt' => '', + 'version' => '6.3.0', + ), + 'block_core_navigation_get_classic_menu_fallback' => array( + 'alt' => 'WP_Navigation_Fallback::get_classic_menu_fallback', + 'version' => '6.3.0', + ), + 'block_core_navigation_get_classic_menu_fallback_blocks' => array( + 'alt' => 'WP_Navigation_Fallback::get_classic_menu_fallback_blocks', + 'version' => '6.3.0', + ), + 'block_core_navigation_get_most_recently_published_navigation' => array( + 'alt' => 'WP_Navigation_Fallback::get_most_recently_published_navigation', + 'version' => '6.3.0', + ), + 'block_core_navigation_maybe_use_classic_menu_fallback' => array( + 'alt' => 'WP_Navigation_Fallback::create_classic_menu_fallback', + 'version' => '6.3.0', + ), + 'block_core_navigation_parse_blocks_from_menu_items' => array( + 'alt' => 'WP_Navigation_Fallback::parse_blocks_from_menu_items', + 'version' => '6.3.0', + ), + 'block_core_navigation_submenu_build_css_colors' => array( + 'alt' => 'wp_apply_colors_support()', + 'version' => '6.3.0', + ), + 'wlwmanifest_link' => array( + 'alt' => '', + 'version' => '6.3.0', + ), + 'wp_get_duotone_filter_id' => array( + 'alt' => '', + 'version' => '6.3.0', + ), + 'wp_get_duotone_filter_property' => array( + 'alt' => '', + 'version' => '6.3.0', + ), + 'wp_get_duotone_filter_svg' => array( + 'alt' => '', + 'version' => '6.3.0', + ), + 'wp_get_global_styles_svg_filters' => array( + 'alt' => '', + 'version' => '6.3.0', + ), + 'wp_get_loading_attr_default' => array( + 'alt' => 'wp_get_loading_optimization_attributes()', + 'version' => '6.3.0', + ), + 'wp_global_styles_render_svg_filters' => array( + 'alt' => '', + 'version' => '6.3.0', + ), + 'wp_img_tag_add_loading_attr' => array( + 'alt' => 'wp_img_tag_add_loading_optimization_attrs()', + 'version' => '6.3.0', + ), + 'wp_queue_comments_for_comment_meta_lazyload' => array( + 'alt' => 'wp_lazyload_comment_meta()', + 'version' => '6.3.0', + ), + 'wp_register_duotone_support' => array( + 'alt' => 'WP_Duotone::register_duotone_support()', + 'version' => '6.3.0', + ), + 'wp_render_duotone_support' => array( + 'alt' => 'WP_Duotone::render_duotone_support()', + 'version' => '6.3.0', + ), + 'wp_tinycolor_bound01' => array( + 'alt' => '', + 'version' => '6.3.0', + ), + 'wp_tinycolor_hsl_to_rgb' => array( + 'alt' => '', + 'version' => '6.3.0', + ), + 'wp_tinycolor_hue_to_rgb' => array( + 'alt' => '', + 'version' => '6.3.0', + ), + 'wp_tinycolor_rgb_to_rgb' => array( + 'alt' => '', + 'version' => '6.3.0', + ), + 'wp_tinycolor_string_to_rgb' => array( + 'alt' => '', + 'version' => '6.3.0', + ), ); /** @@ -1339,13 +1606,11 @@ class DeprecatedFunctionsSniff extends AbstractFunctionRestrictionsSniff { */ public function getGroups() { // Make sure all array keys are lowercase. - $keys = array_keys( $this->deprecated_functions ); - $keys = array_map( 'strtolower', $keys ); - $this->deprecated_functions = array_combine( $keys, $this->deprecated_functions ); + $this->deprecated_functions = array_change_key_case( $this->deprecated_functions, \CASE_LOWER ); return array( 'deprecated_functions' => array( - 'functions' => $keys, + 'functions' => array_keys( $this->deprecated_functions ), ), ); } @@ -1354,36 +1619,35 @@ public function getGroups() { * Process a matched token. * * @param int $stackPtr The position of the current token in the stack. - * @param array $group_name The name of the group which was matched. Will + * @param string $group_name The name of the group which was matched. Will * always be 'deprecated_functions'. - * @param string $matched_content The token content (function name) which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. * * @return void */ public function process_matched_token( $stackPtr, $group_name, $matched_content ) { - $this->get_wp_version_from_cl(); - - $function_name = strtolower( $matched_content ); + $this->set_minimum_wp_version(); $message = '%s() has been deprecated since WordPress version %s.'; $data = array( - $matched_content, - $this->deprecated_functions[ $function_name ]['version'], + $this->tokens[ $stackPtr ]['content'], + $this->deprecated_functions[ $matched_content ]['version'], ); - if ( ! empty( $this->deprecated_functions[ $function_name ]['alt'] ) ) { + if ( ! empty( $this->deprecated_functions[ $matched_content ]['alt'] ) ) { $message .= ' Use %s instead.'; - $data[] = $this->deprecated_functions[ $function_name ]['alt']; + $data[] = $this->deprecated_functions[ $matched_content ]['alt']; } - $this->addMessage( + MessageHelper::addMessage( + $this->phpcsFile, $message, $stackPtr, - ( version_compare( $this->deprecated_functions[ $function_name ]['version'], $this->minimum_supported_version, '<' ) ), - $this->string_to_errorcode( $matched_content . 'Found' ), + ( $this->wp_version_compare( $this->deprecated_functions[ $matched_content ]['version'], $this->minimum_wp_version, '<' ) ), + MessageHelper::stringToErrorcode( $matched_content . 'Found' ), $data ); } - } diff --git a/WordPress/Sniffs/WP/DeprecatedParameterValuesSniff.php b/WordPress/Sniffs/WP/DeprecatedParameterValuesSniff.php index f49d7de011..0806772be4 100644 --- a/WordPress/Sniffs/WP/DeprecatedParameterValuesSniff.php +++ b/WordPress/Sniffs/WP/DeprecatedParameterValuesSniff.php @@ -3,25 +3,29 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\WP; +namespace WordPressCS\WordPress\Sniffs\WP; -use WordPress\AbstractFunctionParameterSniff; -use PHP_CodeSniffer_Tokens as Tokens; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Utils\MessageHelper; +use PHPCSUtils\Utils\PassedParameters; +use PHPCSUtils\Utils\TextStrings; +use WordPressCS\WordPress\AbstractFunctionParameterSniff; +use WordPressCS\WordPress\Helpers\MinimumWPVersionTrait; /** * Check for usage of deprecated parameter values in WP functions and provide alternative based on the parameter passed. * - * @package WPCS\WordPressCodingStandards - * - * @since 1.0.0 + * @since 1.0.0 * - * @uses \WordPress\Sniff::$minimum_supported_version + * @uses \WordPressCS\WordPress\Helpers\MinimumWPVersionTrait::$minimum_wp_version */ -class DeprecatedParameterValuesSniff extends AbstractFunctionParameterSniff { +final class DeprecatedParameterValuesSniff extends AbstractFunctionParameterSniff { + + use MinimumWPVersionTrait; /** * The group name for this group of functions. @@ -41,97 +45,165 @@ class DeprecatedParameterValuesSniff extends AbstractFunctionParameterSniff { * Last updated for WordPress 4.9.6. * * @since 1.0.0 + * @since 3.0.0 The format of the value has changed to support function calls + * using named parameters. * * @var array Multidimensional array with parameter details. * $target_functions = array( * (string) Function name. => array( * (int) Target parameter position, 1-based. => array( - * (string) Parameter value. => array( - * 'alt' => (string) Suggested alternative. - * 'version' => (int) The WordPress version when deprecated. + * (string) 'name' => (string|array) Parameter name(s), + * (string) 'values' => array( + * (string) Parameter value. => array( + * 'alt' => (string) Suggested alternative. + * 'version' => (int) The WordPress version when deprecated. + * ) * ) * ) * ) * ); */ protected $target_functions = array( + 'add_option' => array( + 1 => array( + 'name' => 'option', + 'values' => array( + 'blacklist_keys' => array( + 'alt' => 'disallowed_keys', + 'version' => '5.5.0', + ), + 'comment_whitelist' => array( + 'alt' => 'comment_previously_approved', + 'version' => '5.5.0', + ), + ), + ), + ), 'add_settings_field' => array( 4 => array( - 'misc' => array( - 'alt' => 'another settings group', - 'version' => '3.0.0', - ), - 'privacy' => array( - 'alt' => 'another settings group', - 'version' => '3.5.0', + 'name' => 'page', + 'values' => array( + 'misc' => array( + 'alt' => 'another settings group', + 'version' => '3.0.0', + ), + 'privacy' => array( + 'alt' => 'another settings group', + 'version' => '3.5.0', + ), ), ), ), 'add_settings_section' => array( 4 => array( - 'misc' => array( - 'alt' => 'another settings group', - 'version' => '3.0.0', - ), - 'privacy' => array( - 'alt' => 'another settings group', - 'version' => '3.5.0', + 'name' => 'page', + 'values' => array( + 'misc' => array( + 'alt' => 'another settings group', + 'version' => '3.0.0', + ), + 'privacy' => array( + 'alt' => 'another settings group', + 'version' => '3.5.0', + ), ), ), ), 'bloginfo' => array( 1 => array( - 'home' => array( - 'alt' => 'the "url" argument', - 'version' => '2.2.0', - ), - 'siteurl' => array( - 'alt' => 'the "url" argument', - 'version' => '2.2.0', - ), - 'text_direction' => array( - 'alt' => 'is_rtl()', - 'version' => '2.2.0', + 'name' => 'show', + 'values' => array( + 'home' => array( + 'alt' => 'the "url" argument', + 'version' => '2.2.0', + ), + 'siteurl' => array( + 'alt' => 'the "url" argument', + 'version' => '2.2.0', + ), + 'text_direction' => array( + 'alt' => 'is_rtl()', + 'version' => '2.2.0', + ), ), ), ), 'get_bloginfo' => array( 1 => array( - 'home' => array( - 'alt' => 'the "url" argument', - 'version' => '2.2.0', + 'name' => 'show', + 'values' => array( + 'home' => array( + 'alt' => 'the "url" argument', + 'version' => '2.2.0', + ), + 'siteurl' => array( + 'alt' => 'the "url" argument', + 'version' => '2.2.0', + ), + 'text_direction' => array( + 'alt' => 'is_rtl()', + 'version' => '2.2.0', + ), ), - 'siteurl' => array( - 'alt' => 'the "url" argument', - 'version' => '2.2.0', - ), - 'text_direction' => array( - 'alt' => 'is_rtl()', - 'version' => '2.2.0', + ), + ), + 'get_option' => array( + 1 => array( + 'name' => 'option', + 'values' => array( + 'blacklist_keys' => array( + 'alt' => 'disallowed_keys', + 'version' => '5.5.0', + ), + 'comment_whitelist' => array( + 'alt' => 'comment_previously_approved', + 'version' => '5.5.0', + ), ), ), ), 'register_setting' => array( 1 => array( - 'misc' => array( - 'alt' => 'another settings group', - 'version' => '3.0.0', - ), - 'privacy' => array( - 'alt' => 'another settings group', - 'version' => '3.5.0', + 'name' => 'option_group', + 'values' => array( + 'misc' => array( + 'alt' => 'another settings group', + 'version' => '3.0.0', + ), + 'privacy' => array( + 'alt' => 'another settings group', + 'version' => '3.5.0', + ), ), ), ), 'unregister_setting' => array( 1 => array( - 'misc' => array( - 'alt' => 'another settings group', - 'version' => '3.0.0', + 'name' => 'option_group', + 'values' => array( + 'misc' => array( + 'alt' => 'another settings group', + 'version' => '3.0.0', + ), + 'privacy' => array( + 'alt' => 'another settings group', + 'version' => '3.5.0', + ), ), - 'privacy' => array( - 'alt' => 'another settings group', - 'version' => '3.5.0', + ), + ), + 'update_option' => array( + 1 => array( + 'name' => 'option', + 'values' => array( + 'blacklist_keys' => array( + 'alt' => 'disallowed_keys', + 'version' => '5.5.0', + ), + 'comment_whitelist' => array( + 'alt' => 'comment_previously_approved', + 'version' => '5.5.0', + ), ), ), ), @@ -143,23 +215,25 @@ class DeprecatedParameterValuesSniff extends AbstractFunctionParameterSniff { * @since 1.0.0 * * @param int $stackPtr The position of the current token in the stack. - * @param array $group_name The name of the group which was matched. - * @param string $matched_content The token content (function name) which was matched. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. * @param array $parameters Array with information about the parameters. * * @return void */ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { - $this->get_wp_version_from_cl(); - $param_count = \count( $parameters ); + $this->set_minimum_wp_version(); + foreach ( $this->target_functions[ $matched_content ] as $position => $parameter_args ) { + $found_param = PassedParameters::getParameterFromStack( $parameters, $position, $parameter_args['name'] ); - // Stop if the position is higher then the total number of parameters. - if ( $position > $param_count ) { - break; + // Skip if the parameter was not found. + if ( false === $found_param ) { + continue; } - $this->process_parameter( $matched_content, $parameters[ $position ], $parameter_args ); + $this->process_parameter( $matched_content, $found_param, $parameter_args['values'] ); } } @@ -168,7 +242,8 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p * * @since 1.0.0 * - * @param string $matched_content The token content (function name) which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. * @param array $parameter Array with start and end token positon of the parameter. * @param array $parameter_args Array with alternative and WordPress deprecation version of the parameter. * @@ -187,7 +262,7 @@ protected function process_parameter( $matched_content, $parameter, $parameter_a return; } - $matched_parameter = $this->strip_quotes( $this->tokens[ $parameter_position ]['content'] ); + $matched_parameter = TextStrings::stripQuotes( $this->tokens[ $parameter_position ]['content'] ); if ( ! isset( $parameter_args[ $matched_parameter ] ) ) { return; } @@ -203,14 +278,14 @@ protected function process_parameter( $matched_content, $parameter, $parameter_a $data[] = $parameter_args[ $matched_parameter ]['alt']; } - $is_error = version_compare( $parameter_args[ $matched_parameter ]['version'], $this->minimum_supported_version, '<' ); - $this->addMessage( + $is_error = $this->wp_version_compare( $parameter_args[ $matched_parameter ]['version'], $this->minimum_wp_version, '<' ); + MessageHelper::addMessage( + $this->phpcsFile, $message, $parameter_position, $is_error, - $this->string_to_errorcode( 'Found' ), + 'Found', $data ); } - } diff --git a/WordPress/Sniffs/WP/DeprecatedParametersSniff.php b/WordPress/Sniffs/WP/DeprecatedParametersSniff.php index 4772cb08e1..2675ecfae8 100644 --- a/WordPress/Sniffs/WP/DeprecatedParametersSniff.php +++ b/WordPress/Sniffs/WP/DeprecatedParametersSniff.php @@ -3,13 +3,17 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\WP; +namespace WordPressCS\WordPress\Sniffs\WP; -use WordPress\AbstractFunctionParameterSniff; +use PHPCSUtils\Utils\MessageHelper; +use PHPCSUtils\Utils\PassedParameters; +use PHPCSUtils\Utils\TextStrings; +use WordPressCS\WordPress\AbstractFunctionParameterSniff; +use WordPressCS\WordPress\Helpers\MinimumWPVersionTrait; /** * Check for usage of deprecated parameters in WP functions and suggest alternative based on the parameter passed. @@ -20,17 +24,17 @@ * By default, it is set to presume that a project will support the current * WP version and up to three releases before. * - * @package WPCS\WordPressCodingStandards - * - * @since 0.12.0 - * @since 0.13.0 Class name changed: this class is now namespaced. - * @since 0.14.0 Now has the ability to handle minimum supported WP version - * being provided via the command-line or as as value - * in a custom ruleset. + * @since 0.12.0 + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.14.0 Now has the ability to handle minimum supported WP version + * being provided via the command-line or as as value + * in a custom ruleset. * - * @uses \WordPress\Sniff::$minimum_supported_version + * @uses \WordPressCS\WordPress\Helpers\MinimumWPVersionTrait::$minimum_wp_version */ -class DeprecatedParametersSniff extends AbstractFunctionParameterSniff { +final class DeprecatedParametersSniff extends AbstractFunctionParameterSniff { + + use MinimumWPVersionTrait; /** * The group name for this group of functions. @@ -45,7 +49,7 @@ class DeprecatedParametersSniff extends AbstractFunctionParameterSniff { * Array of function, argument, and default value for deprecated argument. * * The functions are ordered alphabetically. - * Last updated for WordPress 4.8.0. + * Last updated for WordPress 6.3. * * @since 0.12.0 * @@ -53,212 +57,376 @@ class DeprecatedParametersSniff extends AbstractFunctionParameterSniff { * $target_functions = array( * (string) Function name. => array( * (int) Target parameter position, 1-based. => array( - * 'value' => (mixed) Expected default value for the - * deprecated parameter. Currently the default - * values: true, false, null, empty arrays and - * both empty and non-empty strings can be - * handled correctly by the process_parameters() - * method. When an additional default value is - * added, the relevant code in the - * process_parameters() method will need to be - * adjusted. + * 'name' => (string|array) Parameter name or list of names if the parameter + * was renamed since the release of PHP 8.0. + * 'value' => (mixed) Expected default value for the deprecated parameter. + * Currently the default values: true, false, null, empty arrays + * and both empty and non-empty strings can be handled correctly + * by the process_parameters() method. + * When an additional default value is added, the relevant code + * in the process_parameters() method will need to be adjusted. * 'version' => (int) The WordPress version when deprecated. * ) * ) * ); */ protected $target_functions = array( - + '_future_post_hook' => array( + 1 => array( + 'name' => 'deprecated', + 'value' => null, + 'version' => '2.3.0', + ), + ), + '_load_remote_block_patterns' => array( + 1 => array( + 'name' => 'deprecated', + 'value' => null, + 'version' => '5.9.0', + ), + ), + '_wp_post_revision_fields' => array( + 2 => array( + 'name' => 'deprecated', + 'value' => false, + 'version' => '4.5.0', + ), + ), 'add_option' => array( 3 => array( + 'name' => 'deprecated', 'value' => '', 'version' => '2.3.0', ), ), 'comments_link' => array( 1 => array( + 'name' => 'deprecated', 'value' => '', 'version' => '0.72', ), 2 => array( + 'name' => 'deprecated_2', 'value' => '', 'version' => '1.3.0', ), ), - 'comments_number' => array( - 4 => array( + 'convert_chars' => array( + 2 => array( + 'name' => 'deprecated', 'value' => '', - 'version' => '1.3.0', + 'version' => '0.71', ), ), - 'convert_chars' => array( + 'delete_plugins' => array( 2 => array( + 'name' => 'deprecated', 'value' => '', - 'version' => '0.71', + 'version' => '4.0.0', ), ), 'discover_pingback_server_uri' => array( 2 => array( + 'name' => 'deprecated', 'value' => '', 'version' => '2.7.0', ), ), + 'get_blog_list' => array( + 3 => array( + 'name' => 'deprecated', + 'value' => '', + 'version' => '3.0.0', // Was previously part of MU. + ), + ), 'get_category_parents' => array( 5 => array( + 'name' => 'deprecated', 'value' => array(), 'version' => '4.8.0', ), ), 'get_delete_post_link' => array( 2 => array( + 'name' => 'deprecated', 'value' => '', 'version' => '3.0.0', ), ), 'get_last_updated' => array( 1 => array( + 'name' => 'deprecated', 'value' => '', 'version' => '3.0.0', // Was previously part of MU. ), ), + 'get_site_option' => array( + 3 => array( + 'name' => 'deprecated', + 'value' => true, + 'version' => '4.4.0', + ), + ), + 'get_terms' => array( + 2 => array( + 'name' => 'deprecated', + 'value' => '', + 'version' => '4.5.0', + ), + ), 'get_the_author' => array( 1 => array( + 'name' => 'deprecated', 'value' => '', 'version' => '2.1.0', ), ), 'get_user_option' => array( 3 => array( + 'name' => 'deprecated', 'value' => '', - 'version' => '2.3.0', + 'version' => '3.0.0', ), ), 'get_wp_title_rss' => array( 1 => array( + 'name' => 'deprecated', 'value' => '–', 'version' => '4.4.0', ), ), + 'global_terms' => array( + 2 => array( + 'name' => 'deprecated', + 'value' => '', + 'version' => '6.1.0', + ), + ), + 'iframe_header' => array( + 2 => array( + 'name' => 'deprecated', + 'value' => false, + 'version' => '4.2.0', + ), + ), + 'install_search_form' => array( + 1 => array( + 'name' => 'deprecated', + 'value' => true, + 'version' => '4.6.0', + ), + ), 'is_email' => array( 2 => array( + 'name' => 'deprecated', 'value' => false, 'version' => '3.0.0', ), ), 'load_plugin_textdomain' => array( 2 => array( + 'name' => 'deprecated', 'value' => false, 'version' => '2.7.0', ), ), + 'newblog_notify_siteadmin' => array( + 2 => array( + 'name' => 'deprecated', + 'value' => '', + 'version' => '3.0.0', + ), + ), + 'permalink_single_rss' => array( + 1 => array( + 'name' => 'deprecated', + 'value' => '', + 'version' => '2.3.0', + ), + ), + 'redirect_this_site' => array( + 1 => array( + 'name' => 'deprecated', + 'value' => '', + 'version' => '3.0.0', + ), + ), + 'register_meta' => array( + 4 => array( + 'name' => 'deprecated', + 'value' => null, + 'version' => '4.6.0', + ), + ), 'safecss_filter_attr' => array( 2 => array( + 'name' => 'deprecated', 'value' => '', 'version' => '2.8.1', ), ), + 'switch_to_blog' => array( + 2 => array( + 'name' => 'deprecated', + 'value' => null, + 'version' => '3.5.0', // Was previously part of MU. + ), + ), + 'term_description' => array( + 2 => array( + 'name' => 'deprecated', + 'value' => null, + 'version' => '4.9.2', + ), + ), 'the_attachment_link' => array( 3 => array( + 'name' => 'deprecated', 'value' => false, 'version' => '2.5.0', ), ), 'the_author' => array( 1 => array( + 'name' => 'deprecated', 'value' => '', 'version' => '2.1.0', ), 2 => array( + 'name' => 'deprecated_echo', 'value' => true, 'version' => '1.5.0', ), ), 'the_author_posts_link' => array( 1 => array( + 'name' => 'deprecated', 'value' => '', 'version' => '2.1.0', ), ), 'trackback_rdf' => array( 1 => array( + 'name' => 'deprecated', 'value' => '', 'version' => '2.5.0', ), ), 'trackback_url' => array( 1 => array( + 'name' => 'deprecated_echo', 'value' => true, 'version' => '2.5.0', ), ), + 'unregister_setting' => array( + 3 => array( + 'name' => 'deprecated', + 'value' => '', + 'version' => '4.7.0', + ), + ), 'update_blog_option' => array( 4 => array( + 'name' => 'deprecated', 'value' => null, 'version' => '3.1.0', ), ), 'update_blog_status' => array( 4 => array( + 'name' => 'deprecated', 'value' => null, 'version' => '3.1.0', ), ), + 'update_posts_count' => array( + 1 => array( + 'name' => 'deprecated', + 'value' => '', + 'version' => '3.0.0', + ), + ), 'update_user_status' => array( 4 => array( + 'name' => 'deprecated', 'value' => null, 'version' => '3.0.2', ), ), - 'unregister_setting' => array( - 4 => array( + 'wp_count_terms' => array( + 2 => array( + 'name' => 'deprecated', 'value' => '', - 'version' => '4.7.0', + 'version' => '5.6.0', + ), + ), + 'wp_create_thumbnail' => array( + 3 => array( + 'name' => 'deprecated', + 'value' => '', + 'version' => '3.5.0', ), ), 'wp_get_http_headers' => array( 2 => array( + 'name' => 'deprecated', 'value' => false, 'version' => '2.7.0', ), ), 'wp_get_sidebars_widgets' => array( 1 => array( + 'name' => 'deprecated', 'value' => true, 'version' => '2.8.1', ), ), 'wp_install' => array( 5 => array( + 'name' => 'deprecated', 'value' => '', 'version' => '2.6.0', ), ), + 'wp_login' => array( + 3 => array( + 'name' => 'deprecated', + 'value' => '', + 'version' => '2.5.0', + ), + ), 'wp_new_user_notification' => array( 2 => array( + 'name' => 'deprecated', 'value' => null, 'version' => '4.3.1', ), ), 'wp_notify_postauthor' => array( 2 => array( + 'name' => 'deprecated', 'value' => null, 'version' => '3.8.0', ), ), 'wp_title_rss' => array( 1 => array( + 'name' => 'deprecated', 'value' => '–', 'version' => '4.4.0', ), ), 'wp_upload_bits' => array( 2 => array( + 'name' => 'deprecated', 'value' => null, 'version' => '2.0.0', ), ), 'xfn_check' => array( 3 => array( + 'name' => 'deprecated', 'value' => '', 'version' => '2.5.0', ), @@ -271,26 +439,27 @@ class DeprecatedParametersSniff extends AbstractFunctionParameterSniff { * @since 0.12.0 * * @param int $stackPtr The position of the current token in the stack. - * @param array $group_name The name of the group which was matched. - * @param string $matched_content The token content (function name) which was matched. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. * @param array $parameters Array with information about the parameters. * * @return void */ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { - $this->get_wp_version_from_cl(); + $this->set_minimum_wp_version(); $paramCount = \count( $parameters ); foreach ( $this->target_functions[ $matched_content ] as $position => $parameter_args ) { - // Check that number of parameters defined is not less than the position to check. - if ( $position > $paramCount ) { - break; + $found_param = PassedParameters::getParameterFromStack( $parameters, $position, $parameter_args['name'] ); + if ( false === $found_param ) { + continue; } // The list will need to updated if the default value is not supported. - switch ( $parameters[ $position ]['raw'] ) { + switch ( $found_param['raw'] ) { case 'true': $matched_parameter = true; break; @@ -305,7 +474,7 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p $matched_parameter = array(); break; default: - $matched_parameter = $this->strip_quotes( $parameters[ $position ]['raw'] ); + $matched_parameter = TextStrings::stripQuotes( $found_param['raw'] ); break; } @@ -314,25 +483,27 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p } $message = 'The parameter "%s" at position #%s of %s() has been deprecated since WordPress version %s.'; - $is_error = version_compare( $parameter_args['version'], $this->minimum_supported_version, '<' ); - $code = $this->string_to_errorcode( ucfirst( $matched_content ) . 'Param' . $position . 'Found' ); + $is_error = $this->wp_version_compare( $parameter_args['version'], $this->minimum_wp_version, '<' ); + $code = MessageHelper::stringToErrorcode( ucfirst( $matched_content ) . 'Param' . $position . 'Found' ); $data = array( - $parameters[ $position ]['raw'], + $found_param['raw'], $position, $matched_content, $parameter_args['version'], ); - if ( isset( $parameter_args['value'] ) && $position < $paramCount ) { + if ( isset( $parameter_args['value'] ) + && isset( $found_param['name'] ) === false + && $position < $paramCount + ) { $message .= ' Use "%s" instead.'; $data[] = (string) $parameter_args['value']; } else { $message .= ' Instead do not pass the parameter.'; } - $this->addMessage( $message, $stackPtr, $is_error, $code, $data, 0 ); + MessageHelper::addMessage( $this->phpcsFile, $message, $stackPtr, $is_error, $code, $data, 0 ); } } - } diff --git a/WordPress/Sniffs/WP/DiscouragedConstantsSniff.php b/WordPress/Sniffs/WP/DiscouragedConstantsSniff.php index 125b68a48a..7db5a37854 100644 --- a/WordPress/Sniffs/WP/DiscouragedConstantsSniff.php +++ b/WordPress/Sniffs/WP/DiscouragedConstantsSniff.php @@ -3,23 +3,25 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\WP; +namespace WordPressCS\WordPress\Sniffs\WP; -use WordPress\AbstractFunctionParameterSniff; -use PHP_CodeSniffer_Tokens as Tokens; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Utils\MessageHelper; +use PHPCSUtils\Utils\PassedParameters; +use PHPCSUtils\Utils\TextStrings; +use WordPressCS\WordPress\AbstractFunctionParameterSniff; +use WordPressCS\WordPress\Helpers\ConstantsHelper; /** * Warns against usage of discouraged WP CONSTANTS and recommends alternatives. * - * @package WPCS\WordPressCodingStandards - * - * @since 0.14.0 + * @since 0.14.0 */ -class DiscouragedConstantsSniff extends AbstractFunctionParameterSniff { +final class DiscouragedConstantsSniff extends AbstractFunctionParameterSniff { /** * List of discouraged WP constants and their replacements. @@ -46,32 +48,17 @@ class DiscouragedConstantsSniff extends AbstractFunctionParameterSniff { * Array of functions to check. * * @since 0.14.0 + * @since 3.0.0 The format of the value has changed from an integer parameter + * position to an array with the parameter position and name. * - * @var array => + * @var array> Function name as key, array with target + * parameter and name as value. */ protected $target_functions = array( - 'define' => 1, - ); - - /** - * Array of tokens which if found preceding the $stackPtr indicate that a T_STRING is not a constant. - * - * @var array - */ - private $preceding_tokens_to_ignore = array( - \T_NAMESPACE => true, - \T_USE => true, - \T_CLASS => true, - \T_TRAIT => true, - \T_INTERFACE => true, - \T_EXTENDS => true, - \T_IMPLEMENTS => true, - \T_NEW => true, - \T_FUNCTION => true, - \T_DOUBLE_COLON => true, - \T_OBJECT_OPERATOR => true, - \T_INSTANCEOF => true, - \T_GOTO => true, + 'define' => array( + 'position' => 1, + 'name' => 'constant_name', + ), ); /** @@ -112,68 +99,14 @@ public function process_arbitrary_tstring( $stackPtr ) { return; } - $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); - if ( false !== $next && \T_OPEN_PARENTHESIS === $this->tokens[ $next ]['code'] ) { - // Function call or declaration. - return; - } - - $prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); - if ( false !== $prev && isset( $this->preceding_tokens_to_ignore[ $this->tokens[ $prev ]['code'] ] ) ) { - // Not the use of a constant. - return; - } - - if ( false !== $prev - && \T_NS_SEPARATOR === $this->tokens[ $prev ]['code'] - && \T_STRING === $this->tokens[ ( $prev - 1 ) ]['code'] - ) { - // Namespaced constant of the same name. - return; - } - - if ( false !== $prev - && \T_CONST === $this->tokens[ $prev ]['code'] - && true === $this->is_class_constant( $prev ) - ) { - // Class constant of the same name. + if ( ConstantsHelper::is_use_of_global_constant( $this->phpcsFile, $stackPtr ) === false ) { return; } - /* - * Deal with a number of variations of use statements. - */ - for ( $i = $stackPtr; $i > 0; $i-- ) { - if ( $this->tokens[ $i ]['line'] !== $this->tokens[ $stackPtr ]['line'] ) { - break; - } - } - - $first_on_line = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true ); - if ( false !== $first_on_line && \T_USE === $this->tokens[ $first_on_line ]['code'] ) { - $next_on_line = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $first_on_line + 1 ), null, true ); - if ( false !== $next_on_line ) { - if ( ( \T_STRING === $this->tokens[ $next_on_line ]['code'] - && 'const' === $this->tokens[ $next_on_line ]['content'] ) - || \T_CONST === $this->tokens[ $next_on_line ]['code'] // Happens in some PHPCS versions. - ) { - $has_ns_sep = $this->phpcsFile->findNext( \T_NS_SEPARATOR, ( $next_on_line + 1 ), $stackPtr ); - if ( false !== $has_ns_sep ) { - // Namespaced const (group) use statement. - return; - } - } else { - // Not a const use statement. - return; - } - } - } - - // Ok, this is really one of the discouraged constants. $this->phpcsFile->addWarning( 'Found usage of constant "%s". Use %s instead.', $stackPtr, - 'UsageFound', + MessageHelper::stringToErrorcode( $content . 'UsageFound' ), array( $content, $this->discouraged_constants[ $content ], @@ -187,34 +120,41 @@ public function process_arbitrary_tstring( $stackPtr ) { * @since 0.14.0 * * @param int $stackPtr The position of the current token in the stack. - * @param array $group_name The name of the group which was matched. - * @param string $matched_content The token content (function name) which was matched. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. * @param array $parameters Array with information about the parameters. * * @return void */ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { - $function_name = strtolower( $matched_content ); - $target_param = $this->target_functions[ $function_name ]; + $target_param = $this->target_functions[ $matched_content ]; // Was the target parameter passed ? - if ( ! isset( $parameters[ $target_param ] ) ) { + $found_param = PassedParameters::getParameterFromStack( $parameters, $target_param['position'], $target_param['name'] ); + if ( false === $found_param ) { return; } - $raw_content = $this->strip_quotes( $parameters[ $target_param ]['raw'] ); + $clean_content = TextStrings::stripQuotes( $found_param['clean'] ); + + if ( isset( $this->discouraged_constants[ $clean_content ] ) ) { + $first_non_empty = $this->phpcsFile->findNext( + Tokens::$emptyTokens, + $found_param['start'], + ( $found_param['end'] + 1 ), + true + ); - if ( isset( $this->discouraged_constants[ $raw_content ] ) ) { $this->phpcsFile->addWarning( 'Found declaration of constant "%s". Use %s instead.', - $stackPtr, - 'DeclarationFound', + $first_non_empty, + MessageHelper::stringToErrorcode( $clean_content . 'DeclarationFound' ), array( - $raw_content, - $this->discouraged_constants[ $raw_content ], + $clean_content, + $this->discouraged_constants[ $clean_content ], ) ); } } - } diff --git a/WordPress/Sniffs/WP/DiscouragedFunctionsSniff.php b/WordPress/Sniffs/WP/DiscouragedFunctionsSniff.php index 92b3797b92..941b992ba5 100644 --- a/WordPress/Sniffs/WP/DiscouragedFunctionsSniff.php +++ b/WordPress/Sniffs/WP/DiscouragedFunctionsSniff.php @@ -3,23 +3,21 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\WP; +namespace WordPressCS\WordPress\Sniffs\WP; -use WordPress\AbstractFunctionRestrictionsSniff; +use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff; /** * Discourages the use of various WordPress functions and suggests alternatives. * - * @package WPCS\WordPressCodingStandards - * - * @since 0.11.0 - * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.11.0 + * @since 0.13.0 Class name changed: this class is now namespaced. */ -class DiscouragedFunctionsSniff extends AbstractFunctionRestrictionsSniff { +final class DiscouragedFunctionsSniff extends AbstractFunctionRestrictionsSniff { /** * Groups of functions to restrict. @@ -46,12 +44,11 @@ public function getGroups() { 'wp_reset_query' => array( 'type' => 'warning', - 'message' => '%s() is discouraged. Use the wp_reset_postdata() instead.', + 'message' => '%s() is discouraged. Use wp_reset_postdata() instead.', 'functions' => array( 'wp_reset_query', ), ), ); } - } diff --git a/WordPress/Sniffs/WP/EnqueuedResourceParametersSniff.php b/WordPress/Sniffs/WP/EnqueuedResourceParametersSniff.php index a2bbe39b7c..6e4385a923 100644 --- a/WordPress/Sniffs/WP/EnqueuedResourceParametersSniff.php +++ b/WordPress/Sniffs/WP/EnqueuedResourceParametersSniff.php @@ -3,14 +3,16 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\WP; +namespace WordPressCS\WordPress\Sniffs\WP; -use WordPress\AbstractFunctionParameterSniff; -use PHP_CodeSniffer_Tokens as Tokens; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Utils\Numbers; +use PHPCSUtils\Utils\PassedParameters; +use WordPressCS\WordPress\AbstractFunctionParameterSniff; /** * This checks the enqueued 4th and 5th parameters to make sure the version and in_footer are set. @@ -23,11 +25,9 @@ * @link https://developer.wordpress.org/reference/functions/wp_register_style/ * @link https://developer.wordpress.org/reference/functions/wp_enqueue_style/ * - * @package WPCS\WordPressCodingStandards - * * @since 1.0.0 */ -class EnqueuedResourceParametersSniff extends AbstractFunctionParameterSniff { +final class EnqueuedResourceParametersSniff extends AbstractFunctionParameterSniff { /** * The group name for this group of functions. @@ -43,7 +43,7 @@ class EnqueuedResourceParametersSniff extends AbstractFunctionParameterSniff { * * @since 1.0.0 * - * @var array => + * @var array Key is function name, value irrelevant. */ protected $target_functions = array( 'wp_register_script' => true, @@ -57,7 +57,7 @@ class EnqueuedResourceParametersSniff extends AbstractFunctionParameterSniff { * * This array is enriched with the $emptyTokens array in the register() method. * - * @var array + * @var array */ private $false_tokens = array( \T_FALSE => \T_FALSE, @@ -68,7 +68,7 @@ class EnqueuedResourceParametersSniff extends AbstractFunctionParameterSniff { * * This array is enriched with the several of the PHPCS token arrays in the register() method. * - * @var array + * @var array */ private $safe_tokens = array( \T_NULL => \T_NULL, @@ -111,16 +111,17 @@ public function register() { * @since 1.0.0 * * @param int $stackPtr The position of the current token in the stack. - * @param array $group_name The name of the group which was matched. - * @param string $matched_content The token content (function name) which was matched. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. * @param array $parameters Array with information about the parameters. * * @return void */ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { - // Check to see if a source ($src) is specified. - if ( ! isset( $parameters[2] ) ) { + $src_param = PassedParameters::getParameterFromStack( $parameters, 2, 'src' ); + if ( false === $src_param ) { return; } @@ -128,29 +129,37 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p * Version Check: Check to make sure the version is set explicitly. */ - if ( ! isset( $parameters[4] ) || 'null' === $parameters[4]['raw'] ) { + $version_param = PassedParameters::getParameterFromStack( $parameters, 4, 'ver' ); + + $error_ptr = $stackPtr; + if ( false !== $version_param ) { + $error_ptr = $this->phpcsFile->findNext( Tokens::$emptyTokens, $version_param['start'], ( $version_param['end'] + 1 ), true ); + if ( false === $error_ptr ) { + $error_ptr = $version_param['start']; + } + } + + if ( false === $version_param || 'null' === $version_param['clean'] ) { $type = 'script'; if ( strpos( $matched_content, '_style' ) !== false ) { $type = 'style'; } - $this->phpcsFile->addError( - 'Resource version not set in call to %s(). This means new versions of the %s will not always be loaded due to browser caching.', - $stackPtr, + $this->phpcsFile->addWarning( + 'Resource version not set in call to %s(). This means new versions of the %s may not always be loaded due to browser caching.', + $error_ptr, 'MissingVersion', array( $matched_content, $type ) ); - } else { // The version argument should have a non-false value. - if ( $this->is_falsy( $parameters[4]['start'], $parameters[4]['end'] ) ) { - $this->phpcsFile->addError( - 'Version parameter is not explicitly set or has been set to an equivalent of "false" for %s; ' . - 'This means that the WordPress core version will be used which is not recommended for plugin or theme development.', - $stackPtr, - 'NoExplicitVersion', - array( $matched_content ) - ); - } + } elseif ( $this->is_falsy( $version_param['start'], $version_param['end'] ) ) { + $this->phpcsFile->addError( + 'Version parameter is not explicitly set or has been set to an equivalent of "false" for %s; ' . + 'This means that the WordPress core version will be used which is not recommended for plugin or theme development.', + $error_ptr, + 'NoExplicitVersion', + array( $matched_content ) + ); } /* @@ -166,7 +175,8 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p return; } - if ( ! isset( $parameters[5] ) ) { + $infooter_param = PassedParameters::getParameterFromStack( $parameters, 5, 'in_footer' ); + if ( false === $infooter_param ) { // If in footer is not set, throw a warning about the default. $this->phpcsFile->addWarning( 'In footer ($in_footer) is not set explicitly %s; ' . @@ -209,6 +219,14 @@ protected function is_falsy( $start, $end ) { continue; } + // Make sure that PHP 7.4 numeric literals and PHP 8.1 explicit octals don't cause problems. + if ( \T_LNUMBER === $this->tokens[ $i ]['code'] || \T_DNUMBER === $this->tokens[ $i ]['code'] ) { + $number_info = Numbers::getCompleteNumber( $this->phpcsFile, $i ); + $code_string .= $number_info['decimal']; + $i = $number_info['last_token']; + continue; + } + $code_string .= $this->tokens[ $i ]['content']; } diff --git a/WordPress/Sniffs/WP/EnqueuedResourcesSniff.php b/WordPress/Sniffs/WP/EnqueuedResourcesSniff.php index d3fa7861aa..c7ed63c303 100644 --- a/WordPress/Sniffs/WP/EnqueuedResourcesSniff.php +++ b/WordPress/Sniffs/WP/EnqueuedResourcesSniff.php @@ -3,27 +3,28 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\WP; +namespace WordPressCS\WordPress\Sniffs\WP; -use WordPress\Sniff; -use PHP_CodeSniffer_Tokens as Tokens; +use PHP_CodeSniffer\Exceptions\RuntimeException; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\TextStrings; +use WordPressCS\WordPress\Sniff; /** * Makes sure scripts and styles are enqueued and not explicitly echo'd. * - * @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#inline-resources + * @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#inline-resources * - * @package WPCS\WordPressCodingStandards - * - * @since 0.3.0 - * @since 0.12.0 This class now extends WordPress_Sniff. - * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.3.0 + * @since 0.12.0 This class now extends the WordPressCS native `Sniff` class. + * @since 0.13.0 Class name changed: this class is now namespaced. */ -class EnqueuedResourcesSniff extends Sniff { +final class EnqueuedResourcesSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. @@ -31,7 +32,10 @@ class EnqueuedResourcesSniff extends Sniff { * @return array */ public function register() { - return Tokens::$textStringTokens; + $targets = Collections::textStringStartTokens(); + $targets[] = \T_INLINE_HTML; + + return $targets; } /** @@ -39,26 +43,66 @@ public function register() { * * @param int $stackPtr The position of the current token in the stack. * - * @return void + * @return int|void Integer stack pointer to skip forward or void to continue + * normal file processing. */ public function process_token( $stackPtr ) { - $token = $this->tokens[ $stackPtr ]; - if ( preg_match( '#rel=\\\\?[\'"]?stylesheet\\\\?[\'"]?#', $token['content'] ) > 0 ) { - $this->phpcsFile->addError( - 'Stylesheets must be registered/enqueued via wp_enqueue_style', - $stackPtr, - 'NonEnqueuedStylesheet' - ); + $end_ptr = $stackPtr; + $content = $this->tokens[ $stackPtr ]['content']; + if ( \T_INLINE_HTML !== $this->tokens[ $stackPtr ]['code'] ) { + try { + $end_ptr = TextStrings::getEndOfCompleteTextString( $this->phpcsFile, $stackPtr ); + $content = TextStrings::getCompleteTextString( $this->phpcsFile, $stackPtr ); + } catch ( RuntimeException $e ) { + // Parse error/live coding. + return; + } + } + + if ( preg_match_all( '# rel=\\\\?[\'"]?stylesheet\\\\?[\'"]?#', $content, $matches, \PREG_OFFSET_CAPTURE ) > 0 ) { + foreach ( $matches[0] as $match ) { + $this->phpcsFile->addError( + 'Stylesheets must be registered/enqueued via wp_enqueue_style()', + $this->find_token_in_multiline_string( $stackPtr, $content, $match[1] ), + 'NonEnqueuedStylesheet' + ); + } } - if ( preg_match( '#]*(?<=src=)#', $token['content'] ) > 0 ) { - $this->phpcsFile->addError( - 'Scripts must be registered/enqueued via wp_enqueue_script', - $stackPtr, - 'NonEnqueuedScript' - ); + if ( preg_match_all( '#]*(?<=src=)#', $content, $matches, \PREG_OFFSET_CAPTURE ) > 0 ) { + foreach ( $matches[0] as $match ) { + $this->phpcsFile->addError( + 'Scripts must be registered/enqueued via wp_enqueue_script()', + $this->find_token_in_multiline_string( $stackPtr, $content, $match[1] ), + 'NonEnqueuedScript' + ); + } } + + return ( $end_ptr + 1 ); } + /** + * Find the exact token on which the error should be reported for multi-line strings. + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $content The complete, potentially multi-line, text string. + * @param int $match_offset The offset within the content at which the match was found. + * + * @return int The stack pointer to the token containing the start of the match. + */ + private function find_token_in_multiline_string( $stackPtr, $content, $match_offset ) { + $newline_count = 0; + if ( $match_offset > 0 ) { + $newline_count = substr_count( $content, "\n", 0, $match_offset ); + } + + // Account for heredoc/nowdoc text starting at the token *after* the opener. + if ( isset( Tokens::$heredocTokens[ $this->tokens[ $stackPtr ]['code'] ] ) === true ) { + ++$newline_count; + } + + return ( $stackPtr + $newline_count ); + } } diff --git a/WordPress/Sniffs/WP/GlobalVariablesOverrideSniff.php b/WordPress/Sniffs/WP/GlobalVariablesOverrideSniff.php index 1d628bbab3..038aba6aef 100644 --- a/WordPress/Sniffs/WP/GlobalVariablesOverrideSniff.php +++ b/WordPress/Sniffs/WP/GlobalVariablesOverrideSniff.php @@ -3,35 +3,48 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\WP; - -use WordPress\Sniff; -use PHP_CodeSniffer_Tokens as Tokens; +namespace WordPressCS\WordPress\Sniffs\WP; + +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\Conditions; +use PHPCSUtils\Utils\Context; +use PHPCSUtils\Utils\Lists; +use PHPCSUtils\Utils\Parentheses; +use PHPCSUtils\Utils\Scopes; +use PHPCSUtils\Utils\TextStrings; +use WordPressCS\WordPress\Helpers\ContextHelper; +use WordPressCS\WordPress\Helpers\IsUnitTestTrait; +use WordPressCS\WordPress\Helpers\ListHelper; +use WordPressCS\WordPress\Helpers\VariableHelper; +use WordPressCS\WordPress\Helpers\WPGlobalVariablesHelper; +use WordPressCS\WordPress\Sniff; /** * Warns about overwriting WordPress native global variables. * - * @package WPCS\WordPressCodingStandards - * - * @since 0.3.0 - * @since 0.4.0 This class now extends WordPress_Sniff. - * @since 0.12.0 The $wp_globals property has been moved to the WordPress_Sniff. - * @since 0.13.0 Class name changed: this class is now namespaced. - * @since 1.0.0 This sniff has been moved from the `Variables` category to the `WP` - * category and renamed from `GlobalVariables` to `GlobalVariablesOverride`. - * @since 1.1.0 The sniff now also detects variables being overriden in the global namespace. + * @since 0.3.0 + * @since 0.4.0 This class now extends the WordPressCS native `Sniff` class. + * @since 0.12.0 The $wp_globals property has been moved to the `Sniff` class. + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 1.0.0 This sniff has been moved from the `Variables` category to the `WP` + * category and renamed from `GlobalVariables` to `GlobalVariablesOverride`. + * @since 1.1.0 The sniff now also detects variables being overriden in the global namespace. + * @since 2.2.0 The sniff now also detects variable assignments via the list() construct. * - * @uses \WordPress\Sniff::$custom_test_class_whitelist + * @uses \WordPressCS\WordPress\Helpers\IsUnitTestTrait::$custom_test_classes */ -class GlobalVariablesOverrideSniff extends Sniff { +final class GlobalVariablesOverrideSniff extends Sniff { + + use IsUnitTestTrait; /** * Whether to treat all files as if they were included from - * a within function. + * within a function. * * This is mostly useful for projects containing views which are being * included from within a function in another file, like themes. @@ -46,24 +59,18 @@ class GlobalVariablesOverrideSniff extends Sniff { public $treat_files_as_scoped = false; /** - * Scoped object and function structures to skip over as - * variables will have a different scope within those. + * Allow select variables from the WPGlobalVariablesHelper::$wp_globals array to be overwritten. * - * {@internal Once the minimum PHPCS version goes up to PHPCS 3.1.0, - * this array can be partially created in the register() method - * using the upstream `Tokens::$ooScopeTokens` array.}} + * A few select variables in WP Core are _intended_ to be overwritten + * by themes/plugins. This sniff should not throw an error for those. * - * @since 1.1.0 + * @since 2.2.0 * - * @var array + * @var array Key is variable name, value irrelevant. */ - private $skip_over = array( - \T_FUNCTION => true, - \T_CLOSURE => true, - \T_CLASS => true, - \T_ANON_CLASS => true, - \T_INTERFACE => true, - \T_TRAIT => true, + protected $override_allowed = array( + 'content_width' => true, + 'wp_cockneyreplace' => true, ); /** @@ -75,15 +82,16 @@ class GlobalVariablesOverrideSniff extends Sniff { * @return array */ public function register() { - return array( + $targets = array( \T_GLOBAL, \T_VARIABLE, - - // Only used to skip over test classes. - \T_CLASS, - \T_TRAIT, - \T_ANON_CLASS, ); + $targets += Collections::listOpenTokensBC(); + + // Only used to skip over test classes. + $targets += Tokens::$ooScopeTokens; + + return $targets; } /** @@ -102,9 +110,9 @@ public function process_token( $stackPtr ) { $token = $this->tokens[ $stackPtr ]; // Ignore variable overrides in test classes. - if ( \T_CLASS === $token['code'] || \T_TRAIT === $token['code'] || \T_ANON_CLASS === $token['code'] ) { + if ( isset( Tokens::$ooScopeTokens[ $token['code'] ] ) ) { - if ( true === $this->is_test_class( $stackPtr ) + if ( true === $this->is_test_class( $this->phpcsFile, $stackPtr ) && $token['scope_condition'] === $stackPtr && isset( $token['scope_closer'] ) ) { @@ -119,12 +127,26 @@ public function process_token( $stackPtr ) { /* * Examine variables within a function scope based on a `global` statement in the * function. - * Examine variable not within a function scope and access to the `$GLOBALS` + * Examine variables not within a function scope, but within a list construct, based + * on that. + * Examine variables not within a function scope and access to the `$GLOBALS` * variable based on the variable token. + * + * Note: No special handling here for code found within PHP 7.4+ arrow functions. + * Arrow functions are "open", i.e. they have by value access to variables in the + * surrounding scope, but they cannot modify the value. + * Additionally, as they can only have one statement, a `global` statement _within_ + * an arrow function declaration will lead to a parse error as the result is + * not a returnable value. */ - $in_function_scope = $this->phpcsFile->hasCondition( $stackPtr, array( \T_FUNCTION, \T_CLOSURE ) ); + $in_function_scope = Conditions::hasCondition( $this->phpcsFile, $stackPtr, array( \T_FUNCTION, \T_CLOSURE ) ); - if ( \T_VARIABLE === $token['code'] + if ( isset( Collections::listOpenTokensBC()[ $token['code'] ] ) + && false === $in_function_scope + && false === $this->treat_files_as_scoped + ) { + return $this->process_list_assignment( $stackPtr ); + } elseif ( \T_VARIABLE === $token['code'] && ( '$GLOBALS' === $token['content'] || ( false === $in_function_scope && false === $this->treat_files_as_scoped ) ) ) { @@ -137,20 +159,47 @@ public function process_token( $stackPtr ) { } /** - * Check that defined global variables are prefixed. + * Check that global variables declared via a list construct are prefixed. * - * @since 1.1.0 Logic was previously contained in the process_token() method. + * {@internal No need to take special measures for nested lists. Nested or not, + * each list part can only contain one variable being written to.} + * + * @since 2.2.0 * * @param int $stackPtr The position of the current token in the stack. * - * @return void + * @return int|void Integer stack pointer to skip forward or void to continue + * normal file processing. */ - protected function process_variable_assignment( $stackPtr ) { - - if ( $this->has_whitelist_comment( 'override', $stackPtr ) === true ) { + protected function process_list_assignment( $stackPtr ) { + $list_open_close = Lists::getOpenClose( $this->phpcsFile, $stackPtr ); + if ( false === $list_open_close ) { + // Short array, not short list. return; } + $var_pointers = ListHelper::get_list_variables( $this->phpcsFile, $stackPtr ); + foreach ( $var_pointers as $ptr ) { + $this->process_variable_assignment( $ptr, true ); + } + + // No need to re-examine these variables. + return $list_open_close['closer']; + } + + /** + * Check that defined global variables are prefixed. + * + * @since 1.1.0 Logic was previously contained in the process_token() method. + * + * @param int $stackPtr The position of the current token in the stack. + * @param bool $in_list Whether or not this is a variable in a list assignment. + * Defaults to false. + * + * @return void + */ + protected function process_variable_assignment( $stackPtr, $in_list = false ) { + $token = $this->tokens[ $stackPtr ]; $var_name = substr( $token['content'], 1 ); // Strip the dollar sign. $data = array(); @@ -159,7 +208,10 @@ protected function process_variable_assignment( $stackPtr ) { if ( 'GLOBALS' === $var_name ) { $bracketPtr = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); - if ( false === $bracketPtr || \T_OPEN_SQUARE_BRACKET !== $this->tokens[ $bracketPtr ]['code'] || ! isset( $this->tokens[ $bracketPtr ]['bracket_closer'] ) ) { + if ( false === $bracketPtr + || \T_OPEN_SQUARE_BRACKET !== $this->tokens[ $bracketPtr ]['code'] + || ! isset( $this->tokens[ $bracketPtr ]['bracket_closer'] ) + ) { return; } @@ -179,7 +231,7 @@ protected function process_variable_assignment( $stackPtr ) { } if ( \T_CONSTANT_ENCAPSED_STRING === $this->tokens[ $ptr ]['code'] ) { - $var_name .= $this->strip_quotes( $this->tokens[ $ptr ]['content'] ); + $var_name .= TextStrings::stripQuotes( $this->tokens[ $ptr ]['content'] ); } } @@ -195,15 +247,23 @@ protected function process_variable_assignment( $stackPtr ) { /* * Is this one of the WP global variables ? */ - if ( isset( $this->wp_globals[ $var_name ] ) === false ) { + if ( WPGlobalVariablesHelper::is_wp_global( $var_name ) === false ) { + return; + } + + /* + * Is this one of the WP global variables which are allowed to be overwritten ? + */ + if ( isset( $this->override_allowed[ $var_name ] ) === true ) { return; } /* * Check if the variable value is being changed. */ - if ( false === $this->is_assignment( $stackPtr ) - && false === $this->is_foreach_as( $stackPtr ) + if ( false === $in_list + && false === VariableHelper::is_assignment( $this->phpcsFile, $stackPtr ) + && Context::inForeachCondition( $this->phpcsFile, $stackPtr ) !== 'afterAs' ) { return; } @@ -212,22 +272,19 @@ protected function process_variable_assignment( $stackPtr ) { * Function parameters with the same name as a WP global variable are fine, * including when they are being assigned a default value. */ - if ( isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) { - foreach ( $this->tokens[ $stackPtr ]['nested_parenthesis'] as $opener => $closer ) { - if ( isset( $this->tokens[ $opener ]['parenthesis_owner'] ) - && ( \T_FUNCTION === $this->tokens[ $this->tokens[ $opener ]['parenthesis_owner'] ]['code'] - || \T_CLOSURE === $this->tokens[ $this->tokens[ $opener ]['parenthesis_owner'] ]['code'] ) - ) { - return; - } + if ( false === $in_list ) { + $functionPtr = Parentheses::getLastOwner( $this->phpcsFile, $stackPtr, Collections::functionDeclarationTokens() ); + if ( false !== $functionPtr ) { + return; } - unset( $opener, $closer ); + + unset( $functionPtr ); } /* * Class property declarations with the same name as WP global variables are fine. */ - if ( true === $this->is_class_property( $stackPtr ) ) { + if ( false === $in_list && true === Scopes::isOOProperty( $this->phpcsFile, $stackPtr ) ) { return; } @@ -250,25 +307,22 @@ protected function process_global_statement( $stackPtr, $in_function_scope ) { /* * Collect the variables to watch for. */ - $search = array(); - $ptr = ( $stackPtr + 1 ); - while ( isset( $this->tokens[ $ptr ] ) ) { - $var = $this->tokens[ $ptr ]; - - // Halt the loop at end of statement. - if ( \T_SEMICOLON === $var['code'] ) { - break; - } - - if ( \T_VARIABLE === $var['code'] ) { - if ( isset( $this->wp_globals[ substr( $var['content'], 1 ) ] ) ) { - $search[] = $var['content']; + $search = array(); + $ptr = ( $stackPtr + 1 ); + $end_of_statement = $this->phpcsFile->findNext( array( \T_SEMICOLON, \T_CLOSE_TAG ), $ptr ); + + while ( isset( $this->tokens[ $ptr ] ) && $ptr < $end_of_statement ) { + if ( \T_VARIABLE === $this->tokens[ $ptr ]['code'] ) { + $var_name = substr( $this->tokens[ $ptr ]['content'], 1 ); + if ( WPGlobalVariablesHelper::is_wp_global( $var_name ) + && isset( $this->override_allowed[ $var_name ] ) === false + ) { + $search[ $this->tokens[ $ptr ]['content'] ] = true; } } - $ptr++; + ++$ptr; } - unset( $var ); if ( empty( $search ) ) { return; @@ -279,23 +333,21 @@ protected function process_global_statement( $stackPtr, $in_function_scope ) { */ $start = $ptr; if ( true === $in_function_scope ) { - $function_cond = $this->phpcsFile->getCondition( $stackPtr, \T_FUNCTION ); - $closure_cond = $this->phpcsFile->getCondition( $stackPtr, \T_CLOSURE ); - $scope_cond = max( $function_cond, $closure_cond ); // If false, it will evaluate as zero, so this is fine. - if ( isset( $this->tokens[ $scope_cond ]['scope_closer'] ) === false ) { + $functionPtr = Conditions::getLastCondition( $this->phpcsFile, $stackPtr, Collections::functionDeclarationTokens() ); + if ( isset( $this->tokens[ $functionPtr ]['scope_closer'] ) === false ) { // Live coding or parse error. return; } - $end = $this->tokens[ $scope_cond ]['scope_closer']; + $end = $this->tokens[ $functionPtr ]['scope_closer']; } else { - // Global statement in the global namespace with file is being treated as scoped. - $end = ( $this->phpcsFile->numTokens + 1 ); + // Global statement in the global namespace in a file which is being treated as scoped. + $end = $this->phpcsFile->numTokens; } for ( $ptr = $start; $ptr < $end; $ptr++ ) { // Skip over nested functions, classes and the likes. - if ( isset( $this->skip_over[ $this->tokens[ $ptr ]['code'] ] ) ) { + if ( isset( Collections::closedScopes()[ $this->tokens[ $ptr ]['code'] ] ) ) { if ( ! isset( $this->tokens[ $ptr ]['scope_closer'] ) ) { // Live coding or parse error. break; @@ -305,59 +357,58 @@ protected function process_global_statement( $stackPtr, $in_function_scope ) { continue; } + // Make sure to recognize assignments to variables in a list construct. + if ( isset( Collections::listOpenTokensBC()[ $this->tokens[ $ptr ]['code'] ] ) ) { + $list_open_close = Lists::getOpenClose( $this->phpcsFile, $ptr ); + + if ( false === $list_open_close ) { + // Short array, not short list. + continue; + } + + $var_pointers = ListHelper::get_list_variables( $this->phpcsFile, $ptr ); + foreach ( $var_pointers as $ptr ) { + $var_name = $this->tokens[ $ptr ]['content']; + if ( '$GLOBALS' === $var_name ) { + $var_name = '$' . TextStrings::stripQuotes( VariableHelper::get_array_access_key( $this->phpcsFile, $ptr ) ); + } + + if ( isset( $search[ $var_name ] ) ) { + $this->process_variable_assignment( $ptr, true ); + } + } + + // No need to re-examine these variables. + $ptr = $list_open_close['closer']; + continue; + } + if ( \T_VARIABLE !== $this->tokens[ $ptr ]['code'] ) { continue; } - if ( \in_array( $this->tokens[ $ptr ]['content'], $search, true ) === false ) { + if ( isset( $search[ $this->tokens[ $ptr ]['content'] ] ) === false ) { // Not one of the variables we're interested in. continue; } // Don't throw false positives for static class properties. - $previous = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $ptr - 1 ), null, true, null, true ); - if ( false !== $previous && \T_DOUBLE_COLON === $this->tokens[ $previous ]['code'] ) { + if ( ContextHelper::has_object_operator_before( $this->phpcsFile, $ptr ) === true ) { continue; } - if ( true === $this->is_assignment( $ptr ) ) { - $this->maybe_add_error( $ptr ); + if ( true === VariableHelper::is_assignment( $this->phpcsFile, $ptr ) ) { + $this->add_error( $ptr ); continue; } // Check if this is a variable assignment within a `foreach()` declaration. - if ( isset( $this->tokens[ $ptr ]['nested_parenthesis'] ) ) { - $nested_parenthesis = $this->tokens[ $ptr ]['nested_parenthesis']; - $close_parenthesis = end( $nested_parenthesis ); - if ( isset( $this->tokens[ $close_parenthesis ]['parenthesis_owner'] ) - && \T_FOREACH === $this->tokens[ $this->tokens[ $close_parenthesis ]['parenthesis_owner'] ]['code'] - && ( false !== $previous - && ( \T_DOUBLE_ARROW === $this->tokens[ $previous ]['code'] - || \T_AS === $this->tokens[ $previous ]['code'] ) ) - ) { - $this->maybe_add_error( $ptr ); - } + if ( Context::inForeachCondition( $this->phpcsFile, $ptr ) === 'afterAs' ) { + $this->add_error( $ptr ); } } } - /** - * Add the error if there is no whitelist comment present. - * - * @since 0.11.0 - * @since 1.1.0 - Visibility changed from public to protected. - * - Check for being in a test class moved to the process_token() method. - * - * @param int $stackPtr The position of the token to throw the error for. - * - * @return void - */ - protected function maybe_add_error( $stackPtr ) { - if ( $this->has_whitelist_comment( 'override', $stackPtr ) === false ) { - $this->add_error( $stackPtr ); - } - } - /** * Add the error. * @@ -378,9 +429,8 @@ protected function add_error( $stackPtr, $data = array() ) { $this->phpcsFile->addError( 'Overriding WordPress globals is prohibited. Found assignment to %s', $stackPtr, - 'OverrideProhibited', + 'Prohibited', $data ); } - } diff --git a/WordPress/Sniffs/WP/I18nSniff.php b/WordPress/Sniffs/WP/I18nSniff.php index 427846d52e..ab857bec7c 100644 --- a/WordPress/Sniffs/WP/I18nSniff.php +++ b/WordPress/Sniffs/WP/I18nSniff.php @@ -3,38 +3,45 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\WP; +namespace WordPressCS\WordPress\Sniffs\WP; -use WordPress\AbstractFunctionRestrictionsSniff; -use WordPress\PHPCSHelper; -use PHP_CodeSniffer_Tokens as Tokens; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\BackCompat\Helper; +use PHPCSUtils\Utils\MessageHelper; +use PHPCSUtils\Utils\PassedParameters; +use PHPCSUtils\Utils\TextStrings; +use WordPressCS\WordPress\AbstractFunctionParameterSniff; +use WordPressCS\WordPress\Helpers\RulesetPropertyHelper; +use XMLReader; /** * Makes sure WP internationalization functions are used properly. * - * @link https://make.wordpress.org/core/handbook/best-practices/internationalization/ - * @link https://developer.wordpress.org/plugins/internationalization/ + * @link https://make.wordpress.org/core/handbook/best-practices/internationalization/ + * @link https://developer.wordpress.org/plugins/internationalization/ * - * @package WPCS\WordPressCodingStandards - * - * @since 0.10.0 - * @since 0.11.0 - Now also checks for translators comments. - * - Now has the ability to handle text domain set via the command-line - * as a comma-delimited list. - * `phpcs --runtime-set text_domain my-slug,default` - * @since 0.13.0 Class name changed: this class is now namespaced. - * @since 1.0.0 This class now extends the AbstractFunctionRestrictionSniff. - * The parent `exclude` property is, however, disabled as it - * would disable the whole sniff. + * @since 0.10.0 + * @since 0.11.0 - Now also checks for translators comments. + * - Now has the ability to handle text domain set via the command-line + * as a comma-delimited list. + * `phpcs --runtime-set text_domain my-slug,default` + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 1.0.0 This class now extends the WordPressCS native + * `AbstractFunctionRestrictionSniff` class. + * The parent `exclude` property is, however, disabled as it + * would disable the whole sniff. + * @since 3.0.0 This class now extends the WordPressCS native + * `AbstractFunctionParameterSniff` class. + * The parent `exclude` property is still disabled. */ -class I18nSniff extends AbstractFunctionRestrictionsSniff { +final class I18nSniff extends AbstractFunctionParameterSniff { /** - * These Regexes copied from http://php.net/manual/en/function.sprintf.php#93552 + * These Regexes were originally copied from https://www.php.net/function.sprintf#93552 * and adjusted for better precision and updated specs. */ const SPRINTF_PLACEHOLDER_REGEX = '/(?: @@ -54,7 +61,7 @@ class I18nSniff extends AbstractFunctionRestrictionsSniff { [0-9]+ # Width specifier. (?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character. ) - [bcdeEfFgGosuxX] # Type specifier. + [bcdeEfFgGhHosuxX] # Type specifier. ) )/x'; @@ -76,16 +83,13 @@ class I18nSniff extends AbstractFunctionRestrictionsSniff { [0-9]+ # Width specifier. (?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character. ) - [bcdeEfFgGosuxX] # Type specifier. + [bcdeEfFgGhHosuxX] # Type specifier. )/x'; /** * Text domain. * - * @todo Eventually this should be able to be auto-supplied via looking at $this->phpcsFile->getFilename() - * @link https://youtrack.jetbrains.com/issue/WI-17740 - * - * @var string[]|string + * @var string[] */ public $text_domain; @@ -95,7 +99,7 @@ class I18nSniff extends AbstractFunctionRestrictionsSniff { * @since 0.10.0 * @since 0.11.0 Changed visibility from public to protected. * - * @var array => + * @var array Key is function name, value is the function type. */ protected $i18n_functions = array( 'translate' => 'simple', @@ -116,18 +120,6 @@ class I18nSniff extends AbstractFunctionRestrictionsSniff { '_nx_noop' => 'noopnumber_context', ); - /** - * Toggle whether or not to check for translators comments for text string containing placeholders. - * - * Intended to make this part of the sniff unit testable, but can be used by end-users too, - * though they can just as easily disable this via the sniff code. - * - * @since 0.11.0 - * - * @var bool - */ - public $check_translator_comments = true; - /** * Whether or not the `default` text domain is one of the allowed text domains. * @@ -146,6 +138,52 @@ class I18nSniff extends AbstractFunctionRestrictionsSniff { */ private $text_domain_is_default = false; + /** + * Parameter specifications for the functions in each group. + * + * {@internal Even when not all parameters will be examined, the parameter list should still + * be complete in the below array to allow for a correct "total parameters" calculation.} + * + * @since 3.0.0 + * + * @var array Array of the parameter positions and names. + */ + private $parameter_specs = array( + 'simple' => array( + 1 => 'text', + 2 => 'domain', + ), + 'context' => array( + 1 => 'text', + 2 => 'context', + 3 => 'domain', + ), + 'number' => array( + 1 => 'single', + 2 => 'plural', + 3 => 'number', + 4 => 'domain', + ), + 'number_context' => array( + 1 => 'single', + 2 => 'plural', + 3 => 'number', + 4 => 'context', + 5 => 'domain', + ), + 'noopnumber' => array( + 1 => 'singular', + 2 => 'plural', + 3 => 'domain', + ), + 'noopnumber_context' => array( + 1 => 'singular', + 2 => 'plural', + 3 => 'context', + 4 => 'domain', + ), + ); + /** * Groups of functions to restrict. * @@ -179,23 +217,26 @@ public function getGroups() { * whether something is a function call. The logic after that has * been split off to the `process_matched_token()` method. * - * @param int $stack_ptr The position of the current token in the stack. + * @param int $stackPtr The position of the current token in the stack. * * @return void */ - public function process_token( $stack_ptr ) { + public function process_token( $stackPtr ) { // Reset defaults. $this->text_domain_contains_default = false; $this->text_domain_is_default = false; // Allow overruling the text_domain set in a ruleset via the command line. - $cl_text_domain = trim( PHPCSHelper::get_config_data( 'text_domain' ) ); + $cl_text_domain = Helper::getConfigData( 'text_domain' ); if ( ! empty( $cl_text_domain ) ) { - $this->text_domain = $cl_text_domain; + $cl_text_domain = trim( $cl_text_domain ); + if ( '' !== $cl_text_domain ) { + $this->text_domain = array_filter( array_map( 'trim', explode( ',', $cl_text_domain ) ) ); + } } - $this->text_domain = $this->merge_custom_array( $this->text_domain, array(), false ); + $this->text_domain = RulesetPropertyHelper::merge_custom_array( $this->text_domain, array(), false ); if ( ! empty( $this->text_domain ) ) { if ( \in_array( 'default', $this->text_domain, true ) ) { @@ -209,7 +250,7 @@ public function process_token( $stack_ptr ) { // Prevent exclusion of the i18n group. $this->exclude = array(); - parent::process_token( $stack_ptr ); + parent::process_token( $stackPtr ); } /** @@ -217,373 +258,421 @@ public function process_token( $stack_ptr ) { * * @since 1.0.0 Logic split off from the `process_token()` method. * - * @param int $stack_ptr The position of the current token in the stack. + * @param int $stackPtr The position of the current token in the stack. * @param string $group_name The name of the group which was matched. - * @param string $matched_content The token content (function name) which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. * - * @return int|void Integer stack pointer to skip forward or void to continue - * normal file processing. + * @return void */ - public function process_matched_token( $stack_ptr, $group_name, $matched_content ) { + public function process_matched_token( $stackPtr, $group_name, $matched_content ) { - $func_open_paren_token = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stack_ptr + 1 ), null, true ); - if ( false === $func_open_paren_token - || \T_OPEN_PARENTHESIS !== $this->tokens[ $func_open_paren_token ]['code'] - || ! isset( $this->tokens[ $func_open_paren_token ]['parenthesis_closer'] ) - ) { + $func_open_paren_token = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); + if ( ! isset( $this->tokens[ $func_open_paren_token ]['parenthesis_closer'] ) ) { // Live coding, parse error or not a function call. return; } if ( 'typos' === $group_name && '_' === $matched_content ) { - $this->phpcsFile->addError( 'Found single-underscore "_()" function when double-underscore expected.', $stack_ptr, 'SingleUnderscoreGetTextFunction' ); + $this->phpcsFile->addError( + 'Found single-underscore "_()" function when double-underscore expected.', + $stackPtr, + 'SingleUnderscoreGetTextFunction' + ); return; } - if ( \in_array( $matched_content, array( 'translate', 'translate_with_gettext_context' ), true ) ) { - $this->phpcsFile->addWarning( 'Use of the "%s()" function is reserved for low-level API usage.', $stack_ptr, 'LowLevelTranslationFunction', array( $matched_content ) ); + if ( 'translate' === $matched_content || 'translate_with_gettext_context' === $matched_content ) { + $this->phpcsFile->addWarning( + 'Use of the "%s()" function is reserved for low-level API usage.', + $stackPtr, + 'LowLevelTranslationFunction', + array( $matched_content ) + ); + } + + parent::process_matched_token( $stackPtr, $group_name, $matched_content ); + } + + /** + * Process the function if no parameters were found. + * + * @since 3.0.0 + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * + * @return void + */ + public function process_no_parameters( $stackPtr, $group_name, $matched_content ) { + $function_param_specs = $this->parameter_specs[ $this->i18n_functions[ $matched_content ] ]; + + foreach ( $function_param_specs as $param_name ) { + $error_code = MessageHelper::stringToErrorcode( 'MissingArg' . ucfirst( $param_name ) ); + $this->phpcsFile->addError( + 'Missing $%s parameter in function call to %s().', + $stackPtr, + $error_code, + array( $param_name, $matched_content ) + ); } + } - $arguments_tokens = array(); - $argument_tokens = array(); - $tokens = $this->tokens; + /** + * Process the parameters of a matched function. + * + * @since 3.0.0 + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * @param array $parameters Array with information about the parameters. + * + * @return void + */ + public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { + /* + * Retrieve the individual parameters from the array in a way that we know which is which. + */ + $parameter_details = array(); - // Look at arguments. - for ( $i = ( $func_open_paren_token + 1 ); $i < $this->tokens[ $func_open_paren_token ]['parenthesis_closer']; $i++ ) { - $this_token = $this->tokens[ $i ]; - $this_token['token_index'] = $i; - if ( isset( Tokens::$emptyTokens[ $this_token['code'] ] ) ) { + $function_param_specs = $this->parameter_specs[ $this->i18n_functions[ $matched_content ] ]; + $expected_args = count( $function_param_specs ); + + foreach ( $function_param_specs as $position => $name ) { + if ( 'number' === $name ) { + // This sniff does not examine the $number parameter. continue; } - if ( \T_COMMA === $this_token['code'] ) { - $arguments_tokens[] = $argument_tokens; - $argument_tokens = array(); + + $parameter_details[ $name ] = PassedParameters::getParameterFromStack( $parameters, $position, $name ); + } + + /* + * Examine the individual parameters. + */ + $this->check_argument_count( $stackPtr, $matched_content, $parameters, $expected_args ); + + foreach ( $parameter_details as $param_name => $param_info ) { + $is_string_literal = $this->check_argument_is_string_literal( $stackPtr, $matched_content, $param_name, $param_info ); + + /* + * If the parameter exists, remember whether the argument was a valid string literal. + * This is used in a few places to determine whether the checks which examine a text string should run. + */ + if ( false !== $param_info ) { + $parameter_details[ $param_name ]['is_string_literal'] = $is_string_literal; + } + + if ( false === $is_string_literal ) { continue; } - // Merge consecutive single or double quoted strings (when they span multiple lines). - if ( isset( Tokens::$textStringTokens[ $this_token['code'] ] ) ) { - for ( $j = ( $i + 1 ); $j < $this->tokens[ $func_open_paren_token ]['parenthesis_closer']; $j++ ) { - if ( $this_token['code'] === $this->tokens[ $j ]['code'] ) { - $this_token['content'] .= $this->tokens[ $j ]['content']; - $i = $j; - } else { - break; - } - } + if ( 'domain' === $param_name ) { + $this->check_textdomain_matches( $matched_content, $param_name, $param_info ); } - $argument_tokens[] = $this_token; - // Include everything up to and including the parenthesis_closer if this token has one. - if ( ! empty( $this_token['parenthesis_closer'] ) ) { - for ( $j = ( $i + 1 ); $j <= $this_token['parenthesis_closer']; $j++ ) { - $tokens[ $j ]['token_index'] = $j; - $argument_tokens[] = $tokens[ $j ]; + if ( \in_array( $param_name, array( 'text', 'single', 'singular', 'plural' ), true ) ) { + $this->check_placeholders_in_string( $matched_content, $param_name, $param_info ); + $has_content = $this->check_string_has_translatable_content( $matched_content, $param_name, $param_info ); + if ( true === $has_content ) { + $this->check_string_has_no_html_wrapper( $matched_content, $param_name, $param_info ); } - $i = $this_token['parenthesis_closer']; } } - if ( ! empty( $argument_tokens ) ) { - $arguments_tokens[] = $argument_tokens; - } - unset( $argument_tokens ); - - $argument_assertions = array(); - if ( 'simple' === $this->i18n_functions[ $matched_content ] ) { - $argument_assertions[] = array( - 'arg_name' => 'text', - 'tokens' => array_shift( $arguments_tokens ), - ); - $argument_assertions[] = array( - 'arg_name' => 'domain', - 'tokens' => array_shift( $arguments_tokens ), - ); - } elseif ( 'context' === $this->i18n_functions[ $matched_content ] ) { - $argument_assertions[] = array( - 'arg_name' => 'text', - 'tokens' => array_shift( $arguments_tokens ), - ); - $argument_assertions[] = array( - 'arg_name' => 'context', - 'tokens' => array_shift( $arguments_tokens ), - ); - $argument_assertions[] = array( - 'arg_name' => 'domain', - 'tokens' => array_shift( $arguments_tokens ), - ); - } elseif ( 'number' === $this->i18n_functions[ $matched_content ] ) { - $argument_assertions[] = array( - 'arg_name' => 'single', - 'tokens' => array_shift( $arguments_tokens ), - ); - $argument_assertions[] = array( - 'arg_name' => 'plural', - 'tokens' => array_shift( $arguments_tokens ), - ); - array_shift( $arguments_tokens ); - $argument_assertions[] = array( - 'arg_name' => 'domain', - 'tokens' => array_shift( $arguments_tokens ), - ); - } elseif ( 'number_context' === $this->i18n_functions[ $matched_content ] ) { - $argument_assertions[] = array( - 'arg_name' => 'single', - 'tokens' => array_shift( $arguments_tokens ), - ); - $argument_assertions[] = array( - 'arg_name' => 'plural', - 'tokens' => array_shift( $arguments_tokens ), - ); - array_shift( $arguments_tokens ); - $argument_assertions[] = array( - 'arg_name' => 'context', - 'tokens' => array_shift( $arguments_tokens ), - ); - $argument_assertions[] = array( - 'arg_name' => 'domain', - 'tokens' => array_shift( $arguments_tokens ), - ); - } elseif ( 'noopnumber' === $this->i18n_functions[ $matched_content ] ) { - $argument_assertions[] = array( - 'arg_name' => 'single', - 'tokens' => array_shift( $arguments_tokens ), - ); - $argument_assertions[] = array( - 'arg_name' => 'plural', - 'tokens' => array_shift( $arguments_tokens ), - ); - $argument_assertions[] = array( - 'arg_name' => 'domain', - 'tokens' => array_shift( $arguments_tokens ), - ); - } elseif ( 'noopnumber_context' === $this->i18n_functions[ $matched_content ] ) { - $argument_assertions[] = array( - 'arg_name' => 'single', - 'tokens' => array_shift( $arguments_tokens ), - ); - $argument_assertions[] = array( - 'arg_name' => 'plural', - 'tokens' => array_shift( $arguments_tokens ), - ); - $argument_assertions[] = array( - 'arg_name' => 'context', - 'tokens' => array_shift( $arguments_tokens ), - ); - $argument_assertions[] = array( - 'arg_name' => 'domain', - 'tokens' => array_shift( $arguments_tokens ), - ); - } - - if ( ! empty( $arguments_tokens ) ) { - $this->phpcsFile->addError( 'Too many arguments for function "%s".', $func_open_paren_token, 'TooManyFunctionArgs', array( $matched_content ) ); + /* + * For _n*() calls, compare the singular and plural strings. + * + * If either of the arguments is missing, empty or has more than 1 token, skip out. + * An error for that will already have been reported via the `check_argument_is_string_literal()` method. + */ + $single_details = null; + if ( isset( $parameter_details['single'] ) ) { + $single_details = $parameter_details['single']; + } elseif ( isset( $parameter_details['singular'] ) ) { + $single_details = $parameter_details['singular']; } - foreach ( $argument_assertions as $argument_assertion_context ) { - if ( empty( $argument_assertion_context['tokens'][0] ) ) { - $argument_assertion_context['stack_ptr'] = $func_open_paren_token; - } else { - $argument_assertion_context['stack_ptr'] = $argument_assertion_context['tokens'][0]['token_index']; - } - $this->check_argument_tokens( $argument_assertion_context ); + if ( isset( $single_details, $parameter_details['plural'] ) + && false !== $single_details + && false !== $parameter_details['plural'] + && true === $single_details['is_string_literal'] + && true === $parameter_details['plural']['is_string_literal'] + ) { + $this->compare_single_and_plural_arguments( $stackPtr, $single_details, $parameter_details['plural'] ); } - // For _n*() calls, compare the singular and plural strings. - if ( false !== strpos( $this->i18n_functions[ $matched_content ], 'number' ) ) { - $single_context = $argument_assertions[0]; - $plural_context = $argument_assertions[1]; - - $this->compare_single_and_plural_arguments( $stack_ptr, $single_context, $plural_context ); - } + /* + * Check if a translators comments is necessary and if so, if it exists. + */ + $this->check_for_translator_comment( $stackPtr, $matched_content, $parameter_details ); + } - if ( true === $this->check_translator_comments ) { - $this->check_for_translator_comment( $stack_ptr, $argument_assertions ); + /** + * Verify that there are no superfluous function arguments. + * + * @since 3.0.0 Check moved from the `process_matched_token()` method to this method. + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * @param array $parameters The parameters array. + * @param int $expected_count The expected number of passed arguments. + * + * @return void + */ + private function check_argument_count( $stackPtr, $matched_content, $parameters, $expected_count ) { + $actual_count = count( $parameters ); + if ( $actual_count > $expected_count ) { + $this->phpcsFile->addError( + 'Too many parameters passed to function "%s()". Expected: %s parameters, received: %s', + $stackPtr, + 'TooManyFunctionArgs', + array( $matched_content, $expected_count, $actual_count ) + ); } } /** - * Check if supplied tokens represent a translation text string literal. + * Check if an arbitrary function call parameter is a text string literal suitable for use in the translation functions. * - * @param array $context Context (@todo needs better description). - * @return bool + * Will also check and warn about missing parameters. + * + * @since 3.0.0 Most of the logic in this method used to be contained in the, now removed, `check_argument_tokens()` method. + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * @param string $param_name The name of the parameter being examined. + * @param array|false $param_info Parameter info array for an individual parameter, + * as received from the PassedParemeters class. + * + * @return bool Whether or not the argument is a string literal. */ - protected function check_argument_tokens( $context ) { - $stack_ptr = $context['stack_ptr']; - $tokens = $context['tokens']; - $arg_name = $context['arg_name']; - $is_error = empty( $context['warning'] ); - $content = isset( $tokens[0] ) ? $tokens[0]['content'] : ''; - - if ( empty( $tokens ) || 0 === \count( $tokens ) ) { - $code = $this->string_to_errorcode( 'MissingArg' . ucfirst( $arg_name ) ); - if ( 'domain' !== $arg_name ) { - $this->addMessage( 'Missing $%s arg.', $stack_ptr, $is_error, $code, array( $arg_name ) ); - return false; - } + private function check_argument_is_string_literal( $stackPtr, $matched_content, $param_name, $param_info ) { + /* + * Check if the parameter was supplied. + */ + if ( false === $param_info || '' === $param_info['clean'] ) { + $error_code = MessageHelper::stringToErrorcode( 'MissingArg' . ucfirst( $param_name ) ); + + /* + * Special case the text domain parameter, which is allowed to be "missing" + * when set to `default` (= WP Core translation). + */ + if ( 'domain' === $param_name ) { + if ( empty( $this->text_domain ) ) { + // If no text domain is passed, presume WP Core. + return false; + } - // Ok, we're examining a text domain, now deal correctly with the 'default' text domain. - if ( true === $this->text_domain_is_default ) { - return true; - } + if ( true === $this->text_domain_is_default ) { + return false; + } - if ( true === $this->text_domain_contains_default ) { - $this->phpcsFile->addWarning( - 'Missing $%s arg. If this text string is supposed to use a WP Core translation, use the "default" text domain.', - $stack_ptr, - $code . 'Default', - array( $arg_name ) - ); - } elseif ( ! empty( $this->text_domain ) ) { - $this->addMessage( 'Missing $%s arg.', $stack_ptr, $is_error, $code, array( $arg_name ) ); + if ( true === $this->text_domain_contains_default ) { + $this->phpcsFile->addWarning( + 'Missing $%s parameter in function call to %s(). If this text string is supposed to use a WP Core translation, use the "default" text domain.', + $stackPtr, + $error_code . 'Default', + array( $param_name, $matched_content ) + ); + return false; + } } + $this->phpcsFile->addError( + 'Missing $%s parameter in function call to %s().', + $stackPtr, + $error_code, + array( $param_name, $matched_content ) + ); + return false; } - if ( \count( $tokens ) > 1 ) { - $contents = ''; - foreach ( $tokens as $token ) { - $contents .= $token['content']; + /* + * Check if the parameter consists of one singular text string literal. + * Heredoc/nowdocs not allowed. Multi-line single/double quoted strings are allowed. + */ + $first_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, $param_info['start'], ( $param_info['end'] + 1 ), true ); + for ( $i = $first_non_empty; $i <= $param_info['end']; $i++ ) { + if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) { + continue; } - $code = $this->string_to_errorcode( 'NonSingularStringLiteral' . ucfirst( $arg_name ) ); - $this->addMessage( 'The $%s arg must be a single string literal, not "%s".', $stack_ptr, $is_error, $code, array( $arg_name, $contents ) ); - return false; - } - if ( \in_array( $arg_name, array( 'text', 'single', 'plural' ), true ) ) { - $this->check_text( $context ); + if ( isset( Tokens::$stringTokens[ $this->tokens[ $i ]['code'] ] ) === false ) { + $error_code = MessageHelper::stringToErrorcode( 'NonSingularStringLiteral' . ucfirst( $param_name ) ); + $this->phpcsFile->addError( + 'The $%s parameter must be a single text string literal. Found: %s', + $first_non_empty, + $error_code, + array( $param_name, $param_info['clean'] ) + ); + return false; + } } - if ( \T_DOUBLE_QUOTED_STRING === $tokens[0]['code'] || \T_HEREDOC === $tokens[0]['code'] ) { - $interpolated_variables = $this->get_interpolated_variables( $content ); + /* + * Make sure the text string does not contain any interpolated variable. + */ + if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $first_non_empty ]['code'] ) { + $error_code = MessageHelper::stringToErrorcode( 'InterpolatedVariable' . ucfirst( $param_name ) ); + + $interpolated_variables = TextStrings::getEmbeds( $param_info['clean'] ); foreach ( $interpolated_variables as $interpolated_variable ) { - $code = $this->string_to_errorcode( 'InterpolatedVariable' . ucfirst( $arg_name ) ); - $this->addMessage( 'The $%s arg must not contain interpolated variables. Found "$%s".', $stack_ptr, $is_error, $code, array( $arg_name, $interpolated_variable ) ); + $this->phpcsFile->addError( + 'The $%s parameter must not contain interpolated variables or expressions. Found: %s', + $first_non_empty, + $error_code, + array( $param_name, $interpolated_variable ) + ); } + if ( ! empty( $interpolated_variables ) ) { return false; } } - if ( isset( Tokens::$textStringTokens[ $tokens[0]['code'] ] ) ) { - if ( 'domain' === $arg_name && ! empty( $this->text_domain ) ) { - $stripped_content = $this->strip_quotes( $content ); - - if ( ! \in_array( $stripped_content, $this->text_domain, true ) ) { - $this->addMessage( - 'Mismatched text domain. Expected \'%s\' but got %s.', - $stack_ptr, - $is_error, - 'TextDomainMismatch', - array( implode( "' or '", $this->text_domain ), $content ) - ); - return false; - } + return true; + } - if ( true === $this->text_domain_is_default && 'default' === $stripped_content ) { - $fixable = false; - $error = 'No need to supply the text domain when the only accepted text domain is "default".'; - $error_code = 'SuperfluousDefaultTextDomain'; + /** + * Check the correct text domain is being used. + * + * @since 3.0.0 The logic in this method used to be contained in the, now removed, `check_argument_tokens()` method. + * + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * @param string $param_name The name of the parameter being examined. + * @param array|false $param_info Parameter info array for an individual parameter, + * as received from the PassedParemeters class. + * + * @return void + */ + private function check_textdomain_matches( $matched_content, $param_name, $param_info ) { + $stripped_content = TextStrings::stripQuotes( $param_info['clean'] ); - if ( $tokens[0]['token_index'] === $stack_ptr ) { - $prev = $this->phpcsFile->findPrevious( \T_WHITESPACE, ( $stack_ptr - 1 ), null, true ); - if ( false !== $prev && \T_COMMA === $this->tokens[ $prev ]['code'] ) { - $fixable = true; - } - } + if ( empty( $this->text_domain ) && '' === $stripped_content ) { + $first_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, $param_info['start'], ( $param_info['end'] + 1 ), true ); - if ( false === $fixable ) { - $this->phpcsFile->addWarning( $error, $stack_ptr, $error_code ); - return false; - } + $this->phpcsFile->addError( + 'The passed $domain should never be an empty string. Either pass a text domain or remove the parameter.', + $first_non_empty, + 'EmptyTextDomain' + ); + } - $fix = $this->phpcsFile->addFixableWarning( $error, $stack_ptr, $error_code ); - if ( true === $fix ) { - // Remove preceeding comma, whitespace and the text domain token. - $this->phpcsFile->fixer->beginChangeset(); - for ( $i = $prev; $i <= $stack_ptr; $i++ ) { - $this->phpcsFile->fixer->replaceToken( $i, '' ); - } - $this->phpcsFile->fixer->endChangeset(); - } + if ( empty( $this->text_domain ) ) { + // Nothing more to do, the other checks all depend on a text domain being known. + return; + } - return false; - } - } + if ( ! \in_array( $stripped_content, $this->text_domain, true ) ) { + $first_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, $param_info['start'], ( $param_info['end'] + 1 ), true ); - return true; + $this->phpcsFile->addError( + 'Mismatched text domain. Expected \'%s\' but got %s.', + $first_non_empty, + 'TextDomainMismatch', + array( implode( "' or '", $this->text_domain ), $param_info['clean'] ) + ); + return; } - $code = $this->string_to_errorcode( 'NonSingularStringLiteral' . ucfirst( $arg_name ) ); - $this->addMessage( 'The $%s arg must be a single string literal, not "%s".', $stack_ptr, $is_error, $code, array( $arg_name, $content ) ); - return false; - } + if ( true === $this->text_domain_is_default && 'default' === $stripped_content ) { + $fixable = false; + $error = 'No need to supply the text domain in function call to %s() when the only accepted text domain is "default".'; + $error_code = 'SuperfluousDefaultTextDomain'; + $data = array( $matched_content ); - /** - * Check for inconsistencies between single and plural arguments. - * - * @param int $stack_ptr The position of the current token in the stack. - * @param array $single_context Single context (@todo needs better description). - * @param array $plural_context Plural context (@todo needs better description). - * @return void - */ - protected function compare_single_and_plural_arguments( $stack_ptr, $single_context, $plural_context ) { - $single_content = $single_context['tokens'][0]['content']; - $plural_content = $plural_context['tokens'][0]['content']; + $first_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, $param_info['start'], ( $param_info['end'] + 1 ), true ); - preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $single_content, $single_placeholders ); - $single_placeholders = $single_placeholders[0]; + // Prevent removing comments when auto-fixing. + $remove_from = ( $param_info['start'] - 1 ); + $remove_to = $first_non_empty; - preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $plural_content, $plural_placeholders ); - $plural_placeholders = $plural_placeholders[0]; + if ( isset( $param_info['name_token'] ) ) { + $remove_from = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $param_info['name_token'] - 1 ), null, true ); + if ( \T_OPEN_PARENTHESIS === $this->tokens[ $remove_from ]['code'] ) { + ++$remove_from; // Don't remove the open parenthesis. - // English conflates "singular" with "only one", described in the codex: - // https://codex.wordpress.org/I18n_for_WordPress_Developers#Plurals . - if ( \count( $single_placeholders ) < \count( $plural_placeholders ) ) { - $error_string = 'Missing singular placeholder, needed for some languages. See https://codex.wordpress.org/I18n_for_WordPress_Developers#Plurals'; - $single_index = $single_context['tokens'][0]['token_index']; + /* + * Named param as first param in the function call, if we fix this, we need to + * remove the comma _after_ the parameter as well to prevent creating a parse error. + */ + $remove_to = $param_info['end']; + if ( \T_COMMA === $this->tokens[ ( $param_info['end'] + 1 ) ]['code'] ) { + ++$remove_to; // Include the comma. + } + } + } - $this->phpcsFile->addError( $error_string, $single_index, 'MissingSingularPlaceholder' ); - } + // Now, make sure there are no comments in the tokens we want to remove. + if ( $this->phpcsFile->findNext( Tokens::$commentTokens, $remove_from, ( $remove_to + 1 ) ) === false ) { + $fixable = true; + } - // Reordering is fine, but mismatched placeholders is probably wrong. - sort( $single_placeholders ); - sort( $plural_placeholders ); + if ( false === $fixable ) { + $this->phpcsFile->addWarning( $error, $first_non_empty, $error_code, $data ); + return; + } - if ( $single_placeholders !== $plural_placeholders ) { - $this->phpcsFile->addWarning( 'Mismatched placeholders is probably an error', $stack_ptr, 'MismatchedPlaceholders' ); + $fix = $this->phpcsFile->addFixableWarning( $error, $first_non_empty, $error_code, $data ); + if ( true === $fix ) { + $this->phpcsFile->fixer->beginChangeset(); + for ( $i = $remove_from; $i <= $remove_to; $i++ ) { + $this->phpcsFile->fixer->replaceToken( $i, '' ); + } + $this->phpcsFile->fixer->endChangeset(); + } } } /** - * Check the string itself for problems. + * Check the placeholders used in translatable text for common problems. + * + * @since 3.0.0 The logic in this method used to be contained in the, now removed, `check_text()` method. + * + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * @param string $param_name The name of the parameter being examined. + * @param array|false $param_info Parameter info array for an individual parameter, + * as received from the PassedParemeters class. * - * @param array $context Context (@todo needs better description). * @return void */ - protected function check_text( $context ) { - $stack_ptr = $context['stack_ptr']; - $arg_name = $context['arg_name']; - $content = $context['tokens'][0]['content']; - $is_error = empty( $context['warning'] ); + private function check_placeholders_in_string( $matched_content, $param_name, $param_info ) { + $content = $param_info['clean']; // UnorderedPlaceholders: Check for multiple unordered placeholders. $unordered_matches_count = preg_match_all( self::UNORDERED_SPRINTF_PLACEHOLDER_REGEX, $content, $unordered_matches ); $unordered_matches = $unordered_matches[0]; $all_matches_count = preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $content, $all_matches ); - if ( $unordered_matches_count > 0 && $unordered_matches_count !== $all_matches_count && $all_matches_count > 1 ) { - $code = $this->string_to_errorcode( 'MixedOrderedPlaceholders' . ucfirst( $arg_name ) ); + if ( $unordered_matches_count > 0 + && $unordered_matches_count !== $all_matches_count + && $all_matches_count > 1 + ) { + $first_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, $param_info['start'], ( $param_info['end'] + 1 ), true ); + $error_code = MessageHelper::stringToErrorcode( 'MixedOrderedPlaceholders' . ucfirst( $param_name ) ); + $this->phpcsFile->addError( - 'Multiple placeholders should be ordered. Mix of ordered and non-ordered placeholders found. Found: %s.', - $stack_ptr, - $code, - array( implode( ', ', $all_matches[0] ) ) + 'Multiple placeholders in translatable strings should be ordered. Mix of ordered and non-ordered placeholders found. Found: "%s" in %s.', + $first_non_empty, + $error_code, + array( implode( ', ', $all_matches[0] ), $param_info['clean'] ) ); + return; + } - } elseif ( $unordered_matches_count >= 2 ) { - $code = $this->string_to_errorcode( 'UnorderedPlaceholders' . ucfirst( $arg_name ) ); + if ( $unordered_matches_count >= 2 ) { + $first_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, $param_info['start'], ( $param_info['end'] + 1 ), true ); + $error_code = MessageHelper::stringToErrorcode( 'UnorderedPlaceholders' . ucfirst( $param_name ) ); $suggestions = array(); $replace_regexes = array(); @@ -593,138 +682,281 @@ protected function check_text( $context ) { $to_insert .= ( '"' !== $content[0] ) ? '$' : '\$'; $suggestions[ $i ] = substr_replace( $unordered_matches[ $i ], $to_insert, 1, 0 ); - // Prepare the strings for use a regex. + // Prepare the strings for use in a regex. $replace_regexes[ $i ] = '`\Q' . $unordered_matches[ $i ] . '\E`'; - // Note: the initial \\ is a literal \, the four \ in the replacement translate to also to a literal \. + // Note: the initial \\ is a literal \, the four \ in the replacement translate also to a literal \. $replacements[ $i ] = str_replace( '\\', '\\\\', $suggestions[ $i ] ); // Note: the $ needs escaping to prevent numeric sequences after the $ being interpreted as match replacements. $replacements[ $i ] = str_replace( '$', '\\$', $replacements[ $i ] ); } - $fix = $this->addFixableMessage( - 'Multiple placeholders should be ordered. Expected \'%s\', but got %s.', - $stack_ptr, - $is_error, - $code, - array( implode( ', ', $suggestions ), implode( ', ', $unordered_matches ) ) + $fix = $this->phpcsFile->addFixableError( + 'Multiple placeholders in translatable strings should be ordered. Expected "%s", but got "%s" in %s.', + $first_non_empty, + $error_code, + array( implode( ', ', $suggestions ), implode( ', ', $unordered_matches ), $param_info['clean'] ) ); if ( true === $fix ) { + $this->phpcsFile->fixer->beginChangeset(); + $fixed_str = preg_replace( $replace_regexes, $replacements, $content, 1 ); - $this->phpcsFile->fixer->replaceToken( $stack_ptr, $fixed_str ); + $this->phpcsFile->fixer->replaceToken( $first_non_empty, $fixed_str ); + + $i = ( $first_non_empty + 1 ); + while ( $i <= $param_info['end'] && isset( Tokens::$stringTokens[ $this->tokens[ $i ]['code'] ] ) ) { + $this->phpcsFile->fixer->replaceToken( $i, '' ); + ++$i; + } + + $this->phpcsFile->fixer->endChangeset(); } } + } - /* - * NoEmptyStrings. - * - * Strip placeholders and surrounding quotes. - */ - $non_placeholder_content = trim( $this->strip_quotes( $content ) ); - $non_placeholder_content = preg_replace( self::SPRINTF_PLACEHOLDER_REGEX, '', $non_placeholder_content ); + /** + * Check if a parameter which is supposed to hold translatable text actually has translatable text. + * + * @since 3.0.0 The logic in this method used to be contained in the, now removed, `check_text()` method. + * + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * @param string $param_name The name of the parameter being examined. + * @param array|false $param_info Parameter info array for an individual parameter, + * as received from the PassedParemeters class. + * + * @return bool Whether or not the text string has translatable content. + */ + private function check_string_has_translatable_content( $matched_content, $param_name, $param_info ) { + // Strip placeholders and surrounding quotes. + $content_without_quotes = trim( TextStrings::stripQuotes( $param_info['clean'] ) ); + $non_placeholder_content = preg_replace( self::SPRINTF_PLACEHOLDER_REGEX, '', $content_without_quotes ); if ( '' === $non_placeholder_content ) { - $this->phpcsFile->addError( 'Strings should have translatable content', $stack_ptr, 'NoEmptyStrings' ); + $first_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, $param_info['start'], ( $param_info['end'] + 1 ), true ); + + $this->phpcsFile->addError( + 'The $%s text string should have translatable content. Found: %s', + $first_non_empty, + 'NoEmptyStrings', + array( $param_name, $param_info['clean'] ) + ); + return false; + } + + return true; + } + + /** + * Ensure that a translatable text string is not wrapped in HTML code. + * + * If the text is wrapped in HTML, the HTML should be moved out of the translatable text string. + * + * @since 3.0.0 The logic in this method used to be contained in the, now removed, `check_text()` method. + * + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * @param string $param_name The name of the parameter being examined. + * @param array|false $param_info Parameter info array for an individual parameter, + * as received from the PassedParemeters class. + * + * @return void + */ + private function check_string_has_no_html_wrapper( $matched_content, $param_name, $param_info ) { + // Strip surrounding quotes. + $content_without_quotes = trim( TextStrings::stripQuotes( $param_info['clean'] ) ); + + $reader = new XMLReader(); + $reader->XML( $content_without_quotes, 'UTF-8', \LIBXML_NOERROR | \LIBXML_ERR_NONE | \LIBXML_NOWARNING ); + + // Is the first node an HTML element? + if ( ! $reader->read() || XMLReader::ELEMENT !== $reader->nodeType ) { + return; + } + + // If the opening HTML element includes placeholders in its attributes, we don't warn. + // E.g. ''. + $i = 0; + while ( $attr = $reader->getAttributeNo( $i ) ) { + if ( preg_match( self::SPRINTF_PLACEHOLDER_REGEX, $attr ) === 1 ) { + return; + } + + ++$i; + } + + // We don't flag strings wrapped in `...`, as the link target might actually need localization. + if ( 'a' === $reader->name && $reader->getAttribute( 'href' ) ) { + return; + } + + // Does the entire string only consist of this HTML node? + if ( $reader->readOuterXml() === $content_without_quotes ) { + $first_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, $param_info['start'], ( $param_info['end'] + 1 ), true ); + + $this->phpcsFile->addWarning( + 'Translatable string should not be wrapped in HTML. Found: %s', + $first_non_empty, + 'NoHtmlWrappedStrings', + array( $param_info['clean'] ) + ); + } + } + + /** + * Check for inconsistencies in the placeholders between single and plural form of the translatable text string. + * + * @since 3.0.0 - The parameter names and expected format for the $param_info_single + * and the $param_info_plural parameters has changed. + * - The method visibility has been changed from `protected` to `private`. + * + * @param int $stackPtr The position of the function call token in the stack. + * @param array $param_info_single Parameter info array for the `$single` parameter, + * as received from the PassedParemeters class. + * @param array $param_info_plural Parameter info array for the `$plural` parameter, + * as received from the PassedParemeters class. + * + * @return void + */ + private function compare_single_and_plural_arguments( $stackPtr, $param_info_single, $param_info_plural ) { + $single_content = $param_info_single['clean']; + $plural_content = $param_info_plural['clean']; + + preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $single_content, $single_placeholders ); + $single_placeholders = $single_placeholders[0]; + + preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $plural_content, $plural_placeholders ); + $plural_placeholders = $plural_placeholders[0]; + + // English conflates "singular" with "only one", described in the codex: + // https://codex.wordpress.org/I18n_for_WordPress_Developers#Plurals . + if ( \count( $single_placeholders ) < \count( $plural_placeholders ) ) { + $error_string = 'Missing singular placeholder, needed for some languages. See https://codex.wordpress.org/I18n_for_WordPress_Developers#Plurals'; + $first_non_empty_single = $this->phpcsFile->findNext( Tokens::$emptyTokens, $param_info_single['start'], ( $param_info_single['end'] + 1 ), true ); + + $this->phpcsFile->addError( $error_string, $first_non_empty_single, 'MissingSingularPlaceholder' ); + return; + } + + // Reordering is fine, but mismatched placeholders is probably wrong. + sort( $single_placeholders, \SORT_NATURAL ); + sort( $plural_placeholders, \SORT_NATURAL ); + + if ( $single_placeholders !== $plural_placeholders ) { + $this->phpcsFile->addWarning( 'Mismatched placeholders is probably an error', $stackPtr, 'MismatchedPlaceholders' ); } } /** * Check for the presence of a translators comment if one of the text strings contains a placeholder. * - * @param int $stack_ptr The position of the gettext call token in the stack. - * @param array $args The function arguments. + * @since 3.0.0 - The parameter names and expected format for the $parameters parameter has changed. + * - The method visibility has been changed from `protected` to `private`. + * + * @param int $stackPtr The position of the gettext call token in the stack. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * @param array $parameters Array with information about the parameters. + * * @return void */ - protected function check_for_translator_comment( $stack_ptr, $args ) { - foreach ( $args as $arg ) { - if ( false === \in_array( $arg['arg_name'], array( 'text', 'single', 'plural' ), true ) ) { + private function check_for_translator_comment( $stackPtr, $matched_content, $parameters ) { + $needs_translators_comment = false; + + foreach ( $parameters as $param_name => $param_info ) { + if ( false === \in_array( $param_name, array( 'text', 'single', 'singular', 'plural' ), true ) ) { continue; } - if ( empty( $arg['tokens'] ) ) { + if ( false === $param_info || false === $param_info['is_string_literal'] ) { continue; } - foreach ( $arg['tokens'] as $token ) { - if ( empty( $token['content'] ) ) { - continue; - } + if ( preg_match( self::SPRINTF_PLACEHOLDER_REGEX, $param_info['clean'], $placeholders ) === 1 ) { + $needs_translators_comment = true; + break; + } + } - if ( preg_match( self::SPRINTF_PLACEHOLDER_REGEX, $token['content'], $placeholders ) < 1 ) { - // No placeholders found. - continue; - } + if ( false === $needs_translators_comment ) { + // No text string with placeholders found, no translation comment needed. + return; + } - $previous_comment = $this->phpcsFile->findPrevious( Tokens::$commentTokens, ( $stack_ptr - 1 ) ); + $previous_comment = $this->phpcsFile->findPrevious( Tokens::$commentTokens, ( $stackPtr - 1 ) ); - if ( false !== $previous_comment ) { - /* - * Check that the comment is either on the line before the gettext call or - * if it's not, that there is only whitespace between. - */ - $correctly_placed = false; + if ( false !== $previous_comment ) { + /* + * Check that the comment is either on the line before the gettext call or + * if it's not, that there is only whitespace between. + */ + $correctly_placed = false; - if ( ( $this->tokens[ $previous_comment ]['line'] + 1 ) === $this->tokens[ $stack_ptr ]['line'] ) { - $correctly_placed = true; - } else { - $next_non_whitespace = $this->phpcsFile->findNext( \T_WHITESPACE, ( $previous_comment + 1 ), $stack_ptr, true ); - if ( false === $next_non_whitespace || $this->tokens[ $next_non_whitespace ]['line'] === $this->tokens[ $stack_ptr ]['line'] ) { - // No non-whitespace found or next non-whitespace is on same line as gettext call. - $correctly_placed = true; + if ( ( $this->tokens[ $previous_comment ]['line'] + 1 ) === $this->tokens[ $stackPtr ]['line'] ) { + $correctly_placed = true; + } else { + $next_non_whitespace = $this->phpcsFile->findNext( \T_WHITESPACE, ( $previous_comment + 1 ), $stackPtr, true ); + if ( false === $next_non_whitespace || $this->tokens[ $next_non_whitespace ]['line'] === $this->tokens[ $stackPtr ]['line'] ) { + // No non-whitespace found or next non-whitespace is on same line as gettext call. + $correctly_placed = true; + } + unset( $next_non_whitespace ); + } + + /* + * Check that the comment starts with 'translators:'. + */ + if ( true === $correctly_placed ) { + if ( \T_COMMENT === $this->tokens[ $previous_comment ]['code'] ) { + $comment_text = trim( $this->tokens[ $previous_comment ]['content'] ); + + // If it's multi-line /* */ comment, collect all the parts. + if ( '*/' === substr( $comment_text, -2 ) && '/*' !== substr( $comment_text, 0, 2 ) ) { + for ( $i = ( $previous_comment - 1 ); 0 <= $i; $i-- ) { + if ( \T_COMMENT !== $this->tokens[ $i ]['code'] ) { + break; + } + + $comment_text = trim( $this->tokens[ $i ]['content'] ) . $comment_text; } - unset( $next_non_whitespace ); } - /* - * Check that the comment starts with 'translators:'. - */ - if ( true === $correctly_placed ) { - - if ( \T_COMMENT === $this->tokens[ $previous_comment ]['code'] ) { - $comment_text = trim( $this->tokens[ $previous_comment ]['content'] ); + if ( true === $this->is_translators_comment( $comment_text ) ) { + // Comment is ok. + return; + } + } - // If it's multi-line /* */ comment, collect all the parts. - if ( '*/' === substr( $comment_text, -2 ) && '/*' !== substr( $comment_text, 0, 2 ) ) { - for ( $i = ( $previous_comment - 1 ); 0 <= $i; $i-- ) { - if ( \T_COMMENT !== $this->tokens[ $i ]['code'] ) { - break; - } + if ( \T_DOC_COMMENT_CLOSE_TAG === $this->tokens[ $previous_comment ]['code'] ) { + // If it's docblock comment (wrong style) make sure that it's a translators comment. + if ( isset( $this->tokens[ $previous_comment ]['comment_opener'] ) ) { + $db_start = $this->tokens[ $previous_comment ]['comment_opener']; + } else { + $db_start = $this->phpcsFile->findPrevious( \T_DOC_COMMENT_OPEN_TAG, ( $previous_comment - 1 ) ); + } - $comment_text = trim( $this->tokens[ $i ]['content'] ) . $comment_text; - } - } + $db_first_text = $this->phpcsFile->findNext( \T_DOC_COMMENT_STRING, ( $db_start + 1 ), $previous_comment ); - if ( true === $this->is_translators_comment( $comment_text ) ) { - // Comment is ok. - return; - } - } elseif ( \T_DOC_COMMENT_CLOSE_TAG === $this->tokens[ $previous_comment ]['code'] ) { - // If it's docblock comment (wrong style) make sure that it's a translators comment. - $db_start = $this->phpcsFile->findPrevious( \T_DOC_COMMENT_OPEN_TAG, ( $previous_comment - 1 ) ); - $db_first_text = $this->phpcsFile->findNext( \T_DOC_COMMENT_STRING, ( $db_start + 1 ), $previous_comment ); - - if ( true === $this->is_translators_comment( $this->tokens[ $db_first_text ]['content'] ) ) { - $this->phpcsFile->addWarning( - 'A "translators:" comment must be a "/* */" style comment. Docblock comments will not be picked up by the tools to generate a ".pot" file.', - $stack_ptr, - 'TranslatorsCommentWrongStyle' - ); - return; - } - } + if ( true === $this->is_translators_comment( $this->tokens[ $db_first_text ]['content'] ) ) { + $this->phpcsFile->addWarning( + 'A "translators:" comment must be a "/* */" style comment. Docblock comments will not be picked up by the tools to generate a ".pot" file.', + $stackPtr, + 'TranslatorsCommentWrongStyle' + ); + return; } } - - // Found placeholders but no translators comment. - $this->phpcsFile->addWarning( - 'A gettext call containing placeholders was found, but was not accompanied by a "translators:" comment on the line above to clarify the meaning of the placeholders.', - $stack_ptr, - 'MissingTranslatorsComment' - ); - return; } } + + // Found placeholders but no translators comment. + $this->phpcsFile->addWarning( + 'A function call to %s() with texts containing placeholders was found, but was not accompanied by a "translators:" comment on the line above to clarify the meaning of the placeholders.', + $stackPtr, + 'MissingTranslatorsComment', + array( $matched_content ) + ); } /** @@ -733,6 +965,7 @@ protected function check_for_translator_comment( $stack_ptr, $args ) { * @since 0.11.0 * * @param string $content Comment string content. + * * @return bool */ private function is_translators_comment( $content ) { @@ -741,5 +974,4 @@ private function is_translators_comment( $content ) { } return false; } - } diff --git a/WordPress/Sniffs/WP/PostsPerPageSniff.php b/WordPress/Sniffs/WP/PostsPerPageSniff.php index 22f7ef04f7..a3f8ce45a7 100644 --- a/WordPress/Sniffs/WP/PostsPerPageSniff.php +++ b/WordPress/Sniffs/WP/PostsPerPageSniff.php @@ -3,33 +3,31 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\WP; +namespace WordPressCS\WordPress\Sniffs\WP; -use WordPress\AbstractArrayAssignmentRestrictionsSniff; +use PHPCSUtils\Utils\Numbers; +use PHPCSUtils\Utils\TextStrings; +use WordPressCS\WordPress\AbstractArrayAssignmentRestrictionsSniff; /** * Flag returning high or infinite posts_per_page. * - * @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#no-limit-queries + * @link https://vip.wordpress.com/documentation/vip-go/code-review-blockers-warnings-notices/#no-limit-queries * - * @package WPCS\WordPressCodingStandards - * - * @since 0.3.0 - * @since 0.13.0 Class name changed: this class is now namespaced. - * @since 0.14.0 Added the posts_per_page property. - * @since 1.0.0 This sniff has been split into two, with the check for high pagination - * limit being part of the WP category, and the check for pagination - * disabling being part of the VIP category. + * @since 0.3.0 + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.14.0 Added the posts_per_page property. + * @since 1.0.0 This sniff has been split into two, with the check for high pagination + * limit being part of the WP category, and the check for pagination + * disabling being part of the VIP category. */ -class PostsPerPageSniff extends AbstractArrayAssignmentRestrictionsSniff { +final class PostsPerPageSniff extends AbstractArrayAssignmentRestrictionsSniff { /** - * Posts per page property - * * Posts per page limit to check against. * * @since 0.14.0 @@ -46,8 +44,9 @@ class PostsPerPageSniff extends AbstractArrayAssignmentRestrictionsSniff { public function getGroups() { return array( 'posts_per_page' => array( - 'type' => 'warning', - 'keys' => array( + 'type' => 'warning', + 'message' => 'Detected high pagination limit, `%s` is set to `%s`', + 'keys' => array( 'posts_per_page', 'numberposts', ), @@ -62,17 +61,38 @@ public function getGroups() { * @param mixed $val Assigned value. * @param int $line Token line. * @param array $group Group definition. - * @return mixed FALSE if no match, TRUE if matches, STRING if matches - * with custom error message passed to ->process(). + * + * @return bool FALSE if no match, TRUE if matches. */ public function callback( $key, $val, $line, $group ) { - $this->posts_per_page = (int) $this->posts_per_page; + $stripped_val = TextStrings::stripQuotes( $val ); + + if ( $val !== $stripped_val ) { + // The value was a text string. For text strings, we only accept purely numeric values. + if ( preg_match( '`^[0-9]+$`', $stripped_val ) !== 1 ) { + // Not a purely numeric value, so any comparison would be a false comparison. + return false; + } - if ( $val > $this->posts_per_page ) { - return 'Detected high pagination limit, `%s` is set to `%s`'; + // Purely numeric string, treat it as an integer from here on out. + $val = $stripped_val; } - return false; - } + $first_char = $val[0]; + if ( '-' === $first_char || '+' === $first_char ) { + $val = ltrim( $val, '-+' ); + } else { + $first_char = ''; + } + + $real_value = Numbers::getDecimalValue( $val ); + if ( false === $real_value ) { + // This wasn't a purely numeric value, so any comparison would be a false comparison. + return false; + } + $val = $first_char . $real_value; + + return ( (int) $val > (int) $this->posts_per_page ); + } } diff --git a/WordPress/Sniffs/WP/PreparedSQLSniff.php b/WordPress/Sniffs/WP/PreparedSQLSniff.php deleted file mode 100644 index affb8d1b67..0000000000 --- a/WordPress/Sniffs/WP/PreparedSQLSniff.php +++ /dev/null @@ -1,67 +0,0 @@ - false, - ); - - /** - * Don't use. - * - * @deprecated 1.0.0 - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return void|int - */ - public function process_token( $stackPtr ) { - if ( false === $this->thrown['DeprecatedSniff'] ) { - $this->phpcsFile->addWarning( - 'The "WordPress.WP.PreparedSQL" sniff has been renamed to "WordPress.DB.PreparedSQL". Please update your custom ruleset.', - 0, - 'DeprecatedSniff' - ); - - $this->thrown['DeprecatedSniff'] = true; - } - - return parent::process_token( $stackPtr ); - } - -} diff --git a/WordPress/Sniffs/WP/TimezoneChangeSniff.php b/WordPress/Sniffs/WP/TimezoneChangeSniff.php deleted file mode 100644 index 1755c7497f..0000000000 --- a/WordPress/Sniffs/WP/TimezoneChangeSniff.php +++ /dev/null @@ -1,54 +0,0 @@ - array( - * 'lambda' => array( - * 'type' => 'error' | 'warning', - * 'message' => 'Use anonymous functions instead please!', - * 'functions' => array( 'file_get_contents', 'create_function' ), - * ) - * ) - * - * @return array - */ - public function getGroups() { - return array( - 'timezone_change' => array( - 'type' => 'error', - 'message' => 'Using %s() and similar isn\'t allowed, instead use WP internal timezone support.', - 'functions' => array( - 'date_default_timezone_set', - ), - ), - ); - } - -} diff --git a/WordPress/Sniffs/WhiteSpace/ArbitraryParenthesesSpacingSniff.php b/WordPress/Sniffs/WhiteSpace/ArbitraryParenthesesSpacingSniff.php deleted file mode 100644 index 41df4a33c8..0000000000 --- a/WordPress/Sniffs/WhiteSpace/ArbitraryParenthesesSpacingSniff.php +++ /dev/null @@ -1,255 +0,0 @@ -ignoreTokens = Tokens::$functionNameTokens; - $this->ignoreTokens[ \T_VARIABLE ] = \T_VARIABLE; - $this->ignoreTokens[ \T_CLOSE_PARENTHESIS ] = \T_CLOSE_PARENTHESIS; - $this->ignoreTokens[ \T_CLOSE_CURLY_BRACKET ] = \T_CLOSE_CURLY_BRACKET; - $this->ignoreTokens[ \T_CLOSE_SQUARE_BRACKET ] = \T_CLOSE_SQUARE_BRACKET; - $this->ignoreTokens[ \T_CLOSE_SHORT_ARRAY ] = \T_CLOSE_SHORT_ARRAY; - $this->ignoreTokens[ \T_ANON_CLASS ] = \T_ANON_CLASS; - $this->ignoreTokens[ \T_USE ] = \T_USE; - $this->ignoreTokens[ \T_LIST ] = \T_LIST; - $this->ignoreTokens[ \T_DECLARE ] = \T_DECLARE; - - // The below two tokens have been added to the Tokens::$functionNameTokens array in PHPCS 3.1.0, - // so they can be removed once the minimum PHPCS requirement of WPCS has gone up. - $this->ignoreTokens[ \T_SELF ] = \T_SELF; - $this->ignoreTokens[ \T_STATIC ] = \T_STATIC; - - // Language constructs where the use of parentheses should be discouraged instead. - $this->ignoreTokens[ \T_THROW ] = \T_THROW; - $this->ignoreTokens[ \T_YIELD ] = \T_YIELD; - $this->ignoreTokens[ \T_YIELD_FROM ] = \T_YIELD_FROM; - $this->ignoreTokens[ \T_CLONE ] = \T_CLONE; - - return array( - \T_OPEN_PARENTHESIS, - \T_CLOSE_PARENTHESIS, - ); - } - - /** - * Processes this test, when one of its tokens is encountered. - * - * @since 0.14.0 - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return int|void Integer stack pointer to skip forward or void to continue - * normal file processing. - */ - public function process_token( $stackPtr ) { - - if ( isset( $this->tokens[ $stackPtr ]['parenthesis_owner'] ) ) { - // This parenthesis is owned by a function/control structure etc. - return; - } - - // More checking for the type of parenthesis we *don't* want to handle. - $opener = $stackPtr; - if ( \T_CLOSE_PARENTHESIS === $this->tokens[ $stackPtr ]['code'] ) { - if ( ! isset( $this->tokens[ $stackPtr ]['parenthesis_opener'] ) ) { - // Parse error. - return; - } - - $opener = $this->tokens[ $stackPtr ]['parenthesis_opener']; - } - - $preOpener = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $opener - 1 ), null, true ); - if ( false !== $preOpener && isset( $this->ignoreTokens[ $this->tokens[ $preOpener ]['code'] ] ) ) { - // Function or language construct call. - return; - } - - /* - * Check for empty parentheses. - */ - if ( \T_OPEN_PARENTHESIS === $this->tokens[ $stackPtr ]['code'] - && isset( $this->tokens[ $stackPtr ]['parenthesis_closer'] ) - ) { - $nextNonEmpty = $this->phpcsFile->findNext( \T_WHITESPACE, ( $stackPtr + 1 ), null, true ); - if ( $nextNonEmpty === $this->tokens[ $stackPtr ]['parenthesis_closer'] ) { - $this->phpcsFile->addWarning( 'Empty set of arbitrary parentheses found.', $stackPtr, 'FoundEmpty' ); - - return ( $this->tokens[ $stackPtr ]['parenthesis_closer'] + 1 ); - } - } - - /* - * Check the spacing on the inside of the parentheses. - */ - $this->spacingInside = (int) $this->spacingInside; - - if ( \T_OPEN_PARENTHESIS === $this->tokens[ $stackPtr ]['code'] - && isset( $this->tokens[ ( $stackPtr + 1 ) ], $this->tokens[ ( $stackPtr + 2 ) ] ) - ) { - $nextToken = $this->tokens[ ( $stackPtr + 1 ) ]; - - if ( \T_WHITESPACE !== $nextToken['code'] ) { - $inside = 0; - } else { - if ( $this->tokens[ ( $stackPtr + 2 ) ]['line'] !== $this->tokens[ $stackPtr ]['line'] ) { - $inside = 'newline'; - } else { - $inside = $nextToken['length']; - } - } - - if ( $this->spacingInside !== $inside - && ( 'newline' !== $inside || false === $this->ignoreNewlines ) - ) { - $error = 'Expected %s space after open parenthesis; %s found'; - $data = array( - $this->spacingInside, - $inside, - ); - $fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'SpaceAfterOpen', $data ); - - if ( true === $fix ) { - $expected = ''; - if ( $this->spacingInside > 0 ) { - $expected = str_repeat( ' ', $this->spacingInside ); - } - - if ( 0 === $inside ) { - if ( '' !== $expected ) { - $this->phpcsFile->fixer->addContent( $stackPtr, $expected ); - } - } elseif ( 'newline' === $inside ) { - $this->phpcsFile->fixer->beginChangeset(); - for ( $i = ( $stackPtr + 2 ); $i < $this->phpcsFile->numTokens; $i++ ) { - if ( \T_WHITESPACE !== $this->tokens[ $i ]['code'] ) { - break; - } - $this->phpcsFile->fixer->replaceToken( $i, '' ); - } - $this->phpcsFile->fixer->replaceToken( ( $stackPtr + 1 ), $expected ); - $this->phpcsFile->fixer->endChangeset(); - } else { - $this->phpcsFile->fixer->replaceToken( ( $stackPtr + 1 ), $expected ); - } - } - } - } - - if ( \T_CLOSE_PARENTHESIS === $this->tokens[ $stackPtr ]['code'] - && isset( $this->tokens[ ( $stackPtr - 1 ) ], $this->tokens[ ( $stackPtr - 2 ) ] ) - ) { - $prevToken = $this->tokens[ ( $stackPtr - 1 ) ]; - - if ( \T_WHITESPACE !== $prevToken['code'] ) { - $inside = 0; - } else { - if ( $this->tokens[ ( $stackPtr - 2 ) ]['line'] !== $this->tokens[ $stackPtr ]['line'] ) { - $inside = 'newline'; - } else { - $inside = $prevToken['length']; - } - } - - if ( $this->spacingInside !== $inside - && ( 'newline' !== $inside || false === $this->ignoreNewlines ) - ) { - $error = 'Expected %s space before close parenthesis; %s found'; - $data = array( - $this->spacingInside, - $inside, - ); - $fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'SpaceBeforeClose', $data ); - - if ( true === $fix ) { - $expected = ''; - if ( $this->spacingInside > 0 ) { - $expected = str_repeat( ' ', $this->spacingInside ); - } - - if ( 0 === $inside ) { - if ( '' !== $expected ) { - $this->phpcsFile->fixer->addContentBefore( $stackPtr, $expected ); - } - } elseif ( 'newline' === $inside ) { - $this->phpcsFile->fixer->beginChangeset(); - for ( $i = ( $stackPtr - 2 ); $i > 0; $i-- ) { - if ( \T_WHITESPACE !== $this->tokens[ $i ]['code'] ) { - break; - } - $this->phpcsFile->fixer->replaceToken( $i, '' ); - } - $this->phpcsFile->fixer->replaceToken( ( $stackPtr - 1 ), $expected ); - $this->phpcsFile->fixer->endChangeset(); - } else { - $this->phpcsFile->fixer->replaceToken( ( $stackPtr - 1 ), $expected ); - } - } - } - } - } - -} diff --git a/WordPress/Sniffs/WhiteSpace/CastStructureSpacingSniff.php b/WordPress/Sniffs/WhiteSpace/CastStructureSpacingSniff.php index a239cb4d13..b00303ed3f 100755 --- a/WordPress/Sniffs/WhiteSpace/CastStructureSpacingSniff.php +++ b/WordPress/Sniffs/WhiteSpace/CastStructureSpacingSniff.php @@ -3,29 +3,30 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\WhiteSpace; +namespace WordPressCS\WordPress\Sniffs\WhiteSpace; -use WordPress\Sniff; -use PHP_CodeSniffer_Tokens as Tokens; +use PHP_CodeSniffer\Util\Tokens; +use WordPressCS\WordPress\Sniff; /** - * Ensure cast statements don't contain whitespace, but *are* surrounded by whitespace, based upon Squiz code. + * Ensure cast statements are preceded by whitespace. * - * @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#space-usage + * @link https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#space-usage * - * @package WPCS\WordPressCodingStandards - * - * @since 0.3.0 - * @since 0.11.0 This sniff now has the ability to fix the issues it flags. - * @since 0.11.0 The error level for all errors thrown by this sniff has been raised from warning to error. - * @since 0.12.0 This class now extends WordPress_Sniff. - * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.3.0 + * @since 0.11.0 This sniff now has the ability to fix the issues it flags. + * @since 0.11.0 The error level for all errors thrown by this sniff has been raised from warning to error. + * @since 0.12.0 This class now extends the WordPressCS native `Sniff` class. + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 1.2.0 Removed the `NoSpaceAfterCloseParenthesis` error code in favour of the + * upstream `Generic.Formatting.SpaceAfterCast.NoSpace` error. + * @since 2.2.0 Added exception for whitespace between spread operator and cast. */ -class CastStructureSpacingSniff extends Sniff { +final class CastStructureSpacingSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. @@ -45,13 +46,14 @@ public function register() { */ public function process_token( $stackPtr ) { - if ( \T_WHITESPACE !== $this->tokens[ ( $stackPtr - 1 ) ]['code'] ) { - $error = 'No space before opening casting parenthesis is prohibited'; + if ( \T_WHITESPACE !== $this->tokens[ ( $stackPtr - 1 ) ]['code'] + && \T_ELLIPSIS !== $this->tokens[ ( $stackPtr - 1 ) ]['code'] + ) { + $error = 'Expected a space before the type cast open parenthesis; none found'; $fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'NoSpaceBeforeOpenParenthesis' ); if ( true === $fix ) { $this->phpcsFile->fixer->addContentBefore( $stackPtr, ' ' ); } } } - } diff --git a/WordPress/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php b/WordPress/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php index 4f8317c774..11fecd6491 100644 --- a/WordPress/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php +++ b/WordPress/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php @@ -3,32 +3,32 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\WhiteSpace; +namespace WordPressCS\WordPress\Sniffs\WhiteSpace; -use WordPress\Sniff; -use PHP_CodeSniffer_Tokens as Tokens; +use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Tokens\Collections; +use WordPressCS\WordPress\Sniff; /** - * Enforces spacing around logical operators and assignments, based upon Squiz code. + * Checks that control structures have the correct spacing around brackets, based upon Squiz code. * - * @package WPCS\WordPressCodingStandards - * - * @since 0.1.0 - * @since 2013-06-11 This sniff no longer supports JS. - * @since 0.3.0 This sniff now has the ability to fix most errors it flags. - * @since 0.7.0 This class now extends WordPress_Sniff. - * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.1.0 + * @since 2013-06-11 This sniff no longer supports JS. + * @since 0.3.0 This sniff now has the ability to fix most errors it flags. + * @since 0.7.0 This class now extends the WordPressCS native `Sniff` class. + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 3.0.0 Checks related to function declarations have been removed from this sniff. * - * Last synced with base class 2017-01-15 at commit b024ad84656c37ef5733c6998ebc1e60957b2277. + * Last synced with base class 2021-11-20 at commit 7f11ffc8222b123c06345afd3261221561c3bb29. * Note: This class has diverged quite far from the original. All the same, checking occasionally * to see if there are upstream fixes made from which this sniff can benefit, is warranted. - * @link https://github.com/squizlabs/PHP_CodeSniffer/blob/master/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php + * @link https://github.com/squizlabs/PHP_CodeSniffer/blob/master/src/Standards/Squiz/Sniffs/WhiteSpace/ControlStructureSpacingSniff.php */ -class ControlStructureSpacingSniff extends Sniff { +final class ControlStructureSpacingSniff extends Sniff { /** * Check for blank lines on start/end of control structures. @@ -51,33 +51,20 @@ class ControlStructureSpacingSniff extends Sniff { */ public $space_before_colon = 'required'; - /** - * How many spaces should be between a T_CLOSURE and T_OPEN_PARENTHESIS. - * - * `function[*]() {...}` - * - * @since 0.7.0 - * - * @var int - */ - public $spaces_before_closure_open_paren = -1; - /** * Tokens for which to ignore extra space on the inside of parenthesis. * - * For functions, this is already checked by the Squiz.Functions.FunctionDeclarationArgumentSpacing sniff. - * For do / else / try, there are no parenthesis, so skip it. + * For do / else / try / finally, there are no parenthesis, so skip it. * * @since 0.11.0 * * @var array */ private $ignore_extra_space_after_open_paren = array( - \T_FUNCTION => true, - \T_CLOSURE => true, - \T_DO => true, - \T_ELSE => true, - \T_TRY => true, + \T_DO => true, + \T_ELSE => true, + \T_TRY => true, + \T_FINALLY => true, ); /** @@ -95,11 +82,10 @@ public function register() { \T_DO, \T_ELSE, \T_ELSEIF, - \T_FUNCTION, - \T_CLOSURE, - \T_USE, \T_TRY, \T_CATCH, + \T_FINALLY, + \T_MATCH, ); } @@ -111,12 +97,8 @@ public function register() { * @return void */ public function process_token( $stackPtr ) { - $this->spaces_before_closure_open_paren = (int) $this->spaces_before_closure_open_paren; - if ( isset( $this->tokens[ ( $stackPtr + 1 ) ] ) && \T_WHITESPACE !== $this->tokens[ ( $stackPtr + 1 ) ]['code'] && ! ( \T_ELSE === $this->tokens[ $stackPtr ]['code'] && \T_COLON === $this->tokens[ ( $stackPtr + 1 ) ]['code'] ) - && ! ( \T_CLOSURE === $this->tokens[ $stackPtr ]['code'] - && 0 >= $this->spaces_before_closure_open_paren ) ) { $error = 'Space after opening control structure is required'; $fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'NoSpaceAfterStructureOpen' ); @@ -126,12 +108,8 @@ public function process_token( $stackPtr ) { } } - if ( ! isset( $this->tokens[ $stackPtr ]['scope_closer'] ) ) { - - if ( \T_USE === $this->tokens[ $stackPtr ]['code'] && 'closure' === $this->get_use_type( $stackPtr ) ) { - $scopeOpener = $this->phpcsFile->findNext( \T_OPEN_CURLY_BRACKET, ( $stackPtr + 1 ) ); - $scopeCloser = $this->tokens[ $scopeOpener ]['scope_closer']; - } elseif ( \T_WHILE !== $this->tokens[ $stackPtr ]['code'] ) { + if ( ! isset( $this->tokens[ $stackPtr ]['scope_opener'], $this->tokens[ $stackPtr ]['scope_closer'] ) ) { + if ( \T_WHILE !== $this->tokens[ $stackPtr ]['code'] ) { return; } } else { @@ -167,115 +145,28 @@ public function process_token( $stackPtr ) { $parenthesisOpener = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); - // If this is a function declaration. - if ( \T_FUNCTION === $this->tokens[ $stackPtr ]['code'] ) { - - if ( \T_STRING === $this->tokens[ $parenthesisOpener ]['code'] ) { - - $function_name_ptr = $parenthesisOpener; - - } elseif ( \T_BITWISE_AND === $this->tokens[ $parenthesisOpener ]['code'] ) { - - // This function returns by reference (function &function_name() {}). - $parenthesisOpener = $this->phpcsFile->findNext( - Tokens::$emptyTokens, - ( $parenthesisOpener + 1 ), - null, - true - ); - $function_name_ptr = $parenthesisOpener; - } - - if ( isset( $function_name_ptr ) ) { - $parenthesisOpener = $this->phpcsFile->findNext( - Tokens::$emptyTokens, - ( $parenthesisOpener + 1 ), - null, - true - ); - - // Checking this: function my_function[*](...) {}. - if ( ( $function_name_ptr + 1 ) !== $parenthesisOpener ) { - - $error = 'Space between function name and opening parenthesis is prohibited.'; - $fix = $this->phpcsFile->addFixableError( - $error, - $stackPtr, - 'SpaceBeforeFunctionOpenParenthesis', - $this->tokens[ ( $function_name_ptr + 1 ) ]['content'] - ); - - if ( true === $fix ) { - $this->phpcsFile->fixer->replaceToken( ( $function_name_ptr + 1 ), '' ); - } - } - } - } elseif ( \T_CLOSURE === $this->tokens[ $stackPtr ]['code'] ) { - - // Check if there is a use () statement. - if ( isset( $this->tokens[ $parenthesisOpener ]['parenthesis_closer'] ) ) { - - $usePtr = $this->phpcsFile->findNext( - Tokens::$emptyTokens, - ( $this->tokens[ $parenthesisOpener ]['parenthesis_closer'] + 1 ), - null, - true, - null, - true - ); - - // If it is, we set that as the "scope opener". - if ( \T_USE === $this->tokens[ $usePtr ]['code'] ) { - $scopeOpener = $usePtr; - } - } - } - if ( \T_COLON !== $this->tokens[ $parenthesisOpener ]['code'] - && \T_FUNCTION !== $this->tokens[ $stackPtr ]['code'] + && ( $stackPtr + 1 ) === $parenthesisOpener ) { + // Checking space between keyword and open parenthesis, i.e. `if[*](...) {}`. + $error = 'No space before opening parenthesis is prohibited'; + $fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'NoSpaceBeforeOpenParenthesis' ); - if ( \T_CLOSURE === $this->tokens[ $stackPtr ]['code'] - && 0 === $this->spaces_before_closure_open_paren - ) { - - if ( ( $stackPtr + 1 ) !== $parenthesisOpener ) { - // Checking this: function[*](...) {}. - $error = 'Space before closure opening parenthesis is prohibited'; - $fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'SpaceBeforeClosureOpenParenthesis' ); - - if ( true === $fix ) { - $this->phpcsFile->fixer->replaceToken( ( $stackPtr + 1 ), '' ); - } - } - } elseif ( - ( - \T_CLOSURE !== $this->tokens[ $stackPtr ]['code'] - || 1 === $this->spaces_before_closure_open_paren - ) - && ( $stackPtr + 1 ) === $parenthesisOpener - ) { - - // Checking this: if[*](...) {}. - $error = 'No space before opening parenthesis is prohibited'; - $fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'NoSpaceBeforeOpenParenthesis' ); - - if ( true === $fix ) { - $this->phpcsFile->fixer->addContent( $stackPtr, ' ' ); - } + if ( true === $fix ) { + $this->phpcsFile->fixer->addContent( $stackPtr, ' ' ); } } if ( \T_WHITESPACE === $this->tokens[ ( $stackPtr + 1 ) ]['code'] && ' ' !== $this->tokens[ ( $stackPtr + 1 ) ]['content'] ) { - // Checking this: if [*](...) {}. + // Checking (too much) space between keyword and open parenthesis, i.e. `if [*](...) {}`. $error = 'Expected exactly one space before opening parenthesis; "%s" found.'; $fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'ExtraSpaceBeforeOpenParenthesis', - $this->tokens[ ( $stackPtr + 1 ) ]['content'] + array( $this->tokens[ ( $stackPtr + 1 ) ]['content'] ) ); if ( true === $fix ) { @@ -285,7 +176,7 @@ public function process_token( $stackPtr ) { if ( \T_CLOSE_PARENTHESIS !== $this->tokens[ ( $parenthesisOpener + 1 ) ]['code'] ) { if ( \T_WHITESPACE !== $this->tokens[ ( $parenthesisOpener + 1 ) ]['code'] ) { - // Checking this: $value = my_function([*]...). + // Checking space directly after the open parenthesis, i.e. `if ([*]...) {}`. $error = 'No space after opening parenthesis is prohibited'; $fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'NoSpaceAfterOpenParenthesis' ); @@ -297,13 +188,13 @@ public function process_token( $stackPtr ) { && "\r\n" !== $this->tokens[ ( $parenthesisOpener + 1 ) ]['content'] ) && ! isset( $this->ignore_extra_space_after_open_paren[ $this->tokens[ $stackPtr ]['code'] ] ) ) { - // Checking this: if ([*]...) {}. + // Checking (too much) space directly after the open parenthesis, i.e. `if ([*]...) {}`. $error = 'Expected exactly one space after opening parenthesis; "%s" found.'; $fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'ExtraSpaceAfterOpenParenthesis', - $this->tokens[ ( $parenthesisOpener + 1 ) ]['content'] + array( $this->tokens[ ( $parenthesisOpener + 1 ) ]['content'] ) ); if ( true === $fix ) { @@ -318,7 +209,7 @@ public function process_token( $stackPtr ) { if ( \T_CLOSE_PARENTHESIS !== $this->tokens[ ( $parenthesisOpener + 1 ) ]['code'] ) { - // Checking this: if (...[*]) {}. + // Checking space directly before the close parenthesis, i.e. `if (...[*]) {}`. if ( \T_WHITESPACE !== $this->tokens[ ( $parenthesisCloser - 1 ) ]['code'] ) { $error = 'No space before closing parenthesis is prohibited'; $fix = $this->phpcsFile->addFixableError( $error, $parenthesisCloser, 'NoSpaceBeforeCloseParenthesis' ); @@ -334,7 +225,7 @@ public function process_token( $stackPtr ) { $error, $stackPtr, 'ExtraSpaceBeforeCloseParenthesis', - $this->tokens[ ( $parenthesisCloser - 1 ) ]['content'] + array( $this->tokens[ ( $parenthesisCloser - 1 ) ]['content'] ) ); if ( true === $fix ) { @@ -344,10 +235,6 @@ public function process_token( $stackPtr ) { } if ( \T_WHITESPACE !== $this->tokens[ ( $parenthesisCloser + 1 ) ]['code'] - && ! ( // Do NOT flag : immediately following ) for return types declarations. - \T_COLON === $this->tokens[ ( $parenthesisCloser + 1 ) ]['code'] - && in_array( $this->tokens[ $this->tokens[ $parenthesisCloser ]['parenthesis_owner'] ]['code'], array( \T_FUNCTION, \T_CLOSURE ), true ) - ) && ( isset( $scopeOpener ) && \T_COLON !== $this->tokens[ $scopeOpener ]['code'] ) ) { $error = 'Space between opening control structure and closing parenthesis is required'; @@ -359,10 +246,7 @@ public function process_token( $stackPtr ) { } } - // Ignore this for function declarations. Handled by the OpeningFunctionBraceKernighanRitchie sniff. - if ( \T_FUNCTION !== $this->tokens[ $stackPtr ]['code'] - && \T_CLOSURE !== $this->tokens[ $stackPtr ]['code'] - && isset( $this->tokens[ $parenthesisOpener ]['parenthesis_owner'] ) + if ( isset( $this->tokens[ $parenthesisOpener ]['parenthesis_owner'] ) && ( isset( $scopeOpener ) && $this->tokens[ $parenthesisCloser ]['line'] !== $this->tokens[ $scopeOpener ]['line'] ) ) { @@ -384,14 +268,13 @@ public function process_token( $stackPtr ) { } elseif ( \T_WHITESPACE === $this->tokens[ ( $parenthesisCloser + 1 ) ]['code'] && ' ' !== $this->tokens[ ( $parenthesisCloser + 1 ) ]['content'] ) { - - // Checking this: if (...) [*]{}. + // Checking space between the close parenthesis and the open brace, i.e. `if (...) [*]{}`. $error = 'Expected exactly one space between closing parenthesis and opening control structure; "%s" found.'; $fix = $this->phpcsFile->addFixableError( $error, $stackPtr, 'ExtraSpaceAfterCloseParenthesis', - $this->tokens[ ( $parenthesisCloser + 1 ) ]['content'] + array( $this->tokens[ ( $parenthesisCloser + 1 ) ]['content'] ) ); if ( true === $fix ) { @@ -404,21 +287,19 @@ public function process_token( $stackPtr ) { $firstContent = $this->phpcsFile->findNext( \T_WHITESPACE, ( $scopeOpener + 1 ), null, true ); // We ignore spacing for some structures that tend to have their own rules. - $ignore = array( - \T_FUNCTION => true, - \T_CLOSURE => true, - \T_CLASS => true, - \T_ANON_CLASS => true, - \T_INTERFACE => true, - \T_TRAIT => true, + $ignore = array( \T_DOC_COMMENT_OPEN_TAG => true, \T_CLOSE_TAG => true, \T_COMMENT => true, ); + $ignore += Collections::closedScopes(); if ( ! isset( $ignore[ $this->tokens[ $firstContent ]['code'] ] ) && $this->tokens[ $firstContent ]['line'] > ( $this->tokens[ $scopeOpener ]['line'] + 1 ) ) { + $gap = ( $this->tokens[ $firstContent ]['line'] - $this->tokens[ $scopeOpener ]['line'] - 1 ); + $this->phpcsFile->recordMetric( $stackPtr, 'Blank lines at start of control structure', $gap ); + $error = 'Blank line found at start of control structure'; $fix = $this->phpcsFile->addFixableError( $error, $scopeOpener, 'BlankLineAfterStart' ); @@ -435,9 +316,11 @@ public function process_token( $stackPtr ) { $this->phpcsFile->fixer->addNewline( $scopeOpener ); $this->phpcsFile->fixer->endChangeset(); } + } else { + $this->phpcsFile->recordMetric( $stackPtr, 'Blank lines at start of control structure', 0 ); } - if ( $firstContent !== $scopeCloser ) { + if ( isset( $scopeCloser ) && $firstContent !== $scopeCloser ) { $lastContent = $this->phpcsFile->findPrevious( \T_WHITESPACE, ( $scopeCloser - 1 ), null, true ); $lastNonEmptyContent = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $scopeCloser - 1 ), null, true ); @@ -450,6 +333,9 @@ public function process_token( $stackPtr ) { if ( ! isset( $ignore[ $this->tokens[ $checkToken ]['code'] ] ) && $this->tokens[ $lastContent ]['line'] <= ( $this->tokens[ $scopeCloser ]['line'] - 2 ) ) { + $gap = ( $this->tokens[ $scopeCloser ]['line'] - $this->tokens[ $lastContent ]['line'] - 1 ); + $this->phpcsFile->recordMetric( $stackPtr, 'Blank lines at end of control structure', $gap ); + for ( $i = ( $scopeCloser - 1 ); $i > $lastContent; $i-- ) { if ( $this->tokens[ $i ]['line'] < $this->tokens[ $scopeCloser ]['line'] && \T_OPEN_TAG !== $this->tokens[ $firstContent ]['code'] @@ -474,7 +360,7 @@ public function process_token( $stackPtr ) { * conflict. */ if ( \T_COMMENT !== $this->tokens[ $lastContent ]['code'] - && ! isset( $this->phpcsCommentTokens[ $this->tokens[ $lastContent ]['type'] ] ) ) { + && ! isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $lastContent ]['code'] ] ) ) { $this->phpcsFile->fixer->addNewlineBefore( $j ); } @@ -483,6 +369,8 @@ public function process_token( $stackPtr ) { break; } } + } else { + $this->phpcsFile->recordMetric( $stackPtr, 'Blank lines at end of control structure', 0 ); } } unset( $ignore ); @@ -492,6 +380,16 @@ public function process_token( $stackPtr ) { return; } + if ( \T_MATCH === $this->tokens[ $stackPtr ]['code'] ) { + // Move the scope closer to the semicolon/comma. + $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $scopeCloser + 1 ), null, true ); + if ( false !== $next + && ( \T_SEMICOLON === $this->tokens[ $next ]['code'] || \T_COMMA === $this->tokens[ $next ]['code'] ) + ) { + $scopeCloser = $next; + } + } + // {@internal This is just for the blank line check. Only whitespace should be considered, // not "other" empty tokens.}} $trailingContent = $this->phpcsFile->findNext( \T_WHITESPACE, ( $scopeCloser + 1 ), null, true ); @@ -500,7 +398,7 @@ public function process_token( $stackPtr ) { } if ( \T_COMMENT === $this->tokens[ $trailingContent ]['code'] - || isset( $this->phpcsCommentTokens[ $this->tokens[ $trailingContent ]['type'] ] ) + || isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $trailingContent ]['code'] ] ) ) { // Special exception for code where the comment about // an ELSE or ELSEIF is written between the control structures. @@ -529,6 +427,21 @@ public function process_token( $stackPtr ) { return; } + if ( \T_CATCH === $this->tokens[ $trailingContent ]['code'] && \T_TRY === $this->tokens[ $stackPtr ]['code'] ) { + // TRY with CATCH. + return; + } + + if ( \T_FINALLY === $this->tokens[ $trailingContent ]['code'] && \T_CATCH === $this->tokens[ $stackPtr ]['code'] ) { + // CATCH with FINALLY. + return; + } + + if ( \T_FINALLY === $this->tokens[ $trailingContent ]['code'] && \T_TRY === $this->tokens[ $stackPtr ]['code'] ) { + // TRY with FINALLY. + return; + } + if ( \T_CLOSE_TAG === $this->tokens[ $trailingContent ]['code'] ) { // At the end of the script or embedded code. return; @@ -539,7 +452,7 @@ public function process_token( $stackPtr ) { ) { // Another control structure's closing brace. $owner = $this->tokens[ $trailingContent ]['scope_condition']; - if ( \in_array( $this->tokens[ $owner ]['code'], array( \T_FUNCTION, \T_CLOSURE, \T_CLASS, \T_ANON_CLASS, \T_INTERFACE, \T_TRAIT ), true ) ) { + if ( isset( Collections::closedScopes()[ $this->tokens[ $owner ]['code'] ] ) === true ) { // The next content is the closing brace of a function, class, interface or trait // so normal function/class rules apply and we can ignore it. return; @@ -556,12 +469,12 @@ public function process_token( $stackPtr ) { $i = ( $scopeCloser + 1 ); while ( $this->tokens[ $i ]['line'] !== $this->tokens[ $trailingContent ]['line'] ) { $this->phpcsFile->fixer->replaceToken( $i, '' ); - $i++; + ++$i; } // TODO: Instead a separate error should be triggered when content comes right after closing brace. if ( \T_COMMENT !== $this->tokens[ $scopeCloser ]['code'] - && isset( $this->phpcsCommentTokens[ $this->tokens[ $scopeCloser ]['type'] ] ) === false + && isset( Tokens::$phpcsCommentTokens[ $this->tokens[ $scopeCloser ]['code'] ] ) === false ) { $this->phpcsFile->fixer->addNewlineBefore( $trailingContent ); } @@ -570,5 +483,4 @@ public function process_token( $stackPtr ) { } } } - } diff --git a/WordPress/Sniffs/WhiteSpace/DisallowInlineTabsSniff.php b/WordPress/Sniffs/WhiteSpace/DisallowInlineTabsSniff.php deleted file mode 100644 index 7d2411721b..0000000000 --- a/WordPress/Sniffs/WhiteSpace/DisallowInlineTabsSniff.php +++ /dev/null @@ -1,104 +0,0 @@ -tab_width ) ) { - $this->tab_width = PHPCSHelper::get_tab_width( $this->phpcsFile ); - } - - $check_tokens = array( - \T_WHITESPACE => true, - \T_DOC_COMMENT_WHITESPACE => true, - \T_DOC_COMMENT_STRING => true, - ); - - for ( $i = ( $stackPtr + 1 ); $i < $this->phpcsFile->numTokens; $i++ ) { - // Skip all non-whitespace tokens and skip whitespace at the start of a new line. - if ( ! isset( $check_tokens[ $this->tokens[ $i ]['code'] ] ) || 1 === $this->tokens[ $i ]['column'] ) { - continue; - } - - // If tabs are being converted to spaces by the tokenizer, the - // original content should be checked instead of the converted content. - if ( isset( $this->tokens[ $i ]['orig_content'] ) ) { - $content = $this->tokens[ $i ]['orig_content']; - } else { - $content = $this->tokens[ $i ]['content']; - } - - if ( '' === $content || strpos( $content, "\t" ) === false ) { - continue; - } - - $fix = $this->phpcsFile->addFixableError( - 'Spaces must be used for mid-line alignment; tabs are not allowed', - $i, - 'NonIndentTabsUsed' - ); - if ( true === $fix ) { - if ( isset( $this->tokens[ $i ]['orig_content'] ) ) { - // Use the replacement that PHPCS has already done. - $this->phpcsFile->fixer->replaceToken( $i, $this->tokens[ $i ]['content'] ); - } else { - // Replace tabs with spaces, using an indent of $tab_width. - // Other sniffs can then correct the indent if they need to. - $spaces = str_repeat( ' ', $this->tab_width ); - $newContent = str_replace( "\t", $spaces, $this->tokens[ $i ]['content'] ); - $this->phpcsFile->fixer->replaceToken( $i, $newContent ); - } - } - } - - // Ignore the rest of the file. - return ( $this->phpcsFile->numTokens + 1 ); - } - -} diff --git a/WordPress/Sniffs/WhiteSpace/ObjectOperatorSpacingSniff.php b/WordPress/Sniffs/WhiteSpace/ObjectOperatorSpacingSniff.php new file mode 100644 index 0000000000..b54d3c421b --- /dev/null +++ b/WordPress/Sniffs/WhiteSpace/ObjectOperatorSpacingSniff.php @@ -0,0 +1,63 @@ +getTokens(); + $property_adjusted = false; + + // Check for `::class` and don't ignore new lines in that case. + if ( true === $this->ignoreNewlines + && \T_DOUBLE_COLON === $tokens[ $stackPtr ]['code'] + ) { + $next_non_empty = $phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); + if ( \T_STRING === $tokens[ $next_non_empty ]['code'] + && 'class' === strtolower( $tokens[ $next_non_empty ]['content'] ) + ) { + $property_adjusted = true; + $this->ignoreNewlines = false; + } + } + + $return = parent::process( $phpcsFile, $stackPtr ); + + if ( true === $property_adjusted ) { + $this->ignoreNewlines = true; + } + + return $return; + } +} diff --git a/WordPress/Sniffs/WhiteSpace/OperatorSpacingSniff.php b/WordPress/Sniffs/WhiteSpace/OperatorSpacingSniff.php index 71e1e332b8..7824379588 100644 --- a/WordPress/Sniffs/WhiteSpace/OperatorSpacingSniff.php +++ b/WordPress/Sniffs/WhiteSpace/OperatorSpacingSniff.php @@ -3,38 +3,37 @@ * WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Sniffs\WhiteSpace; +namespace WordPressCS\WordPress\Sniffs\WhiteSpace; -use Squiz_Sniffs_WhiteSpace_OperatorSpacingSniff as PHPCS_Squiz_OperatorSpacingSniff; -use PHP_CodeSniffer_Tokens as Tokens; +use PHP_CodeSniffer\Standards\Squiz\Sniffs\WhiteSpace\OperatorSpacingSniff as PHPCS_Squiz_OperatorSpacingSniff; +use PHP_CodeSniffer\Util\Tokens; /** - * Verify operator spacing, uses the Squiz sniff, but additionally also sniffs for the `!` (boolean not) operator. + * Verify operator spacing, uses the Squiz sniff, but additionally also sniffs for the + * `!` (boolean not) and the boolean and logical and/or operators. * * "Always put spaces after commas, and on both sides of logical, comparison, string and assignment operators." * - * @link https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#space-usage + * @link https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#space-usage * - * @package WPCS\WordPressCodingStandards - * - * @since 0.1.0 - * @since 0.3.0 This sniff now has the ability to fix the issues it flags. - * @since 0.12.0 This sniff used to be a copy of a very old and outdated version of the - * upstream sniff. - * Now, the sniff defers completely to the upstream sniff, adding just the - * T_BOOLEAN_NOT and the logical operators (`&&` and the like) - via the - * registration method and changing the value of the customizable - * $ignoreNewlines property. - * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 0.1.0 + * @since 0.3.0 This sniff now has the ability to fix the issues it flags. + * @since 0.12.0 This sniff used to be a copy of a very old and outdated version of the + * upstream sniff. + * Now, the sniff defers completely to the upstream sniff, adding just the + * T_BOOLEAN_NOT and the logical operators (`&&` and the like) - via the + * registration method and changing the value of the customizable + * $ignoreNewlines property. + * @since 0.13.0 Class name changed: this class is now namespaced. * - * Last synced with base class June 2017 at commit 41127aa4764536f38f504fb3f7b8831f05919c89. - * @link https://github.com/squizlabs/PHP_CodeSniffer/blob/master/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/OperatorSpacingSniff.php + * Last verified with base class June 2023 at commit 085b1e091b0f2e451333c0bc26dd50bba39402c4. + * @link https://github.com/squizlabs/PHP_CodeSniffer/blob/master/CodeSniffer/Standards/Squiz/Sniffs/WhiteSpace/OperatorSpacingSniff.php */ -class OperatorSpacingSniff extends PHPCS_Squiz_OperatorSpacingSniff { +final class OperatorSpacingSniff extends PHPCS_Squiz_OperatorSpacingSniff { /** * Allow newlines instead of spaces. @@ -54,10 +53,8 @@ class OperatorSpacingSniff extends PHPCS_Squiz_OperatorSpacingSniff { public function register() { $tokens = parent::register(); $tokens[ \T_BOOLEAN_NOT ] = \T_BOOLEAN_NOT; - $logical_operators = Tokens::$booleanOperators; + $tokens += Tokens::$booleanOperators; - // Using array union to auto-dedup. - return $tokens + $logical_operators; + return $tokens; } - } diff --git a/WordPress/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php b/WordPress/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php deleted file mode 100644 index b287d4a12b..0000000000 --- a/WordPress/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php +++ /dev/null @@ -1,196 +0,0 @@ - - * - * - * - * - * - * @var array - */ - public $ignoreAlignmentTokens = array(); - - /** - * The --tab-width CLI value that is being used. - * - * @var int - */ - private $tab_width; - - /** - * Returns an array of tokens this test wants to listen for. - * - * @return array - */ - public function register() { - return array( - \T_OPEN_TAG, - \T_OPEN_TAG_WITH_ECHO, - ); - } - - /** - * Processes this test, when one of its tokens is encountered. - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return int Integer stack pointer to skip the rest of the file. - */ - public function process_token( $stackPtr ) { - if ( ! isset( $this->tab_width ) ) { - $this->tab_width = PHPCSHelper::get_tab_width( $this->phpcsFile ); - } - - // Handle any custom ignore tokens received from a ruleset. - $ignoreAlignmentTokens = $this->merge_custom_array( $this->ignoreAlignmentTokens ); - - $check_tokens = array( - 'T_WHITESPACE' => true, - 'T_INLINE_HTML' => true, - 'T_DOC_COMMENT_WHITESPACE' => true, - 'T_COMMENT' => true, - ); - $check_tokens += $this->phpcsCommentTokens; - - for ( $i = 0; $i < $this->phpcsFile->numTokens; $i++ ) { - - if ( 1 !== $this->tokens[ $i ]['column'] ) { - continue; - } elseif ( isset( $check_tokens[ $this->tokens[ $i ]['type'] ] ) === false - || ( isset( $this->tokens[ ( $i + 1 ) ] ) - && \T_WHITESPACE === $this->tokens[ ( $i + 1 ) ]['code'] ) - || $this->tokens[ $i ]['content'] === $this->phpcsFile->eolChar - || isset( $ignoreAlignmentTokens[ $this->tokens[ $i ]['type'] ] ) - || ( isset( $this->tokens[ ( $i + 1 ) ] ) - && isset( $ignoreAlignmentTokens[ $this->tokens[ ( $i + 1 ) ]['type'] ] ) ) - ) { - continue; - } - - $spaces = 0; - switch ( $this->tokens[ $i ]['type'] ) { - case 'T_WHITESPACE': - $spaces = ( $this->tokens[ $i ]['length'] % $this->tab_width ); - break; - - case 'T_DOC_COMMENT_WHITESPACE': - $length = $this->tokens[ $i ]['length']; - $spaces = ( $length % $this->tab_width ); - - if ( isset( $this->tokens[ ( $i + 1 ) ] ) - && ( \T_DOC_COMMENT_STAR === $this->tokens[ ( $i + 1 ) ]['code'] - || \T_DOC_COMMENT_CLOSE_TAG === $this->tokens[ ( $i + 1 ) ]['code'] ) - && 0 !== $spaces - ) { - // One alignment space expected before the *. - --$spaces; - } - break; - - case 'T_COMMENT': - case 'T_PHPCS_ENABLE': - case 'T_PHPCS_DISABLE': - case 'T_PHPCS_SET': - case 'T_PHPCS_IGNORE': - case 'T_PHPCS_IGNORE_FILE': - /* - * Indentation whitespace for subsequent lines of multi-line comments - * are tokenized as part of the comment. - */ - $comment = ltrim( $this->tokens[ $i ]['content'] ); - $whitespace = str_replace( $comment, '', $this->tokens[ $i ]['content'] ); - $length = \strlen( $whitespace ); - $spaces = ( $length % $this->tab_width ); - - if ( isset( $comment[0] ) && '*' === $comment[0] && 0 !== $spaces ) { - --$spaces; - } - break; - - case 'T_INLINE_HTML': - if ( $this->tokens[ $i ]['content'] === $this->phpcsFile->eolChar ) { - $spaces = 0; - } else { - /* - * Indentation whitespace for inline HTML is part of the T_INLINE_HTML token. - */ - $content = ltrim( $this->tokens[ $i ]['content'] ); - $whitespace = str_replace( $content, '', $this->tokens[ $i ]['content'] ); - $spaces = ( \strlen( $whitespace ) % $this->tab_width ); - } - - /* - * Prevent triggering on multi-line /*-style inline javascript comments. - * This may cause false negatives as there is no check for being in a - * + +'; +echo ''; + +$double_quoted = " +"; + +$double_quoted = " +"; + +$head = << + +EOT; + +$head = <<<"EOT" + + +EOT; + +$head = <<<'EOD' + + +EOD; + +?> + +jQuery( document ).ready( function() { + $('link[rel="stylesheet"]:not([data-inprogress])').forEach(StyleFix.link); +}); + +'; + +// Test multi-line text string with multiple issues. +echo ' + + EOT; + +$head = <<<"EOT" + + + EOT; + +$head = <<<'EOD' + + + EOD; diff --git a/WordPress/Tests/WP/EnqueuedResourcesUnitTest.inc b/WordPress/Tests/WP/EnqueuedResourcesUnitTest.inc deleted file mode 100644 index 941eacd779..0000000000 --- a/WordPress/Tests/WP/EnqueuedResourcesUnitTest.inc +++ /dev/null @@ -1,32 +0,0 @@ - - - -'; -echo ''; - -$double_quoted = " -"; - -$double_quoted = " -"; - -$head = << - -EOT; - -$head = <<<"EOT" - - -EOT; - -$head = <<<'EOD' - - -EOD; diff --git a/WordPress/Tests/WP/EnqueuedResourcesUnitTest.php b/WordPress/Tests/WP/EnqueuedResourcesUnitTest.php index 77078c498a..d329549306 100644 --- a/WordPress/Tests/WP/EnqueuedResourcesUnitTest.php +++ b/WordPress/Tests/WP/EnqueuedResourcesUnitTest.php @@ -3,57 +3,85 @@ * Unit test class for WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Tests\WP; +namespace WordPressCS\WordPress\Tests\WP; use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; /** * Unit test class for the EnqueuedResources sniff. * - * @package WPCS\WordPressCodingStandards + * @since 0.3.0 + * @since 0.13.0 Class name changed: this class is now namespaced. * - * @since 0.3.0 - * @since 0.13.0 Class name changed: this class is now namespaced. + * @covers \WordPressCS\WordPress\Sniffs\WP\EnqueuedResourcesSniff */ -class EnqueuedResourcesUnitTest extends AbstractSniffUnitTest { +final class EnqueuedResourcesUnitTest extends AbstractSniffUnitTest { /** * Returns the lines where errors should occur. * - * @return array => + * @param string $testFile The name of the file being tested. + * + * @return array Key is the line number, value is the number of expected errors. */ - public function getErrorList() { - return array( - 1 => 1, - 2 => 1, - 6 => 1, - 7 => 1, - 10 => 1, - 11 => 1, - 13 => 1, - 14 => 1, - 16 => 1, - 17 => 1, - 20 => 1, - 21 => 1, - 25 => 1, - 26 => 1, - 30 => 1, - 31 => 1, - ); + public function getErrorList( $testFile = '' ) { + switch ( $testFile ) { + case 'EnqueuedResourcesUnitTest.1.inc': + return array( + 1 => 1, + 2 => 1, + 6 => 1, + 7 => 1, + 10 => 1, + 11 => 1, + 13 => 1, + 14 => 1, + 16 => 1, + 17 => 1, + 20 => 1, + 21 => 1, + 25 => 1, + 26 => 1, + 30 => 1, + 31 => 1, + 42 => 1, + 46 => 1, + 48 => 1, + 49 => 1, + 54 => 1, + 55 => 1, + ); + + case 'EnqueuedResourcesUnitTest.2.inc': + // These tests will only yield reliable results when PHPCS is run on PHP 7.3 or higher. + if ( \PHP_VERSION_ID < 70300 ) { + return array(); + } + + return array( + 7 => 1, + 8 => 1, + 12 => 1, + 13 => 1, + 17 => 1, + 18 => 1, + ); + + default: + return array(); + } } /** * Returns the lines where warnings should occur. * - * @return array => + * @return array Key is the line number, value is the number of expected warnings. */ public function getWarningList() { return array(); } - } diff --git a/WordPress/Tests/WP/GlobalVariablesOverrideUnitTest.1.inc b/WordPress/Tests/WP/GlobalVariablesOverrideUnitTest.1.inc index 1bb2ff7903..80603e7e8b 100644 --- a/WordPress/Tests/WP/GlobalVariablesOverrideUnitTest.1.inc +++ b/WordPress/Tests/WP/GlobalVariablesOverrideUnitTest.1.inc @@ -8,7 +8,7 @@ $wpdb = 'test'; // Bad. $post = get_post( 1 ); // Bad. global $post; -$post = get_post( 1 ); // Override ok. +$post = get_post( 1 ); // Bad: old-style ignore comment. Override ok. // Bad: Using different types of assignment operators. function test_different_assignment_operators() { @@ -39,7 +39,7 @@ function global_vars() { } // Test against cross-contamination of global detection. -// https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/issues/486 +// https://github.com/WordPress/WordPress-Coding-Standards/issues/486 function local_var_only() { $pagenow = 'test'; // Ok, function scope. } @@ -55,10 +55,10 @@ add_filter( 'comments_open', function( $open, $post_id ) { return 'page' === $page->post_type; }, 10, 2 ); -$closure = function() { $page = 'test' }; // Ok, check against cross-contaminiation from within a closure. +$closure = function() { $page = 'test'; }; // Ok, check against cross-contaminiation from within a closure. // Allow overriding globals in functions within unit test classes. -// https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/issues/300#issuecomment-158778606 +// https://github.com/WordPress/WordPress-Coding-Standards/issues/300#issuecomment-158778606 trait WP_UnitTestCase { public function test_something() { @@ -107,8 +107,8 @@ trait My_Class { } } -// Test adding additional test classes to the whitelist. -// @codingStandardsChangeSetting WordPress.WP.GlobalVariablesOverride custom_test_class_whitelist My_TestClass +// Test adding additional test classes to the custom test classes list. +// phpcs:set WordPress.WP.GlobalVariablesOverride custom_test_classes[] My_TestClass class Test_Class_D extends My_TestClass { public function test_something() { @@ -116,7 +116,7 @@ class Test_Class_D extends My_TestClass { $tabs = 50; // Ok. } } -// @codingStandardsChangeSetting WordPress.WP.GlobalVariablesOverride custom_test_class_whitelist false +// phpcs:set WordPress.WP.GlobalVariablesOverride custom_test_classes[] // Test detecting within and skipping over anonymous classes. global $year; @@ -177,26 +177,26 @@ function global_vars() { $closure = function ( $pagenow ) { // OK, local to the closure. $pagenow = 'something'; // OK, local to the closure. }; - + $pagenow = 'something else'; // Bad. return $closure( $pagenow ); // OK, not an assignment. } -// Verify skipping over rest of the function when live coding/parse error in nested scope structure. -function global_vars() { - global $pagenow; - $closure = function ( $pagenow ) { - global $feeds; - $nested_closure_with_parse_error = function ( $feeds ) - $feeds = 'something'; // Bad, but ignored because of the parse error in the closure. - }; - $pagenow = 'something'; // Bad, should be picked up. Tests that skipping on parse error doesn't skip too far. -} + + + + + + + + + + $GLOBALS[] = 'something'; $GLOBALS[103] = 'something'; @@ -210,3 +210,107 @@ class MyClass { // Test assigning to multiple variables at once. $is_NS4 = $is_opera = $is_safari = $GLOBALS['is_winIE'] = true; // Bad x 4. + +// Issue #1043. +function globals_content_width() { + $GLOBALS['content_width'] = apply_filters( 'acronym_content_width', 640 ); +} + +function global_content_width() { + global $content_width; + + $content_width = apply_filters( 'acronym_content_width', 640 ); +} + +$content_width = 1000; + +// Issue #1743: detect var override via list construct. +function acronym_prepare_items() { + global $wp_query, $post_mime_types, $avail_post_mime_types, $mode; + list( $post_mime_types, $avail_post_mime_types ) = get_an_array(); // Bad x 2. + [ $post_mime_types, $avail_post_mime_types ] = get_an_array(); // PHP 7.1 short list syntax, bad x 2. + + // Keyed list. Keys are not assignments. + list( (string) $wp_query => $GLOBALS['mode']["B"], (string) $c => $mode["D"] ) = $e->getIndexable(); // Bad x 2. + [ $mode => $not_global ] = $bar; // OK. +} + +// Nested list. +list( $active_signup, list( $s => $typenow, $GLOBALS['status'], ), $ignore ) = $array; // Bad x 3. + +// List with array assignments. +[ $path[ $type ], , ] = $array; // Bad x 1. + +function use_of_globals_without_key() { + $keys = array_keys( $GLOBALS ); // OK, this is just about handling the $GLOBALS without a key. +} + +function global_import_ended_by_close_tag_false_negative() { + global $pagenow ?> +
Some HTML
+ +
Some HTML
+ $urls + ['extra']; // OK, default value assignment to arrow function parameter should be disregarded. + +function recognize_nullcoalesce_equals() { + global $tax; + $tax ??= 'other'; // Bad, potentially overrides the variable. +} + +// Safeguard that assignments to properties using PHP 8.0+ constructor property promotion don't lead to false positives. +class ConstructorPropertyPromotion { + public function __construct( + public int $timestart = 0, + protected int|bool $timeend = false, + $post = null + ) {} // Ok. +} + +/* + * Safeguard that assignment in PHP 8.1+ enums are treated correctly. + * Note: enums cannot contain property declaration. + */ +enum Suit: string implements Colorful { + case Hearts = 'H'; + case Diamonds = 'D'; + case Clubs = 'C'; + case Spades = 'S'; + + public function color( $_wp_admin_css_colors = 'red' ): string { // OK, parameter, not the global variable. + global $status, $mode; + $status = 'something'; // Bad, global variable in function scope. + $post = '2017'; // Ok, non-global variable in function scope. + [$mode] = $array; // Bad, global variable in function scope. + } +} + +/* + * Safeguard against false positives in combination with PHP 7.1 keyed lists which can have bloody anything as keys... *sigh* + * The below are all Okay: not assignment to a WP global var, but use of the WP global var in a key or as the array key for an assignment. + * Includes test with PHP 7.3 reference assignments. + */ +list( + $map->getKey($type, $urls) => $not_a_wp_global, + array( $tab, $tabs ) => &$not_a_wp_global['key'][$tab], + get($year, $day) => $not_a_wp_global[$year] +) = $array; +[ + $map->getKey($type, $urls) => $not_a_wp_global['key'][$tab], + array( $tab, $tabs ) => $not_a_wp_global, + get($year, $day) => &$not_a_wp_global[$year] +] = $array; + +// Live coding/parse error. +// This has to be the last test in the file! +list( $tab, $tabs diff --git a/WordPress/Tests/WP/GlobalVariablesOverrideUnitTest.3.inc b/WordPress/Tests/WP/GlobalVariablesOverrideUnitTest.3.inc index 9dd7f6c876..c6cf922415 100644 --- a/WordPress/Tests/WP/GlobalVariablesOverrideUnitTest.3.inc +++ b/WordPress/Tests/WP/GlobalVariablesOverrideUnitTest.3.inc @@ -6,7 +6,7 @@ * For this file, none of the variables overrides should throw errors, for the sister-file they all should. */ -// @codingStandardsChangeSetting WordPress.WP.GlobalVariablesOverride treat_files_as_scoped true +// phpcs:set WordPress.WP.GlobalVariablesOverride treat_files_as_scoped true // Overrides in the global namespace should be detected no matter what. No need for a `global` statement. $pagenow = 'abc'; // OK. @@ -28,4 +28,4 @@ $domain['subkey'] = 'something else'; // OK. $GLOBALS['domain']['subkey'] = 'something else'; // Still bad. -// @codingStandardsChangeSetting WordPress.WP.GlobalVariablesOverride treat_files_as_scoped false +// phpcs:set WordPress.WP.GlobalVariablesOverride treat_files_as_scoped false diff --git a/WordPress/Tests/WP/GlobalVariablesOverrideUnitTest.4.inc b/WordPress/Tests/WP/GlobalVariablesOverrideUnitTest.4.inc index 02817b7ba0..28e4185c3c 100644 --- a/WordPress/Tests/WP/GlobalVariablesOverrideUnitTest.4.inc +++ b/WordPress/Tests/WP/GlobalVariablesOverrideUnitTest.4.inc @@ -1,6 +1,6 @@ => + * @return array Key is the line number, value is the number of expected errors. */ public function getErrorList( $testFile = '' ) { switch ( $testFile ) { @@ -37,6 +39,7 @@ public function getErrorList( $testFile = '' ) { 3 => 1, 6 => 1, 8 => 1, + 11 => 1, // Old-style WPCS ignore comments are no longer supported. 16 => 1, 17 => 1, 18 => 1, @@ -54,8 +57,16 @@ public function getErrorList( $testFile = '' ) { 143 => 1, 146 => 1, 181 => 1, - 198 => 1, 212 => 4, + 230 => 2, + 231 => 2, + 234 => 2, + 239 => 3, + 242 => 1, + 252 => 1, + 268 => 1, + 292 => 1, + 294 => 1, ); case 'GlobalVariablesOverrideUnitTest.2.inc': @@ -77,6 +88,11 @@ public function getErrorList( $testFile = '' ) { 29 => 1, ); + case 'GlobalVariablesOverrideUnitTest.7.inc': + return array( + 19 => 1, + ); + default: return array(); } @@ -85,10 +101,9 @@ public function getErrorList( $testFile = '' ) { /** * Returns the lines where warnings should occur. * - * @return array => + * @return array Key is the line number, value is the number of expected warnings. */ public function getWarningList() { return array(); } - } diff --git a/WordPress/Tests/WP/I18nUnitTest.1.inc b/WordPress/Tests/WP/I18nUnitTest.1.inc index 249e902c37..6256e6d84a 100644 --- a/WordPress/Tests/WP/I18nUnitTest.1.inc +++ b/WordPress/Tests/WP/I18nUnitTest.1.inc @@ -1,11 +1,11 @@ translate( $string ); // OK, not a function, but a method call. Something\esc_html_e( $string ); // OK, not the WP function, but namespaced function call. @@ -177,5 +177,144 @@ $offset_or_tz = _x( '0', 'default GMT offset or timezone string', 'my-slug' ); $test = __( '%1$s %2$s', 'my-slug' ); // OK(ish), placeholder order may change depending on language. $test = __( ' %s ', 'my-slug' ); // Bad, no translatable content. -// @codingStandardsChangeSetting WordPress.WP.I18n text_domain false -// @codingStandardsChangeSetting WordPress.WP.I18n check_translator_comments true +// Missing plural argument. +_n_noop($singular); // Bad x 3. + +// This test is needed to verify that the missing plural argument above does not cause an internal error, stopping the run. +_n_noop( 'I have %d cat.', 'I have %d cats.' ); // Bad. + +// HTML wrapped strings. +__( '
123 Fake Street
', 'my-slug' ); // Bad, string shouldn't be wrapped in HTML. +__( 'I live at
123 Fake Street
', 'my-slug' ); // Okay, no consensus on HTML tags within strings. +__( '
123 Fake Street
is my home', 'my-slug' ); // Okay, no consensus on HTML tags within strings. +__( 'Text More text Text', 'my-slug' ); // Good, we're not wrapping +__( '
Translatable content
', 'my-slug' ); // Bad +__( '', 'my-slug' ); // Wrapping is okay, since there are placeholders +__( 'Foo', 'my-slug' ); // Bad +__( 'Foo', 'my-slug' ); // Good + +// Strings wrapped in `...` aren't flagged, since the link target might require localization. +__( 'WordPress', 'my-slug' ); // Good +__( 'translatable text', 'my-slug' ); // Bad, since no href +__( 'translatable text', 'my-slug' ); // Bad, since no href +__( 'translatable text', 'my-slug' ); // Good + +// Strings containing malformed XML, to make sure we're able to parse them. No warnings should be thrown. +__( '
text to translate', 'my-slug' ); +__( 'text to
translate', 'my-slug' ); +__( 'text < to translate', 'my-slug' ); +__( 'text to > translate', 'my-slug' ); +__( '
my address
', 'my-slug' ); +__( '
text to translate
', 'my-slug' ); +__( '<>text to translate', 'my-slug' ); +__( '<>text to translate<>', 'my-slug' ); +__( '
', 'my-slug' ); +__( '<<>>text to translate<<>', 'my-slug' ); +__( '<<>>123', 'my-slug' ); +__( 'Foo', 'my-slug' ); +__( 'Foo', 'my-slug' ); +__( 'Foo', 'my-slug' ); + +// Test handling of more complex embedded variables and expressions. All of these should throw an error. +__( "${foo}", 'my-slug' ); +__( "{$foo['bar']}", 'my-slug' ); +__( "{$foo->bar()}", 'my-slug' ); +__( "{$foo['bar']->baz()()}", 'my-slug' ); +__( "${foo->bar}", 'my-slug' ); +__( "${foo["${bar['baz']}"]}", 'my-slug' ); + +// Safeguard defensive coding. +__( /* comment */, 'my-slug' ); // Bad, missing first param (parse error). + +// phpcs:set WordPress.WP.I18n text_domain[] default + +// Safeguard that a comment after a "default" text domain is not removed. +__( 'String default text domain.', 'default' /*comment*/ ); // Warning, text domain can be omitted. + +// Test mismatched placeholders. +_n_noop( 'I have %1$d cat and %2$d dog.' ); // Bad, missing $plural argument, ignore for single/plural placeholder check. +_n_noop( 'I have %1$d cat and %2$d dog.', $plural ); // Bad, non singular string $plural argument, ignore for single/plural placeholder check. +_n_noop( 'I have %1$d cat and %2$d dog.', $plural . $text ); // Bad, non singular string $plural argument, ignore for single/plural placeholder check. + +_n_noop( 'I have %1$d cat and %2$d dog.', 'I have %1$d cats and %2$d dog.' ); // Ok. +_n_noop( 'I have %2$d cat and %1$d dog.', 'I have %1$d cats and %2$d dog.' ); // Ok, or at least not something we flag. +_n_noop( 'I have %1$d cat and %3$d dog.', 'I have %1$d cats and %2$d dog.' ); // Bad, different placeholders used in single vs plural text. +_n_noop( 'I have %d cat, %d dog and %d canaries.', 'I have %d cats, %d dog and canaries.' ); // Bad, mismatched placeholders count. +_n_noop( 'I have %1$d cat, %2$d dog and %3$d canaries.', 'I have %1$d cats, %2$d dog and canaries.' ); // Bad, mismatched numbered placeholders count. +_n_noop( 'I have %1$d cat, %2$d dog and %2$d canaries.', 'I have %1$d cats, %2$d dog and canaries.' ); // Bad, mismatched numbered placeholders count. + +/* + * Safeguard support for PHP 8.0+ named parameters. + * Each of these function calls uses named parameters with an unconventional param order to test this properly. + */ +// phpcs:set WordPress.WP.I18n text_domain[] my-slug +translate( domain: 'my-slug', text: 'translate me', extra: 'default' ); // Bad: too many arguments + use of translate(). +__( domain: 'my-slug' ); // Bad: missing $text argument. +__( single: 'translate me', domain: 'my-slug' ); // Bad: missing $text argument (invalid 'single' named arg). +esc_attr__( + domain: 'my-slug', + text: 'translate +me multi line', // OK, multi-line text string. +); +esc_html__( + domain: PLUGIN_TEXTDOMAIN, // Bad: not single text string literal. + text: 'translate ' . 'me', // Bad: not single text string literal. +); +_e( + domain: 'my-slug', + text: $translateMe, // Bad: not single text string literal. +); +esc_attr_e( + domain: 'my-slug', + text: "translate +$me multi line", // Bad: interpolated variable found. +); +esc_html_e( + domain: 'different-slug', // Bad: wrong text domain. + text: 'translate me', +); +esc_html_x( + context: 'context', + domain: 'my-slug', + text: 'trans %s late %1$s me', // Bad: mixing ordered and non-ordered placeholders. +); +_x( context: 'context', text: 'translate %s me %s', domain: 'my-slug' ); // Bad: multiple placeholders, but no ordering. +_ex( domain: 'my-slug', text: '%s', context: 'context', ); // Bad: no translatable content. +esc_attr_x( domain: 'my-slug', context: 'context', text: '
translate me
', ); // Bad: wrapped in HTML. +_n( number: $i, domain: 'my-slug', single: 'translate me', plural: 'translate %s me' ); // Bad: missing singular placeholder. +_nx( number: $i, domain: 'my-slug', plural: 'translate %s me', single: 'translate %1$s me', context: 'context', ); // Bad: mismatch between single/plural placeholders. + +// Safeguard that the SuperfluousDefaultTextDomain fixer handles trailing commas correctly. +// Note: if the original code contained a trailing comma, it is fine for the fixed code to also contain a trailing comma. +// phpcs:set WordPress.WP.I18n text_domain[] default +__( 'translate me', 'default', ); // Bad: superfluous domain (positional with trailing comma). +__( 'translate me', 'default' /*comment*/, ); // Bad: superfluous domain (positional with trailing comma and comment). + +// Safeguard handling of named params by the SuperfluousDefaultTextDomain fixer. +__( text: 'translate me', domain: 'default' ); // Bad: superfluous domain. +__( text: 'translate me', domain: 'default', ); // Bad: superfluous domain (with trailing comma). +_n_noop( domain: 'default', singular: 'translate me', plural: 'translate me', ); // Bad: superfluous domain. +_nx_noop( singular: 'translate me', domain: 'default' /*comment*/, plural: 'translate me', context: 'context', ); // Bad: superfluous domain. + +// These should not be auto-fixable as we don't want to remove the comment. +__( text: 'translate me', domain: /*comment*/ 'default' ); // Bad: superfluous domain. +_n_noop( domain: 'default' /*comment*/, singular: 'translate me', plural: 'translate me', ); // Bad: superfluous domain. +_nx_noop( singular: 'translate me', domain /*comment*/: 'default', plural: 'translate me', context: 'context', ); // Bad: superfluous domain. + +// Safeguard bug fix: replacing placeholders in multi-line text strings. +_nx( 'I have +%d cat and %d dog.', 'I have +%d cats and %d dogs.', $number, 'Not +really.' ); // Bad, multiple arguments should be numbered. + +// Show an error when the text domain is an empty string. +esc_html_e( 'foo', '' ); // Bad: text domain mismatch. + +// Issue #1948 - Show an error when the text domain is an empty string and no text domain has been passed. +// phpcs:set WordPress.WP.I18n text_domain[] +esc_html_e( 'foo', '' ); // Bad: text-domain can not be empty. + +// PHP 8.0+: safeguard handling of newly introduced placeholders. +__( 'There are %1$h monkeys in the %H', 'my-slug' ); // Bad: multiple arguments should be numbered. + +// phpcs:enable WordPress.WP.I18n.MissingTranslatorsComment diff --git a/WordPress/Tests/WP/I18nUnitTest.1.inc.fixed b/WordPress/Tests/WP/I18nUnitTest.1.inc.fixed index f0d6848ee3..b23e9b6192 100644 --- a/WordPress/Tests/WP/I18nUnitTest.1.inc.fixed +++ b/WordPress/Tests/WP/I18nUnitTest.1.inc.fixed @@ -1,11 +1,11 @@ translate( $string ); // OK, not a function, but a method call. Something\esc_html_e( $string ); // OK, not the WP function, but namespaced function call. @@ -177,5 +177,144 @@ $offset_or_tz = _x( '0', 'default GMT offset or timezone string', 'my-slug' ); $test = __( '%1$s %2$s', 'my-slug' ); // OK(ish), placeholder order may change depending on language. $test = __( ' %s ', 'my-slug' ); // Bad, no translatable content. -// @codingStandardsChangeSetting WordPress.WP.I18n text_domain false -// @codingStandardsChangeSetting WordPress.WP.I18n check_translator_comments true +// Missing plural argument. +_n_noop($singular); // Bad x 3. + +// This test is needed to verify that the missing plural argument above does not cause an internal error, stopping the run. +_n_noop( 'I have %d cat.', 'I have %d cats.' ); // Bad. + +// HTML wrapped strings. +__( '
123 Fake Street
', 'my-slug' ); // Bad, string shouldn't be wrapped in HTML. +__( 'I live at
123 Fake Street
', 'my-slug' ); // Okay, no consensus on HTML tags within strings. +__( '
123 Fake Street
is my home', 'my-slug' ); // Okay, no consensus on HTML tags within strings. +__( 'Text More text Text', 'my-slug' ); // Good, we're not wrapping +__( '
Translatable content
', 'my-slug' ); // Bad +__( '', 'my-slug' ); // Wrapping is okay, since there are placeholders +__( 'Foo', 'my-slug' ); // Bad +__( 'Foo', 'my-slug' ); // Good + +// Strings wrapped in `...` aren't flagged, since the link target might require localization. +__( 'WordPress', 'my-slug' ); // Good +__( 'translatable text', 'my-slug' ); // Bad, since no href +__( 'translatable text', 'my-slug' ); // Bad, since no href +__( 'translatable text', 'my-slug' ); // Good + +// Strings containing malformed XML, to make sure we're able to parse them. No warnings should be thrown. +__( '
text to translate', 'my-slug' ); +__( 'text to
translate', 'my-slug' ); +__( 'text < to translate', 'my-slug' ); +__( 'text to > translate', 'my-slug' ); +__( '
my address
', 'my-slug' ); +__( '
text to translate
', 'my-slug' ); +__( '<>text to translate', 'my-slug' ); +__( '<>text to translate<>', 'my-slug' ); +__( '
', 'my-slug' ); +__( '<<>>text to translate<<>', 'my-slug' ); +__( '<<>>123', 'my-slug' ); +__( 'Foo', 'my-slug' ); +__( 'Foo', 'my-slug' ); +__( 'Foo', 'my-slug' ); + +// Test handling of more complex embedded variables and expressions. All of these should throw an error. +__( "${foo}", 'my-slug' ); +__( "{$foo['bar']}", 'my-slug' ); +__( "{$foo->bar()}", 'my-slug' ); +__( "{$foo['bar']->baz()()}", 'my-slug' ); +__( "${foo->bar}", 'my-slug' ); +__( "${foo["${bar['baz']}"]}", 'my-slug' ); + +// Safeguard defensive coding. +__( /* comment */, 'my-slug' ); // Bad, missing first param (parse error). + +// phpcs:set WordPress.WP.I18n text_domain[] default + +// Safeguard that a comment after a "default" text domain is not removed. +__( 'String default text domain.' /*comment*/ ); // Warning, text domain can be omitted. + +// Test mismatched placeholders. +_n_noop( 'I have %1$d cat and %2$d dog.' ); // Bad, missing $plural argument, ignore for single/plural placeholder check. +_n_noop( 'I have %1$d cat and %2$d dog.', $plural ); // Bad, non singular string $plural argument, ignore for single/plural placeholder check. +_n_noop( 'I have %1$d cat and %2$d dog.', $plural . $text ); // Bad, non singular string $plural argument, ignore for single/plural placeholder check. + +_n_noop( 'I have %1$d cat and %2$d dog.', 'I have %1$d cats and %2$d dog.' ); // Ok. +_n_noop( 'I have %2$d cat and %1$d dog.', 'I have %1$d cats and %2$d dog.' ); // Ok, or at least not something we flag. +_n_noop( 'I have %1$d cat and %3$d dog.', 'I have %1$d cats and %2$d dog.' ); // Bad, different placeholders used in single vs plural text. +_n_noop( 'I have %1$d cat, %2$d dog and %3$d canaries.', 'I have %1$d cats, %2$d dog and canaries.' ); // Bad, mismatched placeholders count. +_n_noop( 'I have %1$d cat, %2$d dog and %3$d canaries.', 'I have %1$d cats, %2$d dog and canaries.' ); // Bad, mismatched numbered placeholders count. +_n_noop( 'I have %1$d cat, %2$d dog and %2$d canaries.', 'I have %1$d cats, %2$d dog and canaries.' ); // Bad, mismatched numbered placeholders count. + +/* + * Safeguard support for PHP 8.0+ named parameters. + * Each of these function calls uses named parameters with an unconventional param order to test this properly. + */ +// phpcs:set WordPress.WP.I18n text_domain[] my-slug +translate( domain: 'my-slug', text: 'translate me', extra: 'default' ); // Bad: too many arguments + use of translate(). +__( domain: 'my-slug' ); // Bad: missing $text argument. +__( single: 'translate me', domain: 'my-slug' ); // Bad: missing $text argument (invalid 'single' named arg). +esc_attr__( + domain: 'my-slug', + text: 'translate +me multi line', // OK, multi-line text string. +); +esc_html__( + domain: PLUGIN_TEXTDOMAIN, // Bad: not single text string literal. + text: 'translate ' . 'me', // Bad: not single text string literal. +); +_e( + domain: 'my-slug', + text: $translateMe, // Bad: not single text string literal. +); +esc_attr_e( + domain: 'my-slug', + text: "translate +$me multi line", // Bad: interpolated variable found. +); +esc_html_e( + domain: 'different-slug', // Bad: wrong text domain. + text: 'translate me', +); +esc_html_x( + context: 'context', + domain: 'my-slug', + text: 'trans %s late %1$s me', // Bad: mixing ordered and non-ordered placeholders. +); +_x( context: 'context', text: 'translate %1$s me %2$s', domain: 'my-slug' ); // Bad: multiple placeholders, but no ordering. +_ex( domain: 'my-slug', text: '%s', context: 'context', ); // Bad: no translatable content. +esc_attr_x( domain: 'my-slug', context: 'context', text: '
translate me
', ); // Bad: wrapped in HTML. +_n( number: $i, domain: 'my-slug', single: 'translate me', plural: 'translate %s me' ); // Bad: missing singular placeholder. +_nx( number: $i, domain: 'my-slug', plural: 'translate %s me', single: 'translate %1$s me', context: 'context', ); // Bad: mismatch between single/plural placeholders. + +// Safeguard that the SuperfluousDefaultTextDomain fixer handles trailing commas correctly. +// Note: if the original code contained a trailing comma, it is fine for the fixed code to also contain a trailing comma. +// phpcs:set WordPress.WP.I18n text_domain[] default +__( 'translate me', ); // Bad: superfluous domain (positional with trailing comma). +__( 'translate me' /*comment*/, ); // Bad: superfluous domain (positional with trailing comma and comment). + +// Safeguard handling of named params by the SuperfluousDefaultTextDomain fixer. +__( text: 'translate me' ); // Bad: superfluous domain. +__( text: 'translate me', ); // Bad: superfluous domain (with trailing comma). +_n_noop( singular: 'translate me', plural: 'translate me', ); // Bad: superfluous domain. +_nx_noop( singular: 'translate me' /*comment*/, plural: 'translate me', context: 'context', ); // Bad: superfluous domain. + +// These should not be auto-fixable as we don't want to remove the comment. +__( text: 'translate me', domain: /*comment*/ 'default' ); // Bad: superfluous domain. +_n_noop( domain: 'default' /*comment*/, singular: 'translate me', plural: 'translate me', ); // Bad: superfluous domain. +_nx_noop( singular: 'translate me', domain /*comment*/: 'default', plural: 'translate me', context: 'context', ); // Bad: superfluous domain. + +// Safeguard bug fix: replacing placeholders in multi-line text strings. +_nx( 'I have +%1$d cat and %2$d dog.', 'I have +%1$d cats and %2$d dogs.', $number, 'Not +really.' ); // Bad, multiple arguments should be numbered. + +// Show an error when the text domain is an empty string. +esc_html_e( 'foo', '' ); // Bad: text domain mismatch. + +// Issue #1948 - Show an error when the text domain is an empty string and no text domain has been passed. +// phpcs:set WordPress.WP.I18n text_domain[] +esc_html_e( 'foo', '' ); // Bad: text-domain can not be empty. + +// PHP 8.0+: safeguard handling of newly introduced placeholders. +__( 'There are %1$h monkeys in the %H', 'my-slug' ); // Bad: multiple arguments should be numbered. + +// phpcs:enable WordPress.WP.I18n.MissingTranslatorsComment diff --git a/WordPress/Tests/WP/I18nUnitTest.2.inc b/WordPress/Tests/WP/I18nUnitTest.2.inc index 4ad5e2895b..dd5c246215 100644 --- a/WordPress/Tests/WP/I18nUnitTest.2.inc +++ b/WordPress/Tests/WP/I18nUnitTest.2.inc @@ -2,7 +2,7 @@ /* * Test sniffing for translator comments. */ -// @codingStandardsChangeSetting WordPress.WP.I18n text_domain my-slug +// phpcs:set WordPress.WP.I18n text_domain[] my-slug /* Basic test ****************************************************************/ __( 'No placeholders here.', 'my-slug' ); // Ok, no placeholders, so no translators comment needed. @@ -107,4 +107,22 @@ _e(); // Bad. // phpcs:ignore Standard.Category.Sniff -- testing that the PHPCS annotations are handled correctly. printf( __( 'There are %1$d monkeys in the %2$s', 'my-slug' ), intval( $number ), esc_html( $string ) ); // Bad. -// @codingStandardsChangeSetting WordPress.WP.I18n text_domain false +__( $notStringLiteral, 'my-slug' ); // Ignore for translators comment, $text not single string literal. +__( 'text %s' . 'more text', 'my-slug' ); // Ignore for translators comment, $text not single string literal. + + +/* + * Safeguard support for PHP 8.0+ named parameters. + */ +/* translators: %d: number of cats. */ +_n_noop( domain: 'my-slug', singular: 'I have %d cat.', plural: "I have %d cats.", ); // OK. + +esc_attr_e( domain: 'my-slug', translate: 'Text to translate to %1$d languages.' ); // Bad, missing $text param, missing translators comment is ignored. + +_n_noop( // Bad, missing translators comment. + domain: 'my-slug', + plural: "I have %d cats.", + singular: 'I have %d cat.', +); + +// phpcs:set WordPress.WP.I18n text_domain[] diff --git a/WordPress/Tests/WP/I18nUnitTest.3.inc b/WordPress/Tests/WP/I18nUnitTest.3.inc index bac1dadca7..afcc9fe5fa 100644 --- a/WordPress/Tests/WP/I18nUnitTest.3.inc +++ b/WordPress/Tests/WP/I18nUnitTest.3.inc @@ -19,3 +19,9 @@ __( 'string', "something-$domain" ); // Bad, shouldn't use variable for domain. __( 'string', 'something-else' ); // Bad, text domain mismatch. __( 'string', "something-else" ); // Bad, text domain mismatch. + +/* + * Safeguard support for PHP 8.0+ named parameters. + */ +__( domain: 'something', text: 'string', ); // OK. +__( domain: 'something-else', text: 'string', ); // Bad, text domain mismatch. diff --git a/WordPress/Tests/WP/I18nUnitTest.php b/WordPress/Tests/WP/I18nUnitTest.php index 6dc1c4a098..3834eb189f 100644 --- a/WordPress/Tests/WP/I18nUnitTest.php +++ b/WordPress/Tests/WP/I18nUnitTest.php @@ -3,48 +3,27 @@ * Unit test class for WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Tests\WP; +namespace WordPressCS\WordPress\Tests\WP; use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; -use WordPress\PHPCSHelper; /** * Unit test class for the I18n sniff. * - * @package WPCS\WordPressCodingStandards + * @since 0.10.0 + * @since 0.13.0 Class name changed: this class is now namespaced. * - * @since 0.10.0 - * @since 0.13.0 Class name changed: this class is now namespaced. + * @covers \WordPressCS\WordPress\Sniffs\WP\I18nSniff */ -class I18nUnitTest extends AbstractSniffUnitTest { - - /** - * Get a list of CLI values to set before the file is tested. - * - * Used by PHPCS 2.x. - * - * @param string $testFile The name of the file being tested. - * - * @return array - */ - public function getCliValues( $testFile ) { - // Test overruling the text domain from the command line for one test file. - if ( 'I18nUnitTest.3.inc' === $testFile ) { - PHPCSHelper::set_config_data( 'text_domain', 'something', true ); - } - - return array(); - } +final class I18nUnitTest extends AbstractSniffUnitTest { /** * Set CLI values before the file is tested. * - * Used by PHPCS 3.x. - * * @param string $testFile The name of the file being tested. * @param \PHP_CodeSniffer\Config $config The config data for the test run. * @@ -54,6 +33,9 @@ public function setCliValues( $testFile, $config ) { // Test overruling the text domain from the command line for one test file. if ( 'I18nUnitTest.3.inc' === $testFile ) { $config->setConfigData( 'text_domain', 'something', true ); + } else { + // Delete the text domain option so it doesn't persist for subsequent test files. + $config->setConfigData( 'text_domain', null, true ); } } @@ -61,7 +43,8 @@ public function setCliValues( $testFile, $config ) { * Returns the lines where errors should occur. * * @param string $testFile The name of the file being tested. - * @return array => + * + * @return array Key is the line number, value is the number of expected errors. */ public function getErrorList( $testFile = '' ) { @@ -135,11 +118,44 @@ public function getErrorList( $testFile = '' ) { 153 => 1, 157 => 1, 178 => 1, + 181 => 3, + 184 => 1, + 219 => 1, + 220 => 1, + 221 => 1, + 222 => 1, + 223 => 1, + 224 => 1, + 227 => 1, + 235 => 1, + 236 => 1, + 237 => 1, + 242 => 2, + 251 => 1, + 252 => 1, + 253 => 1, + 260 => 1, + 261 => 1, + 265 => 1, + 269 => 1, + 273 => 1, + 279 => 1, + 281 => 1, + 282 => 1, + 284 => 1, + 305 => 1, + 306 => 1, + 311 => 1, + 315 => 1, + 318 => 1, ); case 'I18nUnitTest.2.inc': return array( 104 => 2, + 110 => 1, + 111 => 1, + 120 => 1, ); case 'I18nUnitTest.3.inc': @@ -154,6 +170,7 @@ public function getErrorList( $testFile = '' ) { 18 => 1, 20 => 1, 21 => 1, + 27 => 1, ); default: @@ -165,7 +182,8 @@ public function getErrorList( $testFile = '' ) { * Returns the lines where warnings should occur. * * @param string $testFile The name of the file being tested. - * @return array => + * + * @return array Key is the line number, value is the number of expected warnings. */ public function getWarningList( $testFile = '' ) { switch ( $testFile ) { @@ -173,13 +191,32 @@ public function getWarningList( $testFile = '' ) { return array( 69 => 1, 70 => 1, - 100 => 1, - 101 => 1, - 102 => 1, - 103 => 1, 154 => 1, 158 => 1, 159 => 1, + 187 => 1, + 191 => 1, + 193 => 1, + 194 => 1, + 198 => 1, + 199 => 1, + 232 => 1, + 241 => 1, + 242 => 1, + 243 => 1, + 244 => 1, + 251 => 1, + 283 => 1, + 285 => 1, + 290 => 1, + 291 => 1, + 294 => 1, + 295 => 1, + 296 => 1, + 297 => 1, + 300 => 1, + 301 => 1, + 302 => 1, ); case 'I18nUnitTest.2.inc': @@ -191,11 +228,11 @@ public function getWarningList( $testFile = '' ) { 74 => 1, 85 => 1, 108 => 1, + 122 => 1, ); default: return array(); } } - } diff --git a/WordPress/Tests/WP/PostsPerPageUnitTest.inc b/WordPress/Tests/WP/PostsPerPageUnitTest.inc index 61dbdf43d6..7474ba6c3c 100644 --- a/WordPress/Tests/WP/PostsPerPageUnitTest.inc +++ b/WordPress/Tests/WP/PostsPerPageUnitTest.inc @@ -17,14 +17,110 @@ $query_args['numberposts'] = '-1'; // OK. $query_args['my_posts_per_page'] = -1; // OK. -// @codingStandardsChangeSetting WordPress.WP.PostsPerPage posts_per_page 50 - $query_args['posts_per_page'] = 50; // OK. - $query_args['posts_per_page'] = 100; // Bad. - $query_args['posts_per_page'] = 200; // Bad. - $query_args['posts_per_page'] = 300; // Bad. -// @codingStandardsChangeSetting WordPress.WP.PostsPerPage posts_per_page 200 - $query_args['posts_per_page'] = 50; // OK. - $query_args['posts_per_page'] = 100; // OK. - $query_args['posts_per_page'] = 200; // OK. - $query_args['posts_per_page'] = 300; // Bad. -// @codingStandardsChangeSetting WordPress.WP.PostsPerPage posts_per_page 100 +// phpcs:set WordPress.WP.PostsPerPage posts_per_page 50 +$query_args['posts_per_page'] = 50; // OK. +$query_args['posts_per_page'] = 100; // Bad. +$query_args['posts_per_page'] = 200; // Bad. +$query_args['posts_per_page'] = 300; // Bad. +// phpcs:set WordPress.WP.PostsPerPage posts_per_page 200 +$query_args['posts_per_page'] = 50; // OK. +$query_args['posts_per_page'] = 100; // OK. +$query_args['posts_per_page'] = 200; // OK. +$query_args['posts_per_page'] = 300; // Bad. +// phpcs:set WordPress.WP.PostsPerPage posts_per_page 100 + +// phpcs:set WordPress.WP.PostsPerPage exclude[] posts_per_page +$query_args['posts_per_page'] = 300; // OK, group excluded. +// phpcs:set WordPress.WP.PostsPerPage exclude[] + +// Ensure there will be no false positive for array access brackets when not used for an assignment. +$var = $query_args['posts_per_page']; // OK. +$firstChars = $text[0] . $text[1]; // OK. + +// Text strings which are not query strings should be ignored. +_query_posts( 'numberposts' ); // OK. + +// Assignments to non-string keys should be ignored. Note: PHP will auto-cast numeric strings to ints, so those should also be disregarded. +$var[10] = 300; // OK. +$var[] = 400; // OK. +$var['239'] = 500; // OK. + +// Ensure the sniff disregards comments. +$query_args['posts_per_page' /* high */ ] = 999; // Bad. + +$query_args['posts_per_page'] /* high */ = 999; // Bad. + +$args = array( + 'posts_per_page' /* high */ => 999, // Bad. +); + +$query_args['posts_per_page'] = /* high */ 999; // Bad. +$args = array( + 'posts_per_page' => /* high */ 999, // Bad. +); + +// Safeguard that when a query string contains duplicate key, the value of the last one is used. +_query_posts( 'posts_per_page=999&nopaging=true&posts_per_page=50' ); // OK. + +// Ensure the error gets reported on the key pointer. +$query_args[ + 'posts_per_page' +] = 300; // Bad, error should be reported on the above line. + +// Ensure that PHP 7.4 null coalesce equals get picked up on. +$query_args['posts_per_page'] ??= 50; // OK. +$query_args['posts_per_page'] ??= 200; // Bad. + +// Ensure that the sniff does not report on PHP 8.0 match arms. +$val = match($val) { + 'posts_per_page' => 999, // OK, not an array assignment. +}; + +// Verify handling of arrays without trailing comma after the last array item. +$args = array( 'posts_per_page' => 999 ); // Bad. +$args = [ + 'posts_per_page' => 999 +]; // Bad. + +// Verify that the complete value is being captured correctly and that non-numeric values are disregarded. +$args = array( + 'posts_per_page' => min( max( $first, $last ), $default_min ), // Should be ignored as undetermined. + 'posts_per_page' => 10 + $extra, // Should be ignored as undetermined. + 'posts_per_page' => $value[0][1], // Should be ignored as undetermined. + 'posts_per_page' => $value ? 10 * $value : 300, // Should be ignored as undetermined. + 'posts_per_page' => get_value( name: 'post_per_page', type: 'query' ), // Should be ignored as undetermined. + 'posts_per_page' => function($a): int { + return do_something( $a ); + }, // Should be ignored as undetermined. + 'posts_per_page' => [ 100, 200, 300 ], // Should be ignored as undetermined. + 'posts_per_page' => array(100, 200, 300), // Should be ignored as undetermined. +); +$query_args['posts_per_page'] = my\get_posts_per_page(); // Should be ignored as undetermined. +$query_args['posts_per_page'] = '1e3'; // Should be ignored as undetermined. Would evaluate to 1000 with an int cast, but WP doesn't cast the value. + +// Purely numeric strings should probably be accepted still as this won't make a difference for the database query. +$query_args['posts_per_page'] = '50'; // OK. +$query_args['posts_per_page'] = '200'; // Bad. +$query_args['posts_per_page'] = "200"; // Bad. + +// Verify handling of explicitly positive numbers. +$args = array( + 'posts_per_page' => +50, // OK. + 'posts_per_page' => +200, // Bad. +); + +// Verify handling of PHP 7.4+ numeric literals, PHP 8.1 octal literals, non-decimal numbers and floats. +$args = array( + 'posts_per_page' => 0b1001011, // OK (75). + 'posts_per_page' => 0b10010110, // Bad (150). + 'posts_per_page' => 0x4B, // OK (75). + 'posts_per_page' => 0x96, // Bad (150). + 'posts_per_page' => 0113, // OK (75). + 'posts_per_page' => 0226, // Bad (150). + 'posts_per_page' => 0O113, // OK (75). + 'posts_per_page' => 0o226, // Bad (150). + 'posts_per_page' => 7_5, // OK (75). + 'posts_per_page' => 1_50, // Bad (150). + 'posts_per_page' => 75.0, // OK (75). + 'posts_per_page' => 150.000, // Bad (150). +); diff --git a/WordPress/Tests/WP/PostsPerPageUnitTest.php b/WordPress/Tests/WP/PostsPerPageUnitTest.php index 233609bd8a..bf3296ce25 100644 --- a/WordPress/Tests/WP/PostsPerPageUnitTest.php +++ b/WordPress/Tests/WP/PostsPerPageUnitTest.php @@ -3,31 +3,32 @@ * Unit test class for WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Tests\WP; +namespace WordPressCS\WordPress\Tests\WP; use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; /** * Unit test class for the PostsPerPage sniff. * - * @package WPCS\WordPressCodingStandards + * @since 0.3.0 + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 1.0.0 This sniff has been split into two, with the check for high pagination + * limit being part of the WP category, and the check for pagination + * disabling being part of the VIP category. * - * @since 0.3.0 - * @since 0.13.0 Class name changed: this class is now namespaced. - * @since 1.0.0 This sniff has been split into two, with the check for high pagination - * limit being part of the WP category, and the check for pagination - * disabling being part of the VIP category. + * @covers \WordPressCS\WordPress\AbstractArrayAssignmentRestrictionsSniff + * @covers \WordPressCS\WordPress\Sniffs\WP\PostsPerPageSniff */ -class PostsPerPageUnitTest extends AbstractSniffUnitTest { +final class PostsPerPageUnitTest extends AbstractSniffUnitTest { /** * Returns the lines where errors should occur. * - * @return array => + * @return array Key is the line number, value is the number of expected errors. */ public function getErrorList() { return array(); @@ -36,19 +37,36 @@ public function getErrorList() { /** * Returns the lines where warnings should occur. * - * @return array => + * @return array Key is the line number, value is the number of expected warnings. */ public function getWarningList() { return array( - 4 => 1, - 10 => 1, - 11 => 1, - 13 => 1, - 22 => 1, - 23 => 1, - 24 => 1, - 29 => 1, + 4 => 1, + 10 => 1, + 11 => 1, + 13 => 1, + 22 => 1, + 23 => 1, + 24 => 1, + 29 => 1, + 49 => 1, + 51 => 1, + 54 => 1, + 57 => 1, + 59 => 1, + 67 => 1, + 72 => 1, + 80 => 1, + 82 => 1, + 103 => 1, + 104 => 1, + 109 => 1, + 115 => 1, + 117 => 1, + 119 => 1, + 121 => 1, + 123 => 1, + 125 => 1, ); } - } diff --git a/WordPress/Tests/WP/PreparedSQLUnitTest.inc b/WordPress/Tests/WP/PreparedSQLUnitTest.inc deleted file mode 100644 index a1f5b17d94..0000000000 --- a/WordPress/Tests/WP/PreparedSQLUnitTest.inc +++ /dev/null @@ -1,3 +0,0 @@ - => - */ - public function getErrorList() { - return array(); - } - - /** - * Returns the lines where warnings should occur. - * - * @since 0.8.0 - * - * @return array => - */ - public function getWarningList() { - return array( - 1 => 1, - ); - } - -} diff --git a/WordPress/Tests/WP/TimezoneChangeUnitTest.inc b/WordPress/Tests/WP/TimezoneChangeUnitTest.inc deleted file mode 100644 index 4e38791f9b..0000000000 --- a/WordPress/Tests/WP/TimezoneChangeUnitTest.inc +++ /dev/null @@ -1,6 +0,0 @@ -setTimezone( new DateTimeZone( 'America/Toronto' ) ); // Yay! diff --git a/WordPress/Tests/WP/TimezoneChangeUnitTest.php b/WordPress/Tests/WP/TimezoneChangeUnitTest.php deleted file mode 100644 index 6f16015319..0000000000 --- a/WordPress/Tests/WP/TimezoneChangeUnitTest.php +++ /dev/null @@ -1,45 +0,0 @@ - => - */ - public function getErrorList() { - return array( - 3 => 1, - ); - } - - /** - * Returns the lines where warnings should occur. - * - * @return array => - */ - public function getWarningList() { - return array(); - } - -} diff --git a/WordPress/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc b/WordPress/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc deleted file mode 100644 index 0f3f714031..0000000000 --- a/WordPress/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc +++ /dev/null @@ -1,161 +0,0 @@ -{$var}( $foo,$bar ); - -(function( $a, $b ) { - return function( $c, $d ) use ( $a, $b ) { - echo $a, $b, $c, $d; - }; -})( 'a','b' )( 'c','d' ); - -$closure( $foo,$bar ); -$var = $closure() + $closure( $foo,$bar ) + self::$closure( $foo,$bar ); - -class Test { - public static function baz( $foo, $bar ) { - $a = new self( $foo,$bar ); - $b = new static( $foo,$bar ); - } -} - -/* - * Test warning for empty parentheses. - */ -$a = 4 + (); // Warning. -$a = 4 + ( ); // Warning. -$a = 4 + (/* Not empty */); - -/* - * Test the actual sniff. - */ -if ((null !== $extra) && ($row->extra !== $extra)) {} - -if (( null !== $extra ) && ( $row->extra !== $extra )) {} // Bad x 4. - -if (( null !== $extra // Bad x 1. - && is_int($extra)) - && ( $row->extra !== $extra // Bad x 1. - || $something ) // Bad x 1. -) {} - -if (( null !== $extra ) // Bad x 2. - && ( $row->extra !== $extra ) // Bad x 2. -) {} - -$a = (null !== $extra); -$a = ( null !== $extra ); // Bad x 2. - -$sx = $vert ? ($w - 1) : 0; - -$this->is_overloaded = ( ( ini_get("mbstring.func_overload") & 2 ) != 0 ) && function_exists('mb_substr'); // Bad x 4. - -$image->flip( ($operation->axis & 1) != 0, ($operation->axis & 2) != 0 ); - -if ( $success && ('nothumb' == $target || 'all' == $target) ) {} - -$directory = ('/' == $file[ strlen($file)-1 ]); - -// @codingStandardsChangeSetting WordPress.WhiteSpace.ArbitraryParenthesesSpacing spacingInside 1 -if ((null !== $extra) && ($row->extra !== $extra)) {} // Bad x 4. - -if (( null !== $extra ) && ( $row->extra !== $extra )) {} - -if (( null !== $extra // Bad x 1. - && is_int($extra)) // Bad x 1. - && ( $row->extra !== $extra - || $something ) // Bad x 1. -) {} - -if ((null !== $extra) // Bad x 2. - && ($row->extra !== $extra) // Bad x 2. -) {} - -$a = (null !== $extra); // Bad x 2. -$a = ( null !== $extra ); - -$sx = $vert ? ($w - 1) : 0; // Bad x 2. - -$this->is_overloaded = ((ini_get("mbstring.func_overload") & 2) != 0) && function_exists('mb_substr'); // Bad x 4. - -$image->flip( ($operation->axis & 1) != 0, ($operation->axis & 2) != 0 ); // Bad x 4. - -if ( $success && ('nothumb' == $target || 'all' == $target) ) {} // Bad x 2. - -$directory = ('/' == $file[ strlen($file)-1 ]); // Bad x 2. - -// @codingStandardsChangeSetting WordPress.WhiteSpace.ArbitraryParenthesesSpacing spacingInside 0 - -/* - * Test handling of ignoreNewlines. - */ -if ( - ( - null !== $extra - ) && ( - $row->extra !== $extra - ) -) {} // Bad x 4, 1 x line 123, 2 x line 125, 1 x line 127. - - -$a = ( - null !== $extra -); // Bad x 2, 1 x line 131, 1 x line 133. - -// @codingStandardsChangeSetting WordPress.WhiteSpace.ArbitraryParenthesesSpacing spacingInside 1 -if ( - ( - null !== $extra - ) && ( - $row->extra !== $extra - ) -) {} // Bad x 4, 1 x line 137, 2 x line 139, 1 x line 141. - -$a = ( - null !== $extra -); // Bad x 2, 1 x line 144, 1 x line 146. -// @codingStandardsChangeSetting WordPress.WhiteSpace.ArbitraryParenthesesSpacing spacingInside 0 - -// @codingStandardsChangeSetting WordPress.WhiteSpace.ArbitraryParenthesesSpacing ignoreNewlines true -if ( - ( - null !== $extra - ) && ( - $row->extra !== $extra - ) -) {} - -$a = ( - null !== $extra -); -// @codingStandardsChangeSetting WordPress.WhiteSpace.ArbitraryParenthesesSpacing ignoreNewlines false diff --git a/WordPress/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc.fixed b/WordPress/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc.fixed deleted file mode 100644 index 64c9edd4a7..0000000000 --- a/WordPress/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.inc.fixed +++ /dev/null @@ -1,149 +0,0 @@ -{$var}( $foo,$bar ); - -(function( $a, $b ) { - return function( $c, $d ) use ( $a, $b ) { - echo $a, $b, $c, $d; - }; -})( 'a','b' )( 'c','d' ); - -$closure( $foo,$bar ); -$var = $closure() + $closure( $foo,$bar ) + self::$closure( $foo,$bar ); - -class Test { - public static function baz( $foo, $bar ) { - $a = new self( $foo,$bar ); - $b = new static( $foo,$bar ); - } -} - -/* - * Test warning for empty parentheses. - */ -$a = 4 + (); // Warning. -$a = 4 + ( ); // Warning. -$a = 4 + (/* Not empty */); - -/* - * Test the actual sniff. - */ -if ((null !== $extra) && ($row->extra !== $extra)) {} - -if ((null !== $extra) && ($row->extra !== $extra)) {} // Bad x 4. - -if ((null !== $extra // Bad x 1. - && is_int($extra)) - && ($row->extra !== $extra // Bad x 1. - || $something) // Bad x 1. -) {} - -if ((null !== $extra) // Bad x 2. - && ($row->extra !== $extra) // Bad x 2. -) {} - -$a = (null !== $extra); -$a = (null !== $extra); // Bad x 2. - -$sx = $vert ? ($w - 1) : 0; - -$this->is_overloaded = ((ini_get("mbstring.func_overload") & 2) != 0) && function_exists('mb_substr'); // Bad x 4. - -$image->flip( ($operation->axis & 1) != 0, ($operation->axis & 2) != 0 ); - -if ( $success && ('nothumb' == $target || 'all' == $target) ) {} - -$directory = ('/' == $file[ strlen($file)-1 ]); - -// @codingStandardsChangeSetting WordPress.WhiteSpace.ArbitraryParenthesesSpacing spacingInside 1 -if (( null !== $extra ) && ( $row->extra !== $extra )) {} // Bad x 4. - -if (( null !== $extra ) && ( $row->extra !== $extra )) {} - -if (( null !== $extra // Bad x 1. - && is_int($extra) ) // Bad x 1. - && ( $row->extra !== $extra - || $something ) // Bad x 1. -) {} - -if (( null !== $extra ) // Bad x 2. - && ( $row->extra !== $extra ) // Bad x 2. -) {} - -$a = ( null !== $extra ); // Bad x 2. -$a = ( null !== $extra ); - -$sx = $vert ? ( $w - 1 ) : 0; // Bad x 2. - -$this->is_overloaded = ( ( ini_get("mbstring.func_overload") & 2 ) != 0 ) && function_exists('mb_substr'); // Bad x 4. - -$image->flip( ( $operation->axis & 1 ) != 0, ( $operation->axis & 2 ) != 0 ); // Bad x 4. - -if ( $success && ( 'nothumb' == $target || 'all' == $target ) ) {} // Bad x 2. - -$directory = ( '/' == $file[ strlen($file)-1 ] ); // Bad x 2. - -// @codingStandardsChangeSetting WordPress.WhiteSpace.ArbitraryParenthesesSpacing spacingInside 0 - -/* - * Test handling of ignoreNewlines. - */ -if ( - (null !== $extra) && ($row->extra !== $extra) -) {} // Bad x 4, 1 x line 123, 2 x line 125, 1 x line 127. - - -$a = (null !== $extra); // Bad x 2, 1 x line 131, 1 x line 133. - -// @codingStandardsChangeSetting WordPress.WhiteSpace.ArbitraryParenthesesSpacing spacingInside 1 -if ( - ( null !== $extra ) && ( $row->extra !== $extra ) -) {} // Bad x 4, 1 x line 137, 2 x line 139, 1 x line 141. - -$a = ( null !== $extra ); // Bad x 2, 1 x line 144, 1 x line 146. -// @codingStandardsChangeSetting WordPress.WhiteSpace.ArbitraryParenthesesSpacing spacingInside 0 - -// @codingStandardsChangeSetting WordPress.WhiteSpace.ArbitraryParenthesesSpacing ignoreNewlines true -if ( - ( - null !== $extra - ) && ( - $row->extra !== $extra - ) -) {} - -$a = ( - null !== $extra -); -// @codingStandardsChangeSetting WordPress.WhiteSpace.ArbitraryParenthesesSpacing ignoreNewlines false diff --git a/WordPress/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.php b/WordPress/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.php deleted file mode 100644 index 66c2ddfcc3..0000000000 --- a/WordPress/Tests/WhiteSpace/ArbitraryParenthesesSpacingUnitTest.php +++ /dev/null @@ -1,75 +0,0 @@ - => - */ - public function getErrorList() { - return array( - 64 => 4, - 66 => 1, - 68 => 1, - 69 => 1, - 72 => 2, - 73 => 2, - 77 => 2, - 81 => 4, - 90 => 4, - 94 => 1, - 95 => 1, - 97 => 1, - 100 => 2, - 101 => 2, - 104 => 2, - 107 => 2, - 109 => 4, - 111 => 4, - 113 => 2, - 115 => 2, - 123 => 1, - 125 => 2, - 127 => 1, - 131 => 1, - 133 => 1, - 137 => 1, - 139 => 2, - 141 => 1, - 144 => 1, - 146 => 1, - ); - } - - /** - * Returns the lines where warnings should occur. - * - * @return array => - */ - public function getWarningList() { - return array( - 55 => 1, - 56 => 1, - ); - } - -} diff --git a/WordPress/Tests/WhiteSpace/CastStructureSpacingUnitTest.inc b/WordPress/Tests/WhiteSpace/CastStructureSpacingUnitTest.inc index b17bc2ce92..29c1ce52d5 100644 --- a/WordPress/Tests/WhiteSpace/CastStructureSpacingUnitTest.inc +++ b/WordPress/Tests/WhiteSpace/CastStructureSpacingUnitTest.inc @@ -20,3 +20,5 @@ $unset2 = (unset) $unset; // Ok. $float1 =(float )$float; // Bad; n.b. spacing within the cast is dealt with by an upstream sniff. $float2 = (float) $float; // Ok. + +function_call( ...(array) $mixed ); // OK. diff --git a/WordPress/Tests/WhiteSpace/CastStructureSpacingUnitTest.inc.fixed b/WordPress/Tests/WhiteSpace/CastStructureSpacingUnitTest.inc.fixed index a4ff985fe2..b596241f06 100644 --- a/WordPress/Tests/WhiteSpace/CastStructureSpacingUnitTest.inc.fixed +++ b/WordPress/Tests/WhiteSpace/CastStructureSpacingUnitTest.inc.fixed @@ -20,3 +20,5 @@ $unset2 = (unset) $unset; // Ok. $float1 = (float )$float; // Bad; n.b. spacing within the cast is dealt with by an upstream sniff. $float2 = (float) $float; // Ok. + +function_call( ...(array) $mixed ); // OK. diff --git a/WordPress/Tests/WhiteSpace/CastStructureSpacingUnitTest.php b/WordPress/Tests/WhiteSpace/CastStructureSpacingUnitTest.php index 9cf81858e0..5392cf4852 100644 --- a/WordPress/Tests/WhiteSpace/CastStructureSpacingUnitTest.php +++ b/WordPress/Tests/WhiteSpace/CastStructureSpacingUnitTest.php @@ -3,28 +3,28 @@ * Unit test class for WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Tests\WhiteSpace; +namespace WordPressCS\WordPress\Tests\WhiteSpace; use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; /** * Unit test class for the CastStructureSpacing sniff. * - * @package WPCS\WordPressCodingStandards + * @since 0.3.0 + * @since 0.13.0 Class name changed: this class is now namespaced. * - * @since 0.3.0 - * @since 0.13.0 Class name changed: this class is now namespaced. + * @covers \WordPressCS\WordPress\Sniffs\WhiteSpace\CastStructureSpacingSniff */ -class CastStructureSpacingUnitTest extends AbstractSniffUnitTest { +final class CastStructureSpacingUnitTest extends AbstractSniffUnitTest { /** * Returns the lines where errors should occur. * - * @return array => + * @return array Key is the line number, value is the number of expected errors. */ public function getErrorList() { return array( @@ -41,10 +41,9 @@ public function getErrorList() { /** * Returns the lines where warnings should occur. * - * @return array => + * @return array Key is the line number, value is the number of expected warnings. */ public function getWarningList() { return array(); } - } diff --git a/WordPress/Tests/WhiteSpace/ControlStructureSpacingUnitTest.1.inc b/WordPress/Tests/WhiteSpace/ControlStructureSpacingUnitTest.1.inc index c8cede58b7..73f4b0a00e 100644 --- a/WordPress/Tests/WhiteSpace/ControlStructureSpacingUnitTest.1.inc +++ b/WordPress/Tests/WhiteSpace/ControlStructureSpacingUnitTest.1.inc @@ -5,7 +5,7 @@ while( have_posts() ) { // Okay, comments are okay here. // Okay, comments are okay here as well. } // Okay, comments are okay here. -// See https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/issues/40 . +// See https://github.com/WordPress/WordPress-Coding-Standards/issues/40 . if ( true ) { // code. @@ -45,91 +45,6 @@ endif; if ( false ) : else : endif; -// @codingStandardsChangeSetting WordPress.WhiteSpace.ControlStructureSpacing spaces_before_closure_open_paren 1 -$a = function($arg){}; // Bad. -$a = function ( $arg ) { - // Ok. -}; - -$a = function () { - // Ok. -}; - -function something($arg){} // Bad. -function foo( $arg ) { - // Ok. -} - -function no_params() { - // Ok. -} - -function another () {} // Bad, space before open parenthesis prohibited. -function and_another() {} // Bad, space before function name prohibited. -function -bar() {} // Bad. -function baz() {} // Bad. -function testA() -{} // Bad. - -function &return_by_ref() {} // Ok. - -// @codingStandardsChangeSetting WordPress.WhiteSpace.ControlStructureSpacing spaces_before_closure_open_paren 0 - -$a = function() {}; // Ok. -$a = function( $arg ) {}; // Ok. -$a = function($arg){}; // Bad. -$a = function () {}; // Bad. - -$closureWithArgsAndVars = function( $arg1, $arg2 ) use ( $var1, $var2 ) {}; // Ok. -$closureWithArgsAndVars = function ( $arg1, $arg2 ) use ( $var1, $var2 ) {}; // Bad. - -// @codingStandardsChangeSetting WordPress.WhiteSpace.ControlStructureSpacing spaces_before_closure_open_paren 1 - -$closureWithArgsAndVars = function ( $arg1, $arg2 ) use ( $var1, $var2 ) {}; // Ok. - -$closureWithArgsAndVars = function ( $arg1, $arg2 ) use( $var1, $var2 ) {}; // Bad, no space before open parenthesis prohibited. -$closureWithArgsAndVars = function ( $arg1, $arg2 ) use ( $var1, $var2 ) {}; // Bad, expected exactly one space before opening parenthesis. - -$closureWithArgsAndVars = function ( $arg1, $arg2 ) use ( $var1, $var2 ){}; // Bad, space between closing parenthesis and control structure required. -$closureWithArgsAndVars = function ( $arg1, $arg2 ) use ( $var1, $var2 ) {}; // Bad, expected exactly one space between closing parenthesis and control structure. - -$closureWithArgsAndVars = function ( $arg1, $arg2 )use ( $var1, $var2 ) {}; // Bad, expected exactly one space between closing parenthesis and control structure. -$closureWithArgsAndVars = function ( $arg1, $arg2 ) use ( $var1, $var2 ) {}; // Bad, expected exactly one space between closing parenthesis and control structure. - -// Namespaces. -use Foo\Admin; - -// @codingStandardsChangeSetting WordPress.WhiteSpace.ControlStructureSpacing spaces_before_closure_open_paren -1 - -$a = function( $arg ) {}; // Ok. -$a = function ( $arg ) {}; // Ok. - -$closureWithArgsAndVars = function( $arg1, $arg2 ) use ( $var1, $var2 ) {}; // Ok. -$closureWithArgsAndVars = function ( $arg1, $arg2 ) use ( $var1, $var2 ) {}; // Ok. - -/* - * Test for bug where this sniff was triggering a "Blank line found after control structure" error - * if there is a blank line after the last method in a class. - * - * Bug did not trigger when a comment was found after the closing brace of the method. - * - * Neither of the below examples should trigger the error. - */ -class Bar_Foo { - - function foo() { - } // Now you won't see the bug. - -} - -class Foo_Bar { - - // Now you will. - function bar() { - } - -} // Handle try/catch statements as well. try{ // Bad. @@ -139,12 +54,12 @@ try{ // Bad. } // Upstream bug PEAR #20248. -// @codingStandardsChangeSetting WordPress.WhiteSpace.ControlStructureSpacing blank_line_check true +// phpcs:set WordPress.WhiteSpace.ControlStructureSpacing blank_line_check true // Bad. if ( $one ) { } -// @codingStandardsChangeSetting WordPress.WhiteSpace.ControlStructureSpacing blank_line_check false +// phpcs:set WordPress.WhiteSpace.ControlStructureSpacing blank_line_check false // Upstream bug PEAR #20247. do { @@ -152,7 +67,7 @@ do { } while ($blah); // Bad. // Upstream bug GH #782 -// @codingStandardsChangeSetting WordPress.WhiteSpace.ControlStructureSpacing blank_line_check true +// phpcs:set WordPress.WhiteSpace.ControlStructureSpacing blank_line_check true if ( $foo ) { @@ -173,12 +88,9 @@ if ( $foo ) { } -// @codingStandardsChangeSetting WordPress.WhiteSpace.ControlStructureSpacing blank_line_check false +// phpcs:set WordPress.WhiteSpace.ControlStructureSpacing blank_line_check false // Check for too many spaces as long as the next non-blank token is on the same line. -function test( $blah ) {} // Bad. -$a = function( $bar ) {}; // Bad. - if ( 'abc' === $test ) { // Bad. echo 'hi'; } elseif ( false === $foo ) { // Bad. @@ -269,24 +181,85 @@ if ( $foo ) { } -// Issue 1202. -function hi(): array -{ - return []; +// Handle (try/catch/) finally statements as well. +try { + // Something +} catch ( Exception $e ) { + // Something +} finally { // OK. + // Something +} + +try { + // Something +} finally + +{ // Bad. + // Something +} + +try { + // Something +} finally{ // Bad. + // Something +} + +if ( $condition ) { + try { + // Something + } finally { + // Something + } + +} // Bad: blank line between. + +// Handle PHP 8.0+ match expressions. +$expr = match ( $foo ) { + 1 => 1, + 2 => 2, +}; + +$expr = match($foo){ + 1 => 1, + 2 => 2, +} ; + +// phpcs:set WordPress.WhiteSpace.ControlStructureSpacing blank_line_check true +$expr = match ( $foo ) { + + 1 => 1, + 2 => 2, + +}; + +// phpcs:set WordPress.WhiteSpace.ControlStructureSpacing blank_line_check false + +if ( $condition ) { + $expr = match ( $foo ) { + 1 => 1, + 2 => 2, + }; + + +} // Bad: blank line between. + +// Ignore spacing rules in combination with enums as they tend to have their own rules. +// phpcs:set WordPress.WhiteSpace.ControlStructureSpacing blank_line_check true +if ( $foo ) { + + + enum MyEnumA { + // Code here + } + + } +// phpcs:set WordPress.WhiteSpace.ControlStructureSpacing blank_line_check false + +enum MyEnumB { + if ( $foo ) { + // Code here + } + -// Issue 1420. -function returntype1(): string {} // Ok. -function returntype2( $input ): string {} // Ok. -function returntype3( $input ) : string {} // Ok. -function returntype4( string $input ): string {} // Ok. -function returntype5( string $input ) : string {} // Ok. -function returntype6( string $input, array $inputs ): string {} // Ok. -function returntype7( string $input, array $inputs ) : string {} // Ok. -$returntype = function (): string {}; // Ok. -$returntype = function ( $input ): string {}; // Ok. -$returntype = function ( $input ) : string {}; // Ok. -$returntype = function ( string $input ): string {}; // Ok. -$returntype = function ( string $input ) : string {}; // Ok. -$returntype = function ( string $input, array $inputs ): string {}; // Ok. -$returntype = function ( string $input, array $inputs ) : string {}; // Ok. +} // OK, defer to enum rules. diff --git a/WordPress/Tests/WhiteSpace/ControlStructureSpacingUnitTest.1.inc.fixed b/WordPress/Tests/WhiteSpace/ControlStructureSpacingUnitTest.1.inc.fixed index bed9205f68..1a2bf8ad3c 100644 --- a/WordPress/Tests/WhiteSpace/ControlStructureSpacingUnitTest.1.inc.fixed +++ b/WordPress/Tests/WhiteSpace/ControlStructureSpacingUnitTest.1.inc.fixed @@ -5,7 +5,7 @@ while ( have_posts() ) { // Okay, comments are okay here. // Okay, comments are okay here as well. } // Okay, comments are okay here. -// See https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/issues/40 . +// See https://github.com/WordPress/WordPress-Coding-Standards/issues/40 . if ( true ) { // code. @@ -43,89 +43,6 @@ endif; if ( false ) : else : endif; -// @codingStandardsChangeSetting WordPress.WhiteSpace.ControlStructureSpacing spaces_before_closure_open_paren 1 -$a = function ( $arg ) {}; // Bad. -$a = function ( $arg ) { - // Ok. -}; - -$a = function () { - // Ok. -}; - -function something( $arg ) {} // Bad. -function foo( $arg ) { - // Ok. -} - -function no_params() { - // Ok. -} - -function another() {} // Bad, space before open parenthesis prohibited. -function and_another() {} // Bad, space before function name prohibited. -function bar() {} // Bad. -function baz() {} // Bad. -function testA() {} // Bad. - -function &return_by_ref() {} // Ok. - -// @codingStandardsChangeSetting WordPress.WhiteSpace.ControlStructureSpacing spaces_before_closure_open_paren 0 - -$a = function() {}; // Ok. -$a = function( $arg ) {}; // Ok. -$a = function( $arg ) {}; // Bad. -$a = function() {}; // Bad. - -$closureWithArgsAndVars = function( $arg1, $arg2 ) use ( $var1, $var2 ) {}; // Ok. -$closureWithArgsAndVars = function( $arg1, $arg2 ) use ( $var1, $var2 ) {}; // Bad. - -// @codingStandardsChangeSetting WordPress.WhiteSpace.ControlStructureSpacing spaces_before_closure_open_paren 1 - -$closureWithArgsAndVars = function ( $arg1, $arg2 ) use ( $var1, $var2 ) {}; // Ok. - -$closureWithArgsAndVars = function ( $arg1, $arg2 ) use ( $var1, $var2 ) {}; // Bad, no space before open parenthesis prohibited. -$closureWithArgsAndVars = function ( $arg1, $arg2 ) use ( $var1, $var2 ) {}; // Bad, expected exactly one space before opening parenthesis. - -$closureWithArgsAndVars = function ( $arg1, $arg2 ) use ( $var1, $var2 ) {}; // Bad, space between closing parenthesis and control structure required. -$closureWithArgsAndVars = function ( $arg1, $arg2 ) use ( $var1, $var2 ) {}; // Bad, expected exactly one space between closing parenthesis and control structure. - -$closureWithArgsAndVars = function ( $arg1, $arg2 ) use ( $var1, $var2 ) {}; // Bad, expected exactly one space between closing parenthesis and control structure. -$closureWithArgsAndVars = function ( $arg1, $arg2 ) use ( $var1, $var2 ) {}; // Bad, expected exactly one space between closing parenthesis and control structure. - -// Namespaces. -use Foo\Admin; - -// @codingStandardsChangeSetting WordPress.WhiteSpace.ControlStructureSpacing spaces_before_closure_open_paren -1 - -$a = function( $arg ) {}; // Ok. -$a = function ( $arg ) {}; // Ok. - -$closureWithArgsAndVars = function( $arg1, $arg2 ) use ( $var1, $var2 ) {}; // Ok. -$closureWithArgsAndVars = function ( $arg1, $arg2 ) use ( $var1, $var2 ) {}; // Ok. - -/* - * Test for bug where this sniff was triggering a "Blank line found after control structure" error - * if there is a blank line after the last method in a class. - * - * Bug did not trigger when a comment was found after the closing brace of the method. - * - * Neither of the below examples should trigger the error. - */ -class Bar_Foo { - - function foo() { - } // Now you won't see the bug. - -} - -class Foo_Bar { - - // Now you will. - function bar() { - } - -} // Handle try/catch statements as well. try { // Bad. @@ -135,11 +52,11 @@ try { // Bad. } // Upstream bug PEAR #20248. -// @codingStandardsChangeSetting WordPress.WhiteSpace.ControlStructureSpacing blank_line_check true +// phpcs:set WordPress.WhiteSpace.ControlStructureSpacing blank_line_check true // Bad. if ( $one ) { } -// @codingStandardsChangeSetting WordPress.WhiteSpace.ControlStructureSpacing blank_line_check false +// phpcs:set WordPress.WhiteSpace.ControlStructureSpacing blank_line_check false // Upstream bug PEAR #20247. do { @@ -147,7 +64,7 @@ do { } while ( $blah ); // Bad. // Upstream bug GH #782 -// @codingStandardsChangeSetting WordPress.WhiteSpace.ControlStructureSpacing blank_line_check true +// phpcs:set WordPress.WhiteSpace.ControlStructureSpacing blank_line_check true if ( $foo ) { @@ -168,12 +85,9 @@ if ( $foo ) { } -// @codingStandardsChangeSetting WordPress.WhiteSpace.ControlStructureSpacing blank_line_check false +// phpcs:set WordPress.WhiteSpace.ControlStructureSpacing blank_line_check false // Check for too many spaces as long as the next non-blank token is on the same line. -function test( $blah ) {} // Bad. -$a = function( $bar ) {}; // Bad. - if ( 'abc' === $test ) { // Bad. echo 'hi'; } elseif ( false === $foo ) { // Bad. @@ -258,24 +172,78 @@ if ( $foo ) { } // End try/catch <- Bad: "blank line after". } -// Issue 1202. -function hi(): array -{ - return []; +// Handle (try/catch/) finally statements as well. +try { + // Something +} catch ( Exception $e ) { + // Something +} finally { // OK. + // Something +} + +try { + // Something +} finally { // Bad. + // Something +} + +try { + // Something +} finally { // Bad. + // Something +} + +if ( $condition ) { + try { + // Something + } finally { + // Something + } +} // Bad: blank line between. + +// Handle PHP 8.0+ match expressions. +$expr = match ( $foo ) { + 1 => 1, + 2 => 2, +}; + +$expr = match ( $foo ) { + 1 => 1, + 2 => 2, +} ; + +// phpcs:set WordPress.WhiteSpace.ControlStructureSpacing blank_line_check true +$expr = match ( $foo ) { + 1 => 1, + 2 => 2, +}; + +// phpcs:set WordPress.WhiteSpace.ControlStructureSpacing blank_line_check false + +if ( $condition ) { + $expr = match ( $foo ) { + 1 => 1, + 2 => 2, + }; +} // Bad: blank line between. + +// Ignore spacing rules in combination with enums as they tend to have their own rules. +// phpcs:set WordPress.WhiteSpace.ControlStructureSpacing blank_line_check true +if ( $foo ) { + + + enum MyEnumA { + // Code here + } + + } +// phpcs:set WordPress.WhiteSpace.ControlStructureSpacing blank_line_check false + +enum MyEnumB { + if ( $foo ) { + // Code here + } + -// Issue 1420. -function returntype1(): string {} // Ok. -function returntype2( $input ): string {} // Ok. -function returntype3( $input ) : string {} // Ok. -function returntype4( string $input ): string {} // Ok. -function returntype5( string $input ) : string {} // Ok. -function returntype6( string $input, array $inputs ): string {} // Ok. -function returntype7( string $input, array $inputs ) : string {} // Ok. -$returntype = function (): string {}; // Ok. -$returntype = function ( $input ): string {}; // Ok. -$returntype = function ( $input ) : string {}; // Ok. -$returntype = function ( string $input ): string {}; // Ok. -$returntype = function ( string $input ) : string {}; // Ok. -$returntype = function ( string $input, array $inputs ): string {}; // Ok. -$returntype = function ( string $input, array $inputs ) : string {}; // Ok. +} // OK, defer to enum rules. diff --git a/WordPress/Tests/WhiteSpace/ControlStructureSpacingUnitTest.2.inc b/WordPress/Tests/WhiteSpace/ControlStructureSpacingUnitTest.2.inc index 44f7bd2b95..b70e3c658a 100644 --- a/WordPress/Tests/WhiteSpace/ControlStructureSpacingUnitTest.2.inc +++ b/WordPress/Tests/WhiteSpace/ControlStructureSpacingUnitTest.2.inc @@ -1,7 +1,7 @@ => + * @return array Key is the line number, value is the number of expected errors. */ public function getErrorList( $testFile = '' ) { @@ -67,48 +39,28 @@ public function getErrorList( $testFile = '' ) { 37 => 1, 41 => 1, 42 => 1, - 49 => 5, - 58 => 3, - 67 => 1, - 68 => 1, - 69 => 1, - 71 => 1, - 72 => 1, - 81 => 3, - 82 => 1, - 85 => 1, - 91 => 2, - 92 => 1, + 50 => 2, + 52 => 5, + 59 => 1, + 67 => 2, 94 => 1, - 95 => 1, - 97 => 1, - 98 => 1, - 135 => 2, - 137 => 5, - 144 => 1, - 152 => 2, + 96 => 1, + 102 => 1, + 104 => 1, + 108 => 2, + 112 => 2, + 159 => 1, + 169 => 1, 179 => 1, - 180 => 1, - 182 => 1, - 184 => 1, - 190 => 1, - 192 => 1, - 196 => 2, - 200 => 2, - 247 => 1, - 257 => 1, - 267 => 1, + 195 => 1, + 203 => 2, + 212 => 1, + 222 => 5, + 228 => 1, + 232 => 1, + 241 => 1, ); - /* - Uncomment when "$blank_line_check" parameter will be "true" by default. - - $ret[29] += 1; - $ret[33] = 1; - $ret[36] = 1; - $ret[38] = 1; - */ - return $ret; case 'ControlStructureSpacingUnitTest.2.inc': @@ -126,10 +78,9 @@ public function getErrorList( $testFile = '' ) { /** * Returns the lines where warnings should occur. * - * @return array => + * @return array Key is the line number, value is the number of expected warnings. */ public function getWarningList() { return array(); } - } diff --git a/WordPress/Tests/WhiteSpace/DisallowInlineTabsUnitTest.inc b/WordPress/Tests/WhiteSpace/DisallowInlineTabsUnitTest.inc deleted file mode 100644 index 5d7178c14d..0000000000 --- a/WordPress/Tests/WhiteSpace/DisallowInlineTabsUnitTest.inc +++ /dev/null @@ -1,39 +0,0 @@ -tokens[ $closer ]['column'] - 1 ); - $error = 'Array closer not aligned correctly; expected %s space(s) but found %s'; - $data = array( - $expected_value => 'data', - $found => 'more_data', - ); - -/** - * @param int $var Description - Bad: alignment using tabs. - * @param string $string Another description. - */ - - $expected = ( $column - 1 ); - $found = ( $this->tokens[ $closer ]['column'] - 1 ); // Bad. - $error = 'Array closer not aligned correctly; expected %s space(s) but found %s'; // Bad. - $data = array( // Bad. - $expected_value => 'data', - $found => 'more_data', // Bad. - ); - -/* - * Test that the tab replacements do not negatively influence the existing mid-line alignments. - */ -$a = true; -$aa = true; -$aaa = true; -$aaaa = true; -$aaaaa = true; -$aaaaaa = true; -$aaaaaaa = true; -$aaaaaaaa = true; diff --git a/WordPress/Tests/WhiteSpace/DisallowInlineTabsUnitTest.inc.fixed b/WordPress/Tests/WhiteSpace/DisallowInlineTabsUnitTest.inc.fixed deleted file mode 100644 index 20f2877f69..0000000000 --- a/WordPress/Tests/WhiteSpace/DisallowInlineTabsUnitTest.inc.fixed +++ /dev/null @@ -1,39 +0,0 @@ -tokens[ $closer ]['column'] - 1 ); - $error = 'Array closer not aligned correctly; expected %s space(s) but found %s'; - $data = array( - $expected_value => 'data', - $found => 'more_data', - ); - -/** - * @param int $var Description - Bad: alignment using tabs. - * @param string $string Another description. - */ - - $expected = ( $column - 1 ); - $found = ( $this->tokens[ $closer ]['column'] - 1 ); // Bad. - $error = 'Array closer not aligned correctly; expected %s space(s) but found %s'; // Bad. - $data = array( // Bad. - $expected_value => 'data', - $found => 'more_data', // Bad. - ); - -/* - * Test that the tab replacements do not negatively influence the existing mid-line alignments. - */ -$a = true; -$aa = true; -$aaa = true; -$aaaa = true; -$aaaaa = true; -$aaaaaa = true; -$aaaaaaa = true; -$aaaaaaaa = true; diff --git a/WordPress/Tests/WhiteSpace/DisallowInlineTabsUnitTest.php b/WordPress/Tests/WhiteSpace/DisallowInlineTabsUnitTest.php deleted file mode 100644 index fe446801d6..0000000000 --- a/WordPress/Tests/WhiteSpace/DisallowInlineTabsUnitTest.php +++ /dev/null @@ -1,88 +0,0 @@ -tab_width ); - } - - /** - * Set CLI values before the file is tested. - * - * Used by PHPCS 3.x. - * - * @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 ) { - $config->tabWidth = $this->tab_width; - } - - /** - * Returns the lines where errors should occur. - * - * @return array => - */ - public function getErrorList() { - return array( - 17 => 1, - 22 => 1, - 23 => 1, - 24 => 1, - 26 => 1, - 32 => 1, - 33 => 1, - 34 => 1, - 35 => 1, - 36 => 1, - 37 => 1, - ); - } - - /** - * Returns the lines where warnings should occur. - * - * @return array => - */ - public function getWarningList() { - return array(); - } - -} diff --git a/WordPress/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc b/WordPress/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc new file mode 100644 index 0000000000..ec042a295f --- /dev/null +++ b/WordPress/Tests/WhiteSpace/ObjectOperatorSpacingUnitTest.inc @@ -0,0 +1,55 @@ + + */ + public function getErrorList() { + return array( + 11 => 2, + 12 => 2, + 13 => 2, + 16 => 2, + 19 => 2, + 22 => 2, + 26 => 2, + 36 => 2, + 37 => 2, + 38 => 2, + 47 => 2, + 51 => 2, + ); + } + + /** + * 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 array(); + } +} diff --git a/WordPress/Tests/WhiteSpace/OperatorSpacingUnitTest.inc b/WordPress/Tests/WhiteSpace/OperatorSpacingUnitTest.inc index bad89cc0af..e58abf8cd0 100644 --- a/WordPress/Tests/WhiteSpace/OperatorSpacingUnitTest.inc +++ b/WordPress/Tests/WhiteSpace/OperatorSpacingUnitTest.inc @@ -39,9 +39,9 @@ if ( $a === $b xor $b === $c ) {} // Logical operators: Too little space. if ( $a === $b&&$b === $c ) {} if ( $a === $b||$b === $c ) {} -if ( $a === {$b}and$b === $c ) {} -if ( $a === {$b}or$b === $c ) {} -if ( $a === {$b}xor$b === $c ) {} +if ( $a === ($b)and$b === $c ) {} +if ( $a === ($b)or$b === $c ) {} +if ( $a === ($b)xor$b === $c ) {} // Logical operators: Too much space. if ( $a === $b && $b === $c ) {} @@ -64,3 +64,14 @@ if ( $a === $b if ( $a === $b or $b === $c ) {} + +// Safeguard that the "|" in PHP 8.0 union types is disregarded. +function foo( int|float $param ) : string|false {} + +// Safeguard that the "&" in PHP 8.1 intersection types is disregarded. +function fooBar( TypeA&namespace\TypeB $param ) : \TypeC&Partially\Qualified {} + +// Safeguard handling of union type separator for readonly properties. +class Foo { + public readonly int|string $prop; +} diff --git a/WordPress/Tests/WhiteSpace/OperatorSpacingUnitTest.inc.fixed b/WordPress/Tests/WhiteSpace/OperatorSpacingUnitTest.inc.fixed index 4a3214f57e..43ab5ae914 100644 --- a/WordPress/Tests/WhiteSpace/OperatorSpacingUnitTest.inc.fixed +++ b/WordPress/Tests/WhiteSpace/OperatorSpacingUnitTest.inc.fixed @@ -39,9 +39,9 @@ if ( $a === $b xor $b === $c ) {} // Logical operators: Too little space. if ( $a === $b && $b === $c ) {} if ( $a === $b || $b === $c ) {} -if ( $a === {$b} and $b === $c ) {} -if ( $a === {$b} or $b === $c ) {} -if ( $a === {$b} xor $b === $c ) {} +if ( $a === ($b) and $b === $c ) {} +if ( $a === ($b) or $b === $c ) {} +if ( $a === ($b) xor $b === $c ) {} // Logical operators: Too much space. if ( $a === $b && $b === $c ) {} @@ -64,3 +64,14 @@ if ( $a === $b if ( $a === $b or $b === $c ) {} + +// Safeguard that the "|" in PHP 8.0 union types is disregarded. +function foo( int|float $param ) : string|false {} + +// Safeguard that the "&" in PHP 8.1 intersection types is disregarded. +function fooBar( TypeA&namespace\TypeB $param ) : \TypeC&Partially\Qualified {} + +// Safeguard handling of union type separator for readonly properties. +class Foo { + public readonly int|string $prop; +} diff --git a/WordPress/Tests/WhiteSpace/OperatorSpacingUnitTest.php b/WordPress/Tests/WhiteSpace/OperatorSpacingUnitTest.php index 801cb15f09..d7343f1ec2 100644 --- a/WordPress/Tests/WhiteSpace/OperatorSpacingUnitTest.php +++ b/WordPress/Tests/WhiteSpace/OperatorSpacingUnitTest.php @@ -3,30 +3,30 @@ * Unit test class for WordPress Coding Standard. * * @package WPCS\WordPressCodingStandards - * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards + * @link https://github.com/WordPress/WordPress-Coding-Standards * @license https://opensource.org/licenses/MIT MIT */ -namespace WordPress\Tests\WhiteSpace; +namespace WordPressCS\WordPress\Tests\WhiteSpace; use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; /** * Unit test class for the OperatorSpacing sniff. * - * @package WPCS\WordPressCodingStandards + * @since 2013-06-11 + * @since 0.12.0 Now only tests the WPCS specific addition of T_BOOLEAN_NOT. + * The rest of the sniff is unit tested upstream. + * @since 0.13.0 Class name changed: this class is now namespaced. * - * @since 2013-06-11 - * @since 0.12.0 Now only tests the WPCS specific addition of T_BOOLEAN_NOT. - * The rest of the sniff is unit tested upstream. - * @since 0.13.0 Class name changed: this class is now namespaced. + * @covers \WordPressCS\WordPress\Sniffs\WhiteSpace\OperatorSpacingSniff */ -class OperatorSpacingUnitTest extends AbstractSniffUnitTest { +final class OperatorSpacingUnitTest extends AbstractSniffUnitTest { /** * Returns the lines where errors should occur. * - * @return array => + * @return array Key is the line number, value is the number of expected errors. */ public function getErrorList() { return array( @@ -48,10 +48,9 @@ public function getErrorList() { /** * Returns the lines where warnings should occur. * - * @return array => + * @return array Key is the line number, value is the number of expected warnings. */ public function getWarningList() { return array(); } - } diff --git a/WordPress/Tests/WhiteSpace/PrecisionAlignmentUnitTest.1.inc b/WordPress/Tests/WhiteSpace/PrecisionAlignmentUnitTest.1.inc deleted file mode 100644 index 7bcca1217e..0000000000 --- a/WordPress/Tests/WhiteSpace/PrecisionAlignmentUnitTest.1.inc +++ /dev/null @@ -1,68 +0,0 @@ - - -

- Bad: Some text with precision alignment. -

- - - - - - - -

- Bad: Some text with precision alignment, but not reported as token type is whitelisted. -

- -Hello, Dolly in the upper right of your admin screen on every page. -Author: Matt Mullenweg -Version: 1.5.1 -Author URI: http://ma.tt/ -Text Domain: hello-dolly - -*/ - -// Test for -?> diff --git a/WordPress/Tests/WhiteSpace/PrecisionAlignmentUnitTest.4.inc b/WordPress/Tests/WhiteSpace/PrecisionAlignmentUnitTest.4.inc deleted file mode 100644 index a7c5b5deff..0000000000 --- a/WordPress/Tests/WhiteSpace/PrecisionAlignmentUnitTest.4.inc +++ /dev/null @@ -1,5 +0,0 @@ -
-

- -

-
diff --git a/WordPress/Tests/WhiteSpace/PrecisionAlignmentUnitTest.5.inc b/WordPress/Tests/WhiteSpace/PrecisionAlignmentUnitTest.5.inc deleted file mode 100644 index 4fb786f100..0000000000 --- a/WordPress/Tests/WhiteSpace/PrecisionAlignmentUnitTest.5.inc +++ /dev/null @@ -1,58 +0,0 @@ -tab_width ); - } - - /** - * Set CLI values before the file is tested. - * - * Used by PHPCS 3.x. - * - * @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 ) { - $config->tabWidth = $this->tab_width; - - // Setting "--ignore-annotations" is only possible since PHPCS 3.0. - if ( 'PrecisionAlignmentUnitTest.6.inc' === $testFile ) { - $config->annotations = false; - } - } - - /** - * Get a list of all test files to check. - * - * @param string $testFileBase The base path that the unit tests files will have. - * - * @return string[] - */ - protected function getTestFiles( $testFileBase ) { - - $testFiles = parent::getTestFiles( $testFileBase ); - - /* - * Testing whether the PHPCS annotations are properly respected is only useful on - * PHPCS versions which support the PHPCS annotations. - */ - if ( version_compare( PHPCSHelper::get_version(), '3.2.0', '<' ) === true ) { - $key = array_search( $testFileBase . '5.inc', $testFiles, true ); - if ( false !== $key ) { - unset( $testFiles[ $key ] ); - } - } - - return $testFiles; - } - - /** - * Returns the lines where errors should occur. - * - * @return array => - */ - public function getErrorList() { - return array(); - } - - /** - * Returns the lines where warnings should occur. - * - * @param string $testFile The name of the file being tested. - * - * @return array => - */ - public function getWarningList( $testFile = '' ) { - switch ( $testFile ) { - case 'PrecisionAlignmentUnitTest.1.inc': - return array( - 20 => 1, - 27 => 1, - 30 => 1, - 31 => 1, - 32 => 1, - 39 => 1, - 65 => 1, - ); - - case 'PrecisionAlignmentUnitTest.4.inc': - return array( - 1 => 1, // Will show a `Internal.NoCodeFound` warning in PHP 5.3 with short open tags off. - 2 => ( \PHP_VERSION_ID < 50400 && false === (bool) ini_get( 'short_open_tag' ) ) ? 0 : 1, - 3 => ( \PHP_VERSION_ID < 50400 && false === (bool) ini_get( 'short_open_tag' ) ) ? 0 : 1, - 4 => ( \PHP_VERSION_ID < 50400 && false === (bool) ini_get( 'short_open_tag' ) ) ? 0 : 1, - 5 => ( \PHP_VERSION_ID < 50400 && false === (bool) ini_get( 'short_open_tag' ) ) ? 0 : 1, - ); - - case 'PrecisionAlignmentUnitTest.5.inc': - $warnings = array( - 9 => 1, - 14 => 1, - 19 => 1, - 24 => 0, - 29 => 0, - 34 => 1, - 39 => 1, - 44 => 1, - 54 => 0, - 56 => 0, - 58 => 0, - ); - - /* - * The PHPCS 3.2.x versions contained a bug in the selective disable/enable logic - * compared to the intended behaviour as documented, which prevented the particular - * messages being tested on these lines from being thrown. See upstream issue #1986. - */ - if ( version_compare( PHPCSHelper::get_version(), '3.3.0', '>=' ) === true ) { - $warnings[24] = 1; - $warnings[29] = 1; - $warnings[54] = 1; - $warnings[56] = 1; - $warnings[58] = 1; - } - - return $warnings; - - case 'PrecisionAlignmentUnitTest.6.inc': - /* - * {@internal Always returns 1 warning, as for PHPCS < 3.2.0, the PHPCS annotation - * will be seen as a "normal" comment with precision alignment. - * For PHPCS 3.2.0+, it will be seen as a PHPCS annotation, but annotations are ignored - * for this test file, so the precision alignment will be reported.}} - */ - return array( - 4 => 1, - ); - - case 'PrecisionAlignmentUnitTest.css': - return array( - 4 => 1, - ); - - case 'PrecisionAlignmentUnitTest.js': - return array( - 4 => 1, - 5 => 1, - 6 => 1, - ); - - case 'PrecisionAlignmentUnitTest.2.inc': - case 'PrecisionAlignmentUnitTest.3.inc': - default: - return array(); - } - } - -} diff --git a/WordPress/Tests/WhiteSpace/SemicolonSpacingUnitTest.inc b/WordPress/Tests/WhiteSpace/SemicolonSpacingUnitTest.inc deleted file mode 100644 index ec7f406707..0000000000 --- a/WordPress/Tests/WhiteSpace/SemicolonSpacingUnitTest.inc +++ /dev/null @@ -1,12 +0,0 @@ -= 0; $ptr-- ) {} -for ( ; ; ) {} - -// But it should when the whitespace is between a condition and a semi-colon. -for ( $i = 1 ; ; $i++ ) {} diff --git a/WordPress/Tests/WhiteSpace/SemicolonSpacingUnitTest.inc.fixed b/WordPress/Tests/WhiteSpace/SemicolonSpacingUnitTest.inc.fixed deleted file mode 100644 index 0a0820f150..0000000000 --- a/WordPress/Tests/WhiteSpace/SemicolonSpacingUnitTest.inc.fixed +++ /dev/null @@ -1,12 +0,0 @@ -= 0; $ptr-- ) {} -for ( ; ; ) {} - -// But it should when the whitespace is between a condition and a semi-colon. -for ( $i = 1; ; $i++ ) {} diff --git a/WordPress/Tests/WhiteSpace/SemicolonSpacingUnitTest.php b/WordPress/Tests/WhiteSpace/SemicolonSpacingUnitTest.php deleted file mode 100644 index 0dcd1950ef..0000000000 --- a/WordPress/Tests/WhiteSpace/SemicolonSpacingUnitTest.php +++ /dev/null @@ -1,46 +0,0 @@ - => - */ - public function getErrorList() { - return array( - 12 => 1, - ); - } - - /** - * Returns the lines where warnings should occur. - * - * @return array => - */ - public function getWarningList() { - return array(); - } - -} diff --git a/WordPress/Tests/XSS/EscapeOutputUnitTest.inc b/WordPress/Tests/XSS/EscapeOutputUnitTest.inc deleted file mode 100644 index de23ba675a..0000000000 --- a/WordPress/Tests/XSS/EscapeOutputUnitTest.inc +++ /dev/null @@ -1,6 +0,0 @@ - => - */ - public function getErrorList() { - return array(); - } - - /** - * Returns the lines where warnings should occur. - * - * @return array => - */ - public function getWarningList() { - return array( - 1 => 2, - ); - } - -} diff --git a/WordPress/ruleset.xml b/WordPress/ruleset.xml index cd3cd73dff..fd24ae6318 100644 --- a/WordPress/ruleset.xml +++ b/WordPress/ruleset.xml @@ -1,87 +1,13 @@ - - WordPress Coding Standards - - ./PHPCSAliases.php - - - - - - - + - - - - - - + WordPress Coding Standards - - - - 0 - - - - - 0 - - - - - 0 - - - - - 0 - - - - - 0 - - - - - 0 - - - - - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 0 - - - - - 0 - + + diff --git a/bin/pre-commit b/bin/pre-commit deleted file mode 100755 index 4b3e665d70..0000000000 --- a/bin/pre-commit +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -e -cd "$(git rev-parse --show-toplevel)" - -phpcs_root="$(dirname $(dirname $(which phpcs)))" - -if [ -z "$phpcs_root" ] || [ ! -d "$phpcs_root" ] || [ ! -e "$phpcs_root/tests/AllTests.php" ]; then - echo "Unable to locate phpcs on path to locate the root of the PHP_CodeSniffer project" 1>&2 - exit 1 -fi - -find . \( -name '*.php' \) -exec php -lf {} \; - -PHPCS_DIR=$phpcs_root phpunit --bootstrap="./Test/phpcs3-bootstrap.php" --filter WordPress "$phpcs_root/tests/AllTests.php" diff --git a/composer.json b/composer.json index db2f784258..4a287cba2e 100644 --- a/composer.json +++ b/composer.json @@ -5,36 +5,84 @@ "keywords": [ "phpcs", "standards", + "static analysis", "WordPress" ], "license": "MIT", "authors": [ { "name": "Contributors", - "homepage": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/graphs/contributors" + "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" } ], "require": { - "php": ">=5.3", - "squizlabs/php_codesniffer": "^2.9.0 || ^3.0.2" + "php": ">=5.4", + "ext-filter": "*", + "ext-libxml": "*", + "ext-tokenizer": "*", + "ext-xmlreader": "*", + "squizlabs/php_codesniffer": "^3.7.2", + "phpcsstandards/phpcsutils": "^1.0.8", + "phpcsstandards/phpcsextra": "^1.1.0" }, "require-dev": { - "phpcompatibility/php-compatibility": "^9.0" + "phpcompatibility/php-compatibility": "^9.0", + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0", + "phpcsstandards/phpcsdevtools": "^1.2.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "php-parallel-lint/php-console-highlighter": "^1.0.0" }, "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.4.3 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." + "ext-iconv": "For improved results", + "ext-mbstring": "For improved results" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } }, - "minimum-stability": "RC", "scripts": { - "post-install-cmd": "@install-codestandards", - "post-update-cmd": "@install-codestandards", - "check-cs": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs", - "fix-cs": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf", - "install-codestandards": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs --config-set installed_paths ../../..,../../phpcompatibility/php-compatibility" + "lint": [ + "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . -e php --show-deprecated --exclude vendor --exclude .git" + ], + "check-cs": [ + "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs" + ], + "fix-cs": [ + "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf" + ], + "run-tests": [ + "@php ./vendor/phpunit/phpunit/phpunit --filter WordPress ./vendor/squizlabs/php_codesniffer/tests/AllTests.php --no-coverage" + ], + "coverage": [ + "@php ./vendor/phpunit/phpunit/phpunit --filter WordPress ./vendor/squizlabs/php_codesniffer/tests/AllTests.php" + ], + "check-complete": [ + "@php ./vendor/phpcsstandards/phpcsdevtools/bin/phpcs-check-feature-completeness -q ./WordPress" + ], + "check-complete-strict": [ + "@php ./vendor/phpcsstandards/phpcsdevtools/bin/phpcs-check-feature-completeness ./WordPress" + ], + "check-all": [ + "@lint", + "@check-cs", + "@run-tests", + "@check-complete-strict" + ] + }, + "scripts-descriptions": { + "lint": "Lint PHP files against parse errors.", + "check-cs": "Run the PHPCS script against the entire codebase.", + "fix-cs": "Run the PHPCBF script to fix all the autofixable violations on the codebase.", + "run-tests": "Run all the unit tests for the WordPress Coding Standards sniffs without code coverage.", + "coverage": "Run all the unit tests for the WordPress Coding Standards sniffs with code coverage.", + "check-complete": "Check if all the sniffs have tests.", + "check-complete-strict": "Check if all the sniffs have unit tests and XML documentation.", + "check-all": "Run all checks (lint, phpcs, feature completeness) and tests." }, "support": { - "issues": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/issues", - "wiki": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki", - "source": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards" + "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", + "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki", + "source": "https://github.com/WordPress/WordPress-Coding-Standards" } } diff --git a/phpcs.xml.dist.sample b/phpcs.xml.dist.sample index 4563369602..c20d803f77 100644 --- a/phpcs.xml.dist.sample +++ b/phpcs.xml.dist.sample @@ -1,7 +1,17 @@ - + + A custom set of rules to check for a WPized WordPress project + + + . + /docroot/wp-admin/* /docroot/wp-includes/* @@ -19,6 +29,19 @@ *.min.js + + + + + + + + + @@ -43,7 +66,6 @@ - @@ -56,31 +78,76 @@ https://github.com/PHPCompatibility/PHPCompatibility --> + + + - + - + + + + - + + + + + + + + + /path/to/Tests/*Test\.php + + + /path/to/Tests/*Test\.php + + diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000000..3aafa9a3db --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,26 @@ +parameters: + #phpVersion: 50400 # Needs to be 70100 or higher... sigh... + level: 5 + paths: + - WordPress + bootstrapFiles: + - Tests/bootstrap.php + treatPhpDocTypesAsCertain: false + + ignoreErrors: + # Level 0 + - '#^Result of method \S+ \(void\) is used\.$#' + + # Level 4 + - '#^Property \S+::\$\S+ \([^)]+\) in isset\(\) is not nullable\.$#' + - + count: 1 + message: '#^Result of && is always true\.$#' + path: WordPress/Sniffs/Security/EscapeOutputSniff.php + - + count: 1 + message: '#^Strict comparison using === between true and false will always evaluate to false\.$#' + path: WordPress/Sniffs/Utils/I18nTextDomainFixerSniff.php + + # Level 5 + - '#^Parameter \#3 \$value of method \S+File::recordMetric\(\) expects string, \(?(float|int|bool)(\|(float|int|bool))*\)? given\.$#' diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 95f66654d0..d041cb7d99 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -3,7 +3,32 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/6.3/phpunit.xsd" backupGlobals="true" - bootstrap="./Test/bootstrap.php" + bootstrap="./Tests/bootstrap.php" beStrictAboutTestsThatDoNotTestAnything="false" - colors="true"> + colors="true" + forceCoversAnnotation="true"> + + + + ./WordPress/Tests/ + + + + + + ./WordPress/Sniff.php + ./WordPress/AbstractArrayAssignmentRestrictionsSniff.php + ./WordPress/AbstractClassRestrictionsSniff.php + ./WordPress/AbstractFunctionParameterSniff.php + ./WordPress/AbstractFunctionRestrictionsSniff.php + ./WordPress/Sniffs/ + ./WordPress/Helpers/ + + + + + + + +