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 c12f94cfb3..3472eccbfa 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,19 +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 -/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 34b96be5c7..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 @@ -32,87 +35,70 @@ 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/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. -> -> Support for the WPCS native whitelist comments has been deprecated in WPCS 2.0.0 and will be removed in WPCS 3.0.0. -> -> With that in mind, (new) sniffs should not introduce new WPCS native whitelist comments. - - # Unit Testing ## Pre-requisites * WordPress-Coding-Standards -* PHP_CodeSniffer 3.3.1 or higher +* 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/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. -> Other than that, you're all set already as Composer will have installed PHPUnit for you. +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 -## Installing PHPUnit +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. -N.B.: _If you used Composer to install the WordPress Coding Standards, you can skip this step._ +## Custom develop setups -You can either navigate to the directory where the `PHP_CodeSniffer` repo is checked out and do `composer install` to install the `dev` dependencies or you can [install PHPUnit](https://phpunit.readthedocs.io/en/7.4/installation.html) as a PHAR file. - -You may want to add the directory where PHPUnit is installed to a `PATH` environment variable for your operating system to make the command available everywhere on your system. - -## Before running the unit tests - -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. -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 - - - -``` +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 -* If you didn't install WPCS using Composer, 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: - ```sh - phpunit --filter WordPress --bootstrap="/path/to/PHP_CodeSniffer/tests/bootstrap.php" /path/to/PHP_CodeSniffer/tests/AllTests.php +From the root of your WordPressCS install, run the unit tests like so: +```bash +composer run-tests - # Or if you've installed WPCS with Composer: - composer run-tests - ``` +# 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 7.5.0 by Sebastian Bergmann and contributors. +PHPUnit 7.5.20 by Sebastian Bergmann and contributors. -Runtime: PHP 7.2.13 -Configuration: /WordPressCS/phpunit.xml +Runtime: PHP 7.4.33 +Configuration: /WordPressCS/phpunit.xml.dist -........................................................ 56 / 56 (100%) +......................................................... 57 / 57 (100%) -152 sniff test files generated 487 unique error codes; 52 were fixable (10.68%) +201 sniff test files generated 744 unique error codes; 50 were fixable (6%) -Time: 21.36 seconds, Memory: 22.00MB +Time: 10.19 seconds, Memory: 40.00 MB -OK (56 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? @@ -120,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 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( @@ -142,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 @@ -163,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. @@ -187,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/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..1c6df73886 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: "https://opencollective.com/thewpcc/contribute/wp-php-63406" 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 1208b19678..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/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..97c58b7822 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +# Dependabot configuration. +# +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + time: "09:00" + open-pull-requests-limit: 5 + commit-message: + prefix: "GH Actions:" + labels: + - "Type: Chores/Cleanup" diff --git a/.github/release-checklist.md b/.github/release-checklist.md new file mode 100644 index 0000000000..df5f91ed48 --- /dev/null +++ b/.github/release-checklist.md @@ -0,0 +1,71 @@ +# 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]. +- [ ] Submit for the ["Monthy Dev Roundup"][dev-roundup]. + +[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/ +[dev-roundup]: https://github.com/WordPress/developer-blog-content/issues?q=is%3Aissue+label%3A%22Monthly+Roundup%22 diff --git a/.github/workflows/basic-qa.yml b/.github/workflows/basic-qa.yml new file mode 100644 index 0000000000..97943efa21 --- /dev/null +++ b/.github/workflows/basic-qa.yml @@ -0,0 +1,196 @@ +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@v4 + + - 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@v4 + + - 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 + + - name: Rename the example ruleset to one which can be used for a ruleset + run: cp phpcs.xml.dist.sample sample.xml + + - name: Test the example ruleset + run: $(pwd)/vendor/bin/phpcs -ps ./Tests/RulesetCheck/example-ruleset-test.inc --standard=./sample.xml + + # 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@v4 + + - 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..182c24c1de --- /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@v4 + + # 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..ab5f8f5b50 --- /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@v4 + + # 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 ef779c2dc3..af658a72a5 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -3,34 +3,57 @@ The Coding standard for the WordPress Coding Standards itself. + + . - + + */vendor/* + + + + + + + + + - /bin/class-ruleset-test.php - - */vendor/* + + + - - - - + + - - - - - - + + + + + - + + + + + + @@ -47,10 +70,31 @@ - - - /WordPress/AbstractClassRestrictionsSniff\.php$ + + + + + + + /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 4369ad0926..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,180 +0,0 @@ -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 - - 7.4 - - "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. - - PHPCS_BRANCH="3.3.1" - -# Define the stages used. -# For non-PRs, only the sniff, ruleset and quicktest stages are run. -# For pull requests and merges, the full script is run (skipping quicktest). -# Note: for pull requests, "develop" should be the base branch name. -# See: https://docs.travis-ci.com/user/conditions-v1 -stages: - - name: sniff - - name: rulesets - - name: quicktest - if: type = push AND branch NOT IN (master, develop) - - name: test - if: branch IN (master, develop) - -jobs: - fast_finish: true - include: - #### SNIFF STAGE #### - - stage: sniff - php: 7.4 - env: PHPCS_BRANCH="dev-master" - addons: - apt: - packages: - - libxml2-utils - script: - # WordPress Coding Standards. - # @link https://github.com/WordPress/WordPress-Coding-Standards - # @link http://pear.php.net/package/PHP_CodeSniffer/ - - $(pwd)/vendor/bin/phpcs --runtime-set ignore_warnings_on_exit 1 - - # Validate the xml files. - # @link http://xmlsoft.org/xmllint.html - # For the build to properly error when validating against a scheme, these each have to be in their own condition. - - xmllint --noout --schema ./vendor/squizlabs/php_codesniffer/phpcs.xsd ./*/ruleset.xml - - xmllint --noout --schema ./vendor/squizlabs/php_codesniffer/phpcs.xsd ./phpcs.xml.dist.sample - - # Check the code-style consistency of the xml files. - - 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") - - # Validate the composer.json file. - # @link https://getcomposer.org/doc/03-cli.md#validate - - composer validate --no-check-all --strict - - # 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. - - composer check-complete - - #### RULESET STAGE #### - # Make sure the rulesets don't throw 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. - - stage: rulesets - php: 7.4 - env: PHPCS_BRANCH="dev-master" - script: - - $(pwd)/vendor/bin/phpcs -ps ./bin/class-ruleset-test.php --standard=WordPress-Core - - $(pwd)/vendor/bin/phpcs -ps ./bin/class-ruleset-test.php --standard=WordPress-Docs - - $(pwd)/vendor/bin/phpcs -ps ./bin/class-ruleset-test.php --standard=WordPress-Extra - - $(pwd)/vendor/bin/phpcs -ps ./bin/class-ruleset-test.php --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. - # 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 - - travis_retry $(pwd)/vendor/bin/phpcbf -pq ./WordPress/Tests/ --standard=WordPress --extensions=inc --exclude=Generic.PHP.Syntax --report=summary - - - stage: rulesets - php: 7.4 - env: PHPCS_BRANCH="3.3.1" - script: - - $(pwd)/vendor/bin/phpcs -ps ./bin/class-ruleset-test.php --standard=WordPress-Core - - $(pwd)/vendor/bin/phpcs -ps ./bin/class-ruleset-test.php --standard=WordPress-Docs - - $(pwd)/vendor/bin/phpcs -ps ./bin/class-ruleset-test.php --standard=WordPress-Extra - - $(pwd)/vendor/bin/phpcs -ps ./bin/class-ruleset-test.php --standard=WordPress - - #### QUICK TEST STAGE #### - # This is a much quicker test which only runs the unit tests and linting against the low/high - # supported PHP/PHPCS combinations. - - stage: quicktest - php: 7.4 - env: PHPCS_BRANCH="dev-master" LINT=1 - - php: 7.3 - env: PHPCS_BRANCH="3.3.1" - - php: 5.4 - env: PHPCS_BRANCH="dev-master" LINT=1 - - php: 5.4 - env: PHPCS_BRANCH="3.3.1" - - #### TEST STAGE #### - # Add extra build to test against PHPCS 4. - - stage: test - php: 7.4 - env: PHPCS_BRANCH="4.0.x-dev" - - allow_failures: - # Allow failures for unstable builds. - - php: "nightly" - - env: PHPCS_BRANCH="4.0.x-dev" - -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.' - - # On stable PHPCS versions, allow for PHP deprecation notices. - # Unit tests don't need to fail on those for stable releases where those issues won't get fixed anymore. - - | - if [[ "${TRAVIS_BUILD_STAGE_NAME^}" != "Sniff" && $PHPCS_BRANCH != "dev-master" ]]; then - echo 'error_reporting = E_ALL & ~E_DEPRECATED' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini - fi - - - export XMLLINT_INDENT=" " - - export PHPUNIT_DIR=/tmp/phpunit - - | - if [[ $TRAVIS_PHP_VERSION == "nightly" || $PHPCS_BRANCH == "4.0.x-dev" ]]; then - # Even though we're not doing a dev install, dev requirements are still checked. - # Neither the PHPCS Composer plugin nor PHPCompatibility allows yet for PHPCS 4.x. - # The Composer plugin also doesn't allow for installation on PHP 8.x/nightly. - composer remove --dev dealerdirect/phpcodesniffer-composer-installer phpcompatibility/php-compatibility --no-update --no-scripts - fi - - composer require squizlabs/php_codesniffer:${PHPCS_BRANCH} --update-no-dev --no-suggest --no-scripts - - | - if [[ "${TRAVIS_BUILD_STAGE_NAME^}" == "Sniff" ]]; then - composer install --dev --no-suggest - # The `dev` required DealerDirect Composer plugin 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 7.x for builds on PHP >= 7.2 as the PHPCS - # test suite is currently not compatible with PHPUnit 8.x. - - if [[ ${TRAVIS_PHP_VERSION:0:3} > "7.1" ]]; then wget -P $PHPUNIT_DIR https://phar.phpunit.de/phpunit-7.phar && chmod +x $PHPUNIT_DIR/phpunit-7.phar; 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:3} > "7.1" ]]; then - php $PHPUNIT_DIR/phpunit-7.phar --filter WordPress --bootstrap="$(pwd)/vendor/squizlabs/php_codesniffer/tests/bootstrap.php" $(pwd)/vendor/squizlabs/php_codesniffer/tests/AllTests.php - else - phpunit --filter WordPress --bootstrap="$(pwd)/vendor/squizlabs/php_codesniffer/tests/bootstrap.php" $(pwd)/vendor/squizlabs/php_codesniffer/tests/AllTests.php - fi diff --git a/CHANGELOG.md b/CHANGELOG.md index f4e3c08345..7bfac3e701 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,397 @@ This projects adheres to [Semantic Versioning](https://semver.org/) and [Keep a _No documentation available about unreleased changes as of yet._ +## [3.0.1] - 2023-09-14 + +### Added + +- In WordPressCS 3.0.0, the functionality of the `WordPress.Security.EscapeOutput` sniff was updated to report unescaped message parameters passed to exceptions created in `throw` statements. This specific violation now has a separate error code: `ExceptionNotEscaped`. This will allow users to ignore or exclude that specific error code. Props [@anomiex]. + The error code(s) for other escaping issues flagged by the sniff remain unchanged. + +### Changed + +- Updated the CI workflow to test the example ruleset for issues. +- Funding files and updates in the Readme about funding the project. + +### Fixed + +- Fixed a sniff name in the `phpcs.xml.dist.sample` file (case-sensitive sniff name). Props [@dawidurbanski]. + + +## [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 @@ -33,7 +424,7 @@ _No documentation available about unreleased changes as of yet._ - 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 DealerDirect PHPCS Composer plugin is now `^0.6`. +- 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. @@ -152,7 +543,7 @@ The move does not affect the package name for Packagist. This remains the same: - 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 DealerDirect PHPCS Composer plugin is now `^0.5.0`. +- The recommended version of the suggested [Composer PHPCS plugin] is now `^0.5.0`. - Various minor code tweaks and clean up. ### Removed @@ -274,7 +665,7 @@ If you are a maintainer of an external standard based on WordPressCS and any of - 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 DealerDirect PHPCS Composer plugin has been added to `require-dev`. +- 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+. @@ -406,7 +797,7 @@ Note: This will be the last release supporting PHP_CodeSniffer 2.x. ### 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/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. @@ -501,7 +892,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/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. @@ -846,7 +1237,7 @@ You are also encouraged to check the file history of any WPCS classes you extend - `WordPress.WP.DeprecatedFunctions` sniff to the `WordPress-Extra` ruleset to check for usage of deprecated WP version and show errors/warnings depending on a `minimum_supported_version` which [can be passed to the sniff from a custom ruleset](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). The default value for the `minimum_supported_version` property is three versions before the current WP version. - `WordPress.WP.I18n`: ability to check for missing _translators comments_ when a I18n function call contains translatable text strings containing placeholders. This check will also verify that the _translators comment_ is correctly placed in the code and uses the correct comment type for optimal compatibility with the various tools available to create `.pot` files. - `WordPress.WP.I18n`: ability to pass the `text_domain` to check for [from the command line](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#setting-text_domain-from-the-command-line-wpcs-0110). -- `WordPress.Arrays.ArrayDeclarationSpacing`: check + fixer for single line associative arrays. The [handbook](https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#indentation) states that these should always be multi-line. +- `WordPress.Arrays.ArrayDeclarationSpacing`: check + fixer for single line associative arrays. The [handbook](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/#indentation) states that these should always be multi-line. - `WordPress.Files.FileName`: verification that files containing a class reflect this in the file name as per the core guidelines. This particular check can be disabled in a custom ruleset by setting the new [`strict_class_file_names` property](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#disregard-class-file-name-rules). - `WordPress.Files.FileName`: verification that files in `/wp-includes/` containing template tags - annotated with `@subpackage Template` in the file header - use the `-template` suffix. - `WordPress.Files.FileName`: [`is_theme` property](https://github.com/WordPress/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#themes-allow-filename-exceptions) which can be set in a custom ruleset. This property can be used to indicate that the project being checked is a theme and will allow for a predefined theme hierarchy based set of exceptions to the file name rules. @@ -1179,7 +1570,11 @@ See the comparison for full list. Initial tagged release. -[Unreleased]: https://github.com/WordPress/WordPress-Coding-Standards/compare/master...HEAD +[Composer PHPCS plugin]: https://github.com/PHPCSStandards/composer-installer + +[Unreleased]: https://github.com/WordPress/WordPress-Coding-Standards/compare/main...HEAD +[3.0.1]: https://github.com/WordPress/WordPress-Coding-Standards/compare/3.0.0...3.0.1 +[3.0.0]: https://github.com/WordPress/WordPress-Coding-Standards/compare/2.3.0...3.0.0 [2.3.0]: https://github.com/WordPress/WordPress-Coding-Standards/compare/2.2.1...2.3.0 [2.2.1]: https://github.com/WordPress/WordPress-Coding-Standards/compare/2.2.0...2.2.1 [2.2.0]: https://github.com/WordPress/WordPress-Coding-Standards/compare/2.1.1...2.2.0 @@ -1207,3 +1602,18 @@ Initial tagged release. [0.4.0]: https://github.com/WordPress/WordPress-Coding-Standards/compare/0.3.0...0.4.0 [0.3.0]: https://github.com/WordPress/WordPress-Coding-Standards/compare/2013-10-06...0.3.0 [2013-10-06]: https://github.com/WordPress/WordPress-Coding-Standards/compare/2013-06-11...2013-10-06 + +[@anomiex]: https://github.com/anomiex +[@ckanitz]: https://github.com/ckanitz +[@craigfrancis]: https://github.com/craigfrancis +[@dawidurbanski]: https://github.com/dawidurbanski +[@desrosj]: https://github.com/desrosj +[@grappler]: https://github.com/grappler +[@Ipstenu]: https://github.com/Ipstenu +[@JDGrimes]: https://github.com/JDGrimes +[@khacoder]: https://github.com/khacoder +[@Luc45]: https://github.com/Luc45 +[@marconmartins]: https://github.com/marconmartins +[@NielsdeBlaauw]: https://github.com/NielsdeBlaauw +[@slaFFik]: https://github.com/slaFFik +[@sandeshjangam]: https://github.com/sandeshjangam diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..c0a53dc110 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement in +the [WordPress Slack](https://make.wordpress.org/chat/) in the [#core-coding-standards channel](https://wordpress.slack.com/archives/C5VCTJGH3). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/README.md b/README.md index 42eda804b6..aeb68cd909 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ @@ -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,91 +34,84 @@ + [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) +* [Funding](#funding) * [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/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/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/WordPress-Coding-Standards/releases/tag/1.0.0) of the project was released. - -## Installation - -### Requirements - -The WordPress Coding Standards require PHP 5.4 or higher and [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) version **3.3.1** or higher. - -### Composer - -Standards can be installed with the [Composer](https://getcomposer.org/) dependency manager: - - composer create-project wp-coding-standards/wpcs --no-dev - -Running this command will: +This project needs funding. [Find out how you can help](#funding). -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`. +## Minimum Requirements -For the convenience of using `phpcs` as a global command, you may want to add the path to the `wpcs/vendor/bin` directory to a `PATH` environment variable for your operating system. +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/) -#### Installing WPCS as a dependency +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) -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.6" +As of [WordPressCS 3.0.0](https://make.wordpress.org/core/2023/08/21/wordpresscs-3-0-0-is-now-available/), 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/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/WordPress-Coding-Standards.git wpcs -cd phpcs -./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/bin` 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 @@ -128,158 +122,125 @@ The project encompasses a super-set of the sniffs that the WordPress community m 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` -**Note:** The WPCS package used to include a `WordPress-VIP` ruleset and associated sniffs, prior to WPCS 2.0.0. -The `WordPress-VIP` 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 has been superseded. It is recommended to use the [official VIP coding standards](https://github.com/Automattic/VIP-Coding-Standards) ruleset instead for checking code against the VIP platform requirements. - ### Using a custom ruleset -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`, `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` 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. +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`, `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. + +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.dist` 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/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 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/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/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/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.com/) - -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/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 -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 +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. -## Fixing errors or whitelisting them +## Running your code through WordPressCS automatically using Continuous Integration tools + +- [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 ignoring them 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: @@ -291,6 +252,10 @@ At this moment, WPCS offer the following tools: See [CONTRIBUTING](.github/CONTRIBUTING.md), including information about [unit testing](.github/CONTRIBUTING.md#unit-testing) the standard. +## Funding + +If you want to sponsor the work on WordPressCS, you can do so by donating to the [WP PHP Open Collective](https://opencollective.com//thewpcc/contribute/wp-php-63406). + ## License See [LICENSE](LICENSE) (MIT). diff --git a/bin/class-ruleset-test.php b/Tests/RulesetCheck/class-ruleset-test.inc similarity index 90% rename from bin/class-ruleset-test.php rename to Tests/RulesetCheck/class-ruleset-test.inc index e9a85899c4..b674946ded 100644 --- a/bin/class-ruleset-test.php +++ b/Tests/RulesetCheck/class-ruleset-test.inc @@ -1,14 +1,14 @@ 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 b7555b7a7e..f707d80058 100644 --- a/WordPress-Core/ruleset.xml +++ b/WordPress-Core/ruleset.xml @@ -6,24 +6,290 @@ + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + - + + + + + + + + + + warning + + + warning + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + 0 + + + 0 + + + 0 + + + + 0 + + + + + + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -36,14 +302,6 @@ - - - - - - - - @@ -51,19 +309,28 @@ - + + when the array contains more than one item. --> + + + + + + + + + - + - + @@ -77,10 +344,10 @@ - + - + @@ -89,382 +356,513 @@ - + - - + + - - + + + + + - - + + + + + - - + + - + + + + - - + + - + + + + + + + + + + + - + + - - - - 0 - - - 0 - - - 0 - - - 0 - + + + + + + + + - - - + + + + + + + + - - + - - + + + + + + + + + + + + + - - - - - - - - + + - - + + + - - - - - - - - - - - - - - + + + + + + + + + + + 0 + + - - - - - + + - - - - + + - - + + + - - - - - - - - - - + + + - - + + - + + - - - - - - + + + 0 + + + + + 0 + + + + 0 + + + 0 - + + + + 0 + + - + + - - - - + + + + + + + + + + + + + + 0 + + + + - - - + + + + + + + + + + + - - - - + + - - - - + + + - - - - + + + + + + + - https://github.com/WordPress/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. - + - - + + + + + + + + + + + + + + + + - + + - + - + @@ -499,11 +897,16 @@ - - - - - + + + + + 0 + + + + 0 + + + + + + + + diff --git a/WordPress-Docs/ruleset.xml b/WordPress-Docs/ruleset.xml index e16001ed73..b351118584 100644 --- a/WordPress-Docs/ruleset.xml +++ b/WordPress-Docs/ruleset.xml @@ -5,7 +5,7 @@ diff --git a/WordPress-Extra/ruleset.xml b/WordPress-Extra/ruleset.xml index 4a2f95997a..6d32e45c65 100644 --- a/WordPress-Extra/ruleset.xml +++ b/WordPress-Extra/ruleset.xml @@ -5,6 +5,12 @@ + + + 0 + + @@ -35,39 +41,28 @@ https://github.com/WordPress/WordPress-Coding-Standards/pull/809 --> - - - warning - - - warning + + + + + + + + + + + - - warning - - - - - - - - - - - warning - Best practice suggestion: Declare only one class/interface/trait in a file. - - - - - - - + + 5 + + + 5 + @@ -95,6 +90,9 @@ + + + @@ -111,9 +109,6 @@ https://github.com/WordPress/WordPress-Coding-Standards/issues/1146 --> - - @@ -133,17 +128,6 @@ - - - @@ -153,11 +137,11 @@ https://github.com/WordPress/WordPress-Coding-Standards/issues/1371 --> - - + @@ -169,11 +153,32 @@ - + https://github.com/WordPress/WordPress-Coding-Standards/pull/1777 --> + + + + + + + + + + + + + + + + + + + + -
-
-
- => - */ - public function getErrorList() { - return array(); - } - - /** - * Returns the lines where warnings should occur. - * - * @return array => - */ - public function getWarningList() { - return array( - 29 => 1, - 30 => 1, - 31 => 1, - 32 => 1, - 33 => 1, - 34 => 1, - 35 => 1, - 36 => 1, - 37 => 1, - 38 => 1, - 39 => 1, - 40 => 1, - 41 => 1, - 42 => 1, - 43 => 1, - 44 => 2, - 46 => 1, - 47 => 1, - 50 => 1, - 51 => 1, - 52 => 1, - 53 => 1, - 54 => 1, - 55 => 1, - 56 => 1, - 58 => 1, - 60 => 1, - 63 => 2, - 67 => 1, - 68 => 2, - 71 => 1, - 73 => 1, - 75 => 1, - 79 => 1, - 80 => 1, - 81 => 1, - 83 => 1, - 84 => 1, - 86 => 1, - 87 => 1, - 88 => 1, - 91 => 1, - 95 => 1, - 106 => 1, - 107 => 1, - 108 => 2, - 109 => 1, - 110 => 1, - 111 => 2, - 112 => 3, - 141 => 1, - 142 => 1, - 149 => 1, - 150 => 1, - ); - } - -} diff --git a/WordPress/Tests/CodeAnalysis/AssignmentInTernaryConditionUnitTest.inc b/WordPress/Tests/CodeAnalysis/AssignmentInTernaryConditionUnitTest.inc new file mode 100644 index 0000000000..158068e7f1 --- /dev/null +++ b/WordPress/Tests/CodeAnalysis/AssignmentInTernaryConditionUnitTest.inc @@ -0,0 +1,61 @@ +prop} ); // Bad x 2. +$mode = ( $a = 'on' ? 'on' : 'off' ); +$mode = ( ${$a->prop} = 'on' ?: 'off' ); +$mode = ( $a = 'on' ? 'true' : ( $a = 'off' ? 't' : 'f' ) ); // Bad x 2. +$mode = ( $a['test'] = 'on' ? 'true' : $a['test'] = 'off' ? 't' : 'f' ); // Bad x 3. The first ? triggers 1, the second (correctly) 2. + +// Currently not checked. +$mode = $a = 'on' ? 'on' : 'off'; +$mode = $a = 'on' ?: 'off'; +$mode = $a = 'on' ? 'true' : $a = 'off' ? 't' : 'f'; + +// Issue #1227. +( function () { + $foo = 42; + return 1 === 2 ? 'a' : 'b'; +} ); + +call_user_func( function () { + $foo = 42; + return 1 === 2 ? 'a' : 'b'; +} ); + +$content = preg_replace_callback( + '/(?s)" /> - - -/* - * Test empty statement: no code between PHP open and close tag. - */ - - - - - - - - - - - - - - - - - - - - -/* - * Test empty statement: no code between PHP open and close tag. - */ - - - - - - - - - - - - - - - -" /> - diff --git a/WordPress/Tests/CodeAnalysis/EmptyStatementUnitTest.php b/WordPress/Tests/CodeAnalysis/EmptyStatementUnitTest.php deleted file mode 100644 index 7c0d72be94..0000000000 --- a/WordPress/Tests/CodeAnalysis/EmptyStatementUnitTest.php +++ /dev/null @@ -1,67 +0,0 @@ - => - */ - 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 'EmptyStatementUnitTest.1.inc': - return array( - 9 => 1, - 12 => 1, - 15 => 1, - 18 => 1, - 21 => 1, - 22 => 1, - 31 => 1, - 33 => 1, - 43 => 1, - 45 => 1, - ); - - case 'EmptyStatementUnitTest.2.inc': - return array( - 1 => 1, // Internal.NoCode warning when short open tags is off, otherwise EmptyStatement warning. - 2 => 1, - ); - - default: - return array(); - } - } - -} diff --git a/WordPress/Tests/CodeAnalysis/EscapedNotTranslatedUnitTest.inc b/WordPress/Tests/CodeAnalysis/EscapedNotTranslatedUnitTest.inc index b548525086..c451ccc91b 100644 --- a/WordPress/Tests/CodeAnalysis/EscapedNotTranslatedUnitTest.inc +++ b/WordPress/Tests/CodeAnalysis/EscapedNotTranslatedUnitTest.inc @@ -5,4 +5,7 @@ esc_html( $var ); esc_html( 'text', 'domain' ); // Warning. esc_html( $foo, $bar ); // Warning. -esc_attr( 'text', MY_DOMAIN ); // Warning. +esc_attr( + 'text', // Some comment. + MY_DOMAIN // More comment. +); // Warning. diff --git a/WordPress/Tests/CodeAnalysis/EscapedNotTranslatedUnitTest.php b/WordPress/Tests/CodeAnalysis/EscapedNotTranslatedUnitTest.php index a8b0707234..4218e90dc9 100644 --- a/WordPress/Tests/CodeAnalysis/EscapedNotTranslatedUnitTest.php +++ b/WordPress/Tests/CodeAnalysis/EscapedNotTranslatedUnitTest.php @@ -14,16 +14,16 @@ /** * Unit test class for the EscapedNotTranslated sniff. * - * @package WPCS\WordPressCodingStandards + * @since 2.2.0 * - * @since 2.2.0 + * @covers \WordPressCS\WordPress\Sniffs\CodeAnalysis\EscapedNotTranslatedSniff */ -class EscapedNotTranslatedUnitTest extends AbstractSniffUnitTest { +final class EscapedNotTranslatedUnitTest 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(); @@ -32,7 +32,7 @@ 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( @@ -41,5 +41,4 @@ public function getWarningList() { 8 => 1, ); } - } diff --git a/WordPress/Tests/DB/DirectDatabaseQueryUnitTest.inc b/WordPress/Tests/DB/DirectDatabaseQueryUnitTest.inc index 2f735dd7f5..eb2d0e7473 100644 --- a/WordPress/Tests/DB/DirectDatabaseQueryUnitTest.inc +++ b/WordPress/Tests/DB/DirectDatabaseQueryUnitTest.inc @@ -2,59 +2,46 @@ function foo() { global $wpdb; - - $listofthings = $wpdb->get_col( 'SELECT something FROM somewhere WHERE someotherthing = 1' ); // Error + Warning. - - $listofthings = $wpdb->get_col( 'SELECT something FROM somewhere WHERE someotherthing = 1' ); // DB call okay ( No Warning, but Error for not caching! ). - - return $listofthings; + return $wpdb->get_col( 'SELECT something FROM somewhere WHERE someotherthing = 1' ); // Warning x 2. } function bar() { global $wpdb; if ( ! ( $listofthings = wp_cache_get( $foo ) ) ) { - $listofthings = $wpdb->get_col( 'SELECT something FROM somewhere WHERE someotherthing = 1' ); // Warning. + $listofthings = $wpdb->get_col( 'SELECT something FROM somewhere WHERE someotherthing = 1' ); // Warning direct DB call. wp_cache_set( 'foo', $listofthings ); } return $listofthings; } -function dummy() { -} + + function baz() { global $wpdb; $baz = wp_cache_get( 'baz' ); if ( false !== $baz ) { - - $wpdb->query( 'ALTER TABLE TO ADD SOME FIELDS' ); // DB call okay (but not really because ALTER TABLE!). - - $wpdb->query( $wpdb->prepare( 'CREATE TABLE ' ) ); // DB call okay (but not really because CREATE TABLE!). - - $wpdb->query( 'SELECT QUERY' ); // DB call okay. - - $baz = $wpdb->get_results( $wpdb->prepare( 'SELECT X FROM Y ' ) ); - + $wpdb->query( 'ALTER TABLE TO ADD SOME FIELDS' ); // Warning x 2. + $wpdb->query( $wpdb->prepare( 'CREATE TABLE ' ) ); // Warning x 2. + $wpdb->query( 'SELECT QUERY' ); // Warning. + $baz = $wpdb->get_results( $wpdb->prepare( 'SELECT X FROM Y ' ) ); // Warning. wp_cache_set( 'baz', $baz ); } - - } function quux() { global $wpdb; $quux = wp_cache_get( 'quux' ); if ( false !== $quux ) { - $quux = $wpdb->get_results( $wpdb->prepare( 'SELECT X FROM Y ' ) ); // Bad, no wp_cache_set, results in Error + Warning. + $quux = $wpdb->get_results( $wpdb->prepare( 'SELECT X FROM Y ' ) ); // Bad, no wp_cache_set, results in Warning x 2. } - } function barzd() { global $wpdb; - $autoload = $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $option_name ) ); // DB call ok; no-cache ok. + $autoload = $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $option_name ) ); // Warning x 2. } function taz() { @@ -70,35 +57,33 @@ function cache_delete_only() { $data = $where = array(); // These methods are allowed to be used with just wp_cache_delete(). - $wpdb->update( $wpdb->users, $data, $where ); // DB call ok; OK. - $wpdb->replace( $wpdb->users, $data, $where ); // DB call ok; OK. - $wpdb->delete( $wpdb->users, $data, $where ); // DB call ok; OK. - $wpdb->query( 'SELECT X FROM Y' ); // DB call ok; OK. + $wpdb->update( $wpdb->users, $data, $where ); // Warning direct DB call. + $wpdb->replace( $wpdb->users, $data, $where ); // Warning direct DB call. + $wpdb->delete( $wpdb->users, $data, $where ); // Warning direct DB call. + $wpdb->query( 'SELECT X FROM Y' ); // Warning direct DB call. - $wpdb->get_results( 'SELECT X FROM Y' ); // DB call ok; Bad. - $wpdb->get_row( 'SELECT X FROM Y' ); // DB call ok; Bad. - $wpdb->get_col( 'SELECT X FROM Y' ); // DB call ok; Bad. + $wpdb->get_results( 'SELECT X FROM Y' ); // Warning x 2. + $wpdb->get_row( 'SELECT X FROM Y' ); // Warning x 2. + $wpdb->get_col( 'SELECT X FROM Y' ); // Warning x 2. wp_cache_delete( 'key', 'group' ); } -// It is OK to use the wp_cache_add() function in place of wp_cache_set(). +// It is OK to use the wp_cache_add() function instead of wp_cache_set(). function cache_add_instead_of_set() { global $wpdb; $baz = wp_cache_get( 'baz' ); - if ( false !== $baz ) { - $data = $where = array(); - $wpdb->update( $wpdb->users, $data, $where ); // DB call ok; OK. - $wpdb->replace( $wpdb->users, $data, $where ); // DB call ok; OK. - $wpdb->delete( $wpdb->users, $data, $where ); // DB call ok; OK. - $wpdb->query( 'SELECT X FROM Y' ); // DB call ok; OK. - $wpdb->get_row( 'SELECT X FROM Y' ); // DB call ok; OK. - $wpdb->get_col( 'SELECT X FROM Y' ); // DB call ok; OK. - $baz = $wpdb->get_results( $wpdb->prepare( 'SELECT X FROM Y ' ) ); // DB call ok; OK. + $wpdb->update( $wpdb->users, $data, $where ); // Warning direct DB call. + $wpdb->replace( $wpdb->users, $data, $where ); // Warning direct DB call. + $wpdb->delete( $wpdb->users, $data, $where ); // Warning direct DB call. + $wpdb->query( 'SELECT X FROM Y' ); // Warning direct DB call. + $wpdb->get_row( 'SELECT X FROM Y' ); // Warning direct DB call. + $wpdb->get_col( 'SELECT X FROM Y' ); // Warning direct DB call. + $baz = $wpdb->get_results( $wpdb->prepare( 'SELECT X FROM Y ' ) ); // Warning direct DB call. wp_cache_add( 'baz', $baz ); } @@ -124,28 +109,25 @@ $b = function () { // phpcs:set WordPress.DB.DirectDatabaseQuery customCacheDeleteFunctions[] my_cachedel function cache_customA() { global $wpdb; - $quux = my_cacheget( 'quux' ); if ( false !== $quux ) { - $wpdb->get_results( 'SELECT X FROM Y' ); // DB call ok; OK. + $wpdb->get_results( 'SELECT X FROM Y' ); // Warning direct DB call. my_cacheset( 'key', 'group' ); } } function cache_customB() { global $wpdb; - $quux = my_cacheget( 'quux' ); if ( false !== $quux ) { - $wpdb->get_results( 'SELECT X FROM Y' ); // DB call ok; OK. + $wpdb->get_results( 'SELECT X FROM Y' ); // Warning direct DB call. my_other_cacheset( 'key', 'group' ); } } function cache_customC() { global $wpdb; - - $wpdb->query( 'SELECT X FROM Y' ); // DB call ok; OK. + $wpdb->query( 'SELECT X FROM Y' ); // Warning direct DB call. my_cachedel( 'key', 'group' ); } @@ -154,28 +136,25 @@ function cache_customC() { function cache_customD() { global $wpdb; - $quux = my_cacheget( 'quux' ); if ( false !== $quux ) { - $wpdb->get_results( 'SELECT X FROM Y' ); // DB call ok; OK. + $wpdb->get_results( 'SELECT X FROM Y' ); // Warning direct DB call. my_cacheset( 'key', 'group' ); } } function cache_customE() { global $wpdb; - $quux = my_cacheget( 'quux' ); if ( false !== $quux ) { - $wpdb->get_results( 'SELECT X FROM Y' ); // DB call ok; Bad. + $wpdb->get_results( 'SELECT X FROM Y' ); // Warning x 2. my_other_cacheset( 'key', 'group' ); } } function cache_customF() { global $wpdb; - - $wpdb->query( 'SELECT X FROM Y' ); // DB call ok; Bad. + $wpdb->query( 'SELECT X FROM Y' ); // Warning x 2. my_cachedel( 'key', 'group' ); } @@ -184,62 +163,61 @@ function cache_customF() { function cache_customG() { global $wpdb; - $quux = my_cacheget( 'quux' ); if ( false !== $quux ) { - $quux = $wpdb->get_results( 'SELECT X FROM Y' ); // DB call ok; Bad. + $quux = $wpdb->get_results( 'SELECT X FROM Y' ); // Warning x 2. my_cacheset( 'key', 'group' ); } } function custom_modify_attachment() { global $wpdb; - $wpdb->update( $wpdb->posts, array( 'post_title' => 'Hello' ), array( 'ID' => 1 ) ); // DB call ok; OK. + $wpdb->update( $wpdb->posts, array( 'post_title' => 'Hello' ), array( 'ID' => 1 ) ); // Warning direct DB call. clean_attachment_cache( 1 ); } function custom_modify_post() { global $wpdb; - $wpdb->update( $wpdb->posts, array( 'post_title' => 'Hello' ), array( 'ID' => 1 ) ); // DB call ok; OK. + $wpdb->update( $wpdb->posts, array( 'post_title' => 'Hello' ), array( 'ID' => 1 ) ); // Warning direct DB call. clean_post_cache( 1 ); } function custom_modify_term() { global $wpdb; - $wpdb->update( $wpdb->terms, array( 'slug' => 'test' ), array( 'term_id' => 1 ) ); // DB call ok; OK. + $wpdb->update( $wpdb->terms, array( 'slug' => 'test' ), array( 'term_id' => 1 ) ); // Warning direct DB call. clean_term_cache( 1 ); } function custom_clean_category_cache() { global $wpdb; - $wpdb->update( $wpdb->terms, array( 'slug' => 'test' ), array( 'term_id' => 1 ) ); // DB call ok; OK. + $wpdb->update( $wpdb->terms, array( 'slug' => 'test' ), array( 'term_id' => 1 ) ); // Warning direct DB call. clean_category_cache( 1 ); } function custom_modify_links() { global $wpdb; - $wpdb->update( $wpdb->links, array( 'link_name' => 'Test' ), array( 'link_id' => 1 ) ); // DB call ok; OK. + $wpdb->update( $wpdb->links, array( 'link_name' => 'Test' ), array( 'link_id' => 1 ) ); // Warning direct DB call. clean_bookmark_cache( 1 ); } function custom_modify_comments() { global $wpdb; - $wpdb->update( $wpdb->comments, array( 'comment_content' => 'Test' ), array( 'comment_ID' => 1 ) ); // DB call ok; OK. + $wpdb->update( $wpdb->comments, array( 'comment_content' => 'Test' ), array( 'comment_ID' => 1 ) ); // Warning direct DB call. clean_comment_cache( 1 ); } function custom_modify_users() { global $wpdb; - $wpdb->update( $wpdb->users, array( 'user_email' => 'Test' ), array( 'ID' => 1 ) ); // DB call ok; OK. + $wpdb->update( $wpdb->users, array( 'user_email' => 'Test' ), array( 'ID' => 1 ) ); // Warning direct DB call. clean_user_cache( 1 ); } function custom_modify_blogs() { global $wpdb; - $wpdb->update( $wpdb->blogs, array( 'domain' => 'example.com' ), array( 'blog_id' => 1 ) ); // DB call ok; OK. + $wpdb->update( $wpdb->blogs, array( 'domain' => 'example.com' ), array( 'blog_id' => 1 ) ); // Warning direct DB call. clean_blog_cache( 1 ); } function custom_modify_sites() { global $wpdb; - $wpdb->update( $wpdb->sites, array( 'domain' => 'example.com' ), array( 'id' => 1 ) ); // DB call ok; OK. + $wpdb->update( $wpdb->sites, array( 'domain' => 'example.com' ), array( 'id' => 1 ) ); // Warning direct DB call. clean_network_cache( 1 ); } function custom_modify_term_relationship() { global $wpdb; - $wpdb->update( $wpdb->term_relationships, array( 'term_order' => 1 ), array( 'object_id' => 1 ) ); // DB call ok; OK. + $wpdb->update( $wpdb->term_relationships, array( 'term_order' => 1 ), array( 'object_id' => 1 ) ); // Warning direct DB call. clean_object_term_cache( 1 ); } @@ -252,14 +230,14 @@ function foofoo() { FROM somewhere WHERE someotherthing = 1 EOD - ); // Error + Warning. + ); // Warning x 2. $listofthings = $wpdb->get_col( <<query( <<<'EOD' ALTER TABLE TO ADD SOME FIELDS EOD - ); // DB call okay (but not really because ALTER TABLE!). + ); // Warning on line 273 + 274. wp_cache_set( 'baz', $baz ); } } @@ -282,20 +260,79 @@ function cache_add_instead_of_setter() { global $wpdb; $baz = wp_cache_get( 'baz' ); - if ( false !== $baz ) { - $data = $where = array(); - $wpdb->query( <<get_row( <<<'EOD' SELECT X FROM Y EOD - ); // DB call ok; OK. + );// Warning direct DB call. wp_cache_add( 'baz', $baz ); } -} \ No newline at end of file +} + +// Non-cachable call should bow out after `DirectQuery` warning. +function non_cachable() { + global $wpdb; + $wpdb->insert( 'table', array( 'column' => 'foo', 'field' => 'bar' ) ); // Warning direct DB call. +} + +// Safeguard recognition of PHP 8.0+ nullsafe object operator. +function nullsafe_object_operator() { + global $wpdb; + $listofthings = $wpdb?->get_col( 'SELECT something FROM somewhere WHERE someotherthing = 1' ); // Warning x 2. + $last = $wpdb?->insert( $wpdb->users, $data, $where ); // Warning x 1. +} + +function stabilize_token_walking($other_db) { + global $wpdb; + // Overwritting $wpdb is bad, of course, but that's not the concern of this sniff. + // This test is making sure that the `$other_db->query` code is not flagged as if it were a call to a `$wpdb` method. + $wpdb = $other_db->query; +} + +function ignore_comments() { + global $wpdb; + $wpdb-> // Let's pretend this is a method-chain (this is the comment which should not confuse the sniff). + insert( 'table', array( 'column' => 'foo', 'field' => 'bar' ) ); // Warning direct DB call. +} + +function correctly_determine_end_of_statement() { + global $wpdb; + $wpdb->get_col( $query, $col ) ?><-- Warning x 2 --> + query( + $wpdb->prepare( + 'TRUNCATE TABLE `%1$s`', + plugin_get_table_name( 'Name' ) + ) + ); +} + +function stay_silent_for_truncate_query_lowercase_sql_keywords() { + global $wpdb; + $wpdb->query( + $wpdb->prepare( + 'truncate table `%1$s`', + plugin_get_table_name( 'Name' ) + ) + ); +} + +function method_names_are_caseinsensitive() { + global $wpdb; + $autoload = $wpdb->Get_Var( $wpdb->Prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $option_name ) ); // Warning x 2. +} + +// Live coding/parse error test. +// This must be the last test in the file. +$wpdb->get_col( ' diff --git a/WordPress/Tests/DB/DirectDatabaseQueryUnitTest.php b/WordPress/Tests/DB/DirectDatabaseQueryUnitTest.php index cf15554f92..e12828a1aa 100644 --- a/WordPress/Tests/DB/DirectDatabaseQueryUnitTest.php +++ b/WordPress/Tests/DB/DirectDatabaseQueryUnitTest.php @@ -14,18 +14,19 @@ /** * Unit test class for the DirectDatabaseQuery 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 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. + * @covers \WordPressCS\WordPress\Helpers\RulesetPropertyHelper + * @covers \WordPressCS\WordPress\Sniffs\DB\DirectDatabaseQuerySniff */ -class DirectDatabaseQueryUnitTest extends AbstractSniffUnitTest { +final class DirectDatabaseQueryUnitTest 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(); @@ -34,28 +35,62 @@ 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( - 6 => 2, - 8 => 1, - 17 => 1, - 32 => 1, - 34 => 1, - 38 => 1, - 50 => 2, - 78 => 1, - 79 => 1, + 5 => 2, + 12 => 1, + 26 => 2, + 27 => 2, + 28 => 1, + 29 => 1, + 38 => 2, + 44 => 2, + 60 => 1, + 61 => 1, + 62 => 1, + 63 => 1, + 65 => 2, + 66 => 2, + 67 => 2, 80 => 1, - 112 => 1, - 170 => 1, - 178 => 1, + 81 => 1, + 82 => 1, + 83 => 1, + 84 => 1, + 85 => 1, + 86 => 1, + 97 => 1, + 114 => 1, + 123 => 1, + 130 => 1, + 141 => 1, + 150 => 2, + 157 => 2, + 168 => 2, + 175 => 1, + 180 => 1, + 185 => 1, 190 => 1, - 250 => 2, - 257 => 1, - 274 => 1, + 195 => 1, + 200 => 1, + 205 => 1, + 210 => 1, + 215 => 1, + 220 => 1, + 228 => 2, + 235 => 2, + 251 => 1, + 252 => 1, + 265 => 1, + 269 => 1, + 281 => 1, + 287 => 2, + 288 => 1, + 300 => 1, + 306 => 2, + 333 => 2, ); } - } diff --git a/WordPress/Tests/DB/PreparedSQLPlaceholdersUnitTest.inc b/WordPress/Tests/DB/PreparedSQLPlaceholdersUnitTest.inc index d703013123..bbceb7ce5b 100644 --- a/WordPress/Tests/DB/PreparedSQLPlaceholdersUnitTest.inc +++ b/WordPress/Tests/DB/PreparedSQLPlaceholdersUnitTest.inc @@ -2,7 +2,7 @@ $sql = $wpdb->prepare( $sql, $replacements ); // OK - no query available to examine - this will be handled by the PreparedSQL sniff. $sql = $wpdb->prepare( "SELECT * FROM $wpdb->users WHERE id = %d AND user_login = %s", 1, "admin" ); // OK. -$sql = $wpdb->prepare( "SELECT * FROM $wpdb->users WHERE id = %d AND user_login = %s", array( 1, "admin" ) ); // OK. +$sql = $wpdb?->prepare( "SELECT * FROM $wpdb->users WHERE id = %d AND user_login = %s", array( 1, "admin" ) ); // OK. $sql = $wpdb->prepare( 'SELECT * FROM `table` WHERE `column` = %s AND `field` = %d', 'foo', 1337 ); // OK. $sql = $wpdb->prepare( 'SELECT DATE_FORMAT(`field`, "%%c") FROM `table` WHERE `column` = %s', 'foo' ); // OK. @@ -13,7 +13,7 @@ $sql = $wpdb->prepare( 'SELECT * FROM `table`' ); // Warning. $sql = $wpdb->prepare( 'SELECT * FROM `table` WHERE id = ' . $id ); // OK - this will be handled by the PreparedSQL sniff. $sql = $wpdb->prepare( "SELECT * FROM `table` WHERE id = $id" ); // OK - this will be handled by the PreparedSQL sniff. $sql = $wpdb->prepare( "SELECT * FROM `table` WHERE id = {$id['some%sing']}" ); // OK - this will be handled by the PreparedSQL sniff. -$sql = $wpdb->prepare( 'SELECT * FROM ' . $wpdb->users ); // Warning. +$sql = $wpdb?->prepare( 'SELECT * FROM ' . $wpdb->users ); // Warning. $sql = $wpdb->prepare( "SELECT * FROM `{$wpdb->users}`" ); // Warning. $sql = $wpdb->prepare( "SELECT * FROM `{$wpdb->users}` WHERE id = $id" ); // OK - this will be handled by the PreparedSQL sniff. @@ -27,11 +27,11 @@ $sql = $wpdb->prepare( 'SELECT * FROM `table`', $something ); // Warning. */ $sql = $wpdb->prepare( '%d %1$e %%% % %A %h', 1 ); // Bad x 5. $sql = $wpdb->prepare( '%%%s', 1 ); // OK. -$sql = $wpdb->prepare( "SELECT * FROM $wpdb->users WHERE id = %1\$d AND user_login = %2\$s", 1, "admin" ); // OK. 2 x warning for unquoted complex placeholders. -$sql = $wpdb->prepare( "SELECT * FROM $wpdb->users WHERE id = %01.2f AND user_login = %10.10X", 1, "admin" ); // Bad x 1 + 1 warning unquoted complex placeholders + 1 warning nr of replacements. +$sql = $wpdb->prepare( "SELECT * FROM $wpdb?->users WHERE id = %1\$d AND user_login = %2\$s", 1, "admin" ); // OK. 2 x warning for unquoted complex placeholders. +$sql = $wpdb->prepare( "SELECT * FROM $wpdb?->users WHERE id = %01.2f AND user_login = %10.10X", 1, "admin" ); // Bad x 1 + 1 warning unquoted complex placeholders + 1 warning nr of replacements. $sql = $wpdb->prepare( "SELECT * FROM $wpdb->users WHERE id = %'.09F AND user_login = %1\$04x", 1, "admin" ); // Bad x 1 + 1 warning unquoted complex placeholders + 1 warning nr of replacements. $sql = $wpdb->prepare( "SELECT * FROM $wpdb->users WHERE id = \"%1\$c\" AND user_login = '%2\$e'", 1, "admin" ); // Bad x 2 + 1 warning. -$sql = $wpdb->prepare( 'SELECT * FROM ' . $wpdb->users . ' WHERE id = \'%1\$b\' AND user_login = "%2\$o"', 1, "admin" ); // Bad x 2 + 1 warning. +$sql = $wpdb->prepare( 'SELECT * FROM ' . $wpdb?->users . ' WHERE id = \'%1\$b\' AND user_login = "%2\$o"', 1, "admin" ); // Bad x 2 + 1 warning. /* * Test passing quoted simple replacement placeholder and unquoted complex placeholder. @@ -59,13 +59,13 @@ $sql = $wpdb->prepare( "SELECT * FROM $wpdb->users WHERE id = %d AND user_login $replacements = [1, "admin", $variable]; $sql = $wpdb->prepare( "SELECT * FROM $wpdb->users WHERE id = %d AND user_login = %s", $replacements ); // Bad. -$sql = $wpdb->prepare( "SELECT * FROM $wpdb->users WHERE id = %d AND user_login = %s", $replacements ); // WPCS: PreparedSQLPlaceholders replacement count OK. +$sql = $wpdb->prepare( "SELECT * FROM $wpdb->users WHERE id = %d AND user_login = %s", $replacements ); // Bad - old-style ignore comment. WPCS: PreparedSQLPlaceholders replacement count OK. // Valid test case as found in WP core /wp-admin/includes/export.php $esses = array_fill( 0, count($post_types), '%s' ); $where = $wpdb->prepare( "{$wpdb->posts}.post_type IN (" . implode( ',', $esses ) . ')', $post_types ); // Warning. -// Testing that whitelist comment work for this mismatch too. -$where = $wpdb->prepare( "{$wpdb->posts}.post_type IN (" . implode( ',', $esses ) . ')', $post_types ); // WPCS: PreparedSQLPlaceholders replacement count OK. +// Testing that ignore comment works for this mismatch too. +$where = $wpdb->prepare( "{$wpdb->posts}.post_type IN (" . implode( ',', $esses ) . ')', $post_types ); // Bad - old-style ignore comment. WPCS: PreparedSQLPlaceholders replacement count OK. /* * Test correctly recognizing queries using IN in combination with dynamic placeholder creation. @@ -83,10 +83,10 @@ $where = $wpdb->prepare( sprintf( "{$wpdb->posts}.post_type IN (%s) AND {$wpdb->posts}.post_status IN (%s)", - implode( ',', array_fill( 0, count($post_types), '%s' ) ), - IMPLODE( ',', Array_Fill( 0, count($post_statusses), '%s' ) ) + implode( ',', array_fill( 0, count($post_types), '%s' ), ), + IMPLODE( ',', Array_Fill( 0, count($post_statusses), '%s' ), ), ), - array_merge( $post_types, $post_statusses ) + array_merge( $post_types, $post_statusses, ), ); // OK. $where = $wpdb->prepare( @@ -114,7 +114,7 @@ $results = $wpdb->get_results( $wpdb->prepare( ' SELECT ID FROM ' . $wpdb->posts . ' - WHERE ID NOT IN( SELECT post_id FROM ' . $wpdb->postmeta . ' WHERE meta_key = %s AND meta_value = %s ) + WHERE ID NOT IN( SELECT post_id FROM ' . $wpdb?->postmeta . ' WHERE meta_key = %s AND meta_value = %s ) AND post_status in( "future", "draft", "pending", "private", "publish" ) AND post_type in( ' . implode( ',', array_fill( 0, count( $post_types ), '%s' ) ) . ' ) LIMIT %d', @@ -207,3 +207,314 @@ $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->wp_options WHERE option_name L $where .= $wpdb->prepare( " AND `name` LIKE '%%%%" . '%s' . "%%%%' ", $args['name'] ); // Bad x 2. $wpdb->query($wpdb->prepare("delete from wp_postmeta where post_id = $target_postId AND meta_key like 'google_snippets'")); // Bad, the PreparedSQL sniff will also kick in and throw an error about `$target_postId`. + +// phpcs:set WordPress.DB.PreparedSQLPlaceholders minimum_wp_version 6.2 + +$wpdb->prepare( 'WHERE %i = %s', $field, $value ); // OK. +$wpdb->prepare( 'WHERE %i = %s', $value ); // ReplacementsWrongNumber. +$wpdb->prepare( 'WHERE %i = %x', $field, $value ); // UnsupportedPlaceholder & ReplacementsWrongNumber. +$wpdb->prepare( 'WHERE %i = %2$s', $field, $value ); // UnquotedComplexPlaceholder. +$wpdb->prepare( 'WHERE %i = %10s', $field, $value ); // UnquotedComplexPlaceholder. +$wpdb->prepare( 'WHERE %i = %2$-10s', $field, $value ); // UnquotedComplexPlaceholder. + +$wpdb->prepare( 'WHERE %i = "%s"', $field, $value ); // QuotedSimplePlaceholder. +$wpdb->prepare( 'WHERE "%i" = %s', $field, $value ); // QuotedIdentifierPlaceholder. +$wpdb->prepare( "WHERE \"%i\" = %s", $field, $value ); // QuotedIdentifierPlaceholder. +$wpdb->prepare( "WHERE '%i' = %s", $field, $value ); // QuotedIdentifierPlaceholder. +$wpdb->prepare( 'WHERE \'%i\' = %s', $field, $value ); // QuotedIdentifierPlaceholder. +$wpdb->prepare( 'WHERE `%i` = %s', $field, $value ); // QuotedIdentifierPlaceholder. + +$wpdb->prepare( "WHERE '%10i' IS NULL", $field ); // QuotedIdentifierPlaceholder. +$wpdb->prepare( 'WHERE "%10i" IS NULL', $field ); // QuotedIdentifierPlaceholder. +$wpdb->prepare( 'WHERE \'%1$i\' IS NULL', $field ); // QuotedIdentifierPlaceholder. +$wpdb->prepare( "WHERE \"%10i\" IS NULL", $field ); // QuotedIdentifierPlaceholder. + +$wpdb->prepare( 'WHERE %1$i IS NULL', $field ); // OK. +$wpdb->prepare( 'WHERE %10i IS NULL', $field ); // OK. +$wpdb->prepare( 'WHERE % i IS NULL', $field ); // UnescapedLiteral & UnfinishedPrepare (while this is valid, should avoid). +$wpdb->prepare( 'WHERE %1$-10i IS NULL', $field ); // OK. +$wpdb->prepare( 'WHERE %1$-10.3i IS NULL', $field ); // OK. +$wpdb->prepare( 'WHERE %1$+10.3i IS NULL', $field ); // OK. +$wpdb->prepare( 'WHERE %1$ 10.3i IS NULL', $field ); // UnsupportedPlaceholder (parsed as "%1$", which is valid, but should avoid). +$wpdb->prepare( 'WHERE %1$010.3i IS NULL', $field ); // OK. +$wpdb->prepare( "WHERE %1$'x10.3i IS NULL", $field ); // OK. + +$wpdb->prepare( 'WHERE %.2i IS NULL', 'a``b' ); // Currently ignore, but it might cause a problem (most likely a parse error) "WHERE `a`` IS NULL". + +$wpdb->prepare( 'WHERE `%1$i` IS NULL', $field ); // QuotedIdentifierPlaceholder. +$wpdb->prepare( 'WHERE `%10i` IS NULL', $field ); // QuotedIdentifierPlaceholder. +$wpdb->prepare( 'WHERE `% i` IS NULL', $field ); // UnescapedLiteral & UnfinishedPrepare (if RegEx matched, then it should be QuotedIdentifierPlaceholder). +$wpdb->prepare( 'WHERE `%1$-10i` IS NULL', $field ); // QuotedIdentifierPlaceholder. +$wpdb->prepare( 'WHERE `%1$-10.3i` IS NULL', $field ); // QuotedIdentifierPlaceholder. +$wpdb->prepare( 'WHERE `%1$+10.3i` IS NULL', $field ); // QuotedIdentifierPlaceholder. +$wpdb->prepare( 'WHERE `%1$ 10.3i` IS NULL', $field ); // QuotedIdentifierPlaceholder, and UnsupportedPlaceholder (parsed as "%1$", which is valid, but should avoid). +$wpdb->prepare( 'WHERE `%1$010.3i` IS NULL', $field ); // QuotedIdentifierPlaceholder. +$wpdb->prepare( "WHERE `%1$'x10.3i` IS NULL", $field ); // QuotedIdentifierPlaceholder. + +$wpdb->prepare( 'SELECT ID FROM `%2$i` WHERE `%1$i` = "%3$s"', $field, $wpdb->posts, $value ); // QuotedIdentifierPlaceholder (x2). + +$wpdb->prepare( 'SELECT ID FROM %i.%i WHERE %i = "false"', $db, $table, $field ); // OK. + +$wpdb->prepare( + sprintf( + 'xxx IN (%s)', + implode( ',', array_fill( 0, count($fields), '%i' ) ) + ), + $fields +); // ReplacementsWrongNumber + IdentifierWithinIN. + +$wpdb->prepare( 'xxx IN ( ' . implode( ',', array_fill( 0, count( $post_types ), + '%i' ) ) . ' )', + $fields +); // IdentifierWithinIN. + +$wpdb->prepare( ' + xxx IN ( ' . implode( ',', array_fill( 0, count( $post_types ), "%i" ) ) . ' )', + $fields +); // IdentifierWithinIN. + +// phpcs:set WordPress.DB.PreparedSQLPlaceholders minimum_wp_version 5.8 + +$wpdb->prepare( 'WHERE %1$+10.3i = %s', $field, $value ); // UnsupportedIdentifierPlaceholder. +$wpdb->prepare( "WHERE '%10i' IS NULL", $field ); // UnsupportedIdentifierPlaceholder + QuotedIdentifierPlaceholder. +$wpdb->prepare( 'xxx IN ( ' . implode( ',', array_fill( 0, count( $post_types ), '%i' ) ) . ' )', $fields ); // UnsupportedIdentifierPlaceholder + IdentifierWithinIN. + +// phpcs:set WordPress.DB.PreparedSQLPlaceholders minimum_wp_version + +$wpdb->prepare(); // Ignore. + +$where = $wpdb->prepare( + "{$wpdb->posts}.post_type IN (\"" + . implode() + . "\") AND {$wpdb->posts}.post_id = %s", + $post_id +); // Bad, dynamic placeholder generation quotes with invalid implode call (no params). + +$where = $wpdb->prepare( + "{$wpdb->posts}.post_status IN ('" + . implode( ',' ) + . '\') AND {$wpdb->posts}.post_id = %s', + $post_id +); // Bad, dynamic placeholder generation quotes with invalid implode call (missing second param). + +$where = $wpdb->prepare( + "{$wpdb->posts}.post_status IN ('" + . implode( '|', array_fill( 0, count($post_stati), '%s' ) ) + . '\') AND {$wpdb->posts}.post_id = %s', + $post_id +); // Bad x2, dynamic placeholder generation quotes with invalid implode call (separator parameter does not contain expected ',' value). + +$where = $wpdb->prepare( + "{$wpdb->posts}.post_type IN (\"" + . implode( ',', $array_fill ) + . "\") AND {$wpdb->posts}.post_id = %s", + $post_id +); // Bad, dynamic placeholder generation quotes with invalid implode call (array param is not call to array_fill). + +$where = $wpdb->prepare( + "{$wpdb->posts}.post_type IN (\"" + . implode( ',', array_fill() ) + . "\") AND {$wpdb->posts}.post_id = %s", + $post_id +); // Bad, dynamic placeholder generation quotes with invalid implode call (array param is call to array_fill() without params). + +$where = $wpdb->prepare( + "{$wpdb->posts}.post_type IN (\"" + . implode( ',', array_fill( 0, count($post_types) ) ) + . "\") AND {$wpdb->posts}.post_id = %s", + $post_id +); // Bad, dynamic placeholder generation quotes with invalid implode call (array param is call to array_fill(), but missing third param). + +// Safeguard that short array as $args is handled correctly. +$wpdb->get_col( + $wpdb->prepare( + "SELECT term_taxonomy_id FROM {$wpdb->term_taxonomy} WHERE taxonomy = %s LIMIT %d", + [ 'taxonomy_name', $limit ] + ) // Ok. +); + +// Disregard comments in the implode() $separator param. +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',' /*comment*/, array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); // OK. + +// Disregard comments in the array_fill() $value param. +$wpdb->prepare( ' + xxx IN ( ' . implode( ',', array_fill( 0, count( $post_types ), '%i' /*comment*/ ) ) . ' )', + $fields +); // IdentifierWithinIN. + +$where = $wpdb->prepare( + "{$wpdb->posts}.post_type IN (" + . implode( ',', array_fill( 0, count($post_types), '%C' ) ) + . ')', + array_merge( $post_types ) +); // Bad x 2, UnsupportedPlaceholder + UnfinishedPrepare. + +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', array_fill( 0, count($post_types), '%C' ) ) + ), + $post_types +); // Bad, ReplacementsWrongNumber due to unrecognized placeholder in array_fill(). + +$where = $wpdb->prepare( + "{$wpdb->posts}.post_type IN (" + . implode( ',', array_fill( 0, count($post_types), /*comment*/ '%s' ) ) + . ')', + array_merge( $post_types ) +); // OK. + +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', array_fill( 0, count($post_types), "%s" /*comment*/ ) ) + ), + $post_types +); // OK. + +// Safeguard that FQN function calls to implode() and array_fill() are handled correctly. +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + \implode( ',', array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); // OK. + +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', \array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); // OK. + +$where = $wpdb->prepare( + "{$wpdb->posts}.post_type IN (" + . implode( ',', \array_fill( 0, count($post_types), '%s' ) ) + . ") AND {$wpdb->posts}.post_status IN (" + . implode( ',', \array_fill( 0, count($post_statusses), '%s' ) ) + . ')', + array_merge( $post_types, $post_statusses ) +); // OK. + +/* + * Safeguard support for PHP 8.0+ named parameters. + */ +// WPDB::prepare() with named params. Named args not supported with ...$args, but that's not the concern of this sniff. +$query = $wpdb->prepare( + args : $replacements, + query : 'SELECT ID + FROM ' . $wpdb->posts . ' + WHERE post_type = %s', +); // OK, named args not supported with ...$args, but that's not the concern of this sniff. + +$query = $wpdb->prepare( + query : 'SELECT ID + FROM ' . $wpdb->posts . ' + WHERE post_type = %s', +); // Bad, missing replacements. + +$query = $wpdb->prepare( + args : $replacements, + queri : 'SELECT ID + FROM ' . $wpdb->posts . ' + WHERE post_type = %s', +); // Ignore, incorrect name used for named param `query`, so param not recognized. + +// Implode() with named params. +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( array: array_fill( 0, count( $post_types ), '%s' ), separator: ',', ), + ), + array_merge( $post_types, $post_statusses ) +); // Okay. + +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( array: $something, separator: ',', ), + ), + array_merge( $post_types, $post_statusses ) +); // Bad - will throw incorrect nr of replacements error - `array` param is not an array_fill() function call. + +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( arrays: array_fill( 0, count( $post_types ), '%s' ), separator: ',', ), + ), + array_merge( $post_types, $post_statusses ) +); // Bad - will throw incorrect nr of replacements error - incorrect name used for named param `array`, so param not recognized. + +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( separator: ',', ), + ), + array_merge( $post_types, $post_statusses ) +); // Bad - will throw incorrect nr of replacements error - missing `array` param. + +// Array_fill() with named params. +$where = $wpdb->prepare( + "{$wpdb->posts}.post_type IN (" + . implode( ',', array_fill( start_index: 0, count: count($post_types), value: '%s' ) ) // Expected order. + . ") AND {$wpdb->posts}.post_status IN (" + . implode( ',', array_fill( value: '%s', start_index: 0, count: count($post_statusses), ) ) // Unconventional order. + . ')', + array_merge( $post_types, $post_statusses ) +); // OK. + +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', array_fill( start_index: 0, count: count($post_types) ) ), + ), + array_merge( $post_types, $post_statusses ) +); // Bad - will throw incorrect nr of replacements error - missing $value param in array_fill(). + +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', array_fill( start_index: 0, values: '%s', count: count($post_types) ) ), + ), + array_merge( $post_types, $post_statusses ) +); // Bad - will throw incorrect nr of replacements error - incorrect $values param name in array_fill(). + +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', array_fill( value: 's', start_index: 0, count: count($post_types) ) ), + ), + array_merge( $post_types, $post_statusses ) +); // Bad - will throw incorrect nr of replacements error - $value param name in array_fill() does not contain placeholder. + +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', array_fill( value: $s, start_index: 0, count: count($post_types) ) ), + ), + array_merge( $post_types, $post_statusses ) +); // Bad - will throw incorrect nr of replacements error - $value param name in array_fill() does not contain plain text placeholder. + +// Sprintf() with named params. This is invalid as variadic functions do not support named params. +// The sniff will ignore the sprintf() as it cannot be analyzed correctly. +$where = $wpdb->prepare( + sprintf( + values: implode( ',', array_fill( 0, count($post_types), '%s' ) ), + format: "{$wpdb->posts}.post_type IN ('%s')", + ), + array_merge( $post_types, $post_statusses ) +); // OK, well not really, but not something we can reliably analyze. + +/* + * Safeguard handling of $wpdb->prepare as PHP 8.1+ first class callable. + */ +$callback = $wpdb->prepare(...); // OK. + diff --git a/WordPress/Tests/DB/PreparedSQLPlaceholdersUnitTest.php b/WordPress/Tests/DB/PreparedSQLPlaceholdersUnitTest.php index 1cf42f021f..10acc9493d 100644 --- a/WordPress/Tests/DB/PreparedSQLPlaceholdersUnitTest.php +++ b/WordPress/Tests/DB/PreparedSQLPlaceholdersUnitTest.php @@ -14,16 +14,16 @@ /** * Unit test class for the PreparedSQLPlaceholders sniff. * - * @package WPCS\WordPressCodingStandards + * @since 0.14.0 * - * @since 0.14.0 + * @covers \WordPressCS\WordPress\Sniffs\DB\PreparedSQLPlaceholdersSniff */ -class PreparedSQLPlaceholdersUnitTest extends AbstractSniffUnitTest { +final class PreparedSQLPlaceholdersUnitTest 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( @@ -37,8 +37,8 @@ public function getErrorList() { 41 => 1, 54 => 1, 141 => 1, - 149 => 1, - 151 => 1, + 148 => 1, + 150 => 1, 162 => 1, 163 => 1, 164 => 1, @@ -58,13 +58,61 @@ public function getErrorList() { 205 => 1, 207 => 2, 209 => 1, + + 215 => 1, // UnsupportedPlaceholder. + + 220 => 1, // QuotedSimplePlaceholder. + 221 => 1, // QuotedSimplePlaceholder. + 222 => 1, // QuotedSimplePlaceholder. + 223 => 1, // QuotedSimplePlaceholder. + 224 => 1, // QuotedSimplePlaceholder. + 225 => 1, // QuotedSimplePlaceholder. + + 227 => 1, // QuotedIdentifierPlaceholder. + 228 => 1, // QuotedIdentifierPlaceholder. + 229 => 1, // QuotedIdentifierPlaceholder. + 230 => 1, // QuotedIdentifierPlaceholder. + + 234 => 1, // UnescapedLiteral. + 238 => 1, // UnsupportedPlaceholder. + + 244 => 1, // QuotedIdentifierPlaceholder. + 245 => 1, // QuotedIdentifierPlaceholder. + 246 => 1, // UnescapedLiteral. + 247 => 1, // QuotedIdentifierPlaceholder. + 248 => 1, // QuotedIdentifierPlaceholder. + 249 => 1, // QuotedIdentifierPlaceholder. + 250 => 2, // QuotedIdentifierPlaceholder. + 251 => 1, // QuotedIdentifierPlaceholder. + 252 => 1, // QuotedIdentifierPlaceholder. + + 254 => 2, // QuotedIdentifierPlaceholder x2. + 261 => 1, // IdentifierWithinIN. + 267 => 1, // IdentifierWithinIN. + 272 => 1, // IdentifierWithinIN. + 278 => 1, // UnsupportedIdentifierPlaceholder. + 279 => 2, // UnsupportedIdentifierPlaceholder + QuotedIdentifierPlaceholder. + 280 => 2, // UnsupportedIdentifierPlaceholder + IdentifierWithinIN. + + 287 => 1, + 294 => 1, + 301 => 1, + 308 => 1, + 315 => 1, + 322 => 1, + + 347 => 2, // UnsupportedIdentifierPlaceholder + IdentifierWithinIN. + 353 => 1, + + // Named parameter support. + 418 => 1, ); } /** * 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( @@ -85,15 +133,34 @@ public function getWarningList() { 57 => 1, 58 => 1, 61 => 1, - 62 => 1, // Whitelist comment deprecation warning. + 62 => 1, // Old-style WPCS ignore comments are no longer supported. 66 => 1, - 68 => 1, // Whitelist comment deprecation warning. + 68 => 1, // Old-style WPCS ignore comments are no longer supported. 126 => 1, 139 => 1, 160 => 2, 161 => 2, 177 => 1, + 214 => 1, + 215 => 1, + 216 => 1, + 217 => 1, + 218 => 1, + 234 => 1, // UnfinishedPrepare. + 246 => 1, // UnfinishedPrepare. + 258 => 1, // ReplacementsWrongNumber. + 300 => 1, + 354 => 1, + 358 => 1, + + // Named parameter support. + 440 => 1, + 448 => 1, + 456 => 1, + 474 => 1, + 482 => 1, + 490 => 1, + 498 => 1, ); } - } diff --git a/WordPress/Tests/DB/PreparedSQLUnitTest.inc b/WordPress/Tests/DB/PreparedSQLUnitTest.1.inc similarity index 58% rename from WordPress/Tests/DB/PreparedSQLUnitTest.inc rename to WordPress/Tests/DB/PreparedSQLUnitTest.1.inc index 52ef3a0e31..e9124c4a97 100644 --- a/WordPress/Tests/DB/PreparedSQLUnitTest.inc +++ b/WordPress/Tests/DB/PreparedSQLUnitTest.1.inc @@ -8,8 +8,8 @@ $wpdb->query( $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE post_title LIKE $wpdb->query( $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE post_title LIKE '$var';" ) ); // Bad. $wpdb->query( $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE post_title LIKE %s;", $_GET['title'] ) ); // Ok. -$wpdb->query( "SELECT * FROM $wpdb->posts WHERE post_title LIKE '" . $escaped_var . "';" ); // WPCS: unprepared SQL OK. -$wpdb->query( "SELECT * FROM $wpdb->posts WHERE post_title LIKE '{$escaped_var}';" ); // WPCS: unprepared SQL OK. +$wpdb->query( "SELECT * FROM $wpdb->posts WHERE post_title LIKE '" . $escaped_var . "';" ); // Bad: old-style ignore comment. WPCS: unprepared SQL OK. +$wpdb->query( "SELECT * FROM $wpdb->posts WHERE post_title LIKE '{$escaped_var}';" ); // Bad: old-style ignore comment. WPCS: unprepared SQL OK. $wpdb->query( $wpdb->prepare( "SELECT SUBSTRING( post_name, %d + 1 ) REGEXP '^[0-9]+$'", array( 123 ) ) ); // Ok. $wpdb->query( $wpdb->prepare( "SELECT * FROM $wpdb->posts WHERE post_title = 'The \$_GET var can be evil.' AND ID = %s", array( 123 ) ) ); // Ok. @@ -79,10 +79,10 @@ $all_post_meta = $wpdb->get_results( $wpdb->prepare( sprintf( <<<'ND' AND `post_id` IN (%s) ND , $wpdb->postmeta, - implode( ',', array_fill( 0, count( $post_ids ), '%d' ) ) + IMPLODE( ',', array_fill( 0, count( $post_ids ), '%d' ) ) ), $post_ids ) ); // OK. -wpdb::prepare( "SELECT * FROM $wpdb->posts WHERE post_title LIKE '" . foo() . "';" ); // Bad. +wpdb::prepare( "SELECT * FROM $wpdb?->posts WHERE post_title LIKE '" . foo() . "';" ); // Bad. $wpdb->query( // Some arbitrary comment. "SELECT * @@ -90,18 +90,6 @@ $wpdb->query( // Some arbitrary comment. WHERE post_title LIKE '" . $escaped_var . "';" ); // Bad x 1. -$wpdb->query( - "SELECT * - FROM $wpdb->posts - WHERE post_title LIKE '" . $escaped_var . "';" -); // WPCS: unprepared SQL OK. - -$wpdb->query( // WPCS: unprepared SQL OK. - "SELECT * - FROM $wpdb->posts - WHERE post_title LIKE '" . $escaped_var . "';" -); - $wpdb->query( "SELECT * FROM $wpdb->posts WHERE ID = " . (int) $foo . ";" ); // Ok. $wpdb->query( "SELECT * FROM $wpdb->posts WHERE value = " . (float) $foo . ";" ); // Ok. @@ -114,5 +102,46 @@ $wpdb->query( . ";" ); +// Test handling of more complex embedded variables and expressions. +$wpdb->query( "SELECT * FROM {$wpdb->bar()} WHERE post_title LIKE '{$title->sub()}';" ); // Bad x 1. +$wpdb->query( "SELECT * FROM ${wpdb->bar} WHERE post_title LIKE '${title->sub}';" ); // Bad x 1. +$wpdb->query( "SELECT * FROM ${wpdb->{$baz}} WHERE post_title LIKE '${title->{$sub}}';" ); // Bad x 1. +$wpdb->query( "SELECT * FROM ${wpdb->{${'a'}}} WHERE post_title LIKE '${title->{${'sub'}}}';" ); // Bad x 1. + +// More defensive variable checking +$wpdb->query( "SELECT * FROM $wpdb" ); // Bad x 1, $wpdb on its own is not valid. + +$wpdb + -> /*comment*/ query( "SELECT * FROM $wpdb->posts WHERE post_title LIKE '" . $_GET['title'] . "';" ); // Bad. + +$wpdb?->query( "SELECT * FROM $wpdb->posts WHERE post_title LIKE '" . (int) $foo . "';" ); // OK. +$wpdb?->query( "SELECT * FROM $wpdb->posts WHERE post_title LIKE '" . foo() . "';" ); // Bad. + +WPDB::prepare( "SELECT * FROM $wpdb->posts WHERE post_title LIKE '" . foo() . "';" ); // Bad. +$wpdb->Query( "SELECT * FROM $wpdb->posts WHERE post_title LIKE '" . foo() . "';" ); // Bad. + +$wpdb->query( "SELECT * FROM $wpdb->posts WHERE value = " . {$foo} . ";" ); // Bad - on $foo, not on the {}. +$wpdb->query( "SELECT * FROM $wpdb->posts WHERE value = " . (array) $foo . ";" ); // Bad - on $foo, not on the (array). +$wpdb->query( "SELECT * FROM $wpdb->posts WHERE value = " . -10 . ";" ); // OK. +$wpdb->query( "SELECT * FROM $wpdb->posts WHERE value = " . +1.0 . ";" ); // OK. +$wpdb->query( "SELECT * FROM $wpdb->posts WHERE value = " . 10 / 2.5 . ";" ); // OK. +$wpdb->query( "SELECT * FROM $wpdb->posts WHERE value = " . ++$foo . ";" ); // Bad - on $foo, not on the ++. + +// Safeguard handling of PHP 8.0+ nullsafe object operators found within a query. +$wpdb->query( $wpdb->prepare( 'SELECT * FROM ' . $wpdb::TABLE_NAME . " WHERE post_title LIKE '%s';", '%something' ) ); // OK. +$wpdb->query( $wpdb->prepare( 'SELECT * FROM ' . $notwpdb?->posts . " WHERE post_title LIKE '%s';", '%something' ) ); // Bad. + +// Safeguard handling of PHP 7.4+ numeric literals with undersocres and PHP 8.1 explicit octals. +$wpdb->query( "SELECT * FROM $wpdb->posts WHERE value = " . 10_000 . ";" ); // OK. +$wpdb->query( "SELECT * FROM $wpdb->posts WHERE value = " . 0o34 . ";" ); // OK. + +// Not a method call. +$wpdb = new WPDB(); +$foo = $wpdb->propertyAccess; +echo $wpdb::CONSTANT_NAME; + +// Not an identifyable method call. +$wpdb->{$methodName}('query'); + // Don't throw an error during live coding. wpdb::prepare( "SELECT * FROM $wpdb->posts diff --git a/WordPress/Tests/DB/PreparedSQLUnitTest.2.inc b/WordPress/Tests/DB/PreparedSQLUnitTest.2.inc new file mode 100644 index 0000000000..dd5e80e730 --- /dev/null +++ b/WordPress/Tests/DB/PreparedSQLUnitTest.2.inc @@ -0,0 +1,13 @@ +query( <<posts} + WHERE ID = {$foo}; + EOT +); // Bad. + +// Live coding. +// This needs to be the last test in a file. +$wpdb-> diff --git a/WordPress/Tests/DB/PreparedSQLUnitTest.php b/WordPress/Tests/DB/PreparedSQLUnitTest.php index 03f7c6d8fa..0a296ff984 100644 --- a/WordPress/Tests/DB/PreparedSQLUnitTest.php +++ b/WordPress/Tests/DB/PreparedSQLUnitTest.php @@ -14,55 +14,81 @@ /** * Unit test class for the PreparedSQL sniff. * - * @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. + * @covers \WordPressCS\WordPress\Helpers\ContextHelper::is_safe_casted + * @covers \WordPressCS\WordPress\Helpers\FormattingFunctionsHelper + * @covers \WordPressCS\WordPress\Helpers\WPDBTrait + * @covers \WordPressCS\WordPress\Sniffs\DB\PreparedSQLSniff */ -class PreparedSQLUnitTest extends AbstractSniffUnitTest { +final class PreparedSQLUnitTest extends AbstractSniffUnitTest { /** * Returns the lines where errors should occur. * - * @since 0.8.0 + * @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() { - return array( - 3 => 1, - 4 => 1, - 5 => 1, - 7 => 1, - 8 => 1, - 16 => 1, - 17 => 1, - 18 => 1, - 20 => 1, - 21 => 1, - 54 => 1, - 64 => 1, - 71 => 1, - 85 => 1, - 90 => 1, - ); + public function getErrorList( $testFile = '' ) { + switch ( $testFile ) { + case 'PreparedSQLUnitTest.1.inc': + return array( + 3 => 1, + 4 => 1, + 5 => 1, + 7 => 1, + 8 => 1, + 11 => 1, // Old-style WPCS ignore comments are no longer supported. + 12 => 1, // Old-style WPCS ignore comments are no longer supported. + 16 => 1, + 17 => 1, + 18 => 1, + 20 => 1, + 21 => 1, + 54 => 1, + 64 => 1, + 71 => 1, + 85 => 1, + 90 => 1, + 106 => 1, + 107 => 1, + 108 => 1, + 109 => 1, + 112 => 1, + 115 => 1, + 118 => 1, + 120 => 1, + 121 => 1, + 123 => 1, + 124 => 1, + 128 => 1, + 132 => 2, + ); + + case 'PreparedSQLUnitTest.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, + ); + + default: + return array(); + } } /** * Returns the lines where warnings should occur. * - * @since 0.8.0 - * - * @return array => + * @return array Key is the line number, value is the number of expected warnings. */ public function getWarningList() { - return array( - 11 => 1, // Whitelist comment deprecation warning. - 12 => 1, // Whitelist comment deprecation warning. - 97 => 1, // Whitelist comment deprecation warning. - 99 => 1, // Whitelist comment deprecation warning. - ); + return array(); } - } diff --git a/WordPress/Tests/DB/RestrictedClassesUnitTest.1.inc b/WordPress/Tests/DB/RestrictedClassesUnitTest.1.inc index 60d70609bb..4b8a5e325f 100644 --- a/WordPress/Tests/DB/RestrictedClassesUnitTest.1.inc +++ b/WordPress/Tests/DB/RestrictedClassesUnitTest.1.inc @@ -64,3 +64,43 @@ $db9 = new \My\DBlayer; // Error. echo mysqli::$affected_rows; // Error. class YourMysqliC extends \mysqli {} // Error. + +// Bug fix: namespace keyword as operator is case-insensitive. +class YourMysqliC extends NameSpace\mysqli {} // Error. + +// Issue #2184 Prevent false positives when searching for the class name before a double colon. +class NotOurTarget extends SomethingElse { + public function doSomething() { + echo mysqli(); + if (self::$property === static::$property + || $obj::$property === parent::$property + ) {} + } +} + +// Bug fix: false negative when an object instantiation ends on a PHP close tag without whitespace. +$db1 = new mysqli?> +
Something
+exec(); // Error. +$db11 = (new PDO)?->exec(); // Error. + +// Safeguard handling of class which extends and implements. +class MyMysqliWithArrayAccess extends mysqli implements ArrayAccess {} // Error. + +// Safeguard handling of new with hierarchy keywords. +$obj = new self(); +$obj = new Parent(); +$obj = new STATIC(); + +// Safeguard handling of PHP 7.0+ anonymous classes. +$anon = new class { + public function PDO() {} // OK. +}; + +$anon = new class extends PDOStatement {}; // Error. + +// PHP 8.1: enums can implement. +enum MysqliEnum implements mysqli {} // Error. diff --git a/WordPress/Tests/DB/RestrictedClassesUnitTest.php b/WordPress/Tests/DB/RestrictedClassesUnitTest.php index b5df274d02..91fb95574d 100644 --- a/WordPress/Tests/DB/RestrictedClassesUnitTest.php +++ b/WordPress/Tests/DB/RestrictedClassesUnitTest.php @@ -15,12 +15,15 @@ /** * Unit test class for the DB_RestrictedClasses sniff. * - * @package WPCS\WordPressCodingStandards + * @since 0.10.0 + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 3.0.0 Renamed the fixtures to create compatibility with PHPCS 4.x/PHPUnit >=8. * - * @since 0.10.0 - * @since 0.13.0 Class name changed: this class is now namespaced. + * @covers \WordPressCS\WordPress\AbstractClassRestrictionsSniff + * @covers \WordPressCS\WordPress\Helpers\RulesetPropertyHelper + * @covers \WordPressCS\WordPress\Sniffs\DB\RestrictedClassesSniff */ -class RestrictedClassesUnitTest extends AbstractSniffUnitTest { +final class RestrictedClassesUnitTest extends AbstractSniffUnitTest { /** * Add a number of extra restricted classes to unit test the abstract @@ -28,8 +31,12 @@ class RestrictedClassesUnitTest extends AbstractSniffUnitTest { * * Note: as that class extends the abstract FunctionRestrictions class, that's * where we are passing the parameters to. + * + * @before + * + * @return void */ - protected function setUp() { + protected function enhanceGroups() { parent::setUp(); AbstractFunctionRestrictionsSniff::$unittest_groups = array( @@ -46,8 +53,12 @@ protected function setUp() { /** * Reset the $groups property. + * + * @after + * + * @return void */ - protected function tearDown() { + protected function resetGroups() { AbstractFunctionRestrictionsSniff::$unittest_groups = array(); parent::tearDown(); } @@ -56,37 +67,45 @@ protected function tearDown() { * 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 = '' ) { switch ( $testFile ) { case 'RestrictedClassesUnitTest.1.inc': return array( - 17 => 1, - 18 => 1, - 19 => 1, - 20 => 1, - 22 => 1, - 23 => 1, - 24 => 1, - 25 => 1, - 26 => 1, - 27 => 1, - 29 => 1, - 30 => 1, - 32 => 1, - 33 => 1, - 35 => 1, - 36 => 1, - 37 => 1, - 39 => 1, - 40 => 1, - 42 => 1, - 51 => 1, - 52 => 1, - 63 => 1, - 65 => 1, - 66 => 1, + 17 => 1, + 18 => 1, + 19 => 1, + 20 => 1, + 22 => 1, + 23 => 1, + 24 => 1, + 25 => 1, + 26 => 1, + 27 => 1, + 29 => 1, + 30 => 1, + 32 => 1, + 33 => 1, + 35 => 1, + 36 => 1, + 37 => 1, + 39 => 1, + 40 => 1, + 42 => 1, + 51 => 1, + 52 => 1, + 63 => 1, + 65 => 1, + 66 => 1, + 69 => 1, + 82 => 1, + 87 => 1, + 88 => 1, + 91 => 1, + 103 => 1, + 106 => 1, ); case 'RestrictedClassesUnitTest.2.inc': @@ -141,10 +160,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/DB/RestrictedFunctionsUnitTest.inc b/WordPress/Tests/DB/RestrictedFunctionsUnitTest.inc index 6286e32def..bb976bf8fe 100644 --- a/WordPress/Tests/DB/RestrictedFunctionsUnitTest.inc +++ b/WordPress/Tests/DB/RestrictedFunctionsUnitTest.inc @@ -82,9 +82,15 @@ maxdb_stat(); // WP Native function which was named a bit unfortunately. mysql_to_rfc3339(); // Ok. +Mysql_to_RFC3339(); // Ok, comparison should be done case insensitively. // Other WP Native functions which shouldn't give a problem anyway. mysql2date(); // Ok. wp_check_mysql_version(); // Ok. wp_check_php_mysql_version(); // Ok. WP_Date_Query::build_mysql_datetime(); // Ok. + +// Verify the "allow" key in the function groups is handled case-insensitively. +myFictionFunction(); // Bad. +myFictional(); // OK. +Myfictional(); // OK. diff --git a/WordPress/Tests/DB/RestrictedFunctionsUnitTest.php b/WordPress/Tests/DB/RestrictedFunctionsUnitTest.php index 5149dbb8fb..5bb1fee9af 100644 --- a/WordPress/Tests/DB/RestrictedFunctionsUnitTest.php +++ b/WordPress/Tests/DB/RestrictedFunctionsUnitTest.php @@ -10,21 +10,61 @@ namespace WordPressCS\WordPress\Tests\DB; use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; +use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff; /** * Unit test class for the DB_RestrictedFunctions 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\AbstractFunctionRestrictionsSniff + * @covers \WordPressCS\WordPress\Sniffs\DB\RestrictedFunctionsSniff */ -class RestrictedFunctionsUnitTest extends AbstractSniffUnitTest { +final class RestrictedFunctionsUnitTest extends AbstractSniffUnitTest { + + /** + * Add a number of extra restricted functions to unit test the abstract + * AbstractFunctionRestrictionsSniff class. + * + * @before + * + * @return void + */ + protected function enhanceGroups() { + parent::setUp(); + + AbstractFunctionRestrictionsSniff::$unittest_groups = array( + 'test-empty-funtions-array' => array( + 'type' => 'error', + 'message' => 'Detected usage of %s.', + 'functions' => array(), + ), + 'test_allow-key-handled-case-insensitively' => array( + 'type' => 'error', + 'message' => 'Detected usage of %s.', + 'functions' => array( 'myFiction*' ), + 'allow' => array( 'myFictional' => true ), + ), + ); + } + + /** + * Reset the $groups property. + * + * @after + * + * @return void + */ + protected function resetGroups() { + AbstractFunctionRestrictionsSniff::$unittest_groups = array(); + parent::tearDown(); + } /** * 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( @@ -74,16 +114,17 @@ public function getErrorList() { 74 => 1, 75 => 1, 76 => 1, + + 94 => 1, ); } /** * 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/DB/SlowDBQueryUnitTest.inc b/WordPress/Tests/DB/SlowDBQueryUnitTest.inc index 6c8791aa8b..883673d22e 100644 --- a/WordPress/Tests/DB/SlowDBQueryUnitTest.inc +++ b/WordPress/Tests/DB/SlowDBQueryUnitTest.inc @@ -22,19 +22,6 @@ if ( ! isset( $widget['params'][0] ) ) { $widget['params'][0] = array(); } - -// Testing whitelisting comments. -$test = array( - - // Single-line statements. - 'tax_query' => array(), // Bad. - 'tax_query' => array(), // WPCS: slow query ok. - 'tax_query' => array(), // WPCS: tax_query ok. - - // Multi-line statement. - 'tax_query' => array( // WPCS: slow query ok. - array( - 'taxonomy' => 'foo', - ), - ), -); +$query = 'foo=bar&meta_key=&meta_value=bar'; +$query = 'foo=bar&meta_key=foo&meta_value='; +$query = 'foo=bar&meta_key=&meta_value='; diff --git a/WordPress/Tests/DB/SlowDBQueryUnitTest.php b/WordPress/Tests/DB/SlowDBQueryUnitTest.php index 987e4d412d..46ba3204d1 100644 --- a/WordPress/Tests/DB/SlowDBQueryUnitTest.php +++ b/WordPress/Tests/DB/SlowDBQueryUnitTest.php @@ -14,18 +14,19 @@ /** * Unit test class for the SlowDBQuery 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 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. + * @covers \WordPressCS\WordPress\AbstractArrayAssignmentRestrictionsSniff + * @covers \WordPressCS\WordPress\Sniffs\DB\SlowDBQuerySniff */ -class SlowDBQueryUnitTest extends AbstractSniffUnitTest { +final class SlowDBQueryUnitTest 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(); @@ -34,7 +35,7 @@ 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( @@ -43,11 +44,9 @@ public function getWarningList() { 15 => 1, 16 => 1, 19 => 2, - 30 => 1, - 31 => 1, // Whitelist comment deprecation warning. - 32 => 1, // Whitelist comment deprecation warning. - 35 => 1, // Whitelist comment deprecation warning. + 25 => 2, + 26 => 2, + 27 => 2, ); } - } diff --git a/WordPress/Tests/DateTime/CurrentTimeTimestampUnitTest.inc b/WordPress/Tests/DateTime/CurrentTimeTimestampUnitTest.inc index ac89fb1943..07634e769a 100644 --- a/WordPress/Tests/DateTime/CurrentTimeTimestampUnitTest.inc +++ b/WordPress/Tests/DateTime/CurrentTimeTimestampUnitTest.inc @@ -23,3 +23,10 @@ current_time( 'timestamp', $gmt ); // Warning. current_time( 'timestamp', false ); // Warning. current_time( 'U', 0 ); // Warning. current_time( 'U' ); // Warning. + +// Safeguard support for PHP 8.0+ named parameters. +current_time( gmt: false ); // OK. Well not really (missing required $type), but not our concern. +current_time( gmt: true, type: 'mysql', ); // OK. +current_time( type: 'Y-m-d' ); // OK. +current_time( gmt: true, type: 'timestamp' ); // Error. +current_time( gmt: 0, type : 'U' ); // Warning. diff --git a/WordPress/Tests/DateTime/CurrentTimeTimestampUnitTest.inc.fixed b/WordPress/Tests/DateTime/CurrentTimeTimestampUnitTest.inc.fixed index 6980ae95f0..5b2fa7e28a 100644 --- a/WordPress/Tests/DateTime/CurrentTimeTimestampUnitTest.inc.fixed +++ b/WordPress/Tests/DateTime/CurrentTimeTimestampUnitTest.inc.fixed @@ -20,3 +20,10 @@ current_time( 'timestamp', $gmt ); // Warning. current_time( 'timestamp', false ); // Warning. current_time( 'U', 0 ); // Warning. current_time( 'U' ); // Warning. + +// Safeguard support for PHP 8.0+ named parameters. +current_time( gmt: false ); // OK. Well not really (missing required $type), but not our concern. +current_time( gmt: true, type: 'mysql', ); // OK. +current_time( type: 'Y-m-d' ); // OK. +time(); // Error. +current_time( gmt: 0, type : 'U' ); // Warning. diff --git a/WordPress/Tests/DateTime/CurrentTimeTimestampUnitTest.php b/WordPress/Tests/DateTime/CurrentTimeTimestampUnitTest.php index 9825c121ae..c667af8234 100644 --- a/WordPress/Tests/DateTime/CurrentTimeTimestampUnitTest.php +++ b/WordPress/Tests/DateTime/CurrentTimeTimestampUnitTest.php @@ -14,29 +14,30 @@ /** * Unit test class for the CurrentTimeTimestamp sniff. * - * @package WPCS\WordPressCodingStandards + * @since 2.2.0 * - * @since 2.2.0 + * @covers \WordPressCS\WordPress\Sniffs\DateTime\CurrentTimeTimestampSniff */ -class CurrentTimeTimestampUnitTest extends AbstractSniffUnitTest { +final class CurrentTimeTimestampUnitTest 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( 9 => 1, 11 => 1, 17 => 1, + 31 => 1, ); } /** * 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( @@ -44,7 +45,7 @@ public function getWarningList() { 23 => 1, 24 => 1, 25 => 1, + 32 => 1, ); } - } diff --git a/WordPress/Tests/DateTime/RestrictedFunctionsUnitTest.php b/WordPress/Tests/DateTime/RestrictedFunctionsUnitTest.php index ecfc78867f..f5e160bc62 100644 --- a/WordPress/Tests/DateTime/RestrictedFunctionsUnitTest.php +++ b/WordPress/Tests/DateTime/RestrictedFunctionsUnitTest.php @@ -14,16 +14,16 @@ /** * Unit test class for the DateTime.RestrictedFunctions sniff. * - * @package WPCS\WordPressCodingStandards + * @since 2.2.0 * - * @since 2.2.0 + * @covers \WordPressCS\WordPress\Sniffs\DateTime\RestrictedFunctionsSniff */ -class RestrictedFunctionsUnitTest extends AbstractSniffUnitTest { +final class RestrictedFunctionsUnitTest 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( @@ -35,10 +35,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/Files/FileNameUnitTest.php b/WordPress/Tests/Files/FileNameUnitTest.php index 3d26c76efe..905147956d 100644 --- a/WordPress/Tests/Files/FileNameUnitTest.php +++ b/WordPress/Tests/Files/FileNameUnitTest.php @@ -14,13 +14,13 @@ /** * Unit test class for the FileName sniff. * - * @package WPCS\WordPressCodingStandards + * @since 2013-06-11 + * @since 0.11.0 Actually added tests ;-) + * @since 0.13.0 Class name changed: this class is now namespaced. * - * @since 2013-06-11 - * @since 0.11.0 Actually added tests ;-) - * @since 0.13.0 Class name changed: this class is now namespaced. + * @covers \WordPressCS\WordPress\Sniffs\Files\FileNameSniff */ -class FileNameUnitTest extends AbstractSniffUnitTest { +final class FileNameUnitTest extends AbstractSniffUnitTest { /** * Error files with the expected nr of errors. @@ -38,6 +38,9 @@ class FileNameUnitTest extends AbstractSniffUnitTest { 'SomeFile.inc' => 1, 'some-File.inc' => 1, 'SomeView.inc' => 1, + 'class.with.dot.not.underscore.inc' => 2, + 'class@with#other+punctuation.inc' => 2, + 'class-wrong-with-different-extension.php3' => 1, // Class file names. 'my-class.inc' => 1, @@ -65,26 +68,33 @@ class FileNameUnitTest extends AbstractSniffUnitTest { 'partial-file-disable.inc' => 1, 'rule-disable.inc' => 0, 'wordpress-disable.inc' => 0, + 'disable-non-matching-enable.inc' => 0, /* * In /FileNameUnitTests/TestFiles. */ 'test-sample-phpunit.inc' => 0, 'test-sample-phpunit6.inc' => 0, + 'test-sample-phpunit6-case-insensitive.inc' => 0, 'test-sample-wpunit.inc' => 0, - 'test-sample-custom-unit.inc' => 0, - 'test-sample-namespaced-declaration.1.inc' => 0, - 'test-sample-namespaced-declaration.2.inc' => 1, // Namespaced vs non-namespaced. - 'test-sample-namespaced-declaration.3.inc' => 1, // Wrong namespace. - 'test-sample-namespaced-declaration.4.inc' => 1, // Non-namespaced vs namespaced. - 'test-sample-global-namespace-extends.1.inc' => 0, // Prefixed user input. - 'test-sample-global-namespace-extends.2.inc' => 1, // Non-namespaced vs namespaced. + 'test-sample-custom-unit-1.inc' => 0, + 'test-sample-custom-unit-2.inc' => 0, + 'test-sample-custom-unit-3.inc' => 0, + 'test-sample-custom-unit-4.inc' => 0, + 'test-sample-custom-unit-5.inc' => 1, // Namespaced vs non-namespaced. + 'test-sample-namespaced-declaration-1.inc' => 0, + 'test-sample-namespaced-declaration-2.inc' => 1, // Namespaced vs non-namespaced. + 'test-sample-namespaced-declaration-3.inc' => 1, // Wrong namespace. + 'test-sample-namespaced-declaration-4.inc' => 1, // Non-namespaced vs namespaced. + 'test-sample-global-namespace-extends-1.inc' => 0, // Prefixed user input. + 'test-sample-global-namespace-extends-2.inc' => 1, // Non-namespaced vs namespaced. 'test-sample-extends-with-use.inc' => 0, - 'test-sample-namespaced-extends.1.inc' => 0, - 'test-sample-namespaced-extends.2.inc' => 1, // Wrong namespace. - 'test-sample-namespaced-extends.3.inc' => 1, // Namespaced vs non-namespaced. - 'test-sample-namespaced-extends.4.inc' => 1, // Non-namespaced vs namespaced. - 'test-sample-namespaced-extends.5.inc' => 0, + 'test-sample-namespaced-extends-1.inc' => 0, + 'test-sample-namespaced-extends-2.inc' => 1, // Wrong namespace. + 'test-sample-namespaced-extends-3.inc' => 1, // Namespaced vs non-namespaced. + 'test-sample-namespaced-extends-4.inc' => 1, // Non-namespaced vs namespaced. + 'test-sample-namespaced-extends-5.inc' => 0, + 'Test_Sample.inc' => 0, /* * In /FileNameUnitTests/ThemeExceptions. @@ -122,6 +132,13 @@ protected function getTestFiles( $testFileBase ) { $sep = \DIRECTORY_SEPARATOR; $test_files = glob( dirname( $testFileBase ) . $sep . 'FileNameUnitTests{' . $sep . ',' . $sep . '*' . $sep . '}*.inc', \GLOB_BRACE ); + $php3_test_files = glob( dirname( $testFileBase ) . $sep . 'FileNameUnitTests{' . $sep . ',' . $sep . '*' . $sep . '}*.php3', \GLOB_BRACE ); + if ( is_array( $php3_test_files ) ) { + foreach ( $php3_test_files as $file ) { + $test_files[] = $file; + } + } + if ( ! empty( $test_files ) ) { return $test_files; } @@ -133,7 +150,8 @@ protected function getTestFiles( $testFileBase ) { * 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 = '' ) { @@ -149,10 +167,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/Files/FileNameUnitTests/PHPCSAnnotations/disable-non-matching-enable.inc b/WordPress/Tests/Files/FileNameUnitTests/PHPCSAnnotations/disable-non-matching-enable.inc new file mode 100644 index 0000000000..d6fbc18920 --- /dev/null +++ b/WordPress/Tests/Files/FileNameUnitTests/PHPCSAnnotations/disable-non-matching-enable.inc @@ -0,0 +1,12 @@ + -phpcs:set WordPress.Files.FileName custom_test_class_whitelist[] My_TestClass +phpcs:set WordPress.Files.FileName custom_test_classes[] My_TestClass +phpcs:set WordPress.Files.FileName custom_test_classes[] \My_TestClass + +phpcs:set WordPress.Files.FileName custom_test_classes[] \Plugin\Tests\My_TestClass + +phpcs:set WordPress.Files.FileName custom_test_classes[] \My_TestClass + +phpcs:set WordPress.Files.FileName custom_test_classes[] \My_TestClass + -phpcs:set WordPress.Files.FileName custom_test_class_whitelist[] TestSample +phpcs:set WordPress.Files.FileName custom_test_classes[] TestSample -phpcs:set WordPress.Files.FileName custom_test_class_whitelist[] Some\Name\TestSample +phpcs:set WordPress.Files.FileName custom_test_classes[] Some\Name\TestSample -phpcs:set WordPress.Files.FileName custom_test_class_whitelist[] TestSample +phpcs:set WordPress.Files.FileName custom_test_classes[] Some\Name\TestSample -phpcs:set WordPress.Files.FileName custom_test_class_whitelist[] Some\Name\TestSample +phpcs:set WordPress.Files.FileName custom_test_classes[] TestSample -phpcs:set WordPress.Files.FileName custom_test_class_whitelist[] Some\Name\TestSample +phpcs:set WordPress.Files.FileName custom_test_classes[] Some\Name\TestSample +phpcs:set WordPress.Files.FileName custom_test_classes[] Some\Name\TestSample + -phpcs:set WordPress.Files.FileName custom_test_class_whitelist[] Some\Name\TestSample - -phpcs:set WordPress.Files.FileName custom_test_class_whitelist[] TestSample +phpcs:set WordPress.Files.FileName custom_test_classes[] Some\Name\TestSample -phpcs:set WordPress.Files.FileName custom_test_class_whitelist[] Some\Name\TestSample +phpcs:set WordPress.Files.FileName custom_test_classes[] Some\Name\TestSample -phpcs:set WordPress.Files.FileName custom_test_class_whitelist[] Some\Name\TestSample +phpcs:set WordPress.Files.FileName custom_test_classes[] TestSample -phpcs:set WordPress.Files.FileName custom_test_class_whitelist[] Some\Name\TestSample +phpcs:set WordPress.Files.FileName custom_test_classes[] Some\Name\TestSample -phpcs:set WordPress.Files.FileName custom_test_class_whitelist[] Some\Name\TestSample +phpcs:set WordPress.Files.FileName custom_test_classes[] Some\Name\TestSample $foo; + +// Variable variables local to the arrow function do not need to be prefixed. +$acronym_fn = fn($acronym_name, $acronym_value) => $$acronym_name = $acronym_value; + +// Function local variables in a arrow function declaration do not need to be prefixed. +$acronym_fn = fn($acronym_name, $acronym_value) => $new = $acronym_value; + +// The `$GLOBAL['my_key']` assignment and the function declaration within the closure should still be flagged. +$acronym_fn = fn($a, $b) => + $no_prefix = function($a, $b) { // The `$no_prefix` variable should be ignored. + $GLOBALS['my_key'] = 10; // Bad. + function named() {} // Bad. + return $a + $b; + }; + +// Safeguard that assignments using the PHP 7.4+ null coalesce equals operator are handled correctly. +function acronym_null_coalesce_equals() { + $GLOBALS['my_key'] ??= 10; // Bad. +} + +// Safeguard that property assignments using the PHP 8.0+ nullsafe object operator do not trigger false positives. +$acronym_object?->property = 10; + +/* + * Safeguard support for function calls using PHP 8.0+ named parameters. + */ +define( value: 0 ); // OK. Well, not really as missing a required param, but that's not the concern of this sniff. +define( + value : 'not_prefixed', + constant_name: 'NOT_PREFIXED', // Bad. +); +define( + case_insensitive: true, + constant_name: 'ACRONYM_PREFIXED', // OK. + value: 0, +); + +do_action_ref_array( hook: 'My-Hook', args: $args ); // OK. Well, not really, but using the wrong parameter name, so not our concern. +do_action_ref_array( args: $args, hook_name: 'acronym_hook', ); // OK. +do_action_ref_array( args: $args, hook_name: 'My-Hook', ); // Bad. +do_action( hook_name: "acronym_plugin_action_{$acronym_filter_var}" ); // OK. + +apply_filters_ref_array( args: $args ); // OK. Well, not really, but missing required parameter, so not our concern. +apply_filters_ref_array( args: $var, hook_name: 'acronym_filter', ); // OK. +apply_filters_ref_array( args: $var, hook_name: 'theme_filter', ); // Bad. +apply_filters_ref_array( hook_name: 'theme_filter_' . $acronym_filter_var ); // Bad. + +// Safeguard that comments in the parameters are ignored. +apply_filters( /* test */ 'widget_title', $title ); +do_action( /* test */ 'add_meta_boxes' ); + +define( /* test */ 'FORCE_SSL_ADMIN', true ); + +// Safeguard that assignments to properties using PHP 8.0+ constructor property promotion don't lead to false positives. +class Acronym_ConstructorPropertyPromotion { + public function __construct( + public int $timestart = 0, + protected int|bool $timeend = false, + $post = null + ) {} // Ok. +} + +/* + * Safeguard that PHP 8.1+ enums are treated correctly. + */ +enum Example {} // Bad. +enum Another_Example: int {} // Bad. + +enum Acronym: string implements SomeInterface {} // OK. +enum AcronymExample { // OK. + // Constants and methods declared within an enum do not need to be prefixed. (Properties are not allowed) + const SOME_CONSTANT = 'value'; // OK. + public function do_something( $param = 'default' ) {} // OK x2. + + // Global constants and hook names still do need to be prefixed when defined within an enum. + protected function hello() { + define( 'FOO', 'value' ); // Bad. + apply_filters( 'foo', $args ); // Bad. + } +} + +// Safeguard that the sniff ignores PHP 8.2+ constants in traits correctly. +trait Acronym_Has_Constant { + final const NON_PREFIXED = true; // OK. +} + +// Safeguard improved finding of end of global statement. +function acronym_close_tag_can_end_global_statement() { + global $something, $acronym_else ?> + + => + * + * @return array Key is the line number, value is the number of expected errors. */ public function getErrorList( $testFile = 'PrefixAllGlobalsUnitTest.1.inc' ) { switch ( $testFile ) { case 'PrefixAllGlobalsUnitTest.1.inc': return array( - 1 => 8, // 2 x error for blacklisted prefix passed. 4 x error for short prefixes. 2 x no prefix. + 1 => 8, // 2 x error for blocked prefix passed. 4 x error for short prefixes. 2 x no prefix. 10 => 1, 18 => 1, 21 => 1, @@ -51,6 +53,9 @@ public function getErrorList( $testFile = 'PrefixAllGlobalsUnitTest.1.inc' ) { 39 => 1, 40 => 1, 90 => 1, + 212 => 1, // Old-style WPCS ignore comments are no longer supported. + 215 => 1, // Old-style WPCS ignore comments are no longer supported. + 216 => 1, // Old-style WPCS ignore comments are no longer supported. // Backfills. 225 => ( function_exists( '\mb_strpos' ) ) ? 0 : 1, 230 => ( function_exists( '\array_column' ) ) ? 0 : 1, @@ -79,11 +84,23 @@ public function getErrorList( $testFile = 'PrefixAllGlobalsUnitTest.1.inc' ) { 464 => 2, 465 => 1, 468 => 1, + 550 => 1, + 551 => 1, + 557 => 1, + 569 => 1, + 579 => 1, + 584 => 1, + 585 => 1, + 605 => 1, + 606 => 1, + 616 => 1, + 617 => 1, + 633 => 1, ); case 'PrefixAllGlobalsUnitTest.4.inc': return array( - 1 => 1, // 1 x error for blacklisted prefix passed. + 1 => 1, // 1 x error for blocked prefix passed. 18 => 1, ); @@ -100,7 +117,8 @@ public function getErrorList( $testFile = 'PrefixAllGlobalsUnitTest.1.inc' ) { * 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 = 'PrefixAllGlobalsUnitTest.1.inc' ) { @@ -108,11 +126,6 @@ public function getWarningList( $testFile = 'PrefixAllGlobalsUnitTest.1.inc' ) { case 'PrefixAllGlobalsUnitTest.1.inc': return array( 1 => 3, // 3 x warning for potentially incorrect prefix passed. - 201 => 1, // Whitelist comment deprecation warning. - 208 => 1, // Whitelist comment deprecation warning. - 212 => 1, // Whitelist comment deprecation warning. - 215 => 1, // Whitelist comment deprecation warning. - 216 => 1, // Whitelist comment deprecation warning. 249 => 1, 250 => 1, 253 => 1, @@ -153,5 +166,4 @@ public function getWarningList( $testFile = 'PrefixAllGlobalsUnitTest.1.inc' ) { return array(); } } - } diff --git a/WordPress/Tests/NamingConventions/ValidFunctionNameUnitTest.inc b/WordPress/Tests/NamingConventions/ValidFunctionNameUnitTest.inc index f0403333ee..6a06ba3cab 100644 --- a/WordPress/Tests/NamingConventions/ValidFunctionNameUnitTest.inc +++ b/WordPress/Tests/NamingConventions/ValidFunctionNameUnitTest.inc @@ -52,7 +52,7 @@ class Its_A_Kind_Of_Magic { function __sleep() {} // Ok. function __wakeup() {} // Ok. function __toString() {} // Ok. - function __set_state() {} // Ok. + static function __set_state($properties) {} // Ok. function __clone() {} // Ok. function __invoke() {} // Ok. function __debugInfo() {} // Ok. @@ -145,3 +145,84 @@ class Deprecated { */ public static function __deprecatedMethod() {} } + +class PHP74Magic { + function __serialize() {} // OK. + function __unserialize($data) {} // OK. +} + +class More_Nested { + public function method_name() { + function __autoload() {} // OK - magic function in global namespace. + function __CamelCase() {} // Bad x 2 for *function*, not method. + } +} + +function ___triple_underscore() {} // OK. + +class Triple { + function ___triple_underscore() {} // OK. +} + +class DeprecatedWithAttribute { + /** + * Function description. + * + * @since 1.2.3 + * @deprecated 2.3.4 + * + * @return void + */ + #[SomeAttribute] + #[AnotherAttribute] + public static function __deprecatedMethod() {} +} + +// Safeguard that the suggested replacement name does not suggest undue changes to underscores in the name. +class UnderScoreHandling { + public function ___Leading_Underscores() {} // Bad, replacement suggestion should be: ___leading_underscores. + public function Multiple_______Underscores() {} // Bad, replacement suggestion should be: multiple_______underscores. + public function Trailing_Underscores___() {}// Bad, replacement suggestion should be: trailing_underscores___. +} + +// Safeguard that functions with a name only consisting of underscores are always ignored. +function __() {} + +class OnlyUnderscores { + public function _____() {} +} + +// Class vs function name PHP case-sensitivity quirks. +class FooBÃÈ { + function FooBÃÈ() {} // OK, same case. + function fOOBÃÈ() {} // OK, same case for the non-ascii chars. + function FooBãè() {} // Bad - not PHP 4-type constructor, non ascii chars not in same case - POC: https://3v4l.org/YOc2R. +} + +// Safeguard that methods in enums are correctly handled by the sniff. +enum Suit { + case Hearts; + case Diamonds; + + public function color(): string {} // OK. + public function __changeColor(): string {} // Bad + + public function &getShape(): string // Bad. + { + return "Rectangle"; + } + + // These are the only three magic methods allowed in enums. + public function __call( $a, $b ) {} // Ok. + public static function __callStatic( $a, $b ) {} // Ok. + public function __invoke() {} // Ok. +} + +// Related to #1891 - ensure the sniff does not throw an error if the suggested alternative would be the same as the original name. +function lähtöaika() {} // OK. +function lÄhtÖaika() {} // Bad, but only handled by the sniff if Mbstring is available. +function lÄhtOaika() {} // Bad, handled via transliteration of non-ASCII chars if Mbstring is not available. + +// Live coding/parse error. +// This has to be the last test in the file. +function diff --git a/WordPress/Tests/NamingConventions/ValidFunctionNameUnitTest.php b/WordPress/Tests/NamingConventions/ValidFunctionNameUnitTest.php index 0c1db47bc7..6dbd5e8cf1 100644 --- a/WordPress/Tests/NamingConventions/ValidFunctionNameUnitTest.php +++ b/WordPress/Tests/NamingConventions/ValidFunctionNameUnitTest.php @@ -14,17 +14,18 @@ /** * Unit test class for the ValidFunctionName sniff. * - * @package WPCS\WordPressCodingStandards + * @since 2013-06-11 + * @since 0.13.0 Class name changed: this class is now namespaced. * - * @since 2013-06-11 - * @since 0.13.0 Class name changed: this class is now namespaced. + * @covers \WordPressCS\WordPress\Helpers\DeprecationHelper + * @covers \WordPressCS\WordPress\Sniffs\NamingConventions\ValidFunctionNameSniff */ -class ValidFunctionNameUnitTest extends AbstractSniffUnitTest { +final class ValidFunctionNameUnitTest 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( @@ -46,16 +47,24 @@ public function getErrorList() { 106 => 2, 116 => 1, 117 => 1, + 157 => 2, + 183 => 1, + 184 => 1, + 185 => 1, + 199 => 1, + 208 => 2, + 210 => 1, + 223 => function_exists( 'mb_strtolower' ) ? 1 : 0, + 224 => 1, ); } /** * 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/NamingConventions/ValidHookNameUnitTest.1.inc b/WordPress/Tests/NamingConventions/ValidHookNameUnitTest.1.inc index 039ca6484e..830c3552d6 100644 --- a/WordPress/Tests/NamingConventions/ValidHookNameUnitTest.1.inc +++ b/WordPress/Tests/NamingConventions/ValidHookNameUnitTest.1.inc @@ -94,3 +94,30 @@ do_action( // phpcs:ignore Stnd.Cat.Sniff -- For reasons. 'prefix_hook-name' /* comment */ ); + +// Ignore text strings when passed as parameters to a function call. WPCS #2055. +$value = apply_filters( + get_filter_name( 'UPPERCASE', 'wrong-delimiter' ), + $value, + $attributes +); + +// ... but do not ignore text strings in arbitrary parentheses. +$value = apply_filters( + ( $name ? 'UPPERCASE' : 'wrong-delimiter' ), + $value, + $attributes +); + +// Test handling of more complex embedded variables and expressions. +do_action( "admin_head_${Foo->{$Baz}}_action_$Post->ID" ); // OK. +do_action( "admin_Head_${Foo?->{$Baz}}_Action_{$Post?->ID}_Bla" ); // Error - use lowercase. +do_action( "admin_Head_${Foo->{"${'A'}"}}-Action_$Post[A]_Bla" ); // Error - use lowercase + warning about dash. + +// Safeguard that variable function calls are handled correctly. +do_action( 'admin_head_' . $fn( 'UPPERCASE', 'wrong-delimiter' ) . '_action' ); // Ok. + +// Safeguard support for PHP 8.0+ named parameters. +do_action_ref_array( hook: 'My-Hook', args: $args ); // OK. Well, not really, but using the wrong parameter name, so not our concern. +do_action_ref_array( args: $args, hook_name: 'my_hook', ); // OK. +do_action_ref_array( args: $args, hook_name: 'My-Hook', ); // Error - use lowercase + warning about dash. diff --git a/WordPress/Tests/NamingConventions/ValidHookNameUnitTest.php b/WordPress/Tests/NamingConventions/ValidHookNameUnitTest.php index c55be6b044..aac97da195 100644 --- a/WordPress/Tests/NamingConventions/ValidHookNameUnitTest.php +++ b/WordPress/Tests/NamingConventions/ValidHookNameUnitTest.php @@ -14,61 +14,67 @@ /** * Unit test class for the ValidHookName 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\Helpers\WPHookHelper + * @covers \WordPressCS\WordPress\Sniffs\NamingConventions\ValidHookNameSniff */ -class ValidHookNameUnitTest extends AbstractSniffUnitTest { +final class ValidHookNameUnitTest extends AbstractSniffUnitTest { /** * 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 = 'ValidHookNameUnitTest.1.inc' ) { switch ( $testFile ) { case 'ValidHookNameUnitTest.1.inc': return array( - 14 => 1, - 15 => 1, - 16 => 1, - 17 => 1, - 28 => 1, - 29 => 1, - 30 => 1, - 33 => 1, - 53 => 1, - 54 => 1, - 55 => 1, - 56 => 1, - 57 => 1, - 58 => 1, - 59 => 1, - 60 => 1, - 61 => 1, - 62 => 1, - 63 => 1, - 64 => 1, - 65 => 1, - 66 => 1, - 68 => 1, - 69 => 1, - 70 => 1, - 71 => 1, - 72 => 1, - 73 => 1, - 74 => 1, - 75 => 1, - 76 => 1, - 77 => 1, - 78 => 1, - 79 => 1, - 80 => 1, - 81 => 1, - 89 => 1, + 14 => 1, + 15 => 1, + 16 => 1, + 17 => 1, + 28 => 1, + 29 => 1, + 30 => 1, + 33 => 1, + 53 => 1, + 54 => 1, + 55 => 1, + 56 => 1, + 57 => 1, + 58 => 1, + 59 => 1, + 60 => 1, + 61 => 1, + 62 => 1, + 63 => 1, + 64 => 1, + 65 => 1, + 66 => 1, + 68 => 1, + 69 => 1, + 70 => 1, + 71 => 1, + 72 => 1, + 73 => 1, + 74 => 1, + 75 => 1, + 76 => 1, + 77 => 1, + 78 => 1, + 79 => 1, + 80 => 1, + 81 => 1, + 89 => 1, + 107 => 1, + 114 => 1, + 115 => 1, + 123 => 1, ); case 'ValidHookNameUnitTest.2.inc': @@ -82,21 +88,25 @@ public function getErrorList( $testFile = 'ValidHookNameUnitTest.1.inc' ) { * 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 = 'ValidHookNameUnitTest.1.inc' ) { switch ( $testFile ) { case 'ValidHookNameUnitTest.1.inc': return array( - 8 => 1, - 9 => 1, - 10 => 1, - 11 => 1, - 68 => 1, - 72 => 1, - 77 => 1, - 95 => 1, + 8 => 1, + 9 => 1, + 10 => 1, + 11 => 1, + 68 => 1, + 72 => 1, + 77 => 1, + 95 => 1, + 107 => 1, + 115 => 1, + 123 => 1, ); case 'ValidHookNameUnitTest.2.inc': @@ -112,5 +122,4 @@ public function getWarningList( $testFile = 'ValidHookNameUnitTest.1.inc' ) { return array(); } } - } diff --git a/WordPress/Tests/NamingConventions/ValidPostTypeSlugUnitTest.inc b/WordPress/Tests/NamingConventions/ValidPostTypeSlugUnitTest.1.inc similarity index 74% rename from WordPress/Tests/NamingConventions/ValidPostTypeSlugUnitTest.inc rename to WordPress/Tests/NamingConventions/ValidPostTypeSlugUnitTest.1.inc index fa9d4dcfa8..5c0ed5729a 100644 --- a/WordPress/Tests/NamingConventions/ValidPostTypeSlugUnitTest.inc +++ b/WordPress/Tests/NamingConventions/ValidPostTypeSlugUnitTest.1.inc @@ -50,3 +50,21 @@ register_post_type( "my-own-post-type-too-long-{$i}" ); // 1x Error, Too long. 1 register_post_type( 'my/own/post/type/too/long', array() ); // Bad. Invalid chars: "/" and too long. register_post_type( 'wp_block', array() ); // Bad. Must only error on reserved keyword, not invalid prefix. + +// Test handling of more complex embedded variables and expressions. +register_post_type("testing123-${(foo)}-test"); +register_post_type("testing123-${foo["${bar['baz']}"]}-test"); + +// Test ignoring invalid function calls/live coding. +register_post_type(); // OK, ignore, presume live coding. + +// Safeguard support for PHP 8.0+ named parameters. +register_post_type( args: array() ); // Bad. No post type slug. +register_post_type( args: array(), post_type: 'my_own_post_type', ); // OK. +register_post_type( args: array(), post_type: 'my-own-post-type-too-long' ); // Bad. + +// Safeguard that the information displayed in the error message is cleaned of comments. +register_post_type( + // Post type name. + $name,// Non string literal. Warning with severity: 3 +); diff --git a/WordPress/Tests/NamingConventions/ValidPostTypeSlugUnitTest.2.inc b/WordPress/Tests/NamingConventions/ValidPostTypeSlugUnitTest.2.inc new file mode 100644 index 0000000000..50cde0b68a --- /dev/null +++ b/WordPress/Tests/NamingConventions/ValidPostTypeSlugUnitTest.2.inc @@ -0,0 +1,19 @@ + => + * @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( - 5 => 1, - 6 => 1, - 7 => 1, - 8 => 1, - 20 => 1, - 36 => 1, - 37 => 1, - 39 => 1, - 49 => 1, - 50 => 2, - 52 => 1, - ); + public function getErrorList( $testFile = '' ) { + switch ( $testFile ) { + case 'ValidPostTypeSlugUnitTest.1.inc': + return array( + 5 => 1, + 6 => 1, + 7 => 1, + 8 => 1, + 20 => 1, + 36 => 1, + 37 => 1, + 39 => 1, + 40 => 1, + 49 => 1, + 50 => 2, + 52 => 1, + 62 => 1, + 64 => 1, + ); + + case 'ValidPostTypeSlugUnitTest.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( + 17 => 1, + ); + + default: + return array(); + } } /** * Returns the lines where warnings 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 warnings. */ - public function getWarningList() { - return array( - 24 => 1, - 27 => 1, - 28 => 1, - 29 => 1, - 30 => 1, - 31 => 1, - 33 => 1, - 34 => 1, - 40 => 1, - 45 => 1, - 49 => 1, - ); + public function getWarningList( $testFile = '' ) { + switch ( $testFile ) { + case 'ValidPostTypeSlugUnitTest.1.inc': + return array( + 24 => 1, + 27 => 1, + 28 => 1, + 29 => 1, + 30 => 1, + 31 => 1, + 33 => 1, + 34 => 1, + 45 => 1, + 49 => 1, + 55 => 1, + 56 => 1, + 67 => 1, + ); + + case 'ValidPostTypeSlugUnitTest.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, + ); + + default: + return array(); + } } } diff --git a/WordPress/Tests/NamingConventions/ValidVariableNameUnitTest.inc b/WordPress/Tests/NamingConventions/ValidVariableNameUnitTest.inc index 0186491d75..437c005899 100644 --- a/WordPress/Tests/NamingConventions/ValidVariableNameUnitTest.inc +++ b/WordPress/Tests/NamingConventions/ValidVariableNameUnitTest.inc @@ -11,19 +11,19 @@ class MyClass { var $_varName = 'hello'; // Bad. public $varNamf = 'hello'; // Bad. - public $var_namf = 'hello'; + public bool $var_namf = true; public $varnamf = 'hello'; public $_varNamf = 'hello'; // Bad. protected $varNamg = 'hello'; // Bad. protected $var_namg = 'hello'; protected $varnamg = 'hello'; - protected $_varNamg = 'hello'; // Bad. + protected string $_varNamg = 'hello'; // Bad. private $_varNamh = 'hello'; // Bad. private $_var_namh = 'hello'; private $_varnamh = 'hello'; - private $varNamh = 'hello'; // Bad. + private int|string $varNamh = 'hello'; // Bad. } echo $varName; // Bad. @@ -95,7 +95,7 @@ echo $comment->comment_author_IP; class Foo { public $_public_leading_underscore; private $private_no_underscore_loading; - + function Bar( $VARname ) { // Bad. $localVariable = false; // Bad. echo Some_Class::$VarName; // Bad. @@ -127,15 +127,15 @@ echo "This is a $comment_ID"; // Bad echo "This is $PHP_SELF with $HTTP_RAW_POST_DATA"; // Ok. /* - * Unit test whitelisting. + * Testing custom properties. */ -// phpcs:set WordPress.NamingConventions.ValidVariableName customPropertiesWhitelist[] varName,DOMProperty -echo MyClass::$varName; // Ok, whitelisted. -echo $this->DOMProperty; // Ok, whitelisted. -echo $object->varName; // Ok, whitelisted. -// phpcs:set WordPress.NamingConventions.ValidVariableName customPropertiesWhitelist[] +// phpcs:set WordPress.NamingConventions.ValidVariableName allowed_custom_properties[] varName,DOMProperty +echo MyClass::$varName; // Ok, allowed. +echo $this->DOMProperty; // Ok, allowed. +echo $object->varName; // Ok, allowed. +// phpcs:set WordPress.NamingConventions.ValidVariableName allowed_custom_properties[] -echo $object->varName; // Bad, no longer whitelisted. +echo $object->varName; // Bad, no longer allowed. // Code style independent token checking. echo $object @@ -176,7 +176,7 @@ class MultiVarDeclarations { $multiVar5 = false, // Bad. $multiVar6 = 123, // Bad. $multi_var7 = 'string'; // Ok. - + public function testMultiGlobalAndStatic() { global $multiGlobal1, $multi_global2, // Bad x 1. $multiGlobal3; // Bad. @@ -186,3 +186,54 @@ class MultiVarDeclarations { $multiStatic3 = ''; // Bad. } } + +echo "This is $post_ID with $ThisShouldBeFlagged"; // Bad. + +// Safeguard that illegal property declarations are ignored. +interface PropertiesNotAllowed { + public $notAllowed; +} + +echo "This is \$someName"; // OK, variable is literal text. + +echo "This is ${$someName}"; // Bad. +echo "This is ${Foo}"; // Bad. +echo "This is {${getName()}}"; // OK, expression, should be ignored. +echo "This is $Foo?->bar"; // Bad, expression, but the $Foo in it should still be flagged. +echo "This is {$foo['bar']?->baz()()}"; // OK. +echo "This is {$Foo['bar']?->baz()()}"; // Bad, expression, but the $Foo in it should still be flagged. + +// Safeguard that parameters in all types of function declarations, including PHP 7.4+ arrow functions, are flagged. +function has_params( $without_default, $with_default = 'default' ) {} // OK. +$closure = function ( $without_default, $with_default = 'default' ) {}; // OK. +$arrow = fn ( $without_default, $with_default = 'default' ) => 10; // OK. + +function has_params_too( $withoutDefault, $withDefault = 'default' ) {} // Bad x 2. +$closure = function ( $withoutDefault, $withDefault = 'default' ) {}; // Bad x 2. +$arrow = fn ( $withoutDefault, $withDefault = 'default' ) => 10; // Bad x 2. + +// Safeguard recognizing property access using PHP 8.0 nullsafe operator. +echo $this?->varName2; // Bad. +echo $this?->var_name2; +echo $this?->varname2; +echo $this?->_varName2; // Bad. + +// Safeguard handling of PHP 8.1 enums. +enum EnumExample { + public $notAllowed; // OK, well, not really, but properties are not allowed in enums, so ignore. + + public function method( $paramName ) { // Bad. + $local_variable = 'OK'; + $localVariable = 'Bad'; + } +} + +// Safeguard ignoring of allowed mixed case property names. +class Has_Mixed_Case_Property { + public $post_ID; // OK. +} + +// Issue #1891 - ensure the sniff does not throw an error if the suggested alternative would be the same as the original name. +$lähtöaika = true; // OK. +$lÄhtÖaika = true; // Bad, but only handled by the sniff if Mbstring is available. +$lÄhtOaika = true; // Bad, handled via transliteration of non-ASCII chars if Mbstring is not available. diff --git a/WordPress/Tests/NamingConventions/ValidVariableNameUnitTest.php b/WordPress/Tests/NamingConventions/ValidVariableNameUnitTest.php index 85e1d22524..c26a4936e7 100644 --- a/WordPress/Tests/NamingConventions/ValidVariableNameUnitTest.php +++ b/WordPress/Tests/NamingConventions/ValidVariableNameUnitTest.php @@ -14,17 +14,18 @@ /** * Unit test class for the ValidVariableName sniff. * - * @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. + * @covers \WordPressCS\WordPress\Helpers\SnakeCaseHelper + * @covers \WordPressCS\WordPress\Sniffs\NamingConventions\ValidVariableNameSniff */ -class ValidVariableNameUnitTest extends AbstractSniffUnitTest { +final class ValidVariableNameUnitTest 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( @@ -82,16 +83,29 @@ public function getErrorList() { 182 => 1, 184 => 1, 186 => 1, + 190 => 1, + 199 => 1, + 200 => 1, + 202 => 1, + 204 => 1, + 211 => 2, + 212 => 2, + 213 => 2, + 216 => 1, + 219 => 1, + 225 => 1, + 227 => 1, + 238 => function_exists( 'mb_strtolower' ) ? 1 : 0, + 239 => 1, ); } /** * 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/PHP/DevelopmentFunctionsUnitTest.php b/WordPress/Tests/PHP/DevelopmentFunctionsUnitTest.php index c008eb6afa..8be090a5bf 100644 --- a/WordPress/Tests/PHP/DevelopmentFunctionsUnitTest.php +++ b/WordPress/Tests/PHP/DevelopmentFunctionsUnitTest.php @@ -14,17 +14,17 @@ /** * Unit test class for the PHP_DevelopmentFunctions sniff. * - * @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. + * @covers \WordPressCS\WordPress\Sniffs\PHP\DevelopmentFunctionsSniff */ -class DevelopmentFunctionsUnitTest extends AbstractSniffUnitTest { +final class DevelopmentFunctionsUnitTest 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(); @@ -33,7 +33,7 @@ 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( @@ -54,5 +54,4 @@ public function getWarningList() { 34 => 1, ); } - } diff --git a/WordPress/Tests/PHP/DisallowShortTernaryUnitTest.inc b/WordPress/Tests/PHP/DisallowShortTernaryUnitTest.inc deleted file mode 100644 index 419fc19f89..0000000000 --- a/WordPress/Tests/PHP/DisallowShortTernaryUnitTest.inc +++ /dev/null @@ -1,12 +0,0 @@ - => + * @return array Key is the line number, value is the number of expected errors. */ public function getErrorList() { return array(); @@ -33,7 +33,7 @@ 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( @@ -62,5 +62,4 @@ public function getWarningList() { 39 => 1, ); } - } diff --git a/WordPress/Tests/PHP/DontExtractUnitTest.php b/WordPress/Tests/PHP/DontExtractUnitTest.php index 6654bdac00..3efee7c9a8 100644 --- a/WordPress/Tests/PHP/DontExtractUnitTest.php +++ b/WordPress/Tests/PHP/DontExtractUnitTest.php @@ -14,18 +14,18 @@ /** * Unit test class for the DontExtract sniff. * - * @package WPCS\WordPressCodingStandards + * @since 0.10.0 + * @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 - * @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. + * @covers \WordPressCS\WordPress\Sniffs\PHP\DontExtractSniff */ -class DontExtractUnitTest extends AbstractSniffUnitTest { +final class DontExtractUnitTest 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,10 +36,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/PHP/IniSetUnitTest.inc b/WordPress/Tests/PHP/IniSetUnitTest.inc index b39d3e1a3b..c5ef7295d1 100644 --- a/WordPress/Tests/PHP/IniSetUnitTest.inc +++ b/WordPress/Tests/PHP/IniSetUnitTest.inc @@ -41,3 +41,20 @@ ini_set($test, 1230); // Warning. ini_alter('auto_detect_line_endings', true); // Ok. ini_alter('display_errors', false); // Error. ini_alter('report_memleaks', 1230); // Warning. + +// Ignore missing required parameters. +ini_set('short_open_tag', ); // Ok. Well not really, missing value param, but that's not the concern of this sniff. + +// Safeguard support for PHP 8.0+ named parameters. +ini_set(new_value: 0, option: 'short_open_tag', ); // Ok. Well not really, unrecognized param name, but that's not the concern of this sniff. +ini_set(value: 1, option: 'short_open_tag', ); // Ok. +ini_set(value: 0, option: 'short_open_tag', ); // Error. + +// Safeguard that comments in the parameters are ignored. +ini_set('short_open_tag', /* allowed*/ 'on'); // Ok. +ini_set( + // This affects all function calls to the BCMath extension. + 'bcmath.scale', + // Set the number of decimals. + 0 +); // Error. diff --git a/WordPress/Tests/PHP/IniSetUnitTest.php b/WordPress/Tests/PHP/IniSetUnitTest.php index 11c1f75218..fede4955b2 100644 --- a/WordPress/Tests/PHP/IniSetUnitTest.php +++ b/WordPress/Tests/PHP/IniSetUnitTest.php @@ -14,16 +14,16 @@ /** * Unit test class for the IniSet sniff. * - * @package WPCS\WordPressCodingStandards - * * @since 2.1.0 + * + * @covers \WordPressCS\WordPress\Sniffs\PHP\IniSetSniff */ -class IniSetUnitTest extends AbstractSniffUnitTest { +final class IniSetUnitTest 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( @@ -47,13 +47,15 @@ public function getErrorList() { 33 => 1, 34 => 1, 42 => 1, + 51 => 1, + 55 => 1, ); } /** * 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( @@ -64,5 +66,4 @@ public function getWarningList() { 43 => 1, ); } - } diff --git a/WordPress/Tests/PHP/NoSilencedErrorsUnitTest.inc b/WordPress/Tests/PHP/NoSilencedErrorsUnitTest.inc index 3e52794bc9..f007d150bd 100644 --- a/WordPress/Tests/PHP/NoSilencedErrorsUnitTest.inc +++ b/WordPress/Tests/PHP/NoSilencedErrorsUnitTest.inc @@ -16,7 +16,7 @@ if ( @&file_exists( $filename ) && @ /*comment*/ is_readable( $filename ) ) { $file = @ \file( $filename ); } -$fp = @fopen('http://www.example.com', 'r', false); +$fp = @fopen('https://www.example.com', 'r', false); @fpassthru($fp); // Bad. @fclose($fp); // Bad. @@ -39,20 +39,20 @@ if ( @ftp_fget($conn_id, $handle, $remote_file, FTP_ASCII, 0 ) ) { // Bad. } @ftp_close($conn_id); // Bad. -// phpcs:set WordPress.PHP.NoSilencedErrors custom_whitelist[] fgetcsv,hex2bin +// phpcs:set WordPress.PHP.NoSilencedErrors customAllowedFunctionsList[] fgetcsv,hex2bin while ( ( $csvdata = @fgetcsv( $handle, 2000, $separator ) ) !== false ) {} echo @some_userland_function( $param ); // Bad. $decoded = @hex2bin( $data ); -// phpcs:set WordPress.PHP.NoSilencedErrors custom_whitelist[] +// phpcs:set WordPress.PHP.NoSilencedErrors customAllowedFunctionsList[] $decoded = @hex2bin( $data ); // Bad. $unserialized = @unserialize( $str ); /* - * ... and test the same principle again, but now without using the whitelist. + * ... and test the same principle again, but now without using the PHP function allow list. */ -// phpcs:set WordPress.PHP.NoSilencedErrors use_default_whitelist false +// phpcs:set WordPress.PHP.NoSilencedErrors usePHPFunctionsList false // File extension. if ( @&file_exists( $filename ) && @ /*comment*/ is_readable( $filename ) ) { // Bad x2. @@ -71,12 +71,16 @@ if (@is_dir($dir)) { // Bad. $files1 = @ & scandir($dir); // Bad. /* - * Custom whitelist will be respected even when `use_default_whitelist` is set to false. + * The custom allowed functions list will be respected even when `usePHPFunctionsList` is set to false. */ -// phpcs:set WordPress.PHP.NoSilencedErrors custom_whitelist[] fgetcsv,hex2bin +// phpcs:set WordPress.PHP.NoSilencedErrors customAllowedFunctionsList[] fgetcsv,hex2bin while ( ( $csvdata = @fgetcsv( $handle, 2000, $separator ) ) !== false ) {} echo @some_userland_function( $param ); // Bad. $decoded = @hex2bin( $data ); -// phpcs:set WordPress.PHP.NoSilencedErrors custom_whitelist[] +// phpcs:set WordPress.PHP.NoSilencedErrors customAllowedFunctionsList[] -// phpcs:set WordPress.PHP.NoSilencedErrors use_default_whitelist true +// phpcs:set WordPress.PHP.NoSilencedErrors usePHPFunctionsList true + +// phpcs:set WordPress.PHP.NoSilencedErrors context_length 0 +echo @some_userland_function( $param ); // Bad. +// phpcs:set WordPress.PHP.NoSilencedErrors context_length 6 diff --git a/WordPress/Tests/PHP/NoSilencedErrorsUnitTest.php b/WordPress/Tests/PHP/NoSilencedErrorsUnitTest.php index 5bd5ddfc84..7a576b992c 100644 --- a/WordPress/Tests/PHP/NoSilencedErrorsUnitTest.php +++ b/WordPress/Tests/PHP/NoSilencedErrorsUnitTest.php @@ -14,16 +14,16 @@ /** * Unit test class for the PHP.NoSilencedErrors sniff. * - * @package WPCS\WordPressCodingStandards + * @since 1.1.0 * - * @since 1.1.0 + * @covers \WordPressCS\WordPress\Sniffs\PHP\NoSilencedErrorsSniff */ -class NoSilencedErrorsUnitTest extends AbstractSniffUnitTest { +final class NoSilencedErrorsUnitTest 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(); @@ -32,7 +32,7 @@ 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( @@ -60,7 +60,7 @@ public function getWarningList() { 68 => 1, 71 => 1, 78 => 1, + 85 => 1, ); } - } diff --git a/WordPress/Tests/PHP/POSIXFunctionsUnitTest.php b/WordPress/Tests/PHP/POSIXFunctionsUnitTest.php index e9df9b0d5a..7dbfcd6100 100644 --- a/WordPress/Tests/PHP/POSIXFunctionsUnitTest.php +++ b/WordPress/Tests/PHP/POSIXFunctionsUnitTest.php @@ -14,17 +14,17 @@ /** * Unit test class for the POSIXFunctions 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\PHP\POSIXFunctionsSniff */ -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( @@ -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/PHP/PregQuoteDelimiterUnitTest.inc b/WordPress/Tests/PHP/PregQuoteDelimiterUnitTest.inc index 6a5acc4697..1942b2b07f 100644 --- a/WordPress/Tests/PHP/PregQuoteDelimiterUnitTest.inc +++ b/WordPress/Tests/PHP/PregQuoteDelimiterUnitTest.inc @@ -7,3 +7,9 @@ preg_quote($keywords); // Warning. $textbody = preg_replace ( "/" . preg_quote($word) . "/", // Warning "" . $word . "", $textbody ); + +// Safeguard support for PHP 8.0+ named parameters. +preg_quote(delimiter: '#', str: $keywords); // OK. +preg_quote(str: $keywords); // Warning. +preg_quote(str: $keywords, delimitter: '#'); // Warning (typo in param name). +preg_quote(delimiter: '#'); // OK. Invalid function call, but that's not the concern of this sniff. diff --git a/WordPress/Tests/PHP/PregQuoteDelimiterUnitTest.php b/WordPress/Tests/PHP/PregQuoteDelimiterUnitTest.php index a4b4ac6160..63d4e0eb9f 100644 --- a/WordPress/Tests/PHP/PregQuoteDelimiterUnitTest.php +++ b/WordPress/Tests/PHP/PregQuoteDelimiterUnitTest.php @@ -14,16 +14,16 @@ /** * Unit test class for the PregQuoteDelimiter sniff. * - * @package WPCS\WordPressCodingStandards + * @since 1.0.0 * - * @since 1.0.0 + * @covers \WordPressCS\WordPress\Sniffs\PHP\PregQuoteDelimiterSniff */ -class PregQuoteDelimiterUnitTest extends AbstractSniffUnitTest { +final class PregQuoteDelimiterUnitTest 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(); @@ -32,13 +32,14 @@ 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( - 6 => 1, - 7 => 1, + 6 => 1, + 7 => 1, + 13 => 1, + 14 => 1, ); } - } diff --git a/WordPress/Tests/PHP/RestrictedPHPFunctionsUnitTest.php b/WordPress/Tests/PHP/RestrictedPHPFunctionsUnitTest.php index af8707d3cd..d930ba4329 100644 --- a/WordPress/Tests/PHP/RestrictedPHPFunctionsUnitTest.php +++ b/WordPress/Tests/PHP/RestrictedPHPFunctionsUnitTest.php @@ -14,16 +14,16 @@ /** * Unit test class for the PHP_DiscouragedPHPFunctions sniff. * - * @package WPCS\WordPressCodingStandards + * @since 0.14.0 * - * @since 0.14.0 + * @covers \WordPressCS\WordPress\Sniffs\PHP\RestrictedPHPFunctionsSniff */ -class RestrictedPHPFunctionsUnitTest extends AbstractSniffUnitTest { +final class RestrictedPHPFunctionsUnitTest 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( @@ -34,10 +34,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/PHP/StrictComparisonsUnitTest.inc b/WordPress/Tests/PHP/StrictComparisonsUnitTest.inc deleted file mode 100644 index 7c3ce84131..0000000000 --- a/WordPress/Tests/PHP/StrictComparisonsUnitTest.inc +++ /dev/null @@ -1,31 +0,0 @@ - $true ) { // Bad. - echo 'False'; -} elseif ( false !== $true ) { // Ok. - echo 'False'; -} - -// Test for whitelisting. -if ( true == $true ) { // Loose comparison, OK. - echo 'True'; -} - -// Test that whitelisting is not too eager. -if ( true == $true ) { - // The line above has a loose comparison, but no whitelist comment. - echo 'True'; -} - -if ( true == $true ) { // Loose comparisons FTW! - echo 'True'; -} diff --git a/WordPress/Tests/PHP/StrictComparisonsUnitTest.php b/WordPress/Tests/PHP/StrictComparisonsUnitTest.php deleted file mode 100644 index b7ada4d896..0000000000 --- a/WordPress/Tests/PHP/StrictComparisonsUnitTest.php +++ /dev/null @@ -1,49 +0,0 @@ - => - */ - public function getErrorList() { - return array(); - } - - /** - * Returns the lines where warnings should occur. - * - * @return array => - */ - public function getWarningList() { - return array( - 3 => 1, - 10 => 1, - 12 => 1, - 19 => 1, // Whitelist comment deprecation warning. - 24 => 1, - 29 => 1, - ); - } - -} diff --git a/WordPress/Tests/PHP/StrictInArrayUnitTest.inc b/WordPress/Tests/PHP/StrictInArrayUnitTest.inc index 8cff2c06c3..fd1e194225 100644 --- a/WordPress/Tests/PHP/StrictInArrayUnitTest.inc +++ b/WordPress/Tests/PHP/StrictInArrayUnitTest.inc @@ -14,7 +14,7 @@ $bar->in_array( 1, array( '1', 1, true ) ); // Ok. $bar-> in_array( 1, array( '1', 1, true ) ); // Ok. -in_array(); // Error. +in_array(); // Warning. array_search( 1, $array, true ); // Ok. @@ -38,3 +38,20 @@ array_keys( array( '1', 1, true ), 'my_key', false ); // Warning in_array( 1, array( '1', 1 ), TRUE ); // Ok. use function in_array; // OK. + +// Safeguard support for PHP 8.0+ named parameters. +in_array( strict: true, haystack: $haystack1, needle: 1, ); // Ok. +in_array( needle: 1, haystack: $haystack ); // Warning. + +array_keys( array: $testing ); // Ok. +array_keys( strict: false, array: $testing ); // Ok, will cause fatal error, but that's not the concern of this sniff. +array_keys( $testing, filter_value: 'my_key' ); // Warning. +array_keys( $testing, strict: false, filter_value: 'my_key' ); // Warning. + +// Safeguard that comments in the parameter are ignored. +in_array( 1, array( '1', 1 ), /* strict */ TRUE ); // Ok. +array_search( + 'needle', + $haystack, + true // Use strict typing. +); // Ok. diff --git a/WordPress/Tests/PHP/StrictInArrayUnitTest.php b/WordPress/Tests/PHP/StrictInArrayUnitTest.php index 7394afeda5..41aa9d37c4 100644 --- a/WordPress/Tests/PHP/StrictInArrayUnitTest.php +++ b/WordPress/Tests/PHP/StrictInArrayUnitTest.php @@ -14,17 +14,18 @@ /** * Unit test class for the StrictInArray sniff. * - * @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. + * @covers \WordPressCS\WordPress\AbstractFunctionParameterSniff + * @covers \WordPressCS\WordPress\Sniffs\PHP\StrictInArraySniff */ -class StrictInArrayUnitTest extends AbstractSniffUnitTest { +final class StrictInArrayUnitTest 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(); @@ -33,7 +34,7 @@ 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( @@ -48,7 +49,9 @@ public function getWarningList() { 35 => 1, 36 => 1, 37 => 1, + 44 => 1, + 48 => 1, + 49 => 1, ); } - } diff --git a/WordPress/Tests/PHP/TypeCastsUnitTest.inc b/WordPress/Tests/PHP/TypeCastsUnitTest.inc index e5b6f78b92..730912fc4b 100644 --- a/WordPress/Tests/PHP/TypeCastsUnitTest.inc +++ b/WordPress/Tests/PHP/TypeCastsUnitTest.inc @@ -10,8 +10,8 @@ $a = (object) $b; $a = (double) $b; $a = (real) $b; +$a = (unset) $b; // Error. // Warning: Discouraged casts. -$a = (unset) $b; // Warning. $a = (binary) $b; // Warning. $a = b"binary string"; // Warning. $a = b"binary $string"; // Warning. @@ -25,5 +25,5 @@ $a = (object ) $b; $a = (double ) $b; // Error. $a = ( real ) $b; // Error. -$a = ( unset ) $b; // Warning. +$a = ( unset ) $b; // Error. $a = ( binary ) $b; // Warning. diff --git a/WordPress/Tests/PHP/TypeCastsUnitTest.inc.fixed b/WordPress/Tests/PHP/TypeCastsUnitTest.inc.fixed index 051cb4a459..a45b98de20 100644 --- a/WordPress/Tests/PHP/TypeCastsUnitTest.inc.fixed +++ b/WordPress/Tests/PHP/TypeCastsUnitTest.inc.fixed @@ -10,8 +10,8 @@ $a = (object) $b; $a = (float) $b; $a = (float) $b; +$a = (unset) $b; // Error. // Warning: Discouraged casts. -$a = (unset) $b; // Warning. $a = (binary) $b; // Warning. $a = b"binary string"; // Warning. $a = b"binary $string"; // Warning. @@ -25,5 +25,5 @@ $a = (object ) $b; $a = (float) $b; // Error. $a = (float) $b; // Error. -$a = ( unset ) $b; // Warning. +$a = ( unset ) $b; // Error. $a = ( binary ) $b; // Warning. diff --git a/WordPress/Tests/PHP/TypeCastsUnitTest.php b/WordPress/Tests/PHP/TypeCastsUnitTest.php index 9349f1b838..878b089927 100644 --- a/WordPress/Tests/PHP/TypeCastsUnitTest.php +++ b/WordPress/Tests/PHP/TypeCastsUnitTest.php @@ -10,43 +10,42 @@ namespace WordPressCS\WordPress\Tests\PHP; use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; -use WordPressCS\WordPress\PHPCSHelper; /** * Unit test class for the TypeCasts sniff. * - * @package WPCS\WordPressCodingStandards - * * @since 1.2.0 + * + * @covers \WordPressCS\WordPress\Sniffs\PHP\TypeCastsSniff */ -class TypeCastsUnitTest extends AbstractSniffUnitTest { +final class TypeCastsUnitTest 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( 10 => 1, 11 => 1, + 13 => 1, 26 => 1, 27 => 1, + 28 => 1, ); } /** * 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( - 14 => 1, 15 => 1, - 16 => ( version_compare( PHPCSHelper::get_version(), '3.4.0', '<' ) === true ? 0 : 1 ), + 16 => 1, 17 => 1, - 28 => 1, 29 => 1, ); } diff --git a/WordPress/Tests/PHP/YodaConditionsUnitTest.php b/WordPress/Tests/PHP/YodaConditionsUnitTest.php index cab9a34b36..3cb8604025 100644 --- a/WordPress/Tests/PHP/YodaConditionsUnitTest.php +++ b/WordPress/Tests/PHP/YodaConditionsUnitTest.php @@ -14,17 +14,17 @@ /** * Unit test class for the YodaConditions 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\PHP\YodaConditionsSniff */ -class YodaConditionsUnitTest extends AbstractSniffUnitTest { +final class YodaConditionsUnitTest 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( @@ -53,10 +53,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/Security/EscapeOutputUnitTest.1.inc b/WordPress/Tests/Security/EscapeOutputUnitTest.1.inc new file mode 100644 index 0000000000..cfe20caeee --- /dev/null +++ b/WordPress/Tests/Security/EscapeOutputUnitTest.1.inc @@ -0,0 +1,657 @@ + + + + +

