From 428bcc914c3b180a9c96dae09dc2b99b5a11fe00 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 03:55:38 +0100 Subject: [PATCH 01/24] Add LGPLv3 License --- LICENSE | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..0a041280 --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. From 4e79df3724605856e9c55fdfdaf6fa82968b056d Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 12 Aug 2019 09:14:31 +0200 Subject: [PATCH 02/24] Initial development environment setup --- .gitattributes | 23 +++++++++++++++++++++++ .gitignore | 2 ++ .travis.yml | 32 ++++++++++++++++++++++++++++++++ composer.json | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 composer.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..30f7d6a7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,23 @@ +# +# Exclude these files from release archives. +# This will also make them unavailable when using Composer with `--prefer-dist`. +# If you develop for this repo using Composer, use `--prefer-source`. +# https://www.reddit.com/r/PHP/comments/2jzp6k/i_dont_need_your_tests_in_my_production +# https://blog.madewithlove.be/post/gitattributes/ +# +/.gitattributes export-ignore +/.gitignore export-ignore +/.travis.yml export-ignore + +# +# Auto detect text files and perform LF normalization +# http://davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/ +# +* text=auto + +# +# The above will handle all files NOT found below +# +*.md text +*.php text +*.inc text diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4f4acd35 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +vendor/ +composer.lock \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..5fc495a2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,32 @@ +dist: trusty + +language: php + +## Cache composer and apt downloads. +cache: + directories: + # Cache directory for older Composer versions. + - $HOME/.composer/cache/files + # Cache directory for more recent Composer versions. + - $HOME/.cache/composer/files + +php: + - 5.4 + - 7.3 + +jobs: + fast_finish: true + + +before_install: + # Speed up build time by disabling Xdebug when its not needed. + - phpenv config-rm xdebug.ini || echo 'No xdebug config.' + + # --prefer-dist will allow for optimal use of the travis caching ability. + - composer install --prefer-dist --no-suggest + + +script: + # Validate the composer.json file on low/high PHP versions. + # @link https://getcomposer.org/doc/03-cli.md#validate + - composer validate --no-check-all --strict diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..587f1ba4 --- /dev/null +++ b/composer.json @@ -0,0 +1,33 @@ +{ + "name" : "phpcsstandards/phpcsextra", + "description" : "A collection of sniffs and standards for use with PHP_CodeSniffer.", + "type" : "phpcodesniffer-standard", + "keywords" : [ "phpcs", "phpcbf", "standards", "php_codesniffer", "phpcodesniffer-standard" ], + "license" : "LGPL-3.0-or-later", + "authors" : [ + { + "name" : "Juliette Reinders Folmer", + "role" : "lead", + "homepage" : "https://github.com/jrfnl" + }, + { + "name" : "Contributors", + "homepage" : "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" + } + ], + "support" : { + "issues" : "https://github.com/PHPCSStandards/PHPCSExtra/issues", + "source" : "https://github.com/PHPCSStandards/PHPCSExtra" + }, + "require" : { + "php" : ">=5.4", + "squizlabs/php_codesniffer" : "^3.3.1", + "dealerdirect/phpcodesniffer-composer-installer" : "^0.3 || ^0.4.1 || ^0.5 || ^0.6", + "phpcsstandards/phpcsutils" : "^1.0 || dev-develop" + }, + "scripts" : { + "install-standards": [ + "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin::run" + ] + } +} From 2b0478e9268e85f98b4c43bb4e286f3b04839b50 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 19 Dec 2019 07:36:27 +0100 Subject: [PATCH 03/24] Universal: add ruleset --- Universal/ruleset.xml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Universal/ruleset.xml diff --git a/Universal/ruleset.xml b/Universal/ruleset.xml new file mode 100644 index 00000000..33067f2b --- /dev/null +++ b/Universal/ruleset.xml @@ -0,0 +1,5 @@ + + + + A collection of universal sniffs. This standard is not designed to be used to check code. Include individual sniffs from this standard in a custom ruleset instead. + From 41e13772e8bfaf94211a3df7a0e4135a046b4e8f Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 04:06:16 +0100 Subject: [PATCH 04/24] NormalizedArrays: add ruleset --- NormalizedArrays/ruleset.xml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 NormalizedArrays/ruleset.xml diff --git a/NormalizedArrays/ruleset.xml b/NormalizedArrays/ruleset.xml new file mode 100644 index 00000000..ce35adb5 --- /dev/null +++ b/NormalizedArrays/ruleset.xml @@ -0,0 +1,5 @@ + + + + A ruleset for PHP_CodeSniffer to check arrays for normalized format. + From 57e885d2b3675a2f56ee02d50e0c83f90c1ef2ff Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 04:21:04 +0100 Subject: [PATCH 05/24] :wrench: QA: lint the ruleset XML files --- .travis.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.travis.yml b/.travis.yml index 5fc495a2..53f2d95d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ language: php ## Cache composer and apt downloads. cache: + apt: true directories: # Cache directory for older Composer versions. - $HOME/.composer/cache/files @@ -14,14 +15,38 @@ php: - 5.4 - 7.3 +# Define the stages used. +stages: + - name: sniff + - name: test + jobs: fast_finish: true + include: + #### SNIFF STAGE #### + - stage: sniff + php: 7.4 + addons: + apt: + packages: + - libxml2-utils + script: + # Validate the xml files. + # @link http://xmlsoft.org/xmllint.html + - xmllint --noout --schema ./vendor/squizlabs/php_codesniffer/phpcs.xsd ./NormalizedArrays/ruleset.xml + - xmllint --noout --schema ./vendor/squizlabs/php_codesniffer/phpcs.xsd ./Universal/ruleset.xml + + # Check the code-style consistency of the xml files. + - diff -B ./NormalizedArrays/ruleset.xml <(xmllint --format "./NormalizedArrays/ruleset.xml") + - diff -B ./Universal/ruleset.xml <(xmllint --format "./Universal/ruleset.xml") before_install: # Speed up build time by disabling Xdebug when its not needed. - phpenv config-rm xdebug.ini || echo 'No xdebug config.' + - export XMLLINT_INDENT=" " + # --prefer-dist will allow for optimal use of the travis caching ability. - composer install --prefer-dist --no-suggest From 39c2c6db2216f191352d3bab48112d7261b5774c Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 04:43:57 +0100 Subject: [PATCH 06/24] :sparkles: New `Universal.Lists.DisallowShortListSyntax` sniff Sister-sniff to the PHPCS native `Generic.Arrays.DisallowShortArraySyntax` sniffs to disallow the use of short lists. Includes fixer. Includes unit tests. Includes documentation. Includes partial metrics. Combine this sniff with the `Universal.Lists.DisallowLongListSyntax` sniff to get the full picture. --- .../Lists/DisallowShortListSyntaxStandard.xml | 19 +++++ .../Lists/DisallowShortListSyntaxSniff.php | 80 +++++++++++++++++++ .../Lists/DisallowShortListSyntaxUnitTest.inc | 20 +++++ .../DisallowShortListSyntaxUnitTest.inc.fixed | 20 +++++ .../Lists/DisallowShortListSyntaxUnitTest.php | 56 +++++++++++++ 5 files changed, 195 insertions(+) create mode 100644 Universal/Docs/Lists/DisallowShortListSyntaxStandard.xml create mode 100644 Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php create mode 100644 Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc create mode 100644 Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc.fixed create mode 100644 Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.php diff --git a/Universal/Docs/Lists/DisallowShortListSyntaxStandard.xml b/Universal/Docs/Lists/DisallowShortListSyntaxStandard.xml new file mode 100644 index 00000000..db6e66e6 --- /dev/null +++ b/Universal/Docs/Lists/DisallowShortListSyntaxStandard.xml @@ -0,0 +1,19 @@ + + + + + + + list($a, $b) = $array; + ]]> + + + [$a, $b] = $array; + ]]> + + + diff --git a/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php b/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php new file mode 100644 index 00000000..7a052e7b --- /dev/null +++ b/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php @@ -0,0 +1,80 @@ +getTokens(); + $openClose = Lists::getOpenClose($phpcsFile, $stackPtr); + + if ($openClose === false) { + // Not a short list, live coding or parse error. + if (isset($tokens[$stackPtr]['bracket_closer']) === true) { + // No need to examine nested subs of this short array/array access. + return $tokens[$stackPtr]['bracket_closer']; + } + + return; + } + + $phpcsFile->recordMetric($stackPtr, 'Short list syntax used', 'yes'); + + $fix = $phpcsFile->addFixableError('Short list syntax is not allowed', $stackPtr, 'Found'); + + if ($fix === true) { + $opener = $openClose['opener']; + $closer = $openClose['closer']; + + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($opener, 'list('); + $phpcsFile->fixer->replaceToken($closer, ')'); + $phpcsFile->fixer->endChangeset(); + } + } +} diff --git a/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc b/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc new file mode 100644 index 00000000..f10974b9 --- /dev/null +++ b/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc @@ -0,0 +1,20 @@ + + */ + public function getErrorList() + { + return [ + 9 => 1, + 11 => 2, + 12 => 2, + 13 => 1, + 15 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * The key of the array should represent the line number and the value + * should represent the number of warnings that should occur on that line. + * + * @return array + */ + public function getWarningList() + { + return []; + } +} From 4d3147ca251f955e5ca316d116031a3cf6d99aa8 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 04:46:25 +0100 Subject: [PATCH 07/24] :sparkles: New `Universal.Lists.DisallowLongListSyntax` sniff Sister-sniff to the PHPCS native `Generic.Arrays.DisallowLongArraySyntax` and the `Universal.Lists.DisallowShortListSyntax` sniffs to disallow the use of long lists. Includes fixer. Includes unit tests. Includes documentation. Includes partial metrics. Combine this sniff with the `Universal.Lists.DisallowShortListSyntax` sniff to get the full picture. --- .../Lists/DisallowLongListSyntaxStandard.xml | 19 +++++ .../Lists/DisallowLongListSyntaxSniff.php | 73 +++++++++++++++++++ .../Lists/DisallowLongListSyntaxUnitTest.inc | 21 ++++++ .../DisallowLongListSyntaxUnitTest.inc.fixed | 21 ++++++ .../Lists/DisallowLongListSyntaxUnitTest.php | 57 +++++++++++++++ 5 files changed, 191 insertions(+) create mode 100644 Universal/Docs/Lists/DisallowLongListSyntaxStandard.xml create mode 100644 Universal/Sniffs/Lists/DisallowLongListSyntaxSniff.php create mode 100644 Universal/Tests/Lists/DisallowLongListSyntaxUnitTest.inc create mode 100644 Universal/Tests/Lists/DisallowLongListSyntaxUnitTest.inc.fixed create mode 100644 Universal/Tests/Lists/DisallowLongListSyntaxUnitTest.php diff --git a/Universal/Docs/Lists/DisallowLongListSyntaxStandard.xml b/Universal/Docs/Lists/DisallowLongListSyntaxStandard.xml new file mode 100644 index 00000000..d1b0ea61 --- /dev/null +++ b/Universal/Docs/Lists/DisallowLongListSyntaxStandard.xml @@ -0,0 +1,19 @@ + + + + + + + [$a, $b] = $array; + ]]> + + + list($a, $b) = $array; + ]]> + + + diff --git a/Universal/Sniffs/Lists/DisallowLongListSyntaxSniff.php b/Universal/Sniffs/Lists/DisallowLongListSyntaxSniff.php new file mode 100644 index 00000000..19499268 --- /dev/null +++ b/Universal/Sniffs/Lists/DisallowLongListSyntaxSniff.php @@ -0,0 +1,73 @@ +recordMetric($stackPtr, 'Short list syntax used', 'no'); + + $fix = $phpcsFile->addFixableError('Long list syntax is not allowed', $stackPtr, 'Found'); + + if ($fix === true) { + $opener = $openClose['opener']; + $closer = $openClose['closer']; + + $phpcsFile->fixer->beginChangeset(); + + $phpcsFile->fixer->replaceToken($stackPtr, ''); + $phpcsFile->fixer->replaceToken($opener, '['); + $phpcsFile->fixer->replaceToken($closer, ']'); + + $phpcsFile->fixer->endChangeset(); + } + } +} diff --git a/Universal/Tests/Lists/DisallowLongListSyntaxUnitTest.inc b/Universal/Tests/Lists/DisallowLongListSyntaxUnitTest.inc new file mode 100644 index 00000000..9c450caf --- /dev/null +++ b/Universal/Tests/Lists/DisallowLongListSyntaxUnitTest.inc @@ -0,0 +1,21 @@ + + */ + public function getErrorList() + { + return [ + 2 => 1, + 4 => 2, + 6 => 2, + 7 => 1, + 9 => 1, + 16 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * The key of the array should represent the line number and the value + * should represent the number of warnings that should occur on that line. + * + * @return array + */ + public function getWarningList() + { + return []; + } +} From 9ef6a9598961e984ef622cf5a438bdf4c2a2354f Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 04:48:33 +0100 Subject: [PATCH 08/24] :wrench: QA: lint PHP files in this repo --- .travis.yml | 8 ++++++++ composer.json | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/.travis.yml b/.travis.yml index 53f2d95d..92d581f3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,7 @@ cache: php: - 5.4 - 7.3 + - "nightly" # Define the stages used. stages: @@ -41,6 +42,10 @@ jobs: - diff -B ./NormalizedArrays/ruleset.xml <(xmllint --format "./NormalizedArrays/ruleset.xml") - diff -B ./Universal/ruleset.xml <(xmllint --format "./Universal/ruleset.xml") + allow_failures: + # Allow failures for unstable builds. + - php: "nightly" + before_install: # Speed up build time by disabling Xdebug when its not needed. - phpenv config-rm xdebug.ini || echo 'No xdebug config.' @@ -52,6 +57,9 @@ before_install: script: + # Lint PHP files against parse errors. + - composer lint + # Validate the composer.json file on low/high PHP versions. # @link https://getcomposer.org/doc/03-cli.md#validate - composer validate --no-check-all --strict diff --git a/composer.json b/composer.json index 587f1ba4..f00b36f2 100644 --- a/composer.json +++ b/composer.json @@ -25,9 +25,16 @@ "dealerdirect/phpcodesniffer-composer-installer" : "^0.3 || ^0.4.1 || ^0.5 || ^0.6", "phpcsstandards/phpcsutils" : "^1.0 || dev-develop" }, + "require-dev" : { + "jakub-onderka/php-parallel-lint": "^1.0", + "jakub-onderka/php-console-highlighter": "^0.4" + }, "scripts" : { "install-standards": [ "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin::run" + ], + "lint": [ + "@php ./vendor/jakub-onderka/php-parallel-lint/parallel-lint . -e php --exclude vendor --exclude .git" ] } } From 6168b0be490fe9a05ae464263183ccf51278b673 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 05:01:06 +0100 Subject: [PATCH 09/24] :wrench: QA: add code style check for the code in this repo This: * Adds a custom PHPCS ruleset which uses the `PHPCSDev` ruleset to check the code style of code in this repo, with a few, very select, exclusions. * Adds convenience scripts to the `composer.json` file to check the code of the repo. * And allows for individual developers to overload the `phpcs.xml.dist` file by ignoring the typical overload files. **Important notes**: For the time being - until v 1.0.0 has been tagged for the `PHPCSDevTools` package -, the `require` will use the `dev-develop` branch of `PHPCSDevTools`. Also, the `composer validate` check doesn't need to be run on every build. Running it on just one build is sufficient, so moving it to the `sniff` stage. --- .gitattributes | 1 + .gitignore | 4 +++- .travis.yml | 12 ++++++++---- composer.json | 11 ++++++++++- phpcs.xml.dist | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 phpcs.xml.dist diff --git a/.gitattributes b/.gitattributes index 30f7d6a7..85b3b221 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,6 +8,7 @@ /.gitattributes export-ignore /.gitignore export-ignore /.travis.yml export-ignore +/phpcs.xml.dist export-ignore # # Auto detect text files and perform LF normalization diff --git a/.gitignore b/.gitignore index 4f4acd35..d813c11a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ vendor/ -composer.lock \ No newline at end of file +composer.lock +/.phpcs.xml +/phpcs.xml diff --git a/.travis.yml b/.travis.yml index 92d581f3..b1f17670 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,6 +33,13 @@ jobs: packages: - libxml2-utils script: + # Validate the composer.json file. + # @link https://getcomposer.org/doc/03-cli.md#validate + - composer validate --no-check-all --strict + + # Check the code style of the code base. + - composer checkcs + # Validate the xml files. # @link http://xmlsoft.org/xmllint.html - xmllint --noout --schema ./vendor/squizlabs/php_codesniffer/phpcs.xsd ./NormalizedArrays/ruleset.xml @@ -53,13 +60,10 @@ before_install: - export XMLLINT_INDENT=" " # --prefer-dist will allow for optimal use of the travis caching ability. + # The Composer PHPCS plugin takes care of setting the installed_paths for PHPCS. - composer install --prefer-dist --no-suggest script: # Lint PHP files against parse errors. - composer lint - - # Validate the composer.json file on low/high PHP versions. - # @link https://getcomposer.org/doc/03-cli.md#validate - - composer validate --no-check-all --strict diff --git a/composer.json b/composer.json index f00b36f2..6eb3c97a 100644 --- a/composer.json +++ b/composer.json @@ -27,14 +27,23 @@ }, "require-dev" : { "jakub-onderka/php-parallel-lint": "^1.0", - "jakub-onderka/php-console-highlighter": "^0.4" + "jakub-onderka/php-console-highlighter": "^0.4", + "phpcsstandards/phpcsdevtools": "^1.0 || dev-develop" }, + "minimum-stability": "dev", + "prefer-stable": true, "scripts" : { "install-standards": [ "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin::run" ], "lint": [ "@php ./vendor/jakub-onderka/php-parallel-lint/parallel-lint . -e php --exclude vendor --exclude .git" + ], + "checkcs": [ + "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs" + ], + "fixcs": [ + "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf" ] } } diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 00000000..ed54db69 --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,50 @@ + + + Check the code of the PHPCSExtra package itself. + + + + . + + + */vendor/* + + + + + + + + + + + + + + + + + + + + + + + + + + + From 3c3c7f10852d8d6ef8b1087310799f8378cede97 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 05:04:20 +0100 Subject: [PATCH 10/24] :wrench: QA: check all sniffs contributed are feature complete The `PHPCSDevTools` repo offers a script to check that all sniffs are "feature complete", i.e. are accompanied by documentation in `..Standard.xml` format, as well as unit tests. This check is now enabled. --- .travis.yml | 3 +++ composer.json | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index b1f17670..63f3d42a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,6 +49,9 @@ jobs: - diff -B ./NormalizedArrays/ruleset.xml <(xmllint --format "./NormalizedArrays/ruleset.xml") - diff -B ./Universal/ruleset.xml <(xmllint --format "./Universal/ruleset.xml") + # Check that the sniffs available are feature complete. + - composer check-complete + allow_failures: # Allow failures for unstable builds. - php: "nightly" diff --git a/composer.json b/composer.json index 6eb3c97a..da967e60 100644 --- a/composer.json +++ b/composer.json @@ -44,6 +44,9 @@ ], "fixcs": [ "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf" + ], + "check-complete": [ + "@php ./vendor/phpcsstandards/phpcsdevtools/bin/phpcs-check-feature-completeness ./NormalizedArrays ./Universal" ] } } From ab09e9723d86fb0c931fcd36b99044848559d35c Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 05:28:05 +0100 Subject: [PATCH 11/24] :wrench: QA: enable unit testing for the code in this repo This: * Adds a PHPUnit `phpunit.xml.dist` configuration file. * Adds a `phpunit-bootstrap.php` file to load the necessary prerequisites for the unit testing. * Adds convenience script to the `composer.json` file to run the unit tests for the repo. * Adds the necessary changes to the Travis script to test against the relevant PHP / PHPCS combinations. Includes moving the build against `nightly` to the `test` stage to allow it to lint files. The `PHPCS_VERSION` has been set to `n/a` to skip unit testing (for now). * And allow for individual developers to overload the `phpunit.xml.dist` file by ignoring the typical overload files. Other tweaks included: * Moving the `composer install` to the Travis `install` step and skipping that step for the `sniff` stage as we don't need a full composer install for that stage. --- .gitattributes | 4 +++ .gitignore | 5 ++- .travis.yml | 74 ++++++++++++++++++++++++++++++++++++-- composer.json | 7 +++- phpcs.xml.dist | 12 +++++++ phpunit-bootstrap.php | 82 +++++++++++++++++++++++++++++++++++++++++++ phpunit.xml.dist | 18 ++++++++++ 7 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 phpunit-bootstrap.php create mode 100644 phpunit.xml.dist diff --git a/.gitattributes b/.gitattributes index 85b3b221..77b6c56c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,6 +9,10 @@ /.gitignore export-ignore /.travis.yml export-ignore /phpcs.xml.dist export-ignore +/phpunit.xml.dist export-ignore +/phpunit-bootstrap.php export-ignore +/NormalizedArrays/Tests/ export-ignore +/Universal/Tests/ export-ignore # # Auto detect text files and perform LF normalization diff --git a/.gitignore b/.gitignore index d813c11a..de278d07 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ +build/ vendor/ -composer.lock +/composer.lock /.phpcs.xml /phpcs.xml +/phpunit.xml +/.phpunit.result.cache diff --git a/.travis.yml b/.travis.yml index 63f3d42a..aeff6a61 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,13 +13,31 @@ cache: php: - 5.4 + - 5.5 + - 5.6 + - 7.0 + - 7.1 + - 7.2 - 7.3 - - "nightly" + +env: + jobs: + # `master` + - PHPCS_VERSION="dev-master" LINT=1 + # Lowest supported PHPCS version. + - PHPCS_VERSION="3.3.1" # Define the stages used. +# For non-PRs, only the sniff and quicktest stages are run. +# For pull requests and merges, the full script is run (skipping quicktest). +# Note: for pull requests, "develop" is the base branch name. +# See: https://docs.travis-ci.com/user/conditions-v1 stages: - name: sniff + - name: quicktest + if: type = push AND branch NOT IN (master, develop) - name: test + if: branch IN (master, develop) jobs: fast_finish: true @@ -28,6 +46,7 @@ jobs: #### SNIFF STAGE #### - stage: sniff php: 7.4 + env: PHPCS_VERSION="dev-master" addons: apt: packages: @@ -52,16 +71,64 @@ jobs: # Check that the sniffs available are feature complete. - composer check-complete + #### QUICK TEST STAGE #### + # This is a much quicker test which only runs the unit tests and linting against the low/high + # supported PHP/PHPCS combinations. + - stage: quicktest + php: 7.4 + env: PHPCS_VERSION="dev-master" LINT=1 + - php: 7.3 + env: PHPCS_VERSION="3.3.1" + + - php: 5.4 + env: PHPCS_VERSION="dev-master" LINT=1 + - php: 5.4 + env: PHPCS_VERSION="3.3.1" + + #### TEST STAGE #### + # Additional builds to prevent issues with PHPCS versions incompatible with certain PHP versions. + - stage: test + php: 7.4 + env: PHPCS_VERSION="dev-master" LINT=1 + # PHPCS is only compatible with PHP 7.4 as of version 3.5.0. + - php: 7.4 + env: PHPCS_VERSION="3.5.0" + + - php: "nightly" + env: PHPCS_VERSION="n/a" LINT=1 + allow_failures: # Allow failures for unstable builds. - php: "nightly" + before_install: # Speed up build time by disabling Xdebug when its not needed. - phpenv config-rm xdebug.ini || echo 'No xdebug config.' - export XMLLINT_INDENT=" " + # On stable PHPCS versions, allow for PHP deprecation notices. + # Unit tests don't need to fail on those for stable releases where those issues won't get fixed anymore. + - | + if [[ "$TRAVIS_BUILD_STAGE_NAME" != "Sniff" && $PHPCS_BRANCH != "dev-master" && "$PHPCS_VERSION" != "n/a" ]]; then + echo 'error_reporting = E_ALL & ~E_DEPRECATED' >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini + fi + + +install: + # Set up test environment using Composer. + - | + if [[ $PHPCS_VERSION != "n/a" ]]; then + composer require --no-update --no-scripts squizlabs/php_codesniffer:${PHPCS_VERSION} + fi + - | + if [[ "$TRAVIS_BUILD_STAGE_NAME" == "Sniff" || $PHPCS_VERSION == "n/a" ]]; then + # The sniff stage doesn't run the unit tests, so no need for PHPUnit. + # The build on nightly also doesn't run the tests (yet). + composer remove --dev phpunit/phpunit --no-update --no-scripts + fi + # --prefer-dist will allow for optimal use of the travis caching ability. # The Composer PHPCS plugin takes care of setting the installed_paths for PHPCS. - composer install --prefer-dist --no-suggest @@ -69,4 +136,7 @@ before_install: script: # Lint PHP files against parse errors. - - composer lint + - if [[ "$LINT" == "1" ]]; then composer lint; fi + + # Run the tests. + - if [[ $PHPCS_VERSION != "n/a" ]]; then composer test; fi diff --git a/composer.json b/composer.json index da967e60..74595fe6 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,8 @@ "require-dev" : { "jakub-onderka/php-parallel-lint": "^1.0", "jakub-onderka/php-console-highlighter": "^0.4", - "phpcsstandards/phpcsdevtools": "^1.0 || dev-develop" + "phpcsstandards/phpcsdevtools": "^1.0 || dev-develop", + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0" }, "minimum-stability": "dev", "prefer-stable": true, @@ -47,6 +48,10 @@ ], "check-complete": [ "@php ./vendor/phpcsstandards/phpcsdevtools/bin/phpcs-check-feature-completeness ./NormalizedArrays ./Universal" + ], + "test": [ + "@php ./vendor/phpunit/phpunit/phpunit --filter NormalizedArrays ./vendor/squizlabs/php_codesniffer/tests/AllTests.php", + "@php ./vendor/phpunit/phpunit/phpunit --filter Universal ./vendor/squizlabs/php_codesniffer/tests/AllTests.php" ] } } diff --git a/phpcs.xml.dist b/phpcs.xml.dist index ed54db69..a410e719 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -47,4 +47,16 @@ + + + + + /phpunit-bootstrap\.php$ + + diff --git a/phpunit-bootstrap.php b/phpunit-bootstrap.php new file mode 100644 index 00000000..c76945ed --- /dev/null +++ b/phpunit-bootstrap.php @@ -0,0 +1,82 @@ + true, + 'Universal' => true, +]; + +$allStandards = PHP_CodeSniffer\Util\Standards::getInstalledStandards(); +$allStandards[] = 'Generic'; + +$standardsToIgnore = []; +foreach ($allStandards as $standard) { + if (isset($phpcsExtraStandards[$standard]) === true) { + continue; + } + + $standardsToIgnore[] = $standard; +} + +$standardsToIgnoreString = \implode(',', $standardsToIgnore); +\putenv("PHPCS_IGNORE_TESTS={$standardsToIgnoreString}"); + +// Clean up. +unset($ds, $phpcsDir, $composerPHPCSPath, $allStandards, $standardsToIgnore, $standard, $standardsToIgnoreString); diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..fbfbd163 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,18 @@ + + + + + + ./NormalizedArrays/Tests/ + ./Universal/Tests/ + + + + From 91f4f6fdaadeffeadfe38bc233fd65071dcea35b Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 07:55:14 +0100 Subject: [PATCH 12/24] :sparkles: New `Universal.Namespaces.DisallowCurlyBraceSyntax` sniff New sniff to disallow the alternative namespace syntax using curly braces: ```php namespace Vendor\Project { // Code... } ``` Includes unit tests. Includes documentation. Includes metrics. --- .../DisallowCurlyBraceSyntaxStandard.xml | 24 +++++++ .../DisallowCurlyBraceSyntaxSniff.php | 72 +++++++++++++++++++ .../DisallowCurlyBraceSyntaxUnitTest.inc | 17 +++++ .../DisallowCurlyBraceSyntaxUnitTest.php | 48 +++++++++++++ 4 files changed, 161 insertions(+) create mode 100644 Universal/Docs/Namespaces/DisallowCurlyBraceSyntaxStandard.xml create mode 100644 Universal/Sniffs/Namespaces/DisallowCurlyBraceSyntaxSniff.php create mode 100644 Universal/Tests/Namespaces/DisallowCurlyBraceSyntaxUnitTest.inc create mode 100644 Universal/Tests/Namespaces/DisallowCurlyBraceSyntaxUnitTest.php diff --git a/Universal/Docs/Namespaces/DisallowCurlyBraceSyntaxStandard.xml b/Universal/Docs/Namespaces/DisallowCurlyBraceSyntaxStandard.xml new file mode 100644 index 00000000..91eabf4e --- /dev/null +++ b/Universal/Docs/Namespaces/DisallowCurlyBraceSyntaxStandard.xml @@ -0,0 +1,24 @@ + + + + + + + + ; + +// Code + ]]> + + + { + // Code. +} + ]]> + + + diff --git a/Universal/Sniffs/Namespaces/DisallowCurlyBraceSyntaxSniff.php b/Universal/Sniffs/Namespaces/DisallowCurlyBraceSyntaxSniff.php new file mode 100644 index 00000000..b0eed5f3 --- /dev/null +++ b/Universal/Sniffs/Namespaces/DisallowCurlyBraceSyntaxSniff.php @@ -0,0 +1,72 @@ +getTokens(); + + if (isset($tokens[$stackPtr]['scope_condition']) === false + || $tokens[$stackPtr]['scope_condition'] !== $stackPtr + ) { + $phpcsFile->recordMetric($stackPtr, 'Namespace declaration using curly brace syntax', 'no'); + return; + } + + $phpcsFile->recordMetric($stackPtr, 'Namespace declaration using curly brace syntax', 'yes'); + + $phpcsFile->addError( + 'Namespace declarations using the curly brace syntax are not allowed.', + $stackPtr, + 'Forbidden' + ); + } +} diff --git a/Universal/Tests/Namespaces/DisallowCurlyBraceSyntaxUnitTest.inc b/Universal/Tests/Namespaces/DisallowCurlyBraceSyntaxUnitTest.inc new file mode 100644 index 00000000..5505b88f --- /dev/null +++ b/Universal/Tests/Namespaces/DisallowCurlyBraceSyntaxUnitTest.inc @@ -0,0 +1,17 @@ + => + */ + public function getErrorList() + { + return [ + 7 => 1, + 9 => 1, + 14 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return []; + } +} From 770e23b6f81084aeb83e827d0c60a98643603d4e Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 07:55:57 +0100 Subject: [PATCH 13/24] :sparkles: New `Universal.Namespaces.EnforceCurlyBraceSyntax` sniff New sniff to enforce using the alternative namespace syntax using curly braces: ```php namespace Vendor\Project { // Code... } ``` Includes unit tests. Includes documentation. Includes metrics. --- .../EnforceCurlyBraceSyntaxStandard.xml | 24 +++++++ .../EnforceCurlyBraceSyntaxSniff.php | 72 +++++++++++++++++++ .../EnforceCurlyBraceSyntaxUnitTest.inc | 15 ++++ .../EnforceCurlyBraceSyntaxUnitTest.php | 47 ++++++++++++ 4 files changed, 158 insertions(+) create mode 100644 Universal/Docs/Namespaces/EnforceCurlyBraceSyntaxStandard.xml create mode 100644 Universal/Sniffs/Namespaces/EnforceCurlyBraceSyntaxSniff.php create mode 100644 Universal/Tests/Namespaces/EnforceCurlyBraceSyntaxUnitTest.inc create mode 100644 Universal/Tests/Namespaces/EnforceCurlyBraceSyntaxUnitTest.php diff --git a/Universal/Docs/Namespaces/EnforceCurlyBraceSyntaxStandard.xml b/Universal/Docs/Namespaces/EnforceCurlyBraceSyntaxStandard.xml new file mode 100644 index 00000000..411a1022 --- /dev/null +++ b/Universal/Docs/Namespaces/EnforceCurlyBraceSyntaxStandard.xml @@ -0,0 +1,24 @@ + + + + + + + + { + // Code. +} + ]]> + + + ; + +// Code + ]]> + + + diff --git a/Universal/Sniffs/Namespaces/EnforceCurlyBraceSyntaxSniff.php b/Universal/Sniffs/Namespaces/EnforceCurlyBraceSyntaxSniff.php new file mode 100644 index 00000000..a5a9825d --- /dev/null +++ b/Universal/Sniffs/Namespaces/EnforceCurlyBraceSyntaxSniff.php @@ -0,0 +1,72 @@ +getTokens(); + + if (isset($tokens[$stackPtr]['scope_condition']) === true + && $tokens[$stackPtr]['scope_condition'] === $stackPtr + ) { + $phpcsFile->recordMetric($stackPtr, 'Namespace declaration using curly brace syntax', 'yes'); + return; + } + + $phpcsFile->recordMetric($stackPtr, 'Namespace declaration using curly brace syntax', 'no'); + + $phpcsFile->addError( + 'Namespace declarations without curly braces are not allowed.', + $stackPtr, + 'Forbidden' + ); + } +} diff --git a/Universal/Tests/Namespaces/EnforceCurlyBraceSyntaxUnitTest.inc b/Universal/Tests/Namespaces/EnforceCurlyBraceSyntaxUnitTest.inc new file mode 100644 index 00000000..cfadb1e1 --- /dev/null +++ b/Universal/Tests/Namespaces/EnforceCurlyBraceSyntaxUnitTest.inc @@ -0,0 +1,15 @@ + => + */ + public function getErrorList() + { + return [ + 10 => 1, + 12 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return []; + } +} From e033f0d77fe3b626dd7763529226d087d0d22cc1 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 07:59:04 +0100 Subject: [PATCH 14/24] :sparkles: New `Universal.Namespaces.OneDeclarationPerFile` sniff New sniff to disallow the use of multiple namespaces within a file. Includes unit tests. Includes documentation. --- .../OneDeclarationPerFileStandard.xml | 24 +++++ .../Namespaces/OneDeclarationPerFileSniff.php | 96 +++++++++++++++++++ .../OneDeclarationPerFileUnitTest.1.inc | 22 +++++ .../OneDeclarationPerFileUnitTest.2.inc | 26 +++++ .../OneDeclarationPerFileUnitTest.3.inc | 11 +++ .../OneDeclarationPerFileUnitTest.php | 64 +++++++++++++ 6 files changed, 243 insertions(+) create mode 100644 Universal/Docs/Namespaces/OneDeclarationPerFileStandard.xml create mode 100644 Universal/Sniffs/Namespaces/OneDeclarationPerFileSniff.php create mode 100644 Universal/Tests/Namespaces/OneDeclarationPerFileUnitTest.1.inc create mode 100644 Universal/Tests/Namespaces/OneDeclarationPerFileUnitTest.2.inc create mode 100644 Universal/Tests/Namespaces/OneDeclarationPerFileUnitTest.3.inc create mode 100644 Universal/Tests/Namespaces/OneDeclarationPerFileUnitTest.php diff --git a/Universal/Docs/Namespaces/OneDeclarationPerFileStandard.xml b/Universal/Docs/Namespaces/OneDeclarationPerFileStandard.xml new file mode 100644 index 00000000..5d5de028 --- /dev/null +++ b/Universal/Docs/Namespaces/OneDeclarationPerFileStandard.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + namespace Vendor\Project\Sub\B { +} + ]]> + + + diff --git a/Universal/Sniffs/Namespaces/OneDeclarationPerFileSniff.php b/Universal/Sniffs/Namespaces/OneDeclarationPerFileSniff.php new file mode 100644 index 00000000..d18c59a0 --- /dev/null +++ b/Universal/Sniffs/Namespaces/OneDeclarationPerFileSniff.php @@ -0,0 +1,96 @@ +getFilename(); + if ($this->currentFile !== $fileName) { + // Reset the properties for each new file. + $this->currentFile = $fileName; + $this->declarationSeen = false; + } + + if (Namespaces::isDeclaration($phpcsFile, $stackPtr) === false) { + // Namespace operator, not a declaration; or live coding/parse error. + return; + } + + if ($this->declarationSeen === false) { + // This is the first namespace declaration in the file. + $this->declarationSeen = $stackPtr; + return; + } + + $tokens = $phpcsFile->getTokens(); + + // OK, so this is a file with multiple namespace declarations. + $phpcsFile->addError( + 'There should be only one namespace declaration per file. The first declaration was found on line %d', + $stackPtr, + 'MultipleFound', + [$tokens[$this->declarationSeen]['line']] + ); + } +} diff --git a/Universal/Tests/Namespaces/OneDeclarationPerFileUnitTest.1.inc b/Universal/Tests/Namespaces/OneDeclarationPerFileUnitTest.1.inc new file mode 100644 index 00000000..9db88f07 --- /dev/null +++ b/Universal/Tests/Namespaces/OneDeclarationPerFileUnitTest.1.inc @@ -0,0 +1,22 @@ + => + */ + public function getErrorList($testFile = '') + { + switch ($testFile) { + case 'OneDeclarationPerFileUnitTest.1.inc': + return [ + 9 => 1, + 13 => 1, + 17 => 1, + ]; + + case 'OneDeclarationPerFileUnitTest.2.inc': + return [ + 10 => 1, + 15 => 1, + 20 => 1, + 26 => 1, + ]; + + default: + return []; + } + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return []; + } +} From 8d5bb109cf4ade0507486813bb9ac4b94a381777 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 08:00:18 +0100 Subject: [PATCH 15/24] :sparkles: New `Universal.UseStatements.DisallowUseClass` sniff Sniff to forbid using import use statements for classes/traits/interfaces. The sniff contains two error codes `FoundWithoutAlias` and `FoundWithAlias` to allow for only forbidding class `use` import statements with or without alias. Includes unit tests. Includes documentation. Includes metrics. --- .../DisallowUseClassStandard.xml | 22 ++++ .../UseStatements/DisallowUseClassSniff.php | 107 ++++++++++++++++++ .../DisallowUseClassUnitTest.inc | 38 +++++++ .../DisallowUseClassUnitTest.php | 56 +++++++++ 4 files changed, 223 insertions(+) create mode 100644 Universal/Docs/UseStatements/DisallowUseClassStandard.xml create mode 100644 Universal/Sniffs/UseStatements/DisallowUseClassSniff.php create mode 100644 Universal/Tests/UseStatements/DisallowUseClassUnitTest.inc create mode 100644 Universal/Tests/UseStatements/DisallowUseClassUnitTest.php diff --git a/Universal/Docs/UseStatements/DisallowUseClassStandard.xml b/Universal/Docs/UseStatements/DisallowUseClassStandard.xml new file mode 100644 index 00000000..00b69ba0 --- /dev/null +++ b/Universal/Docs/UseStatements/DisallowUseClassStandard.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/Universal/Sniffs/UseStatements/DisallowUseClassSniff.php b/Universal/Sniffs/UseStatements/DisallowUseClassSniff.php new file mode 100644 index 00000000..4ada6533 --- /dev/null +++ b/Universal/Sniffs/UseStatements/DisallowUseClassSniff.php @@ -0,0 +1,107 @@ +getTokens(); + $endOfStatement = $phpcsFile->findNext([\T_SEMICOLON, \T_CLOSE_TAG], ($stackPtr + 1)); + + foreach ($statements['name'] as $alias => $fullName) { + $reportPtr = $stackPtr; + do { + $reportPtr = $phpcsFile->findNext(\T_STRING, ($reportPtr + 1), $endOfStatement, false, $alias); + if ($reportPtr === false) { + // Shouldn't be possible. + continue 2; + } + + $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($reportPtr + 1), $endOfStatement, true); + if ($next !== false && $tokens[$next]['code'] === \T_NS_SEPARATOR) { + // Namespace level with same name. Continue searching + continue; + } + + break; + } while (true); + + $error = 'Use import statements for classes/traits/interfaces are not allowed.'; + $error .= ' Found import statement for: "%s"'; + $data = [$fullName, $alias]; + + $offsetFromEnd = (\strlen($alias) + 1); + if (\substr($fullName, -$offsetFromEnd) === '\\' . $alias) { + $phpcsFile->recordMetric($reportPtr, 'Use import statement for class/interface/trait', 'without alias'); + + $phpcsFile->addError($error, $reportPtr, 'FoundWithoutAlias', $data); + continue; + } + + $phpcsFile->recordMetric($reportPtr, 'Use import statement for class/interface/trait', 'with alias'); + + $error .= ' with alias: "%s"'; + $phpcsFile->addError($error, $reportPtr, 'FoundWithAlias', $data); + } + } +} diff --git a/Universal/Tests/UseStatements/DisallowUseClassUnitTest.inc b/Universal/Tests/UseStatements/DisallowUseClassUnitTest.inc new file mode 100644 index 00000000..bb94e3b0 --- /dev/null +++ b/Universal/Tests/UseStatements/DisallowUseClassUnitTest.inc @@ -0,0 +1,38 @@ + => + */ + public function getErrorList() + { + return [ + 8 => 1, + 9 => 1, + 11 => 1, + 12 => 1, + 13 => 1, + 14 => 1, + 17 => 1, + 18 => 1, + 19 => 1, + 24 => 1, + 28 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return []; + } +} From 84d205c93c1a96ec9f370fe0a241ffcf481a96bf Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 08:01:06 +0100 Subject: [PATCH 16/24] :sparkles: New `Universal.UseStatements.DisallowUseFunction` sniff Sniff to forbid using import use statements for functions. The sniff contains two error codes `FoundWithoutAlias` and `FoundWithAlias` to allow for only forbidding function `use` import statements with or without alias. Includes unit tests. Includes documentation. Includes metrics. --- .../DisallowUseFunctionStandard.xml | 22 ++++ .../DisallowUseFunctionSniff.php | 107 ++++++++++++++++++ .../DisallowUseFunctionUnitTest.inc | 37 ++++++ .../DisallowUseFunctionUnitTest.php | 55 +++++++++ 4 files changed, 221 insertions(+) create mode 100644 Universal/Docs/UseStatements/DisallowUseFunctionStandard.xml create mode 100644 Universal/Sniffs/UseStatements/DisallowUseFunctionSniff.php create mode 100644 Universal/Tests/UseStatements/DisallowUseFunctionUnitTest.inc create mode 100644 Universal/Tests/UseStatements/DisallowUseFunctionUnitTest.php diff --git a/Universal/Docs/UseStatements/DisallowUseFunctionStandard.xml b/Universal/Docs/UseStatements/DisallowUseFunctionStandard.xml new file mode 100644 index 00000000..fc327bce --- /dev/null +++ b/Universal/Docs/UseStatements/DisallowUseFunctionStandard.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/Universal/Sniffs/UseStatements/DisallowUseFunctionSniff.php b/Universal/Sniffs/UseStatements/DisallowUseFunctionSniff.php new file mode 100644 index 00000000..0b5d7ead --- /dev/null +++ b/Universal/Sniffs/UseStatements/DisallowUseFunctionSniff.php @@ -0,0 +1,107 @@ +getTokens(); + $endOfStatement = $phpcsFile->findNext([\T_SEMICOLON, \T_CLOSE_TAG], ($stackPtr + 1)); + + foreach ($statements['function'] as $alias => $fullName) { + $reportPtr = $stackPtr; + do { + $reportPtr = $phpcsFile->findNext(\T_STRING, ($reportPtr + 1), $endOfStatement, false, $alias); + if ($reportPtr === false) { + // Shouldn't be possible. + continue 2; + } + + $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($reportPtr + 1), $endOfStatement, true); + if ($next !== false && $tokens[$next]['code'] === \T_NS_SEPARATOR) { + // Namespace level with same name. Continue searching + continue; + } + + break; + } while (true); + + $error = 'Use import statements for functions are not allowed.'; + $error .= ' Found import statement for: "%s"'; + $data = [$fullName, $alias]; + + $offsetFromEnd = (\strlen($alias) + 1); + if (\substr($fullName, -$offsetFromEnd) === '\\' . $alias) { + $phpcsFile->recordMetric($reportPtr, 'Use import statement for functions', 'without alias'); + + $phpcsFile->addError($error, $reportPtr, 'FoundWithoutAlias', $data); + continue; + } + + $phpcsFile->recordMetric($reportPtr, 'Use import statement for functions', 'with alias'); + + $error .= ' with alias: "%s"'; + $phpcsFile->addError($error, $reportPtr, 'FoundWithAlias', $data); + } + } +} diff --git a/Universal/Tests/UseStatements/DisallowUseFunctionUnitTest.inc b/Universal/Tests/UseStatements/DisallowUseFunctionUnitTest.inc new file mode 100644 index 00000000..95186776 --- /dev/null +++ b/Universal/Tests/UseStatements/DisallowUseFunctionUnitTest.inc @@ -0,0 +1,37 @@ + => + */ + public function getErrorList() + { + return [ + 8 => 1, + 9 => 1, + 11 => 1, + 12 => 1, + 13 => 1, + 16 => 1, + 17 => 1, + 18 => 1, + 24 => 1, + 26 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return []; + } +} From 4e27fd76b3a169e7864ca03c3c98dd6e606cc6ca Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 08:01:47 +0100 Subject: [PATCH 17/24] :sparkles: New `Universal.UseStatements.DisallowUseConst` sniff Sniff to forbid using import use statements for constants. The sniff contains two error codes `FoundWithoutAlias` and `FoundWithAlias` to allow for only forbidding constant `use` import statements with or without alias. Includes unit tests. Includes documentation. Includes metrics. --- .../DisallowUseConstStandard.xml | 22 ++++ .../UseStatements/DisallowUseConstSniff.php | 107 ++++++++++++++++++ .../DisallowUseConstUnitTest.inc | 35 ++++++ .../DisallowUseConstUnitTest.php | 52 +++++++++ 4 files changed, 216 insertions(+) create mode 100644 Universal/Docs/UseStatements/DisallowUseConstStandard.xml create mode 100644 Universal/Sniffs/UseStatements/DisallowUseConstSniff.php create mode 100644 Universal/Tests/UseStatements/DisallowUseConstUnitTest.inc create mode 100644 Universal/Tests/UseStatements/DisallowUseConstUnitTest.php diff --git a/Universal/Docs/UseStatements/DisallowUseConstStandard.xml b/Universal/Docs/UseStatements/DisallowUseConstStandard.xml new file mode 100644 index 00000000..8a331a0a --- /dev/null +++ b/Universal/Docs/UseStatements/DisallowUseConstStandard.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/Universal/Sniffs/UseStatements/DisallowUseConstSniff.php b/Universal/Sniffs/UseStatements/DisallowUseConstSniff.php new file mode 100644 index 00000000..77a4a325 --- /dev/null +++ b/Universal/Sniffs/UseStatements/DisallowUseConstSniff.php @@ -0,0 +1,107 @@ +getTokens(); + $endOfStatement = $phpcsFile->findNext([\T_SEMICOLON, \T_CLOSE_TAG], ($stackPtr + 1)); + + foreach ($statements['const'] as $alias => $fullName) { + $reportPtr = $stackPtr; + do { + $reportPtr = $phpcsFile->findNext(\T_STRING, ($reportPtr + 1), $endOfStatement, false, $alias); + if ($reportPtr === false) { + // Shouldn't be possible. + continue 2; + } + + $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($reportPtr + 1), $endOfStatement, true); + if ($next !== false && $tokens[$next]['code'] === \T_NS_SEPARATOR) { + // Namespace level with same name. Continue searching + continue; + } + + break; + } while (true); + + $error = 'Use import statements for constants are not allowed.'; + $error .= ' Found import statement for: "%s"'; + $data = [$fullName, $alias]; + + $offsetFromEnd = (\strlen($alias) + 1); + if (\substr($fullName, -$offsetFromEnd) === '\\' . $alias) { + $phpcsFile->recordMetric($reportPtr, 'Use import statement for constant', 'without alias'); + + $phpcsFile->addError($error, $reportPtr, 'FoundWithoutAlias', $data); + continue; + } + + $phpcsFile->recordMetric($reportPtr, 'Use import statement for constant', 'with alias'); + + $error .= ' with alias: "%s"'; + $phpcsFile->addError($error, $reportPtr, 'FoundWithAlias', $data); + } + } +} diff --git a/Universal/Tests/UseStatements/DisallowUseConstUnitTest.inc b/Universal/Tests/UseStatements/DisallowUseConstUnitTest.inc new file mode 100644 index 00000000..f03f5b5f --- /dev/null +++ b/Universal/Tests/UseStatements/DisallowUseConstUnitTest.inc @@ -0,0 +1,35 @@ + => + */ + public function getErrorList() + { + return [ + 8 => 1, + 9 => 1, + 11 => 1, + 12 => 1, + 15 => 1, + 16 => 1, + 23 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return []; + } +} From 516d06be9a6a61a2e74cb3519355d53ed385162e Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 08:02:41 +0100 Subject: [PATCH 18/24] :sparkles: New `Universal.ControlStructures.IfElseDeclaration` sniff New sniff to verify that `else(if)` statements with braces are on a new line. Sister-sniff to the following two PHPCS native sniffs which each demand that `else[]if` is on the same line as the closing curly of the preceding `(else)if`: - `PEAR.ControlStructures.ControlSignature[.Found]` - `Squiz.ControlStructures.ControlSignature.SpaceAfterCloseBrace` Other related sniffs: - `Squiz.ControlStructures.ElseIfDeclaration` Forbids the use of "elseif", demands "else if". - `PSR2.ControlStructures.ElseIfDeclaration` Forbids the use of "else if", demands "elseif". Includes fixer. Includes unit tests. Includes documentation. Includes metrics. --- .../IfElseDeclarationStandard.xml | 34 ++++ .../IfElseDeclarationSniff.php | 150 ++++++++++++++++++ .../IfElseDeclarationUnitTest.inc | 138 ++++++++++++++++ .../IfElseDeclarationUnitTest.inc.fixed | 150 ++++++++++++++++++ .../IfElseDeclarationUnitTest.php | 57 +++++++ 5 files changed, 529 insertions(+) create mode 100644 Universal/Docs/ControlStructures/IfElseDeclarationStandard.xml create mode 100644 Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php create mode 100644 Universal/Tests/ControlStructures/IfElseDeclarationUnitTest.inc create mode 100644 Universal/Tests/ControlStructures/IfElseDeclarationUnitTest.inc.fixed create mode 100644 Universal/Tests/ControlStructures/IfElseDeclarationUnitTest.php diff --git a/Universal/Docs/ControlStructures/IfElseDeclarationStandard.xml b/Universal/Docs/ControlStructures/IfElseDeclarationStandard.xml new file mode 100644 index 00000000..fe3cb9a6 --- /dev/null +++ b/Universal/Docs/ControlStructures/IfElseDeclarationStandard.xml @@ -0,0 +1,34 @@ + + + + + + + + +elseif ($bar) { + $var = 2; +} +else { + $var = 3; +} + ]]> + + + elseif ($bar) { + $var = 2; +} else { + $var = 3; +} + ]]> + + + diff --git a/Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php b/Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php new file mode 100644 index 00000000..738428da --- /dev/null +++ b/Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php @@ -0,0 +1,150 @@ +getTokens(); + + /* + * Check for control structures without braces and alternative syntax. + */ + $scopePtr = $stackPtr; + if (isset($tokens[$stackPtr]['scope_opener']) === false) { + // Deal with "else if". + $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if ($tokens[$next]['code'] === \T_IF) { + $scopePtr = $next; + } + } + + if (isset($tokens[$scopePtr]['scope_opener']) === false + || $tokens[$tokens[$scopePtr]['scope_opener']]['code'] === \T_COLON + ) { + // No scope opener found or alternative syntax (not our concern). + return; + } + + /* + * Check whether the else(if) is on a new line. + */ + $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + if ($prevNonEmpty === false || $tokens[$prevNonEmpty]['code'] !== \T_CLOSE_CURLY_BRACKET) { + // Parse error. Not our concern. + return; + } + + if ($tokens[$prevNonEmpty]['line'] !== $tokens[$stackPtr]['line']) { + $phpcsFile->recordMetric($stackPtr, 'Else(if) on a new line', 'yes'); + return; + } + + $phpcsFile->recordMetric($stackPtr, 'Else(if) on a new line', 'no'); + + $errorBase = \strtoupper($tokens[$stackPtr]['content']); + $error = $errorBase . ' statement must be on a new line.'; + + $prevNonWhitespace = $phpcsFile->findPrevious(\T_WHITESPACE, ($stackPtr - 1), null, true); + + if ($prevNonWhitespace !== $prevNonEmpty) { + // Comment found between previous scope closer and the keyword. + $fix = $phpcsFile->addError($error, $stackPtr, 'NoNewLine'); + return; + } + + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'NoNewLine'); + if ($fix === false) { + return; + } + + /* + * Fix it. + */ + + // Figure out the indentation for the else(if). + $indentBase = $prevNonEmpty; + if (isset($tokens[$prevNonEmpty]['scope_condition']) === true + && ($tokens[$tokens[$prevNonEmpty]['scope_condition']]['column'] === 1 + || ($tokens[($tokens[$prevNonEmpty]['scope_condition'] - 1)]['code'] === \T_WHITESPACE + && $tokens[($tokens[$prevNonEmpty]['scope_condition'] - 1)]['column'] === 1)) + ) { + // Base the indentation off the previous if/elseif if on a line by itself. + $indentBase = $tokens[$prevNonEmpty]['scope_condition']; + } + + $indent = ''; + $firstOnIndentLine = $indentBase; + if ($tokens[$firstOnIndentLine]['column'] !== 1) { + while (isset($tokens[($firstOnIndentLine - 1)]) && $tokens[--$firstOnIndentLine]['column'] !== 1); + + if ($tokens[$firstOnIndentLine]['code'] === \T_WHITESPACE) { + $indent = $tokens[$firstOnIndentLine]['content']; + } + } + + $phpcsFile->fixer->beginChangeset(); + + // Remove any whitespace between the previous scope closer and the else(if). + for ($i = ($prevNonEmpty + 1); $i < $stackPtr; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->addContent($prevNonEmpty, $phpcsFile->eolChar . $indent); + $phpcsFile->fixer->endChangeset(); + } +} diff --git a/Universal/Tests/ControlStructures/IfElseDeclarationUnitTest.inc b/Universal/Tests/ControlStructures/IfElseDeclarationUnitTest.inc new file mode 100644 index 00000000..78fc04b2 --- /dev/null +++ b/Universal/Tests/ControlStructures/IfElseDeclarationUnitTest.inc @@ -0,0 +1,138 @@ + => + */ + public function getErrorList() + { + return [ + 79 => 1, + 85 => 1, + 87 => 1, + 91 => 1, + 94 => 1, + 96 => 1, + 107 => 1, + 113 => 1, + 115 => 1, + 119 => 1, + 126 => 1, + 131 => 2, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return []; + } +} From f51cacb6cc74e3312ae4165029ce0dfb1acc6107 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 08:02:54 +0100 Subject: [PATCH 19/24] PHPCS ruleset: add temporary exclusion for bug in PHPCS upstream --- phpcs.xml.dist | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index a410e719..3a5f19a4 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -59,4 +59,20 @@ /phpunit-bootstrap\.php$ + + + + + + /Universal/Sniffs/ControlStructures/IfElseDeclarationSniff\.php$ + + From 5163e7f43e92e8fd14a989a9418c2c97f9db53b2 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 08:03:54 +0100 Subject: [PATCH 20/24] :sparkles: New `NormalizedArrays.Arrays.CommaAfterLast` sniff Configurable sniff to enforce/forbid a comma after the last array item. By default, this sniff will: - forbid a comma after the last array item for _single-line arrays_. - enforce a comma after the last array item for _multi-line arrays_. This can be changed for each type or array individually by setting the `singleLine` or `multiLine` properties in a custom ruleset. The valid values are: `enforce`, `forbid` or `skip` to not check the comma after the last array item for a particular type of array. Includes fixers. Includes unit tests. Includes documentation. Includes metrics. --- .../Docs/Arrays/CommaAfterLastStandard.xml | 39 ++++ .../Sniffs/Arrays/CommaAfterLastSniff.php | 210 ++++++++++++++++++ .../Tests/Arrays/CommaAfterLastUnitTest.inc | 177 +++++++++++++++ .../Arrays/CommaAfterLastUnitTest.inc.fixed | 177 +++++++++++++++ .../Tests/Arrays/CommaAfterLastUnitTest.php | 63 ++++++ 5 files changed, 666 insertions(+) create mode 100644 NormalizedArrays/Docs/Arrays/CommaAfterLastStandard.xml create mode 100644 NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php create mode 100644 NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.inc create mode 100644 NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.inc.fixed create mode 100644 NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.php diff --git a/NormalizedArrays/Docs/Arrays/CommaAfterLastStandard.xml b/NormalizedArrays/Docs/Arrays/CommaAfterLastStandard.xml new file mode 100644 index 00000000..321608e4 --- /dev/null +++ b/NormalizedArrays/Docs/Arrays/CommaAfterLastStandard.xml @@ -0,0 +1,39 @@ + + + no comma after the last array item. + + However, for multi-line arrays, there should be a comma after the last array item. + ]]> + + + + + + + , ); + ]]> + + + + + 'foo', + 2 => 'bar', +]; + ]]> + + + 'foo', + 2 => 'bar' +]; + ]]> + + + diff --git a/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php b/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php new file mode 100644 index 00000000..2adf4a1f --- /dev/null +++ b/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php @@ -0,0 +1,210 @@ + true, + 'forbid' => true, + 'skip' => true, + ]; + + /** + * Returns an array of tokens this test wants to listen for. + * + * @since 1.0.0 + * + * @return array + */ + public function register() + { + return [ + \T_ARRAY, + \T_OPEN_SHORT_ARRAY, + \T_OPEN_SQUARE_BRACKET, + ]; + } + + /** + * Processes this test, when one of its tokens is encountered. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. + * + * @return void + */ + public function process(File $phpcsFile, $stackPtr) + { + // Validate the property input. Invalid values will result in the check being skipped. + if (isset($this->validValues[$this->singleLine]) === false) { + $this->singleLine = 'skip'; + } + if (isset($this->validValues[$this->multiLine]) === false) { + $this->multiLine = 'skip'; + } + + $openClose = Arrays::getOpenClose($phpcsFile, $stackPtr); + if ($openClose === false) { + // Short list, real square bracket, live coding or parse error. + return; + } + + $tokens = $phpcsFile->getTokens(); + $opener = $openClose['opener']; + $closer = $openClose['closer']; + + $action = $this->singleLine; + $phrase = 'single-line'; + $errorCode = 'SingleLine'; + if ($tokens[$opener]['line'] !== $tokens[$closer]['line']) { + $action = $this->multiLine; + $phrase = 'multi-line'; + $errorCode = 'MultiLine'; + } + + if ($action === 'skip') { + // Nothing to do. + return; + } + + $lastNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($closer - 1), $opener, true); + if ($lastNonEmpty === false || $lastNonEmpty === $opener) { + // Bow out: empty array. + return; + } + + $isComma = ($tokens[$lastNonEmpty]['code'] === \T_COMMA); + + $phpcsFile->recordMetric( + $stackPtr, + \ucfirst($phrase) . ' array - comma after last item', + ($isComma === true ? 'yes' : 'no') + ); + + switch ($action) { + case 'enforce': + if ($isComma === true) { + return; + } + + $error = 'There should be a comma after the last array item in a %s array.'; + $errorCode = 'Missing' . $errorCode; + $data = [$phrase]; + $fix = $phpcsFile->addFixableError($error, $lastNonEmpty, $errorCode, $data); + if ($fix === true) { + $extraContent = ','; + + if ($tokens[$lastNonEmpty]['code'] === \T_END_HEREDOC + || $tokens[$lastNonEmpty]['code'] === \T_END_NOWDOC + ) { + // Prevent parse errors in PHP < 7.3 which doesn't support flexible heredoc/nowdoc. + $extraContent = $phpcsFile->eolChar . $extraContent; + } + + $phpcsFile->fixer->addContent($lastNonEmpty, $extraContent); + } + + return; + + case 'forbid': + if ($isComma === false) { + return; + } + + $error = 'A comma after the last array item in a %s array is not allowed.'; + $errorCode = 'Found' . $errorCode; + $data = [$phrase]; + $fix = $phpcsFile->addFixableError($error, $lastNonEmpty, $errorCode, $data); + if ($fix === true) { + $start = $lastNonEmpty; + $end = $lastNonEmpty; + + // Make sure we're not leaving a superfluous blank line behind. + $prevNonWhitespace = $phpcsFile->findPrevious(\T_WHITESPACE, ($lastNonEmpty - 1), $opener, true); + $nextNonWhitespace = $phpcsFile->findNext(\T_WHITESPACE, ($lastNonEmpty + 1), ($closer + 1), true); + if ($prevNonWhitespace !== false + && $tokens[$prevNonWhitespace]['line'] < $tokens[$lastNonEmpty]['line'] + && $nextNonWhitespace !== false + && $tokens[$nextNonWhitespace]['line'] > $tokens[$lastNonEmpty]['line'] + ) { + $start = ($prevNonWhitespace + 1); + } + + $phpcsFile->fixer->beginChangeset(); + for ($i = $start; $i <= $end; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->endChangeset(); + } + + return; + } + } +} diff --git a/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.inc b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.inc new file mode 100644 index 00000000..8615ee9e --- /dev/null +++ b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.inc @@ -0,0 +1,177 @@ +method_name( array( + 'phrase' => << 'value' , + 2 => [ + 'a' => 'value' ,// phpcs:disable Standard.Category.Sniff - the extra spacing is fine, might be for alignment with other comments. + 'b' => array( + 1 + ), + 'c' => apply_filters( 'filter', $input, $var ) + ], + 3 => apply_filters( 'filter', $input, $var )/* phpcs:ignore Standard.Category.Sniff */ +); + +$missing = array( + 'first', + 'second' + //'third', + ); + +$missingNowdoc = function_call()->method_name( array( + 'phrase' => <<<'EOD' +Here comes some text. +EOD +) ); + +/* + * Test forbidding a comma after the last array item. + */ +// phpcs:set NormalizedArrays.Arrays.CommaAfterLast singleLine forbid + +$good = array( 1, 2, 3 ); +$good = [ 'a', 'b', 'c' ]; + +$found = array( 1, 2, 3, ); +$found = [ 'a', 'b', 'c', ]; + +// phpcs:set NormalizedArrays.Arrays.CommaAfterLast multiLine forbid + +$good = array( + 1, + 3 +); +$good = [ + 'a', + 'c' +]; + +$goodNowdoc = function_call()->method_name( array( + 'phrase' => <<<'EOD' +Here comes some text. +EOD +) ); + +$found = array( + 1, + 3,/* Comment. */ +); +$found = [ + 'a', + 'c', +]; + +$foundInNested = array( + 1 => 'value' , + 2 => [ + 'a' => 'value' ,// phpcs:disable Standard.Category.Sniff - the extra spacing is fine, might be for alignment with other comments. + 'b' => array( + 1, + ), + 'c' => apply_filters( 'filter', $input, $var ), + ], + 3 => apply_filters( 'filter', $input, $var ), /* phpcs:ignore Standard.Category.Sniff */ +); + +$foundHeredoc = function_call()->method_name( array( + 'phrase' => <<<"EOD" +Here comes some text. +EOD +, +) ); + +$foundHeredoc = function_call()->method_name( array( + 'phrase' => <<<"EOD" +Here comes some text. +EOD +, /*comment*/ +) ); + +// Reset the properties to the defaults. +// phpcs:set NormalizedArrays.Arrays.CommaAfterLast singleLine forbid +// phpcs:set NormalizedArrays.Arrays.CommaAfterLast multiLine enforce + +/* + * Test live coding. This should be the last test in the file. + */ +// Intentional parse error. +$ignore = array( diff --git a/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.inc.fixed b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.inc.fixed new file mode 100644 index 00000000..d06573c1 --- /dev/null +++ b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.inc.fixed @@ -0,0 +1,177 @@ +method_name( array( + 'phrase' => << 'value' , + 2 => [ + 'a' => 'value' ,// phpcs:disable Standard.Category.Sniff - the extra spacing is fine, might be for alignment with other comments. + 'b' => array( + 1, + ), + 'c' => apply_filters( 'filter', $input, $var ), + ], + 3 => apply_filters( 'filter', $input, $var ),/* phpcs:ignore Standard.Category.Sniff */ +); + +$missing = array( + 'first', + 'second', + //'third', + ); + +$missingNowdoc = function_call()->method_name( array( + 'phrase' => <<<'EOD' +Here comes some text. +EOD +, +) ); + +/* + * Test forbidding a comma after the last array item. + */ +// phpcs:set NormalizedArrays.Arrays.CommaAfterLast singleLine forbid + +$good = array( 1, 2, 3 ); +$good = [ 'a', 'b', 'c' ]; + +$found = array( 1, 2, 3 ); +$found = [ 'a', 'b', 'c' ]; + +// phpcs:set NormalizedArrays.Arrays.CommaAfterLast multiLine forbid + +$good = array( + 1, + 3 +); +$good = [ + 'a', + 'c' +]; + +$goodNowdoc = function_call()->method_name( array( + 'phrase' => <<<'EOD' +Here comes some text. +EOD +) ); + +$found = array( + 1, + 3/* Comment. */ +); +$found = [ + 'a', + 'c' +]; + +$foundInNested = array( + 1 => 'value' , + 2 => [ + 'a' => 'value' ,// phpcs:disable Standard.Category.Sniff - the extra spacing is fine, might be for alignment with other comments. + 'b' => array( + 1 + ), + 'c' => apply_filters( 'filter', $input, $var ) + ], + 3 => apply_filters( 'filter', $input, $var ) /* phpcs:ignore Standard.Category.Sniff */ +); + +$foundHeredoc = function_call()->method_name( array( + 'phrase' => <<<"EOD" +Here comes some text. +EOD +) ); + +$foundHeredoc = function_call()->method_name( array( + 'phrase' => <<<"EOD" +Here comes some text. +EOD + /*comment*/ +) ); + +// Reset the properties to the defaults. +// phpcs:set NormalizedArrays.Arrays.CommaAfterLast singleLine forbid +// phpcs:set NormalizedArrays.Arrays.CommaAfterLast multiLine enforce + +/* + * Test live coding. This should be the last test in the file. + */ +// Intentional parse error. +$ignore = array( diff --git a/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.php b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.php new file mode 100644 index 00000000..31d34634 --- /dev/null +++ b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.php @@ -0,0 +1,63 @@ + => + */ + public function getErrorList() + { + return [ + 52 => 1, + 53 => 1, + 75 => 1, + 79 => 1, + 87 => 1, + 89 => 1, + 91 => 1, + 96 => 1, + 103 => 1, + 114 => 1, + 115 => 1, + 136 => 1, + 140 => 1, + 148 => 1, + 150 => 1, + 152 => 1, + 159 => 1, + 166 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return []; + } +} From 1365caa509230fdbd3bd01179b52a5795c4d45fc Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 08:23:23 +0100 Subject: [PATCH 21/24] :sparkles: New `NormalizedArrays.Arrays.ArrayBraceSpacing` sniff Enforce consistent spacing for the open/close braces of arrays. The sniff allows for having different settings for: * Space between the `array` keyword and the open parenthesis for long arrays via the `keywordSpacing` property. Accepted values: (int) number of spaces or `false` to turn this check off. Defaults to `0` spaces. * Spaces on the inside of the braces for empty arrays via the `spacesWhenEmpty` property. Accepted values: (string) 'newline', (int) number of spaces or `false` to turn this check off. Defaults to `0` spaces. * Spaces on the inside of the braces for single-line arrays via the `spacesSingleLine` property; Accepted values: (int) number of spaces or `false` to turn this check off. Defaults to `0` spaces. * Spaces on the inside of the braces for multi-line arrays via the `spacesMultiLine` property. Accepted values: (string) 'newline', (int) number of spaces or `false` to turn this check off. Defaults to `newline`. Note: if any of the above properties are set to `newline`, it is recommended to also include an array indentation sniff. This sniff will not handle the indentation. Includes fixers. Includes unit tests. Includes documentation. Includes metrics via the SpacesFixer. --- .../Docs/Arrays/ArrayBraceSpacingStandard.xml | 93 ++++++ .../Sniffs/Arrays/ArrayBraceSpacingSniff.php | 294 ++++++++++++++++++ .../Arrays/ArrayBraceSpacingUnitTest.inc | 211 +++++++++++++ .../ArrayBraceSpacingUnitTest.inc.fixed | 199 ++++++++++++ .../Arrays/ArrayBraceSpacingUnitTest.php | 83 +++++ 5 files changed, 880 insertions(+) create mode 100644 NormalizedArrays/Docs/Arrays/ArrayBraceSpacingStandard.xml create mode 100644 NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php create mode 100644 NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc create mode 100644 NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc.fixed create mode 100644 NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.php diff --git a/NormalizedArrays/Docs/Arrays/ArrayBraceSpacingStandard.xml b/NormalizedArrays/Docs/Arrays/ArrayBraceSpacingStandard.xml new file mode 100644 index 00000000..b09af7b5 --- /dev/null +++ b/NormalizedArrays/Docs/Arrays/ArrayBraceSpacingStandard.xml @@ -0,0 +1,93 @@ + + + + + + + + + + (1, 2); + ]]> + + + + + + + + + + + ); + +$args = [ ]; + ]]> + + + + + + + + + + + 1, 2 ); + +$args = [ 1, 2 ]; + ]]> + + + + + + + + + 1, + 2 +); + +$args = [ + 1, + 2 +]; + ]]> + + + + + +]; + ]]> + + + diff --git a/NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php b/NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php new file mode 100644 index 00000000..bc0f0c5e --- /dev/null +++ b/NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php @@ -0,0 +1,294 @@ +keywordSpacing !== false) { + $this->keywordSpacing = \max((int) $this->keywordSpacing, 0); + } + + if ($this->spacesSingleLine !== false) { + $this->spacesSingleLine = \max((int) $this->spacesSingleLine, 0); + } + + if ($this->spacesMultiLine !== false && $this->spacesMultiLine !== 'newline') { + $this->spacesMultiLine = \max((int) $this->spacesMultiLine, 0); + } + + if ($this->spacesWhenEmpty !== false && $this->spacesWhenEmpty !== 'newline') { + $this->spacesWhenEmpty = \max((int) $this->spacesWhenEmpty, 0); + } + + if ($this->keywordSpacing === false + && $this->spacesSingleLine === false + && $this->spacesMultiLine === false + && $this->spacesWhenEmpty === false + ) { + // Nothing to do. Why was the sniff turned on at all ? + return; + } + + $openClose = Arrays::getOpenClose($phpcsFile, $stackPtr); + if ($openClose === false) { + // Short list or real square brackets. + return; + } + + $tokens = $phpcsFile->getTokens(); + $opener = $openClose['opener']; + $closer = $openClose['closer']; + + /* + * Check the spacing between the array keyword and the open parenthesis for long arrays. + */ + if ($tokens[$stackPtr]['code'] === \T_ARRAY && $this->keywordSpacing !== false) { + $error = 'There should be %s between the "array" keyword and the open parenthesis. Found: %s'; + $code = 'SpaceAfterKeyword'; + + SpacesFixer::checkAndFix( + $phpcsFile, + $stackPtr, + $opener, + $this->keywordSpacing, + $error, + $code, + 'error', + 0, + 'Space between array keyword and open brace' + ); + } + + /* + * Check for empty arrays. + */ + $nextNonWhiteSpace = $phpcsFile->findNext(\T_WHITESPACE, ($opener + 1), null, true); + if ($nextNonWhiteSpace === $closer) { + if ($this->spacesWhenEmpty === false) { + // Check was turned off. + return; + } + + $error = 'There should be %s between the array opener and closer for an empty array. Found: %s'; + $code = 'EmptyArraySpacing'; + + SpacesFixer::checkAndFix( + $phpcsFile, + $opener, + $closer, + $this->spacesWhenEmpty, + $error, + $code, + 'error', + 0, + 'Space between open and close brace for an empty array' + ); + + return; + } + + /* + * Check non-empty arrays. + */ + if ($tokens[$opener]['line'] === $tokens[$closer]['line']) { + // Single line array. + if ($this->spacesSingleLine === false) { + // Check was turned off. + return; + } + + $error = 'Expected %s after the array opener in a single line array. Found: %s'; + $code = 'SpaceAfterArrayOpenerSingleLine'; + + SpacesFixer::checkAndFix( + $phpcsFile, + $opener, + $phpcsFile->findNext(\T_WHITESPACE, ($opener + 1), null, true), + $this->spacesSingleLine, + $error, + $code, + 'error', + 0, + 'Space after array opener, single line array' + ); + + $error = 'Expected %s before the array closer in a single line array. Found: %s'; + $code = 'SpaceBeforeArrayCloserSingleLine'; + + SpacesFixer::checkAndFix( + $phpcsFile, + $closer, + $phpcsFile->findPrevious(\T_WHITESPACE, ($closer - 1), null, true), + $this->spacesSingleLine, + $error, + $code, + 'error', + 0, + 'Space before array closer, single line array' + ); + + return; + } + + // Multi-line array. + if ($this->spacesMultiLine === false) { + // Check was turned off. + return; + } + + $error = 'Expected %s after the array opener in a multi line array. Found: %s'; + $code = 'SpaceAfterArrayOpenerMultiLine'; + + SpacesFixer::checkAndFix( + $phpcsFile, + $opener, + $phpcsFile->findNext(\T_WHITESPACE, ($opener + 1), null, true), + $this->spacesMultiLine, + $error, + $code, + 'error', + 0, + 'Space after array opener, multi-line array' + ); + + $error = 'Expected %s before the array closer in a multi line array. Found: %s'; + $code = 'SpaceBeforeArrayCloserMultiLine'; + + SpacesFixer::checkAndFix( + $phpcsFile, + $closer, + $phpcsFile->findPrevious(\T_WHITESPACE, ($closer - 1), null, true), + $this->spacesMultiLine, + $error, + $code, + 'error', + 0, + 'Space before array closer, multi-line array' + ); + } +} diff --git a/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc b/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc new file mode 100644 index 00000000..ed59db94 --- /dev/null +++ b/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc @@ -0,0 +1,211 @@ + => + */ + public function getErrorList() + { + return [ + 11 => 1, + 12 => 1, + 13 => 1, + 18 => 1, + 22 => 1, + 23 => 1, + 25 => 1, + 43 => 1, + 44 => 1, + 52 => 1, + 53 => 1, + 54 => 1, + 57 => 1, + 65 => 1, + 66 => 1, + 67 => 1, + 68 => 1, + 92 => 2, + 93 => 2, + 94 => 1, + 101 => 2, + 102 => 2, + 103 => 2, + 104 => 2, + 129 => 1, + 130 => 1, + 137 => 1, + 139 => 1, + 150 => 1, + 153 => 1, + 155 => 1, + 164 => 1, + 173 => 1, + 176 => 1, + 178 => 1, + 183 => 1, + 185 => 1, + 187 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return []; + } +} From 9b8ef156dcb3bdf51b370f6c9f441f33b530552a Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 08:25:18 +0100 Subject: [PATCH 22/24] :sparkles: New `Universal.Arrays.DuplicateArrayKey` sniff Best practice sniff: detects duplicate array keys in array declarations. Includes unit tests. Includes documentation. --- .../Docs/Arrays/DuplicateArrayKeyStandard.xml | 40 +++++ .../Sniffs/Arrays/DuplicateArrayKeySniff.php | 156 ++++++++++++++++++ .../Arrays/DuplicateArrayKeyUnitTest.inc | 153 +++++++++++++++++ .../Arrays/DuplicateArrayKeyUnitTest.php | 124 ++++++++++++++ 4 files changed, 473 insertions(+) create mode 100644 Universal/Docs/Arrays/DuplicateArrayKeyStandard.xml create mode 100644 Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php create mode 100644 Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.inc create mode 100644 Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.php diff --git a/Universal/Docs/Arrays/DuplicateArrayKeyStandard.xml b/Universal/Docs/Arrays/DuplicateArrayKeyStandard.xml new file mode 100644 index 00000000..f940a3fc --- /dev/null +++ b/Universal/Docs/Arrays/DuplicateArrayKeyStandard.xml @@ -0,0 +1,40 @@ + + + + + + + 'foo' => 22, + 'bar' => 25, + 'baz' => 28, +); + +$args = array( + 22, + 25, + 2 => 28, +); + ]]> + + + 'foo' => 22, + 'bar' => 25, + 'bar' => 28, +); + +$args = array( + 22, + 25, + 1 => 28, +); + ]]> + + + diff --git a/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php b/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php new file mode 100644 index 00000000..9f467009 --- /dev/null +++ b/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php @@ -0,0 +1,156 @@ +keysSeen = []; + $this->currentMaxIntKey = -1; + + parent::processArray($phpcsFile); + } + + /** + * Process the tokens in an array key. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the + * token was found. + * @param int $startPtr The stack pointer to the first token in the "key" part of + * an array item. + * @param int $endPtr The stack pointer to the last token in the "key" part of + * an array item. + * @param int $itemNr Which item in the array is being handled. + * + * @return void + */ + public function processKey(File $phpcsFile, $startPtr, $endPtr, $itemNr) + { + $key = $this->getActualArrayKey($phpcsFile, $startPtr, $endPtr); + + if (isset($key) === false) { + // Key could not be determined. + return; + } + + $integerKey = \is_int($key); + + /* + * Check if we've seen it before. + */ + if (isset($this->keysSeen[$key]) === true) { + $firstSeen = $this->keysSeen[$key]; + $firstNonEmptyFirstSeen = $phpcsFile->findNext(Tokens::$emptyTokens, $firstSeen['ptr'], null, true); + $firstNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $startPtr, null, true); + + $data = [ + ($integerKey === true) ? 'integer' : 'string', + $key, + $firstSeen['item'], + $this->tokens[$firstNonEmptyFirstSeen]['line'], + ]; + + $phpcsFile->addError( + 'Duplicate array key found. The value will be overwritten.' + . ' The %s array key "%s" was first seen for array item %d on line %d', + $firstNonEmpty, + 'Found', + $data + ); + + return; + } + + /* + * Key not seen before. Add to array. + */ + $this->keysSeen[$key] = [ + 'item' => $itemNr, + 'ptr' => $startPtr, + ]; + + if ($integerKey === true && $key > $this->currentMaxIntKey) { + $this->currentMaxIntKey = $key; + } + } + + /** + * Process an array item without an array key. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the + * token was found. + * @param int $startPtr The stack pointer to the first token in the array item, + * which in this case will be the first token of the array + * value part of the array item. + * @param int $itemNr Which item in the array is being handled. + * + * @return void + */ + public function processNoKey(File $phpcsFile, $startPtr, $itemNr) + { + ++$this->currentMaxIntKey; + $this->keysSeen[$this->currentMaxIntKey] = [ + 'item' => $itemNr, + 'ptr' => $startPtr, + ]; + } +} diff --git a/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.inc b/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.inc new file mode 100644 index 00000000..4871b645 --- /dev/null +++ b/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.inc @@ -0,0 +1,153 @@ + 'excluded', + MY_CONSTANT => 'excluded', + PHP_INT_MAX => 'excluded', + str_replace('.', '', '1.1') => 'excluded', + self::CONSTANT => 'excluded', + $obj->get_key() => 'excluded', + $obj->prop => 'excluded', + "my $var text" => 'excluded', + << 'excluded', + $var['key']{1} => 'excluded', +]; + +/* + * Let's find some duplicates. + */ + +$emptyStringKey = array( + '' => 'empty', + // All below will error. + null => 'null', + (string) false => 'false', +); + +$everythingZero = [ + '0', + // All below will error. + 0 => 'a', + 0.0 => 'b', + '0' => 'c', + 0b0 => 'd', + 0x0 => 'e', + 00 => 'f', + false => 'g', + 0.4 => 'h', + -0.8 => 'i', + 0e0 => 'j', + 0_0 => 'k', + -1 + 1 => 'l', + 3 * 0 => 'm', + 00.00 => 'n', + (int) 'nothing' => 'o', + 15 > 200 => 'p', + "0" => 'q', + 0. => 'r', + .0 => 's', + (true) ? 0 : 1 => 't', + ! true => 'u', +]; + +$everythingOne = [ + '0', + '1', + // All below will error. + 1 => 'a', + 1.1 => 'b', + '1' => 'c', + 0b1 => 'd', + 0x1 => 'e', + 01 => 'f', + true => 'g', + 1.2 => 'h', + 1e0 => 'i', + 0_1 => 'j', + -1 + 2 => 'k', + 3 * 0.5 => 'l', + 01.00 => 'm', + (int) '1 penny' => 'n', + 15 < 200 => 'o', + "1" => 'p', + 1. => 'q', + 001. => 'r', + (true) ? 1 : 0 => 's', + ! false => 't', + (string) true => 'u', +]; + +$everythingEleven = [ + 11 => 'a', + // All below will error. + 11.0 => 'b', + '11' => 'c', + 0b1011 => 'd', + 0Xb => 'e', + 013 => 'f', + 11.8 => 'g', + 1.1e1 => 'h', + 1_1 => 'i', + 0_13 => 'j', + -1 + 12 => 'k', + 22 / 2 => 'l', + 0011.0011 => 'm', + (int) '11 lane' => 'n', + "11" => 'o', + 11. => 'p', + 35 % 12 => 'q', +]; + +$textualStringKeyVariations = [ + 'abc' => 1, + 'def' => 2, + 'ghi' => 3, + // All below will error. + 'ab' . 'c' => 4, // Error. + << 5, // Error. + <<< 'NOW' +ghi +NOW + => 6, // Error. + "abc" => 7, // Error. +]; + +$testKeepingTrackOfHighestIntKey = array( + '' => 'empty', + 'a', // Int 0 + 'b', // Int 1 + 1 => 'c', // Int 1 - Error. + 5 => 'd', // Int 5 + 'e', // Int 6 + 6 => 'f', // Int 6 - Error. + false => 'g', // Int 0 - Error. + true => 'h', // Int 1 - Error. + 1.1 => 'i', // Int 1 - Error. + 6.5 => 'j', // Int 6 - Error. + 05 => 'k', // Int 5 - Error. + 0_6 => 'l', // Int 6 - Error. PHP 7.4 octal numeric literal. + 0x1 => 'm', // Int 1 - Error. + 0b0 => 'n', // Int 0 - Error. + null => 'o', // Empty string - Error. + 02.6e7 => 'p', // Int 26000000 + '96' => 'q', // Int 96 + 'r', // Int 26000001 + '26000001' => 's', // Int 26000001 - Error. + '96.3' => 't', // String '96.3' + 1 + 0 => 'u', // Int 1 - Error. + 1.1 - 0.5 => 'v', // Int 0 - Error. + -1 => 'w', // Int -1 + 'x', // Int 26000002 + '9' . '6' => 'y', // Int 96 - Error. + '1.' => 'z', // String '1.' +); diff --git a/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.php b/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.php new file mode 100644 index 00000000..e1e7335c --- /dev/null +++ b/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.php @@ -0,0 +1,124 @@ + => + */ + public function getErrorList() + { + return [ + 30 => 1, + 31 => 1, + 37 => 1, + 38 => 1, + 39 => 1, + 40 => 1, + 41 => 1, + 42 => 1, + 43 => 1, + 44 => 1, + 45 => 1, + 46 => 1, + 47 => 1, + 48 => 1, + 49 => 1, + 50 => 1, + 51 => 1, + 52 => 1, + 53 => 1, + 54 => 1, + 55 => 1, + 56 => 1, + 57 => 1, + 64 => 1, + 65 => 1, + 66 => 1, + 67 => 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, + 82 => 1, + 83 => 1, + 84 => 1, + 90 => 1, + 91 => 1, + 92 => 1, + 93 => 1, + 94 => 1, + 95 => 1, + 96 => 1, + 97 => 1, + 98 => 1, + 99 => 1, + 100 => 1, + 101 => 1, + 102 => 1, + 103 => 1, + 104 => 1, + 105 => 1, + 113 => 1, + 114 => 1, + 118 => 1, + 122 => 1, + 129 => 1, + 132 => 1, + 133 => 1, + 134 => 1, + 135 => 1, + 136 => 1, + 137 => 1, + 138 => 1, + 139 => 1, + 140 => 1, + 141 => 1, + 145 => 1, + 147 => 1, + 148 => 1, + 151 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return []; + } +} From 39148886214630f018c8e1766ac59129af3bf506 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 08:29:31 +0100 Subject: [PATCH 23/24] :sparkles: New `Universal.Arrays.MixedKeyedUnkeyedArray` sniff Best practice sniff: either have all array items with keys or none. Don't use a mix of keyed and unkeyed array items. Includes unit tests. Includes documentation. --- .../Arrays/MixedKeyedUnkeyedArrayStandard.xml | 27 ++++ .../Arrays/MixedKeyedUnkeyedArraySniff.php | 134 ++++++++++++++++++ .../Arrays/MixedKeyedUnkeyedArrayUnitTest.inc | 20 +++ .../Arrays/MixedKeyedUnkeyedArrayUnitTest.php | 47 ++++++ 4 files changed, 228 insertions(+) create mode 100644 Universal/Docs/Arrays/MixedKeyedUnkeyedArrayStandard.xml create mode 100644 Universal/Sniffs/Arrays/MixedKeyedUnkeyedArraySniff.php create mode 100644 Universal/Tests/Arrays/MixedKeyedUnkeyedArrayUnitTest.inc create mode 100644 Universal/Tests/Arrays/MixedKeyedUnkeyedArrayUnitTest.php diff --git a/Universal/Docs/Arrays/MixedKeyedUnkeyedArrayStandard.xml b/Universal/Docs/Arrays/MixedKeyedUnkeyedArrayStandard.xml new file mode 100644 index 00000000..e28f6990 --- /dev/null +++ b/Universal/Docs/Arrays/MixedKeyedUnkeyedArrayStandard.xml @@ -0,0 +1,27 @@ + + + + + + + 'foo' => 22, + 'bar' => 25, +); + +$args = array(22, 25); + ]]> + + + 22, + 25, +); + ]]> + + + diff --git a/Universal/Sniffs/Arrays/MixedKeyedUnkeyedArraySniff.php b/Universal/Sniffs/Arrays/MixedKeyedUnkeyedArraySniff.php new file mode 100644 index 00000000..ee7679d1 --- /dev/null +++ b/Universal/Sniffs/Arrays/MixedKeyedUnkeyedArraySniff.php @@ -0,0 +1,134 @@ + => + */ + private $itemsWithoutKey = []; + + /** + * Process the array declaration. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the + * token was found. + * + * @return void + */ + public function processArray(File $phpcsFile) + { + // Reset properties before processing this array. + $this->hasKeys = false; + $this->itemsWithoutKey = []; + + parent::processArray($phpcsFile); + } + + /** + * Process the tokens in an array key. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the + * token was found. + * @param int $startPtr The stack pointer to the first token in the "key" part of + * an array item. + * @param int $endPtr The stack pointer to the last token in the "key" part of + * an array item. + * @param int $itemNr Which item in the array is being handled. + * + * @return void + */ + public function processKey(File $phpcsFile, $startPtr, $endPtr, $itemNr) + { + $this->hasKeys = true; + + // Process any previously encountered items without keys. + if (empty($this->itemsWithoutKey) === false) { + foreach ($this->itemsWithoutKey as $itemNr => $stackPtr) { + $phpcsFile->addError( + 'Inconsistent array detected. A mix of keyed and unkeyed array items is not allowed.' + . ' The array item in position %d does not have an array key.', + $stackPtr, + 'Found', + [$itemNr] + ); + } + + // No need to do this again. + $this->itemsWithoutKey = []; + } + } + + /** + * Process an array item without an array key. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the + * token was found. + * @param int $startPtr The stack pointer to the first token in the array item, + * which in this case will be the first token of the array + * value part of the array item. + * @param int $itemNr Which item in the array is being handled. + * + * @return void + */ + public function processNoKey(File $phpcsFile, $startPtr, $itemNr) + { + $firstNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $startPtr, null, true); + if ($firstNonEmpty === false) { + // Shouldn't be possible. + return; + } + + // If we already know there are keys in the array, throw an error message straight away. + if ($this->hasKeys === true) { + $phpcsFile->addError( + 'Inconsistent array detected. A mix of keyed and unkeyed array items is not allowed.' + . ' The array item in position %d does not have an array key.', + $firstNonEmpty, + 'Found', + [$itemNr] + ); + } else { + // Save the array item info for later in case we do encounter an array key later on in the array. + $this->itemsWithoutKey[$itemNr] = $firstNonEmpty; + } + } +} diff --git a/Universal/Tests/Arrays/MixedKeyedUnkeyedArrayUnitTest.inc b/Universal/Tests/Arrays/MixedKeyedUnkeyedArrayUnitTest.inc new file mode 100644 index 00000000..513d6068 --- /dev/null +++ b/Universal/Tests/Arrays/MixedKeyedUnkeyedArrayUnitTest.inc @@ -0,0 +1,20 @@ + 'a', + 2 => 'b', + 3 => 'c', + 4 => 'd', +); + +// Mixed. +$array = [ + 'value', + 12 => 'numeric key', + 'string' => 'string key', + 'value', +]; diff --git a/Universal/Tests/Arrays/MixedKeyedUnkeyedArrayUnitTest.php b/Universal/Tests/Arrays/MixedKeyedUnkeyedArrayUnitTest.php new file mode 100644 index 00000000..3d66ac44 --- /dev/null +++ b/Universal/Tests/Arrays/MixedKeyedUnkeyedArrayUnitTest.php @@ -0,0 +1,47 @@ + => + */ + public function getErrorList() + { + return [ + 16 => 1, + 19 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return []; + } +} From badd720042f14803bcd88312ffe3c1697a6fab63 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 23 Jan 2020 08:33:07 +0100 Subject: [PATCH 24/24] :sparkles: New `Universal.Arrays.MixedArrayKeyTypes` sniff Best practice sniff: either have all array items with numeric keys or with string keys. Don't use a mix of integer and numeric keys for array items. Includes unit tests. Includes documentation. --- .../Arrays/MixedArrayKeyTypesStandard.xml | 36 ++++ .../Sniffs/Arrays/MixedArrayKeyTypesSniff.php | 170 ++++++++++++++++++ .../Arrays/MixedArrayKeyTypesUnitTest.inc | 49 +++++ .../Arrays/MixedArrayKeyTypesUnitTest.php | 48 +++++ 4 files changed, 303 insertions(+) create mode 100644 Universal/Docs/Arrays/MixedArrayKeyTypesStandard.xml create mode 100644 Universal/Sniffs/Arrays/MixedArrayKeyTypesSniff.php create mode 100644 Universal/Tests/Arrays/MixedArrayKeyTypesUnitTest.inc create mode 100644 Universal/Tests/Arrays/MixedArrayKeyTypesUnitTest.php diff --git a/Universal/Docs/Arrays/MixedArrayKeyTypesStandard.xml b/Universal/Docs/Arrays/MixedArrayKeyTypesStandard.xml new file mode 100644 index 00000000..f8fd8541 --- /dev/null +++ b/Universal/Docs/Arrays/MixedArrayKeyTypesStandard.xml @@ -0,0 +1,36 @@ + + + + + + + 'foo' => 22, + 'bar' => 25, +); + +$args = array( + 0 => 22, + 1 => 25, +); + ]]> + + + 22, + 25, +); + +$args = array( + 'foo' => 22, + 12 => 25, +); + + ]]> + + + diff --git a/Universal/Sniffs/Arrays/MixedArrayKeyTypesSniff.php b/Universal/Sniffs/Arrays/MixedArrayKeyTypesSniff.php new file mode 100644 index 00000000..516472ef --- /dev/null +++ b/Universal/Sniffs/Arrays/MixedArrayKeyTypesSniff.php @@ -0,0 +1,170 @@ +seenStringKey = false; + $this->seenNumericKey = false; + + parent::processArray($phpcsFile); + } + + /** + * Process the tokens in an array key. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the + * token was found. + * @param int $startPtr The stack pointer to the first token in the "key" part of + * an array item. + * @param int $endPtr The stack pointer to the last token in the "key" part of + * an array item. + * @param int $itemNr Which item in the array is being handled. + * + * @return void + */ + public function processKey(File $phpcsFile, $startPtr, $endPtr, $itemNr) + { + $key = $this->getActualArrayKey($phpcsFile, $startPtr, $endPtr); + if (isset($key) === false) { + // Key could not be determined. + return; + } + + $integerKey = \is_int($key); + + // Handle integer key. + if ($integerKey === true) { + if ($this->seenStringKey === false) { + if ($this->seenNumericKey !== false) { + // Already seen a numeric key before. + return; + } + + $this->seenNumericKey = true; + return; + } + + // Ok, so we've seen a string key before and now see an explicit numeric key. + $firstNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $startPtr, null, true); + $phpcsFile->addError( + 'Arrays should have either numeric keys or string keys. Explicit numeric key detected,' + . ' while all previous keys in this array were string keys.', + $firstNonEmpty, + 'ExplicitNumericKey' + ); + + // Stop the loop. + return true; + } + + // Handle string key. + if ($this->seenNumericKey === false) { + if ($this->seenStringKey !== false) { + // Already seen a string key before. + return; + } + + $this->seenStringKey = true; + return; + } + + // Ok, so we've seen a numeric key before and now see a string key. + $firstNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $startPtr, null, true); + $phpcsFile->addError( + 'Arrays should have either numeric keys or string keys. String key detected,' + . ' while all previous keys in this array were integer based keys.', + $firstNonEmpty, + 'StringKey' + ); + + // Stop the loop. + return true; + } + + /** + * Process an array item without an array key. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the + * token was found. + * @param int $startPtr The stack pointer to the first token in the array item, + * which in this case will be the first token of the array + * value part of the array item. + * @param int $itemNr Which item in the array is being handled. + * + * @return void + */ + public function processNoKey(File $phpcsFile, $startPtr, $itemNr) + { + if ($this->seenStringKey === false) { + if ($this->seenNumericKey !== false) { + // Already seen a numeric key before. + return; + } + + $this->seenNumericKey = true; + return; + } + + // Ok, so we've seen a string key before and now see an implicit numeric key. + $firstNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $startPtr, null, true); + $phpcsFile->addError( + 'Arrays should have either numeric keys or string keys. Implicit numeric key detected,' + . ' while all previous keys in this array were string keys.', + $firstNonEmpty, + 'ImplicitNumericKey' + ); + + // Stop the loop. + return true; + } +} diff --git a/Universal/Tests/Arrays/MixedArrayKeyTypesUnitTest.inc b/Universal/Tests/Arrays/MixedArrayKeyTypesUnitTest.inc new file mode 100644 index 00000000..df364c1b --- /dev/null +++ b/Universal/Tests/Arrays/MixedArrayKeyTypesUnitTest.inc @@ -0,0 +1,49 @@ + 'a', + 2 => 'b', + 3 => 'c', + 4 => 'd', +); + +// OK: All items have numeric keys. +$array = array( + 'a', + 2 => 'b', + '3' => 'c', + 4 => 'd', +); + +// OK: All items have string keys. +$array = array( + 'a' => 'a', + 'b' => 'b', + 'c' => 'c', + 'd' => 'd', +); + +// Mixed numeric first. +$array = [ + 12 => 'numeric key', + 'value', + 'string' => 'string key', // Error. +]; + +// Mixed string first. +$array = [ + 'stringA' => 'string key', + 'stringB' => 'string key', + 12 => 'numeric key', // Error. +]; + +// Mixed string first, implicit numeric. +$array = [ + 'stringA' => 'string key', + 'numeric key', // Error. + 'stringB' => 'string key', +]; diff --git a/Universal/Tests/Arrays/MixedArrayKeyTypesUnitTest.php b/Universal/Tests/Arrays/MixedArrayKeyTypesUnitTest.php new file mode 100644 index 00000000..ea599da8 --- /dev/null +++ b/Universal/Tests/Arrays/MixedArrayKeyTypesUnitTest.php @@ -0,0 +1,48 @@ + => + */ + public function getErrorList() + { + return [ + 34 => 1, + 41 => 1, + 47 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return []; + } +}