+

+

+ + '; + +echo ent2ncr( $text ); // Bad. + +echo number_format( 1024 ); + +echo ent2ncr( esc_html( $_data ) ); + +echo $foo ? $foo : 'no foo'; // Bad. +echo empty( $foo ) ? 'no foo' : $foo; // Bad. +echo $foo ? esc_html( $foo ) : 'no foo'; // Ok. + +echo 4; // Ok. + +exit( $foo ); // Bad. +exit( esc_html( $foo ) ); // Ok. + +die( $foo ); // Bad. +die( esc_html( $foo ) ); // Ok. + +printf( 'Hello %s', $foo ); // Bad. +printf( 'Hello %s', esc_html( $foo ) ); // Ok. +printf( 'Hello %s! Hi %s!', esc_html( $foo ), $bar ); // Bad. + +vprintf( 'Hello %s', array( $foo ) ); // Bad. +vprintf( 'Hello %s', array( esc_html( $foo ) ) ); // Ok. + +// The below checks that functions which are marked as needed further sanitization +// don't spill over into later arguments when nested in a function call. There was +// a bug which would cause line 84 to be marked as needing sanitization because _x() +// is marked as needing sanitization. +do_something( + _x( 'Some string', 'context', 'domain' ) + , array( $foo ) // Ok. +); + +// There was a bug where an empty exit followed by other code would give an error. +if ( ! defined( 'ABSPATH' ) ) { + exit; // Ok. +} else { + other(); +} + +printf( + /* translators: this comment is just for you. */ + esc_html__( 'Hello %s.', 'domain' ) + , 'world' + // There were other long arguments down here "in real life", which is why this was multi-line. +); + +wp_die( $message ); // Bad. +wp_die( esc_html( $message ) ); // Ok. +wp_die( esc_html( $message ), $title ); // Bad. +wp_die( esc_html( $message ), esc_html( $title ) ); // Ok. +wp_die( esc_html( $message ), '', array( 'back_link' => true ) ); // Ok. +wp_die( esc_html( $message ), '', array( 'back_link' => false ) ); // Ok. +wp_die( esc_html( $message ), '', array( 'response' => 200 ) ); // Ok. + +echo '

', esc_html( $foo ), '

'; // Ok. +echo 'a', 'b'; // Ok. +echo 'Hello, ', $foo; // Bad. +echo esc_html( $foo ), $bar; // Bad. +echo (int) $foo, $bar; // Bad. +echo (int) get_post_meta( $post_id, SOME_NUMBER, true ), do_something( $else ); // Bad. + +wp_die( -1 ); // Ok. + +?> +

+ + +' . sprintf( esc_html__( 'Some text -> %sLink text%s', 'textdomain' ), '', '' ). '

'; // Ok. + +echo '
' . sprintf( esc_html__( 'Found %d results', 'textdomain' ), (int) $result_count ) . '

'; // Ok. + +echo sprintf( 'Hello %s', $foo ); // Bad. +echo sprintf( 'Hello %s', esc_html( $foo ) ); // Ok. +echo sprintf( 'Hello %s! Hi %s!', esc_html( $foo ), $bar ); // Bad. + +echo vsprintf( 'Hello %s', array( $foo ) ); // Bad. +echo vsprintf( 'Hello %s', array( esc_html( $foo ) ) ); // Ok. + +echo sprintf( __( 'Welcome to Genesis %s', 'genesis' ), PARENT_THEME_BRANCH ); // Bad x 2. +echo sprintf( esc_html__( 'Welcome to Genesis %s', 'genesis' ), esc_html( PARENT_THEME_BRANCH ) ); // Ok. + +echo esc_html( strval( $_var ) ? $_var : gettype( $_var ) ); // Ok. +echo ( $is_hidden ) ? ' style="display:none;"' : ''; // Ok. +echo sprintf( 'Howdy, %s', esc_html( $name ? $name : __( 'Partner' ) ) ); // Ok. + +_e( 'Something' ); // Bad. +esc_html_e( 'Something' ); // Ok. + +echo $something // Bad. + . esc_attr( 'baz-' // Rest is OK. + . $r + . ( $r === $active_round ? ' foo' : '' ) + . ( $r < $active_round ? ' bar' : '' ) + ) . 'something'; + +echo implode( '
', $items ); // Bad. +echo implode( '
', urlencode_deep( $items ) ); // Ok. +echo implode( '
', array_map( 'esc_html', $items ) ); // Ok. +echo implode( '
', array_map( 'foo', $items ) ); // Bad. +echo join( '
', $items ); // Bad. +echo join( '
', Array_Map( 'esc_html', $items ) ); // Ok. + +echo ''; + +_deprecated_hook( 'some_filter', '1.3.0', esc_html__( 'The $arg is deprecated.' ), 'some_other_filter' ); // Ok. +_deprecated_hook( "filter_{$context}", '1.3.0', __( 'The $arg is deprecated.' ), sprintf( __( 'Some parsed message %s', $variable ) ) ); // Bad. + + + +/* + * Test using custom properties, setting & unsetting (resetting). + */ +// phpcs:set WordPress.Security.EscapeOutput customPrintingFunctions[] to_screen,my_print +to_screen( $var1, esc_attr( $var2 ) ); // Bad x 1. +my_print( $var1, $var2 ); // Bad x 2. + +// phpcs:set WordPress.Security.EscapeOutput customEscapingFunctions[] esc_form_field +// phpcs:set WordPress.Security.EscapeOutput customAutoEscapedFunctions[] post_info,cpt_info + +echo esc_form_field( $var ); // Ok. +echo post_info( $post_id, 'field' ); // Ok. +echo cpt_info( $post_type, 'query' ); // Ok. +to_screen( esc_form_field( $var1), esc_attr( $var2 ) ); // Ok. + +// phpcs:set WordPress.Security.EscapeOutput customPrintingFunctions[] +// phpcs:set WordPress.Security.EscapeOutput customEscapingFunctions[] +// phpcs:set WordPress.Security.EscapeOutput customAutoEscapedFunctions[] + +echo esc_form_field( $var ); // Bad. +echo post_info( $post_id, 'field' ); // Bad. +echo cpt_info( $post_type, 'query' ); // Bad. + +echo (unset) $var; // Ok. + +// Nowdocs are OK. +echo <<<'EOD' +Some Raw String +EOD; + +echo 1.234; // Ok. + +echo ( 1.234 + 10 + 2.5 ); // Ok. +echo 10 % 2; // Ok. +echo 8 * 1.2; // Ok. + +?> + + + +foo ?> + 'menu genesis-nav-menu menu-footer', + 'theme_location' => 'footer', + ] + ) + ); +} + +?> + +print = new \Printer(); + $obj->exit->customExit(); + return $obj->print->transform( 'something' ); +} + +class Silly { + function echo() {} + function print() {} +} + +echo // phpcs:ignore WP.Secur1ty.EscapeOutput -- WPCS: XSS ok. (sniff name mangled on purpose). + esc_html( $something ), + $something_else, + esc_html( $something_more ); + +echo esc_html( $something ), + $something_else, + esc_html( $something_more ); // phpcs:ignore WP.Secur1ty.EscapeOutput -- WPCS: XSS ok. (sniff name mangled on purpose). + +echo get_the_title(); // Bad. +echo wp_kses_post( get_the_title() ); // Ok. +echo esc_html( get_the_title() ); // Ok. + +echo implode( '
', map_deep( $items, 'esc_html' ) ); // Ok. +echo implode( '
', map_deep( $items, 'foo' ) ); // Bad. + +_deprecated_file( basename( __FILE__ ), '1.3.0' ); // Ok. +_deprecated_file( $file, '1.3.0' ); // Error. + +trigger_error(); // Ignore. +_deprecated_file(); // Ignore. + +\_deprecated_file( \basename( __FILE__ ), '1.3.0' ); // Ok. + +// Issue #1246. +echo antispambot( 'john.doe@mysite.com' ); // OK. +echo antiSpambot( esc_html( $email ) ); // OK. +echo antispambot( $email ); // Bad. + +/* + * Safeguard support for PHP 8.0+ named parameters for array walking functions. + */ +echo implode( '
', map_deep( callback: 'esc_html', value: $items ) ); // Ok. +echo implode( '
', map_deep( value: $items ) ); // Bad, missing callback param, so escaping can not be verified. +echo implode( '
', map_deep( call_back: 'esc_html', value: $items ) ); // Bad, wrong param name, so escaping can not be verified. +echo implode( '
', map_deep( callback: 'foo', value: $items, ) ); // Bad, non-escaping function as callback. + +// Note: named params not supported due to the `...$arrays` in array_map()`, but that's not the concern of this sniff. +echo implode( '
', array_map( array: $items, callback: 'esc_html', ) ); // Ok. +echo implode( '
', array_map( array: $items, callback: 'foo', ) ); // Bad. + +// Operators should be ignored. +print 10 ** 20; +print 10 & 20; +print 10 | 20; +print 10 ^ 20; +print 10 << 20; +print 10 >> 20; +print 10 == 20; +print 10 != 20; +print 10 === 20; +print 10 !== 20; +print 10 < 20; +print 10 > 20; +print 10 <= 20; +print 10 >= 20; +print 10 <=> 20; +print ! 'hello'; +print 'hello' && 'world'; +print 'hello' || 'world'; +print 'hello' and 'world'; +print 'hello' or 'world'; +print 'hello' xor 'world'; +print 10++; +print --10; + +// This includes the PHP 7.0+ null coalesce operator. +echo $var ?? 'default'; // Bad. +echo esc_html( $var ?? 'default' ); // OK. + +// Make sure the sniff does not get confused over constants/properties using the same name as one of the target functions. +$a = _ex; // OK, constant, not function call. +$a = $obj->wp_dropdown_pages; // OK, property access, not function call. +use function wp_dropdown_pages; // OK, import use statement, not function call. + +// Make sure the sniff does not get confused over methods/namespaced functions etc vs global function calls. +$obj->_deprecated_file( $file, '1.3.0' ); // OK. +$obj?->_deprecated_file( $file, '1.3.0' ); // OK. +MyClass::_deprecated_file( $file, '1.3.0' ); // OK. +My\NS\_ex( $some_nasty_var, 'context' ); // OK. +class IgnoreFunctionDeclarations { + function wp_die( $foo ) {} // OK. + function &trigger_error( $foo ) {} // OK. +} +$obj = new User_Error( $foo ); // OK. + +// Make sure special casing of select functions is handled case-insensitively. +Trigger_ERROR( 'This is fine', $second_param_should_be_ignored ); // OK. +_Deprecated_File( basename( __FILE__ ), '1.3.0' ); // OK. +_EX( 'all_params_should_be_ignored_if_function_is_reported_as_unsafe', 'another_param' ); // Bad x 1 for unsafe function. + +// Allow for comments in the $file parameter. +_deprecated_file( + /* comment */ + basename( __FILE__ ), + '1.3.0' +); // Ok. + +// Exit/die should only be examined when there are parentheses. +$var = (true || exit ) && empty( $bar ) ? $drink_alone : $drink_together; // OK, exit is not passing status. +$var = (true or die ) and empty( $bar ) ? $drink_alone : $drink_together; // OK, die is not passing status. +$var = exit( $var ? $ok : $error ); // Bad x 2. + +// Print can be used in expressions, so end of statement can be all sorts. +if ( print "hello" ) // OK. + $var = 'foo'; // OK, not part of the print. + +if ( print("hello") && $var ) // Bad x1, `$var`. + $var = 'foo'; // OK, not part of the print. + +// Bug #2209. +( 'auto' === $key ) ? print ' disabled ' : print ' enabled '; // OK. +( $fop === 1 ) ? print $foo : print $bar; // Bad x 2, each print statement should be examined separately. +( 1 === $foo ) ? print ( $foo ? 'ten' : $twenty ) : print $bar; // Bad x 2, $twenty & $bar. + +// Ensure nesting levels are handled correctly. +( ( $fop === 1 ) ? print ( $var ? 10 : 20 ) : print 100 ); // OK. + +// Ensure print statements with ternaries and without wrapping parentheses are handled correctly. +( 1 === 1 ) ? print ( $foo ? 'ten' : 'twenty' ) . $baz : print $bar; // Bad x 2, $baz and $bar. +$var = true && print $foo ? $bar : $baz; // Bad x 2, `$bar` and `$baz` should be flagged, not $foo. +print $foo ? 'ten' : 'twenty'; // OK. + +// Ensure print statements with ternaries, without wrapping parentheses, but nested within parentheses are also handled correctly. +if ( print $foo ? 'ten' : 'twenty' ) {} // OK. +if ( print $foo ? $bar : $baz ) {} // Bad x 2, `$bar` and `$baz` should be flagged, not $foo. +$var = ( ( $fop === 1 ) ? ( print $foo ? 'ten' : $twenty ) : print $bar ); // Bad x 2, $twenty & $bar. + +// Ternary statements can be chained and nested. +echo ( ! empty( $var ) && ( $var > 10 ? $foo : $bar ) ) ? 'go' : 'stop'; // OK. +echo isset( $var[ $keyA ? $keyA : $keyB ] ) ? 'go' : 'stop'; // OK. +echo '' !== implode( '', [ $valueA ? $valueA : $valueB, $valueC ? $valueD : $valueE ] ) ? 'go' : 'stop'; // OK. +echo '' !== ${true ? $foo : $bar} ? 'go' : 'stop'; // OK. + +// Bug #677 (and #1507C). +echo ( ! empty( $my_bc_title ) ) ? wp_kses( $my_bc_title, allowed_tags() ) : (10 + 20); // OK. +echo ( ! empty( $my_bc_title ) ) ? wp_kses( $my_bc_title, allowed_tags() ) : get_the_title(); // Bad, should flag get_the_title(), not `!`. +// Without parentheses wrapping the empty(), this was already okay. +echo ! empty( $my_bc_title ) ? wp_kses( $my_bc_title, allowed_tags() ) : (10 + 20); // OK. +echo ( $is_mobile ) ? wp_json_encode( 'true' ) : wp_json_encode( 'false' ); // OK. + +// Bug #1219. +echo ( $webinar->is_too_late_to_register ? '' : '' ); // OK. +echo $webinar->is_too_late_to_register ? '' : ''; // OK. +array_walk( + $upcoming_webinars, + function ( $webinar ) { + echo ( $webinar->is_too_late_to_register ? '' : '' ); + } +); // OK. + +// Bug #1617: code before a ternary should not be ignored if a short ternary is used. +echo $var ?: ''; // Bad, $var needs escaping. +echo $var ? /*comment*/ : ''; // Bad, $var needs escaping. +array_walk( + $upcoming_webinars, + function ( $webinar ) { + echo ( $webinar->is_too_late_to_register ? : '' ); + } +); // Bad. + +echo ESC_HTML . $var . unrelatedFunction( $var ); // Bad x 3. + +// Bug #677#issuecomment-470407780: Parameters should be examined individually. +printf( + '
  • %5$s
  • ', + ( '' !== $this->link_title ) ? ' title="' . esc_attr( $this->link_title ) . '"' : '', + ( '' !== $this->link_aria_label ) ? ' aria-label="' . esc_attr( $this->link_aria_label ) . '"' : '', +); // OK. + +// The fix for #677 will also prevent false positives on parameter labels. +wp_die( + title: 'label', + message: 'error message', +); + +// Safeguard support for PHP 7.4+ numeric literals with underscores and PHP 8.1 octal literals +// (and throw in some other non-decimal numbers as well). +echo 1_000_000 + 2_0_0 + 0o12 + 0o20_00, 0XAB953C, 6.674_083e+11, 0b1010; + +/* + * Safeguard handling of PHP 8.0+ function calls with named parameters for [trigger|user]_error(). + */ +user_error( error_level: E_USER_NOTICE ); // OK, well not really, required $message parameter missing, but that's not our concern. +trigger_error( + messege: "There was an error: {$message}", + error_level: E_USER_NOTICE, +); // OK, well not really, typo in $message param name, but that's not our concern. +user_error( + error_level: E_USER_WARNING, + message: 'There was an error: ' . esc_html( $message ), +); // OK. +trigger_error( + error_level: E_USER_WARNING, + message: "There was an error: {$message}", +); // Bad. + +/* + * Safeguard handling of PHP 8.0+ function calls with named parameters for _deprecated_file(). + */ +_deprecated_file( version: '1.3.0', file: basename( __FILE__ ) ); // Ok. +_deprecated_file( version: '1.3.0', files: basename( __FILE__ ) ); // Error, well not really, typo in $file param name, but that's not our concern. +_deprecated_file( version: '1.3.0', replacement: $name ); // Error. $file param missing, but that's not our concern. +_deprecated_file( + replacement: $name, + version: $version, + file: $file, +); // Error x 3. + +/* + * Safeguard handling of PHP 8.0+ match expression. + */ +echo esc_html( match($var) { + $a, $b, $c => $fine, + default => $value + $other, +}); // OK. + +echo (int) match($var) { + $a, $b, $c => $fine, + default => $value + $other, +}; // OK. + +echo match($var) { // OK. + $nr => 10 + 20, // OK. + $a, $b, $c => 'this line should NOT be flagged, vars are conditions, not output', // OK. + 'my array' => [ 1, 2, 'foo' ], // OK. + 'some value', $key, 'more' => esc_html($escaped), // OK. + 'callback' => match($foo) { + 10 => 10, + default => 20, + }, // OK. + false => ( $cond ? esc_html($valueA) : \esc_attr($valueB) ), // OK. + 101 => sprintf( + 'some %s format %d', + esc_html( $text ), // OK. + (int) $nr, // OK. + ), + default => esc_attr($value + $other), // OK. +}; + +echo match($var) { // OK. + 'some value', 'more' => $this_line_SHOULD_be_flagged['key'], // Bad x 1. + 'other value' => $cond ? $valueA : $valueB['key'], // Bad x 2. + false => ( $cond ? $valueA : $valueB ), // Bad x 2. + 101 => sprintf( + 'some %s format %d', + $text, // Bad. + $nr, // Bad. + ), + default => $value . $other, // Bad x 2. +}; + +echo match($var) { // OK. + 'some value', 'more' => $this_line_SHOULD_be_flagged // Bad x 1. Note: no trailing comma! +}; + +// Bug #1989: allow for Name::class and PHP 8.0+ $obj::class. +_deprecated_function( __METHOD__, 'x.x.x', ClassName::class ); // OK. +die( self::CLASS . ' has been abandoned' ); // OK. +_deprecated_function( __METHOD__, 'x.x.x', parent::Class ); // OK. +_deprecated_function( __METHOD__, 'x.x.x', static::class ); // OK. +echo 'Do not use ' . $object::class ); // OK. + +/* + * Examine the parameters passed for exception creation via throw. + */ +throw new MyException(); // OK. +throw new Exception( esc_html( $message ), (int) $code ); // OK. +throw new /*comment*/ parent( esc_html( $message ), (int) $code ); // OK. +throw MyException::get( esc_html( $message ), (int) $code ); // OK. +throw $obj->getException( esc_html( $message ), (int) $code ); // OK. + +throw new Exception( $message, $code ); // Bad x 2. +throw new self( $message, $code ); // Bad x 2. +throw + MyException :: get /*comment*/ ( $message, $code ); // Bad x 2. +throw $obj?->getException( $message, $code ); // Bad x 2. +throw new $exceptionName( $message, $code ); // Bad x 2. + +throw static::get( esc_html( $message ), $code ); // Bad x 1. +throw \Vendor\Name\MyException::get( $message, (int) $code ); // Bad x 1. +throw Name\MyException::get( esc_html( $message ), $code ); // Bad x 1. +throw namespace\MyException::get( $message, (int) $code ); // Bad x 1. +throw new class('text', 0) extends Exception {}; // OK. + +// Since PHP 8.0, throw can be used as an expression. +$var = ( ( $fop === 1 ) ? throw new Exception( esc_html( $message ), (int) $code ) : $not_part_of_the_throw ); // OK. +$var = ( ( $fop === 1 ) ? throw /*comment*/ new Exception( $message ) : $not_part_of_the_throw; // Bad x 1. + +// The following should be ignored as this is not exception creation, so we don't have access to the parameters. +throw $stored_exceptions['key']; +throw $obj->stored_exception; + +// We should also ignore any exception which is being caught straight away. +try { + throw new Exception( $message, $code ); // OK, as exception is being caught. +} catch ( Exception $e ) { +} finally { +} + +// .. but only if it is not within a closed scope nested within the try. +try { + $callback = function() { + throw new Exception( $message, 10 ); // Bad. Unclear if the exception will be caught or not. + }; +} catch ( Exception $e ) { +} + +// Bug #1861 - don't throw an error for expression examined separately. +echo $var == 'foo' ? 'bar' : die( 'world' ); // OK. +echo $var == 'foo' ? throw new Exception( 'message' ) : 'bar'; // OK. +echo $var == 'foo' ? print 'message' : 'bar'; // OK. + +echo $var == 'foo' ? 'bar' : die( $var ); // Bad x 1, $var, not die(). +echo $var == 'foo' ? throw new Exception( $var ) : 'bar'; // Bad x 1, $var, not throw. +echo $var == 'foo' ? print $var : 'bar'; // Bad x 1, $var, not print. + +/* + * When the `UnsafePrintingFunction` error code is ignored, the parameters + * for the unsafe functions should still be examined, but only the $text + * parameter needs escaping. + */ +// phpcs:disable WordPress.Security.EscapeOutput.UnsafePrintingFunction +_e( $text, $domain ); // Bad x 1, only text param. +_e( domain: $domain ); // OK, well not really, required $text parameter missing, but that's not our concern. +_ex( domain: $domain, text: 'plain text', context: $context ); // OK. +_e( domain: $domain, text: $text, ); // Bad x 1, only text param. +// phpcs:enable + +// Array keys and values should be examined individually. +wp_die( esc_html( $message ), '', array() ); // OK. +wp_die( + esc_html( $message ), // OK. + '', + array( + 'back_link' => true, // OK. + 'response' => $response ?? 200, // Bad. + 'link_url' => ( '' !== $this->link_url ) ? $this->link_url : '', // Bad. + 'link_text' => ( '' !== $link_title ) ? esc_attr( $link_title ) : '', // OK. + 'charset' => [$set, $rtl] = $array, // Bad x 2 (silly code, but the list shouldn't be treated as an array). + 'exit' => $do_exit, // Bad x 1. + ) +); + +// Heredocs should only be flagged when they contain interpolated variables or expressions. +echo <<interpolation}. +Some text without interpolation. +EOD; + +// Parameters in formatting functions should be examined individually. +echo sprintf( + '
  • %5$s
  • ', + ( '' !== $this->link_title ) ? ' title="' . esc_attr( $this->link_title ) . '"' : '', + ( '' !== $this->link_aria_label ) ? ' aria-label="' . $this->link_aria_label . '"' : '', +); // Bad x 1. + +echo ''; // OK. +echo ''; // OK. +echo ''; // Bad. +echo ''; // Bad. +echo ''; // OK, well not really, typo in param name, but that's not our concern. +echo ''; // Bad. diff --git a/WordPress/Tests/Security/EscapeOutputUnitTest.10.inc b/WordPress/Tests/Security/EscapeOutputUnitTest.10.inc new file mode 100644 index 0000000000..149338a445 --- /dev/null +++ b/WordPress/Tests/Security/EscapeOutputUnitTest.10.inc @@ -0,0 +1,4 @@ + + $fine, diff --git a/WordPress/Tests/Security/EscapeOutputUnitTest.15.inc b/WordPress/Tests/Security/EscapeOutputUnitTest.15.inc new file mode 100644 index 0000000000..e7e7706efc --- /dev/null +++ b/WordPress/Tests/Security/EscapeOutputUnitTest.15.inc @@ -0,0 +1,7 @@ +interpolation}. + Some text without interpolation. + EOD; // Bad. diff --git a/WordPress/Tests/Security/EscapeOutputUnitTest.3.inc b/WordPress/Tests/Security/EscapeOutputUnitTest.3.inc new file mode 100644 index 0000000000..f4cc5d9fec --- /dev/null +++ b/WordPress/Tests/Security/EscapeOutputUnitTest.3.inc @@ -0,0 +1,5 @@ + diff --git a/WordPress/Tests/Security/EscapeOutputUnitTest.8.inc b/WordPress/Tests/Security/EscapeOutputUnitTest.8.inc new file mode 100644 index 0000000000..05a328ec13 --- /dev/null +++ b/WordPress/Tests/Security/EscapeOutputUnitTest.8.inc @@ -0,0 +1,5 @@ + - - - -

    -

    -

    - - '; - -echo ent2ncr( $text ); // Bad. - -echo number_format( 1024 ); - -echo ent2ncr( esc_html( $_data ) ); - -echo $foo ? $foo : 'no foo'; // Bad. -echo empty( $foo ) ? 'no foo' : $foo; // Bad. -echo $foo ? esc_html( $foo ) : 'no foo'; // Ok. - -echo 4; // Ok. - -exit( $foo ); // Bad. -exit( esc_html( $foo ) ); // Ok. - -die( $foo ); // Bad. -die( esc_html( $foo ) ); // Ok. - -printf( 'Hello %s', $foo ); // Bad. -printf( 'Hello %s', esc_html( $foo ) ); // Ok. -printf( 'Hello %s! Hi %s!', esc_html( $foo ), $bar ); // Bad. - -vprintf( 'Hello %s', array( $foo ) ); // Bad. -vprintf( 'Hello %s', array( esc_html( $foo ) ) ); // Ok. - -// The below checks that functions which are marked as needed further sanitization -// don't spill over into later arguments when nested in a function call. There was -// a bug which would cause line 84 to be marked as needing sanitization because _x() -// is marked as needing sanitization. -do_something( - _x( 'Some string', 'context', 'domain' ) - , array( $foo ) // Ok. -); - -// There was a bug where an empty exit followed by other code would give an error. -if ( ! defined( 'ABSPATH' ) ) { - exit; // Ok. -} else { - other(); -} - -printf( - /* translators: this comment is just for you. */ - esc_html__( 'Hello %s.', 'domain' ) - , 'world' - // There were other long arguments down here "in real life", which is why this was multi-line. -); - -wp_die( $message ); // Bad. -wp_die( esc_html( $message ) ); // Ok. -wp_die( esc_html( $message ), $title ); // Bad. -wp_die( esc_html( $message ), esc_html( $title ) ); // Ok. -wp_die( esc_html( $message ), '', array( 'back_link' => true ) ); // Ok. -wp_die( esc_html( $message ), '', array( 'back_link' => false ) ); // Ok. -wp_die( esc_html( $message ), '', array( 'response' => 200 ) ); // Ok. - -echo '

    ', esc_html( $foo ), '

    '; // Ok. -echo 'a', 'b'; // Ok. -echo 'Hello, ', $foo; // Bad. -echo esc_html( $foo ), $bar; // Bad. -echo (int) $foo, $bar; // Bad. -echo (int) get_post_meta( $post_id, SOME_NUMBER, true ), do_something( $else ); // Bad. - -wp_die( -1 ); // Ok. - -?> -

    - - -' . sprintf( esc_html__( 'Some text -> %sLink text%s', 'textdomain' ), '', '' ). '

    '; // Ok. - -echo '
    ' . sprintf( esc_html__( 'Found %d results', 'textdomain' ), (int) $result_count ) . '

    '; // Ok. - -echo sprintf( 'Hello %s', $foo ); // Bad. -echo sprintf( 'Hello %s', esc_html( $foo ) ); // Ok. -echo sprintf( 'Hello %s! Hi %s!', esc_html( $foo ), $bar ); // Bad. - -echo vsprintf( 'Hello %s', array( $foo ) ); // Bad. -echo vsprintf( 'Hello %s', array( esc_html( $foo ) ) ); // Ok. - -echo sprintf( __( 'Welcome to Genesis %s', 'genesis' ), PARENT_THEME_BRANCH ); // Bad x 2. -echo sprintf( esc_html__( 'Welcome to Genesis %s', 'genesis' ), esc_html( PARENT_THEME_BRANCH ) ); // Ok. - -echo esc_html( strval( $_var ) ? $_var : gettype( $_var ) ); // Ok. -echo ( $is_hidden ) ? ' style="display:none;"' : ''; // Ok. -echo sprintf( 'Howdy, %s', esc_html( $name ? $name : __( 'Partner' ) ) ); // Ok. - -_e( 'Something' ); // Bad. -esc_html_e( 'Something' ); // Ok. - -echo $something // Bad. - . esc_attr( 'baz-' // Rest is OK. - . $r - . ( $r === $active_round ? ' foo' : '' ) - . ( $r < $active_round ? ' bar' : '' ) - ) . 'something'; - -echo implode( '
    ', $items ); // Bad. -echo implode( '
    ', urlencode_deep( $items ) ); // Ok. -echo implode( '
    ', array_map( 'esc_html', $items ) ); // Ok. -echo implode( '
    ', array_map( 'foo', $items ) ); // Bad. -echo join( '
    ', $items ); // Bad. -echo join( '
    ', array_map( 'esc_html', $items ) ); // Ok. - -echo ''; - -_deprecated_hook( 'some_filter', '1.3.0', esc_html__( 'The $arg is deprecated.' ), 'some_other_filter' ); // Ok. -_deprecated_hook( "filter_{$context}", '1.3.0', __( 'The $arg is deprecated.' ), sprintf( __( 'Some parsed message %s', $variable ) ) ); // Bad. - -echo add_filter( get_the_excerpt( get_the_ID() ) ; // Bad, but ignored as code contains a parse error. - -/* - * Test using custom properties, setting & unsetting (resetting). - */ -// phpcs:set WordPress.Security.EscapeOutput customPrintingFunctions[] to_screen,my_print -to_screen( $var1, esc_attr( $var2 ) ); // Bad x 1. -my_print( $var1, $var2 ); // Bad x 2. - -// phpcs:set WordPress.Security.EscapeOutput customEscapingFunctions[] esc_form_field -// phpcs:set WordPress.Security.EscapeOutput customAutoEscapedFunctions[] post_info,cpt_info - -echo esc_form_field( $var ); // Ok. -echo post_info( $post_id, 'field' ); // Ok. -echo cpt_info( $post_type, 'query' ); // Ok. -to_screen( esc_form_field( $var1), esc_attr( $var2 ) ); // Ok. - -// phpcs:set WordPress.Security.EscapeOutput customPrintingFunctions[] -// phpcs:set WordPress.Security.EscapeOutput customEscapingFunctions[] -// phpcs:set WordPress.Security.EscapeOutput customAutoEscapedFunctions[] - -echo esc_form_field( $var ); // Bad. -echo post_info( $post_id, 'field' ); // Bad. -echo cpt_info( $post_type, 'query' ); // Bad. - -echo (unset) $var; // Ok. - -// Nowdocs are OK. -echo <<<'EOD' -Some Raw String -EOD; - -echo 1.234; // Ok. - -echo ( 1.234 + 10 + 2.5 ); // Ok. -echo 10 % 2; // Ok. -echo 8 * 1.2; // Ok. - -?> - - - -foo ?> - 'menu genesis-nav-menu menu-footer', - 'theme_location' => 'footer', - ] - ) - ); -} - -?> - -print = new \Printer(); - $obj->exit->customExit(); - return $obj->print->transform( 'something' ); -} - -class Silly { - function echo() {} - function print() {} -} - -echo // phpcs:ignore WP.Secur1ty.EscapeOutput -- WPCS: XSS ok. (sniff name mangled on purpose). - esc_html( $something ), - $something_else, - esc_html( $something_more ); - -echo esc_html( $something ), - $something_else, - esc_html( $something_more ); // phpcs:ignore WP.Secur1ty.EscapeOutput -- WPCS: XSS ok. (sniff name mangled on purpose). - -echo get_the_title(); // Bad. -echo wp_kses_post( get_the_title() ); // Ok. -echo esc_html( get_the_title() ); // Ok. - -echo implode( '
    ', map_deep( $items, 'esc_html' ) ); // Ok. -echo implode( '
    ', map_deep( $items, 'foo' ) ); // Bad. - -_deprecated_file( basename( __FILE__ ), '1.3.0' ); // Ok. -_deprecated_file( $file, '1.3.0' ); // Error. diff --git a/WordPress/Tests/Security/EscapeOutputUnitTest.php b/WordPress/Tests/Security/EscapeOutputUnitTest.php index bd81edd244..6150a4eeb4 100644 --- a/WordPress/Tests/Security/EscapeOutputUnitTest.php +++ b/WordPress/Tests/Security/EscapeOutputUnitTest.php @@ -14,90 +14,183 @@ /** * Unit test class for the EscapeOutput sniff. * - * @package WPCS\WordPressCodingStandards + * @since 2013-06-11 + * @since 0.13.0 Class name changed: this class is now namespaced. + * @since 1.0.0 This sniff has been moved from the `XSS` category to the `Security` category. * - * @since 2013-06-11 - * @since 0.13.0 Class name changed: this class is now namespaced. - * @since 1.0.0 This sniff has been moved from the `XSS` category to the `Security` category. + * @covers \WordPressCS\WordPress\Helpers\ArrayWalkingFunctionsHelper + * @covers \WordPressCS\WordPress\Helpers\ContextHelper::get_safe_cast_tokens + * @covers \WordPressCS\WordPress\Helpers\ConstantsHelper::is_use_of_global_constant + * @covers \WordPressCS\WordPress\Helpers\EscapingFunctionsTrait + * @covers \WordPressCS\WordPress\Helpers\PrintingFunctionsTrait + * @covers \WordPressCS\WordPress\Sniffs\Security\EscapeOutputSniff */ -class EscapeOutputUnitTest extends AbstractSniffUnitTest { +final class EscapeOutputUnitTest 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( - 17 => 1, - 19 => 1, - 36 => 1, - 39 => 1, - 40 => 1, - 41 => 1, - 43 => 1, - 46 => 1, - 53 => 1, - 59 => 1, - 60 => 1, - 65 => 1, - 68 => 1, - 71 => 1, - 73 => 1, - 75 => 1, - 101 => 1, - 103 => 1, - 111 => 1, - 112 => 1, - 113 => 1, - 114 => 1, - 125 => 1, - 131 => 1, - 135 => 1, - 138 => 1, - 145 => 1, - 147 => 1, - 149 => 1, - 152 => 2, - 159 => 1, - 162 => 1, - 169 => 1, - 172 => 1, - 173 => 1, - 182 => 3, - 190 => 1, - 191 => 2, - 205 => 1, - 206 => 1, - 207 => 1, - 223 => 1, - 225 => 1, - 226 => 1, - 252 => 1, - 253 => 1, - 263 => 1, - 264 => 1, - 266 => 1, - 289 => 1, - 294 => 1, - 297 => 1, - ); + public function getErrorList( $testFile = '' ) { + switch ( $testFile ) { + case 'EscapeOutputUnitTest.1.inc': + return array( + 17 => 1, + 19 => 1, + 36 => 1, + 39 => 1, + 40 => 1, + 41 => 1, + 43 => 1, + 46 => 1, + 53 => 1, + 59 => 1, + 60 => 1, + 65 => 1, + 68 => 1, + 71 => 1, + 73 => 1, + 75 => 1, + 101 => 1, + 103 => 1, + 111 => 1, + 112 => 1, + 113 => 1, + 114 => 1, + 125 => 1, + 126 => 1, // Old-style WPCS ignore comments are no longer supported. + 127 => 1, // Old-style WPCS ignore comments are no longer supported. + 128 => 1, // Old-style WPCS ignore comments are no longer supported. + 131 => 1, + 135 => 1, + 138 => 1, + 145 => 1, + 147 => 1, + 149 => 1, + 152 => 2, + 159 => 1, + 162 => 1, + 169 => 1, + 172 => 1, + 173 => 1, + 182 => 3, + 190 => 1, + 191 => 2, + 205 => 1, + 206 => 1, + 207 => 1, + 223 => 1, + 225 => 1, + 226 => 1, + 241 => 1, // Old-style WPCS ignore comments are no longer supported. + 245 => 1, // Old-style WPCS ignore comments are no longer supported. + 249 => 1, // Old-style WPCS ignore comments are no longer supported. + 252 => 1, + 253 => 1, + 263 => 1, + 264 => 1, + 266 => 1, + 282 => 1, + 286 => 1, + 289 => 1, + 294 => 1, + 297 => 1, + 307 => 1, + 313 => 1, + 314 => 1, + 315 => 1, + 319 => 1, + 347 => 1, + 369 => 1, + 381 => 2, + 387 => 1, + 392 => 2, + 393 => 2, + 399 => 2, + 400 => 2, + 405 => 2, + 406 => 2, + 416 => 1, + 432 => 1, + 433 => 1, + 437 => 1, + 441 => 2, + 474 => 1, + 481 => 1, + 482 => 1, + 484 => 1, + 485 => 1, + 486 => 1, + 521 => 1, + 522 => 2, + 523 => 2, + 526 => 1, + 527 => 1, + 529 => 2, + 533 => 1, + 552 => 2, + 553 => 2, + 555 => 2, + 556 => 2, + 557 => 2, + 559 => 1, + 560 => 1, + 561 => 1, + 562 => 1, + 567 => 1, + 583 => 1, + 593 => 1, + 594 => 1, + 595 => 1, + 603 => 1, + 606 => 1, + 616 => 1, + 617 => 1, + 619 => 2, + 620 => 1, + 634 => 1, + 635 => 1, + 636 => 1, + 641 => 1, + 649 => 1, + 654 => 1, + 655 => 1, + 657 => 1, + ); + + case 'EscapeOutputUnitTest.6.inc': + case 'EscapeOutputUnitTest.7.inc': + return array( + 4 => 1, + ); + + case 'EscapeOutputUnitTest.20.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( + 18 => 1, + 19 => 1, + 20 => 1, + 25 => 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( - 126 => 1, // Whitelist comment deprecation warning. - 127 => 1, // Whitelist comment deprecation warning. - 128 => 1, // Whitelist comment deprecation warning. - 241 => 1, // Whitelist comment deprecation warning. - 243 => 1, // Whitelist comment deprecation warning. - 250 => 1, // Whitelist comment deprecation warning. - ); + return array(); } - } diff --git a/WordPress/Tests/Security/NonceVerificationUnitTest.inc b/WordPress/Tests/Security/NonceVerificationUnitTest.1.inc similarity index 52% rename from WordPress/Tests/Security/NonceVerificationUnitTest.inc rename to WordPress/Tests/Security/NonceVerificationUnitTest.1.inc index 41cf566594..cb8348b4ad 100644 --- a/WordPress/Tests/Security/NonceVerificationUnitTest.inc +++ b/WordPress/Tests/Security/NonceVerificationUnitTest.1.inc @@ -30,7 +30,7 @@ function foo() { function process() { do_something( $_POST['foo'] ); // Bad. - if ( ! isset( $_POST['test'] ) || ! wp_verify_nonce( 'some_action' ) ) { + if ( empty( $_POST['test'] ) || ! wp_verify_nonce( 'some_action' ) ) { exit; } @@ -41,7 +41,7 @@ class Some_Class { // Bad, needs nonce check. function bar() { - if ( ! isset( $_POST['test'] ) ) { // Bad. + if ( empty( $_POST['test'] ) ) { // Bad. return; } @@ -83,7 +83,7 @@ function foo_2() { $_POST['settings'][ $setting ] = 'bb'; // OK. } -// Particular cases can be whitelisted with a comment. +// Bad - ignored via old-style ignore comment. function foo_3() { bar( $_POST['var'] ); // WPCS: CSRF OK. bar( $_POST['var'] ); // Bad. @@ -91,7 +91,7 @@ function foo_3() { // We need to account for when there are multiple vars in a single isset(). function foo_4() { - if ( ! isset( $_POST['foo'], $_POST['bar'], $_POST['_wpnonce'] ) ) { // OK. + if ( ! isset( $_POST['foo'], $_FILES['bar'], $_POST['_wpnonce'] ) ) { // OK. return; } @@ -111,21 +111,11 @@ function sanitization_allowed() { function foo_5() { do_something( (int) $_POST['foo'] ); // Bad. - do_something( sanitize_key( $_POST['bar'] ) ); // Bad. + do_something( sanitize_key( $_FILES['bar'] ) ); // Bad. check_ajax_referer( 'something' ); } -// Test anonymous function - Bad, needs nonce check. -check_ajax_referer( 'something' ); // Nonce check is not in function scope. -$b = function () { - if ( ! isset( $_POST['abc'] ) ) { // Bad. - return; - } - - do_something( $_POST['abc'] ); // Bad. -}; - /* * Test using custom properties, setting & unsetting (resetting). */ @@ -165,7 +155,7 @@ function foo_8() { * Using a superglobal in a is_...() function is OK as long as a nonce check is done * before the variable is *really* used. */ -function test_whitelisting_use_in_type_test_functions() { +function test_ignoring_use_in_type_test_functions() { if ( ! is_numeric ( $_POST['foo'] ) ) { // OK. return; } @@ -235,7 +225,7 @@ function allow_for_nonce_check_within_switch() { } function allow_for_array_compare_before_noncecheck() { - if ( array_search( array( 'subscribe', 'unsubscribe', $_POST['action'], true ) // OK. + if ( array_search( array( 'subscribe', 'unsubscribe' ), $_POST['action'], true ) // OK. && wp_verify_nonce( $_POST['newsletter_nonce'] ) ) {} } @@ -265,12 +255,12 @@ function allow_for_unslash_in_sanitization() { echo $var; } -function dont_allow_bypass_nonce_via_sanitization() { +function dont_allow_bypass_nonce_via_sanitization_bad() { $var = sanitize_text_field( $_POST['foo'] ); // Bad. echo $var; } -function dont_allow_bypass_nonce_via_sanitization() { +function dont_allow_bypass_nonce_via_sanitization_good() { $var = sanitize_text_field( $_POST['foo'] ); // OK. wp_verify_nonce( $var ); echo $var; @@ -301,3 +291,198 @@ function function_containing_nested_closure() { }; } +// Tests specifically for the ContextHelper::is_in_function_call(). +function disallow_custom_unslash_before_noncecheck_via_method() { + $var = MyClass::stripslashes_from_strings_only( $_POST['foo'] ); // Bad. + wp_verify_nonce( $var ); + echo $var; +} + +function disallow_custom_unslash_before_noncecheck_via_namespaced_function() { + $var = MyNamespace\stripslashes_from_strings_only( $_POST['foo'] ); // Bad. + wp_verify_nonce( $var ); + echo $var; +} + +// Tests specifically for the ContextHelper::is_in_isset_or_empty(). +function allow_in_array_key_exists_before_noncecheck() { + if (array_key_exists('foo', $_POST) === false) { // OK. + return; + } + + wp_verify_nonce( 'some_action' ); +} + +function allow_in_key_exists_before_noncecheck() { + if (key_exists('foo', $_POST['subset']) === false) { // OK. + return; + } + + wp_verify_nonce( 'some_action' ); +} + +function disallow_in_custom_key_exists_before_noncecheck() { + if (My\key_exists('foo', $_POST['subset']) === false) { // Bad. + return; + } + + if ($obj?->array_key_exists('foo', $_POST['subset']) === false) { // Bad. + return; + } + + wp_verify_nonce( 'some_action' ); +} + +function disallow_in_array_key_exists_before_noncecheck_when_not_in_array_param() { + if ( array_key_exists( $_POST, $GLOBALS ) === false ) { // Bad (not that it makes sense anyhow). + return; + } + + if ( array_key_exists( arrays: $_POST, key: 'foo', ) === false ) { // Bad (typo in param label). + return; + } + + wp_verify_nonce( 'some_action' ); +} + +function allow_in_array_key_exists_before_noncecheck_with_named_params() { + if (array_key_exists( array: $_POST, key: 'foo', ) === false) { // OK. + return; + } + + wp_verify_nonce( 'some_action' ); +} + +// Tests specifically for the ContextHelper::is_in_array_comparison(). +function allow_for_array_comparison_in_condition_non_lowercase_function_call() { + if ( Array_Keys( $_GET['actions'], 'my_action', true ) ) { // OK. + check_admin_referer( 'foo' ); + foo(); + } +} + +function disallow_for_non_array_comparison_in_condition() { + if ( array_keys( $_GET['actions'] ) ) { // Bad. + check_admin_referer( 'foo' ); + foo(); + } +} + +function allow_for_array_comparison_in_condition_with_named_params() { + if ( array_keys( filter_value: 'my_action', array: $_GET['actions'], strict: true, ) ) { // OK. + check_admin_referer( 'foo' ); + foo(); + } +} + +function disallow_for_non_array_comparison_in_condition_with_named_params() { + if ( array_keys( strict: true, array: $_GET['actions'], ) ) { // Bad, missing $filter_value param. Invalid function call, but not our concern. + check_admin_referer( 'foo' ); + foo(); + } +} + +function test_long_list_assignment() { + list( $_POST['key1'], list( $_POST['key2'] ) ) = $something; // OK. +} +function test_short_list_assignment() { + [ $_POST['key1'], [ $_POST['key2'] ] ] = $something; // OK. +} + +function test_assignment_to_long_list_with_noncecheck() { + wp_verify_nonce( $_POST['nonce'] ); + list( $key, list( $key2 ) ) = $_POST; // OK. +} +function test_assignment_to_short_list_with_noncecheck() { + wp_verify_nonce( $_POST['nonce'] ); + [ $key, [ $key2 ] ] = $_POST; // OK. +} + +function test_assignment_to_long_list_without_noncecheck() { + list( $key, list( $key2 ) ) = $_POST; // Bad. +} +function test_assignment_to_short_list_without_noncecheck() { + [ $key, [ $key2 ] ] = $_POST; // Bad. +} + +function dont_throw_error_when_only_used_in_unset() { + unset( $_POST['foo'] ); // OK. +} + +function dont_throw_error_when_only_used_in_unset_but_error_for_other_use() { + unset( $_POST['foo'] ); // OK. + echo $_POST['bar']; // Bad. +} + +function dont_throw_error_when_only_used_in_unset_when_there_is_nonce_check_before_other_use() { + unset( $_POST['foo'] ); // OK. + wp_verify_nonce( $_POST['prefix_nonce'] ); + echo $_POST['bar']; // OK. +} + +function test_null_coalesce() { + $var = $_POST['foo'] ?? 10; // OK. + wp_verify_nonce( $_POST['nonce'] ); +} +function test_null_coalesce_without_noncecheck() { + $var = $_POST['foo'] ?? 10; // Bad. + // Do something. +} + +function test_null_coalesce_equals() { + $_POST['foo'][0] ??= 10; // OK. + wp_verify_nonce( $_POST['nonce'] ); +} + +function test_null_coalesce_equals_without_noncecheck() { + $_POST['foo'][0] ??= 10; // Bad. + // Do something. +} + +function test_open_arrow_fn_with_noncecheck() { + wp_verify_nonce( $_POST['nonce'] ); + $callback = fn() => $_POST['key']++; // OK. +} + +function test_open_arrow_fn_without_noncecheck() { + $callback = fn() => $_POST['key']++; // Bad. +} + +function test_disregard_noncecheck_in_nested_arrow_function() { + $callback = fn() => check_admin_referer( 'foo' ); + echo $_POST['foo']; // Bad, we don't know if the callback has been called or not. +} + +function test_match() { + $var = match($_POST['key']) { // OK, it's a comparison, with the nonce check after. + 'value' => wp_verify_nonce( $_POST['nonce'] ), // OK. + default => $_POST['key'], // OK, due to check above. Realistically, this is wrong, but that goes for all conditional checks. + }; +} + +function function_containing_nested_enum_with_nonce_check() { + enum MyEnum { + public function nested_method() { + wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['my_nonce'] ) ), 'the_nonce' ); + } + } + + echo $_POST['foo']; // Bad. +} + +function function_containing_nested_enum_with_nonce_check_outside() { + wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['my_nonce'] ) ), 'the_nonce' ); + + enum MyEnum { + public function nested_method() { + echo $_POST['foo']; // Bad. + } + } +} + +enum MyEnum { + public function nested_method() { + wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['my_nonce'] ) ), 'the_nonce' ); + echo $_POST['foo']; // OK. + } +} diff --git a/WordPress/Tests/Security/NonceVerificationUnitTest.2.inc b/WordPress/Tests/Security/NonceVerificationUnitTest.2.inc new file mode 100644 index 0000000000..1171e2f6df --- /dev/null +++ b/WordPress/Tests/Security/NonceVerificationUnitTest.2.inc @@ -0,0 +1,15 @@ + 'something' ); // OK. + public $_POST; // OK. +} diff --git a/WordPress/Tests/Security/NonceVerificationUnitTest.php b/WordPress/Tests/Security/NonceVerificationUnitTest.php index c3fb8a67b0..edb18099c4 100644 --- a/WordPress/Tests/Security/NonceVerificationUnitTest.php +++ b/WordPress/Tests/Security/NonceVerificationUnitTest.php @@ -14,57 +14,112 @@ /** * Unit test class for the NonceVerification sniff. * - * @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 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. + * @covers \WordPressCS\WordPress\Helpers\ContextHelper::is_in_function_call + * @covers \WordPressCS\WordPress\Helpers\ContextHelper::is_in_type_test + * @covers \WordPressCS\WordPress\Helpers\ContextHelper::is_in_isset_or_empty + * @covers \WordPressCS\WordPress\Helpers\ContextHelper::is_in_array_comparison + * @covers \WordPressCS\WordPress\Sniffs\Security\NonceVerificationSniff */ -class NonceVerificationUnitTest extends AbstractSniffUnitTest { +final class NonceVerificationUnitTest 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() { + public function getErrorList( $testFile = '' ) { + switch ( $testFile ) { + case 'NonceVerificationUnitTest.1.inc': + return array( + 5 => 1, + 9 => 1, + 31 => 1, + 44 => 1, + 48 => 1, + 69 => 1, + 88 => 1, // Old-style WPCS ignore comments are no longer supported. + 89 => 1, + 113 => 1, + 114 => 1, + 138 => 1, + 140 => 1, + 149 => 1, + 150 => 1, + 151 => 1, + 167 => 1, + 175 => 1, + 180 => 1, + 188 => 1, + 192 => 1, + 242 => 1, + 259 => 1, + 296 => 1, + 302 => 1, + 325 => 1, + 329 => 1, + 337 => 1, + 341 => 1, + 402 => 1, + 405 => 1, + 414 => 1, + 428 => 1, + 438 => 1, + 448 => 1, + 453 => 1, + 470 => 1, + 478 => 1, + ); + + case 'NonceVerificationUnitTest.2.inc': + return array( + 10 => 1, + 14 => 1, + ); - return array( - 5 => 1, - 9 => 1, - 31 => 1, - 44 => 1, - 48 => 1, - 69 => 1, - 89 => 1, - 113 => 1, - 114 => 1, - 122 => 1, - 126 => 1, - 148 => 1, - 150 => 1, - 159 => 1, - 160 => 1, - 161 => 1, - 177 => 1, - 185 => 1, - 190 => 1, - 198 => 1, - 202 => 1, - 252 => 1, - 269 => 1, - ); + case 'NonceVerificationUnitTest.7.inc': + return array( + 17 => 1, + 23 => 1, + ); + + default: + return array(); + } } /** * Returns the lines where warnings 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 warnings. */ - public function getWarningList() { - return array( - 88 => 1, // Whitelist comment deprecation warning. - ); - } + public function getWarningList( $testFile = '' ) { + switch ( $testFile ) { + case 'NonceVerificationUnitTest.1.inc': + return array( + 365 => 1, + 379 => 1, + ); + + case 'NonceVerificationUnitTest.4.inc': + return array( + 25 => 1, + ); + case 'NonceVerificationUnitTest.6.inc': + return array( + 8 => 1, + ); + + default: + return array(); + } + } } diff --git a/WordPress/Tests/Security/PluginMenuSlugUnitTest.inc b/WordPress/Tests/Security/PluginMenuSlugUnitTest.inc index 265b981b91..2c77ee621e 100644 --- a/WordPress/Tests/Security/PluginMenuSlugUnitTest.inc +++ b/WordPress/Tests/Security/PluginMenuSlugUnitTest.inc @@ -2,7 +2,7 @@ add_menu_page( $page_title, $menu_title, $capability, __FILE__, $function, $icon_url, $position ); // Bad. -add_dashboard_page( $page_title, $menu_title, $capability, __FILE__, $function); // Bad. +add_dashboard_page( $page_title, $menu_title, $capability, __file__, $function); // Bad. add_submenu_page( $parent_slug, $page_title, $menu_title, $capability, 'awesome-submenu-page', $function ); // Ok. @@ -12,3 +12,11 @@ add_submenu_page( __FILE__ . 'parent', $page_title, $menu_title, $capability, __ $my_class->add_dashboard_page( $page_title, $menu_title, $capability, __FILE__, $function); // Ok. Some_Class::add_dashboard_page( $page_title, $menu_title, $capability, __FILE__, $function); // Ok. \My_Namespace\add_dashboard_page( $page_title, $menu_title, $capability, __FILE__, $function); // Ok. + +// Safeguard support for PHP 8.0+ named parameters. +add_submenu_page( + page_title: $page_title, + menu_title: $menu_title, + parent_slug: __FILE__, // Bad. + capability: $capability, +); diff --git a/WordPress/Tests/Security/PluginMenuSlugUnitTest.php b/WordPress/Tests/Security/PluginMenuSlugUnitTest.php index 3b81976073..6741d048a1 100644 --- a/WordPress/Tests/Security/PluginMenuSlugUnitTest.php +++ b/WordPress/Tests/Security/PluginMenuSlugUnitTest.php @@ -14,18 +14,18 @@ /** * Unit test class for the PluginMenuSlug 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 moved from the `VIP` category to the `Security` 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 `Security` category. + * @covers \WordPressCS\WordPress\Sniffs\Security\PluginMenuSlugSniff */ -class PluginMenuSlugUnitTest extends AbstractSniffUnitTest { +final class PluginMenuSlugUnitTest 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(); @@ -34,14 +34,14 @@ 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( - 3 => 1, - 5 => 1, - 9 => 2, + 3 => 1, + 5 => 1, + 9 => 2, + 20 => 1, ); } - } diff --git a/WordPress/Tests/Security/SafeRedirectUnitTest.php b/WordPress/Tests/Security/SafeRedirectUnitTest.php index d16634cbe5..b62e6f86c8 100644 --- a/WordPress/Tests/Security/SafeRedirectUnitTest.php +++ b/WordPress/Tests/Security/SafeRedirectUnitTest.php @@ -14,16 +14,16 @@ /** * Unit test class for the Security_SafeRedirect sniff. * - * @package WPCS\WordPressCodingStandards + * @since 1.0.0 * - * @since 1.0.0 + * @covers \WordPressCS\WordPress\Sniffs\Security\SafeRedirectSniff */ -class SafeRedirectUnitTest extends AbstractSniffUnitTest { +final class SafeRedirectUnitTest 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(); @@ -32,12 +32,11 @@ 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( 3 => 1, ); } - } diff --git a/WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.inc b/WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.1.inc similarity index 63% rename from WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.inc rename to WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.1.inc index b70afe7f0a..cc4edc147a 100644 --- a/WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.inc +++ b/WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.1.inc @@ -64,7 +64,7 @@ unset( $_GET['test'] ); // Ok. output( "some string {$_POST['some_var']}" ); // Bad. -echo (int) $_GET['test']; // Ok. +echo isset( $_GET['test'] ) ? (int) $_GET['test'] : 0; // Ok. some_func( $some_arg, (int) $_GET['test'] ); // Ok. function zebra() { @@ -73,7 +73,7 @@ function zebra() { } } -echo $_GET['test']; // WPCS: sanitization OK. +echo $_GET['test']; // Bad: old-style ignore comment. WPCS: sanitization OK. echo array_map( 'sanitize_text_field', wp_unslash( $_GET['test'] ) ); // Ok. echo array_map( 'foo', wp_unslash( $_GET['test'] ) ); // Bad. @@ -94,7 +94,7 @@ switch ( do_something( wp_unslash( $_POST['foo'] ) ) ) {} // Bad. // Sanitization is required even when the value is being escaped. echo esc_html( wp_unslash( $_POST['foo'] ) ); // Bad. -echo esc_html( sanitize_text_field( wp_unslash( $_POST['foo'] ) ) ); // Ok. +echo esc_html( Sanitize_Text_Field( wp_unslash( $_POST['foo'] ) ) ); // Ok. $current_tax_slug = isset( $_GET['a'] ) ? sanitize_key( $_GET['a'] ) : false; // Ok. $current_tax_slug = isset( $_GET['a'] ) ? $_GET['a'] : false; // Bad x 2 @@ -103,9 +103,9 @@ $current_tax_slug = isset( $_GET['a'] ) ? sanitize_text_field( wp_unslash( $_GET echo sanitize_text_field( $_POST['foo545'] ); // Error for no validation, unslashing. echo array_map( 'sanitize_text_field', $_GET['test'] ); // Bad, no unslashing. -echo array_map( 'sanitize_key', $_GET['test'] ); // Ok. +echo Array_Map( 'sanitize_key', $_GET['test'] ); // Ok. -foo( absint( $_GET['foo'] ) ); // Ok. +isset( $_GET['foo'] ) && foo( AbsINT( $_GET['foo'] ) ); // Ok. $ids = array_map( 'absint', $_GET['test'] ); // Ok. if ( is_array( $_GET['test'] ) ) {} // Ok. @@ -113,7 +113,7 @@ if ( is_array( $_GET['test'] ) ) {} // Ok. output( "some string \$_POST[some_var]" ); // Ok. output( "some string \\$_POST[some_var] $_GET[evil]" ); // Bad x2. -echo esc_html( wp_strip_all_tags( wp_unslash( $_GET['a'] ) ) ); // Ok. +echo esc_html( wp_strip_all_tags( WP_Unslash( $_GET['a'] ) ) ); // Ok. // Test validation check vs anonymous functions. isset( $_POST['abc'] ); // Validation in global scope, not function scope. @@ -128,7 +128,7 @@ function test_this() { if ( ! isset( $_POST['abc_field'] ) ) { return; } - + $abc = sanitize_color( wp_unslash( $_POST['abc_field'] ) ); // Bad x1 - sanitize. // phpcs:set WordPress.Security.ValidatedSanitizedInput customSanitizingFunctions[] sanitize_color,sanitize_twitter_handle @@ -194,7 +194,7 @@ if ( array_key_exists( 'my_field1', $_POST ) ) { $id = (int) $_POST['my_field1']; // OK. } -if ( \array_key_exists( 'my_field2', $_POST ) ) { +if ( \Array_Key_Exists( 'my_field2', $_POST ) ) { $id = (int) $_POST['my_field2']; // OK. } @@ -228,21 +228,21 @@ function test_more_safe_functions() { function test_allow_array_key_exists_alias() { if ( key_exists( 'my_field1', $_POST ) ) { - $id = (int) $_POST['my_field1']; // OK. + $id = (int) $_POST['my_field1']; // OK. + } } - function test_correct_multi_level_array_validation() { - if ( isset( $_POST['toplevel']['sublevel'] ) ) { - $id = (int) $_POST['toplevel']; // OK, if a subkey exists, the top level key *must* also exist. - $id = (int) $_POST['toplevel']['sublevel']; // OK. - $id = (int) $_POST['toplevel']['sublevel']['subsub']; // Bad x1 - validate, this sub has not been validated. + if ( isset( $_COOKIE['toplevel']['sublevel'] ) ) { + $id = (int) $_COOKIE['toplevel']; // OK, if a subkey exists, the top level key *must* also exist. + $id = (int) $_COOKIE['toplevel']['sublevel']; // OK. + $id = (int) $_COOKIE['toplevel']['sublevel']['subsub']; // Bad x1 - validate, this sub has not been validated. } - if ( array_key_exists( 'bar', $_POST['foo'] ) ) { - $id = (int) $_POST['bar']; // Bad x 1 - validate. - $id = (int) $_POST['foo']; // OK. - $id = (int) $_POST['foo']['bar']; // OK. - $id = (int) $_POST['foo']['bar']['baz']; // Bad x 1 - validate. + if ( array_key_exists( 'bar', $_COOKIE['foo'] ) ) { + $id = (int) $_COOKIE['bar']; // Bad x 1 - validate. + $id = (int) $_COOKIE['foo']; // OK. + $id = (int) $_COOKIE['foo']['bar']; // OK. + $id = (int) $_COOKIE['foo']['bar']['baz']; // Bad x 1 - validate. } } @@ -333,3 +333,170 @@ function test_using_different_unslashing_functions() { } echo wp_sanitize_redirect( wp_unslash( $_GET['test'] ) ); // OK. + +$result = match ( $_POST['foo'] ) {}; // Ok. +$result = match ( do_something( wp_unslash( $_POST['foo'] ) ) ) {}; // Bad. + +// Test handling of more complex embedded variables and expressions. +function test_handling_embeds() { + echo "My ${_POST} and {$_GET['bar']} and ${_REQUEST['bar']}"; // Bad x 3. + // Below heredoc: bad x 3. + echo <<<"EOD" +Do ${(_POST)} and ${_GET["${bar}"]} and ${_FILES["${bar['baz']}"]} +EOD; +} + +/* + * Safeguard support for PHP 8.0+ named parameters. + */ +function test_allow_array_key_exists_with_named_params_wrong_param_name_array() { + if ( array_key_exists( arrays: $_POST, key: 'my_field1', ) ) { + $id = (int) $_POST['my_field1']; // Bad. + } +} + +function test_allow_array_key_exists_with_named_params_correct_param_name() { + if ( array_key_exists( array: $_POST, key: 'my_field1', ) ) { + $id = (int) $_POST['my_field1']; // OK. + } +} + +function test_allow_array_key_exists_with_named_params_empty_array_param() { + if ( array_key_exists( array: /*comment*/, key: 'my_field1', ) ) { + $id = (int) $_POST['my_field1']; // Bad. + } +} + +function test_allow_array_key_exists_with_named_params_array_param_first_token_not_variable() { + if ( array_key_exists( array: [1, 2], key: 'my_field1', ) ) { + $id = (int) $_POST['my_field1']; // Bad. + } +} + +function test_allow_array_key_exists_with_named_params_multilevel_access_wrong_param_name_key() { + if ( array_key_exists( array: $_POST['foo'], keys: 'bar' ) ) { + $id = (int) $_POST['foo']['bar']; // Bad. + } +} + +function test_allow_array_key_exists_with_named_params_multilevel_access_test() { + if ( array_key_exists( array: $_SERVER['foo'], key: 'bar' ) ) { + $id = (int) $_SERVER['bar']; // Bad x 1 - validate. + $id = (int) $_SERVER['foo']; // OK. + $id = (int) $_SERVER['foo']['bar']; // OK. + $id = (int) $_SERVER['foo']['bar']['baz']; // Bad x 1 - validate. + } +} + +/* + * Prevent the sniff trying to examine PHP 8.0 attributes. + */ +function test_ignore_array_key_exists_in_attribute() { + #[Array_Key_Exists('key', ['a', 2])] + $callback = function() {}; + $id = (int) $_POST['foo']; // Bad. +} + +/* + * Ensure the sniff doesn't examine PHP 8.1 first class callables. + */ +function test_ignore_array_key_exists_as_first_class_callable() { + $callback = array_key_exists(...); + $id = (int) $_POST['foo']; // Bad. +} + +/* + * Unsetting a superglobal key is not the same as validating it. + */ +function test_unset_is_not_validation() { + unset( $_REQUEST['key'] ); + $id = (int) $_REQUEST['key']; // Bad, missing validation. +} + +/* + * Only count a variable as validated if the validation happened in the same scope. + */ +function test_nested_closed_scopes() { + $closure = function() { + return isset( $_POST['bar'] ); + }; + + $anon = new class() { + public function __construct() { + if ( isset( $_POST['bar'] ) ) { + // Do something. + } + } + }; + + $arrow = fn() => isset( $_POST['bar'] ); + + $id = (int) $_POST['bar']; // Bad x 1 - validate. +} + +function test_arrow_open_scope() { + if ( ! isset( $_SERVER['key'] ) ) { + return; + } + + $arrow = fn() => (int) $_SERVER['key']; // OK, validation outside the scope of the arrow function counts. + + $arrow = fn() => isset( $_SERVER['abc'] ) ? (int) $_SERVER['abc'] : 0; // OK. +} + +// Ensure the sniff flags $_ENV and $_SESSION too. +function test_examine_additional_superglobals_in_textstrings() { + $text = "Use {$_SESSION['key']} for something"; // Bad. + $text = "Use {$_ENV['key']} for something"; // Bad. + $text = "Use {$GLOBALS['key']} for something"; // OK. +} + +function test_examine_additional_superglobals_as_vars() { + $key = sanitize_text_field( $_SESSION['key'] ); // Bad - missing validation. + $key = sanitize_text_field( $_ENV['key'] ); // Bad - missing validation. + $key = sanitize_text_field( $_FILES['key'] ); // Bad - missing validation. + + if ( isset( $_SESSION['key'], $_ENV['key'], $_FILES['key'] ) === false ) { + return; + } + + // OK. + $key = sanitize_text_field( wp_unslash( $_SESSION['key'] ) ); + $key = sanitize_text_field( wp_unslash( $_ENV['key'] ) ); + $key = sanitize_text_field( wp_unslash( $_FILES['key'] ) ); + + // OK - unslashing not needed for $_SESSION, $_ENV and $_FILES. + $key = sanitize_text_field( $_SESSION['key'] ); + $key = sanitize_text_field( $_ENV['key'] ); + $key = sanitize_text_field( $_FILES['key'] ); + + // Bad - missing sanitization. + do_something( $_SESSION['key'] ); + do_something( $_ENV['key'] ); + do_something( $_FILES['key'] ); +} + +function test_null_coalesce_equals_validation_extra_safeguard() { + $_POST['key'] ??= 'default'; // OK, assignment. + $key = $_POST['key']; // Bad, missing unslash + sanitization, validation okay. +} + +function test_in_match_condition_is_regarded_as_comparison() { + if ( isset( $_REQUEST['key'] ) ) { + $test = match( $_REQUEST['key'] ) { + 'valueA' => 'A', + default => 'B', + }; + } +} + +function test_in_match_condition_is_regarded_as_comparison() { + if ( isset( $_REQUEST['keyA'], $_REQUEST['keyB'], $_REQUEST['keyC'] ) ) { + $test = match( $toggle ) { + true => sanitize_text_field( wp_unslash( $_REQUEST['keyA'] ) ), // OK. + false => sanitize_text_field( $_REQUEST['keyB'] ), // Bad - missing unslash. + 10 => wp_unslash( $_REQUEST['keyC'] ), // Bad - missing sanitization. + default => $_REQUEST['keyD'], // Bad - missing sanitization, unslash, validation. + }; + } +} diff --git a/WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.2.inc b/WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.2.inc new file mode 100644 index 0000000000..a4bad0ee47 --- /dev/null +++ b/WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.2.inc @@ -0,0 +1,7 @@ + => + * @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( - 5 => 3, - 7 => 1, - 10 => 1, - 20 => 1, - 33 => 3, - 65 => 1, - 79 => 1, - 80 => 1, - 81 => 1, - 82 => 1, - 85 => 1, - 90 => 1, - 93 => 1, - 96 => 1, - 100 => 2, - 101 => 1, - 104 => 2, - 105 => 1, - 114 => 2, - 121 => 1, - 132 => 1, - 137 => 1, - 138 => 1, - 150 => 2, - 160 => 2, - 164 => 2, - 189 => 1, - 202 => 1, - 206 => 1, - 210 => 1, - 216 => 1, - 217 => 1, - 238 => 1, - 242 => 1, - 245 => 1, - 251 => 1, - 257 => 1, - 266 => 1, - 277 => 1, - 290 => 2, - 300 => 1, - 305 => 2, - 306 => 2, - 307 => 2, - 309 => 2, - 310 => 2, - 311 => 2, - 315 => 2, - 317 => 1, - 323 => 1, - ); + public function getErrorList( $testFile = '' ) { + switch ( $testFile ) { + case 'ValidatedSanitizedInputUnitTest.1.inc': + return array( + 5 => 3, + 7 => 1, + 10 => 1, + 20 => 1, + 33 => 3, + 65 => 1, + 76 => 2, // Old-style WPCS ignore comments are no longer supported. + 79 => 1, + 80 => 1, + 81 => 1, + 82 => 1, + 85 => 1, + 90 => 1, + 93 => 1, + 96 => 1, + 100 => 2, + 101 => 1, + 104 => 2, + 105 => 1, + 114 => 2, + 121 => 1, + 132 => 1, + 137 => 1, + 138 => 1, + 150 => 2, + 160 => 2, + 164 => 2, + 189 => 1, + 202 => 1, + 206 => 1, + 210 => 1, + 216 => 1, + 217 => 1, + 238 => 1, + 242 => 1, + 245 => 1, + 251 => 1, + 257 => 1, + 266 => 1, + 277 => 1, + 290 => 2, + 300 => 1, + 305 => 2, + 306 => 2, + 307 => 2, + 309 => 2, + 310 => 2, + 311 => 2, + 315 => 2, + 317 => 1, + 323 => 1, + 338 => 1, + 342 => 3, + 345 => 3, + 354 => 1, + 366 => 1, + 372 => 1, + 378 => 1, + 384 => 1, + 387 => 1, + 397 => 1, + 405 => 1, + 413 => 1, + 434 => 1, + 449 => 1, + 450 => 1, + 455 => 1, + 456 => 1, + 457 => 1, + 474 => 1, + 475 => 1, + 476 => 1, + 481 => 2, + 497 => 1, + 498 => 1, + 499 => 3, + ); + + case 'ValidatedSanitizedInputUnitTest.2.inc': + case 'ValidatedSanitizedInputUnitTest.3.inc': + case 'ValidatedSanitizedInputUnitTest.4.inc': + case 'ValidatedSanitizedInputUnitTest.5.inc': + return array( + 7 => 3, + ); + + 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( - 76 => 1, // Whitelist comment deprecation warning. - ); + return array(); } - } diff --git a/WordPress/Tests/Utils/I18nTextDomainFixerUnitTest.3.inc b/WordPress/Tests/Utils/I18nTextDomainFixerUnitTest.3.inc index adf61b3332..2082c75b15 100644 --- a/WordPress/Tests/Utils/I18nTextDomainFixerUnitTest.3.inc +++ b/WordPress/Tests/Utils/I18nTextDomainFixerUnitTest.3.inc @@ -71,7 +71,7 @@ class Debug_Bar { * @version 2.0.0 * * @copyright 2013-2018 Juliette Reinders Folmer - * @license http://creativecommons.org/licenses/GPL/2.0/ GNU General Public License, version 2 or higher + * @license https://www.gnu.org/licenses/old-licenses/gpl-2.0.html GNU General Public License, version 2 or higher * * @wordpress-plugin * Plugin Name : Debug Bar Constants @@ -79,7 +79,7 @@ class Debug_Bar { * Description : Debug Bar Constants adds new panels to Debug Bar that display all the defined constants for the current request. Requires "Debug Bar" plugin. * Version : 2.0.0 * Author : Juliette Reinders Folmer - * Author URI : http://www.adviesenzo.nl/ + * Author URI : https://www.adviesenzo.nl/ * Depends : Debug Bar * Text Domain : debug-bar-constants * Domain Path : /languages @@ -102,11 +102,11 @@ class Debug_Bar { /** * Plugin Name: Missing text domain, docblock format. - * Plugin URI: http://www.bigvoodoo.com + * Plugin URI: https://www.bigvoodoo.com/ * Description: Ensures the Git repositories are kept current and up to date with uploads made within WordPress. * Author: Big Voodoo Interactive * Version: 0.1.0 - * Author URI: http://www.bigvoodoo.com + * Author URI: https://www.bigvoodoo.com/ * @author Big Voodoo Interactive * @TODO error reporting */ diff --git a/WordPress/Tests/Utils/I18nTextDomainFixerUnitTest.3.inc.fixed b/WordPress/Tests/Utils/I18nTextDomainFixerUnitTest.3.inc.fixed index d4a95a9735..bb8bfcfcb9 100644 --- a/WordPress/Tests/Utils/I18nTextDomainFixerUnitTest.3.inc.fixed +++ b/WordPress/Tests/Utils/I18nTextDomainFixerUnitTest.3.inc.fixed @@ -71,7 +71,7 @@ class Debug_Bar { * @version 2.0.0 * * @copyright 2013-2018 Juliette Reinders Folmer - * @license http://creativecommons.org/licenses/GPL/2.0/ GNU General Public License, version 2 or higher + * @license https://www.gnu.org/licenses/old-licenses/gpl-2.0.html GNU General Public License, version 2 or higher * * @wordpress-plugin * Plugin Name : Debug Bar Constants @@ -79,7 +79,7 @@ class Debug_Bar { * Description : Debug Bar Constants adds new panels to Debug Bar that display all the defined constants for the current request. Requires "Debug Bar" plugin. * Version : 2.0.0 * Author : Juliette Reinders Folmer - * Author URI : http://www.adviesenzo.nl/ + * Author URI : https://www.adviesenzo.nl/ * Depends : Debug Bar * Text Domain : something-else * Domain Path : /languages @@ -102,11 +102,11 @@ class Debug_Bar { /** * Plugin Name: Missing text domain, docblock format. - * Plugin URI: http://www.bigvoodoo.com + * Plugin URI: https://www.bigvoodoo.com/ * Description: Ensures the Git repositories are kept current and up to date with uploads made within WordPress. * Author: Big Voodoo Interactive * Version: 0.1.0 - * Author URI: http://www.bigvoodoo.com + * Author URI: https://www.bigvoodoo.com/ * Text Domain: something-else * @author Big Voodoo Interactive * @TODO error reporting diff --git a/WordPress/Tests/Utils/I18nTextDomainFixerUnitTest.4.inc b/WordPress/Tests/Utils/I18nTextDomainFixerUnitTest.4.inc index 87d52fbba5..22035dca15 100644 --- a/WordPress/Tests/Utils/I18nTextDomainFixerUnitTest.4.inc +++ b/WordPress/Tests/Utils/I18nTextDomainFixerUnitTest.4.inc @@ -67,7 +67,7 @@ _x( $text, $context, $variableTextdomain ); _ex( $text, $context, CONSTANT_TEXTDOMAIN ); /* - * Text domains *not* in the whitelisted "old" domain list should be ignored. + * Text domains *not* in the "old" domain list should be ignored. */ load_plugin_textdomain( 'tgmpa', false, '/languages/' ); _e( $text, 'default' ); @@ -203,5 +203,48 @@ __ngettext( $singular, $plural, $number ); // Error. __ngettext_noop( $singular, $plural, 'other-text-domain' ); // Error. translate_with_context( $text, 'third-text-domain' ); // Error. +// New WP function. +load_script_textdomain( $handle, 'something-else', '/path/to/languages/' ); // OK. +load_script_textdomain( $handle, 'third-text-domain', '/path/to/languages/' ); // Error. + +// Test ignoring multi-token text domains. +__( $text, 'my' 'domain' ); // Parse error, but not our concern. + +// Test with space based code indentation +function foo() { + unload_textdomain( + /* Missing domain. */ + ); +} + +/* + * Safeguard support for PHP 8.0+ named parameters. + */ +// Missing domain parameter. +_n( plural: $plural, single: $single ); // Error. +esc_attr_x( + context : $context, + text : $text, +); + +// Has correct domain parameter. +load_textdomain( mofile: '/path/to/file.mo', domain: 'something-else', ); +_e( $text, domain: 'something-else' ); +_nx_noop( + domain: 'something-else', + context: $context, + singular: $singular, + plural: $plural, +); + +// Has incorrect domain parameter. +load_muplugin_textdomain( mu_plugin_rel_path: '/languages/', domain: 'other-text-domain', ); +__( $text, domain: 'text-domain' ); +esc_html_x( + $text, + domain: 'text-domain', + context: $context, +); + // phpcs:set WordPress.Utils.I18nTextDomainFixer old_text_domain[] // phpcs:set WordPress.Utils.I18nTextDomainFixer new_text_domain false diff --git a/WordPress/Tests/Utils/I18nTextDomainFixerUnitTest.4.inc.fixed b/WordPress/Tests/Utils/I18nTextDomainFixerUnitTest.4.inc.fixed index 2c0c176ebd..1b01c02c4b 100644 --- a/WordPress/Tests/Utils/I18nTextDomainFixerUnitTest.4.inc.fixed +++ b/WordPress/Tests/Utils/I18nTextDomainFixerUnitTest.4.inc.fixed @@ -67,7 +67,7 @@ _x( $text, $context, $variableTextdomain ); _ex( $text, $context, CONSTANT_TEXTDOMAIN ); /* - * Text domains *not* in the whitelisted "old" domain list should be ignored. + * Text domains *not* in the "old" domain list should be ignored. */ load_plugin_textdomain( 'tgmpa', false, '/languages/' ); _e( $text, 'default' ); @@ -207,5 +207,49 @@ __ngettext( $singular, $plural, $number, 'something-else' ); // Error. __ngettext_noop( $singular, $plural, 'something-else' ); // Error. translate_with_context( $text, 'something-else' ); // Error. +// New WP function. +load_script_textdomain( $handle, 'something-else', '/path/to/languages/' ); // OK. +load_script_textdomain( $handle, 'something-else', '/path/to/languages/' ); // Error. + +// Test ignoring multi-token text domains. +__( $text, 'my' 'domain' ); // Parse error, but not our concern. + +// Test with space based code indentation +function foo() { + unload_textdomain( + /* Missing domain. */ + 'something-else' + ); +} + +/* + * Safeguard support for PHP 8.0+ named parameters. + */ +// Missing domain parameter. +_n( plural: $plural, single: $single ); // Error. +esc_attr_x( + context : $context, + text : $text, +); + +// Has correct domain parameter. +load_textdomain( mofile: '/path/to/file.mo', domain: 'something-else', ); +_e( $text, domain: 'something-else' ); +_nx_noop( + domain: 'something-else', + context: $context, + singular: $singular, + plural: $plural, +); + +// Has incorrect domain parameter. +load_muplugin_textdomain( mu_plugin_rel_path: '/languages/', domain: 'something-else', ); +__( $text, domain: 'something-else' ); +esc_html_x( + $text, + domain: 'something-else', + context: $context, +); + // phpcs:set WordPress.Utils.I18nTextDomainFixer old_text_domain[] // phpcs:set WordPress.Utils.I18nTextDomainFixer new_text_domain false diff --git a/WordPress/Tests/Utils/I18nTextDomainFixerUnitTest.5.inc b/WordPress/Tests/Utils/I18nTextDomainFixerUnitTest.5.inc new file mode 100644 index 0000000000..0592c31a0a --- /dev/null +++ b/WordPress/Tests/Utils/I18nTextDomainFixerUnitTest.5.inc @@ -0,0 +1,9 @@ + => + * + * @return array Key is the line number, value is the number of expected errors. */ public function getErrorList( $testFile = '' ) { + $phpcs_version = Helper::getVersion(); + $is_phpcs_4 = version_compare( $phpcs_version, '3.99.99', '>' ); + switch ( $testFile ) { case 'I18nTextDomainFixerUnitTest.css': return array( - 29 => 1, - 92 => 1, - 107 => 1, - 120 => 1, - 133 => 1, - 149 => 1, + 29 => ( true === $is_phpcs_4 ? 0 : 1 ), + 92 => ( true === $is_phpcs_4 ? 0 : 1 ), + 107 => ( true === $is_phpcs_4 ? 0 : 1 ), + 120 => ( true === $is_phpcs_4 ? 0 : 1 ), + 133 => ( true === $is_phpcs_4 ? 0 : 1 ), + 149 => ( true === $is_phpcs_4 ? 0 : 1 ), ); case 'I18nTextDomainFixerUnitTest.3.inc': @@ -136,6 +141,13 @@ public function getErrorList( $testFile = '' ) { 202 => 1, 203 => 1, 204 => 1, + 208 => 1, + 215 => 1, + 224 => 1, + 225 => 1, + 241 => 1, + 242 => 1, + 245 => 1, ); default: @@ -147,7 +159,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 ) { @@ -184,5 +197,4 @@ public function getWarningList( $testFile = '' ) { return array(); } } - } diff --git a/WordPress/Tests/WP/AlternativeFunctionsUnitTest.inc b/WordPress/Tests/WP/AlternativeFunctionsUnitTest.inc index d819f25a11..a88e5638a9 100644 --- a/WordPress/Tests/WP/AlternativeFunctionsUnitTest.inc +++ b/WordPress/Tests/WP/AlternativeFunctionsUnitTest.inc @@ -1,13 +1,11 @@ ' ); // OK. -// phpcs:set WordPress.WP.AlternativeFunctions minimum_supported_version 4.0 -parse_url($url, PHP_URL_QUERY); // OK. -// phpcs:set WordPress.WP.AlternativeFunctions minimum_supported_version 4.7 -parse_url($url, PHP_URL_SCHEME); // Warning. -// phpcs:set WordPress.WP.AlternativeFunctions minimum_supported_version 4.6 - file_get_contents( $local_file, true ); // OK. file_get_contents( $url, false ); // Warning. file_get_contents(); // OK - no params, so nothing to do. @@ -78,3 +66,84 @@ function curl_version_ssl() {} // OK. use function curl_version; // OK. use function something as curl_version; // OK. use function curl_init as curl_version; // Bad. + +unlink(); // Warning. +rename(); // Warning. +chgrp(); // Warning. +chmod(); // Warning. +chown(); // Warning. +is_writable(); // Warning. +is_writeable(); // Warning. +mkdir(); // Warning. +rmdir(); // Warning. +touch(); // Warning. +fputs(); // Warning. + +// phpcs:set WordPress.WP.AlternativeFunctions minimum_wp_version 4.0 +parse_url( 'http://example.com/' ); // OK, alternative was not yet available. +// phpcs:set WordPress.WP.AlternativeFunctions minimum_wp_version 4.4 +parse_url( 'http://example.com/' ); // Warning, not using $component param, so can switch over. +// phpcs:set WordPress.WP.AlternativeFunctions minimum_wp_version + +// phpcs:set WordPress.WP.AlternativeFunctions minimum_wp_version 4.0 +parse_url($url, PHP_URL_QUERY); // OK, alternative was not yet available. +// phpcs:set WordPress.WP.AlternativeFunctions minimum_wp_version 4.5 +parse_url($url, PHP_URL_QUERY); // OK, $component param not yet available. +// phpcs:set WordPress.WP.AlternativeFunctions minimum_wp_version 4.7 +parse_url($url, PHP_URL_SCHEME); // Warning, using $component param, but also using WP 4.7+, so can switch over. +// phpcs:set WordPress.WP.AlternativeFunctions minimum_wp_version + +/* + * Tests for support for PHP 8.0+ named parameters. + */ +// Safeguard support for PHP 8.0+ named parameters for the custom logic related to strip_tags(). +strip_tags( allowed_tags: '