diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index aa2b3201..fefcc5c5 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -168,27 +168,38 @@ jobs:
- name: Run the unit tests with code coverage
run: composer coverage
- # Uploading the results with PHP Coveralls v1 won't work from GH Actions, so switch the PHP version.
- - name: Switch to PHP 7.4
- if: ${{ success() && matrix.php != '7.4' }}
+ # PHP Coveralls v2 (which supports GH Actions) has a PHP 5.5 minimum, so switch the PHP version.
+ - name: Switch to PHP latest
+ if: ${{ success() && matrix.php == '5.4' }}
uses: shivammathur/setup-php@v2
with:
- php-version: 7.4
+ php-version: 'latest'
coverage: none
- # Global install is used to prevent a conflict with the local composer.lock in PHP 8.0+.
+ # Global install is used to prevent a conflict with the local composer.lock.
- name: Install Coveralls
if: ${{ success() }}
- run: composer global require php-coveralls/php-coveralls:"^2.5.3" --no-interaction
+ run: composer global require php-coveralls/php-coveralls:"^2.6.0" --no-interaction
- - name: Upload coverage results to Coveralls
- if: ${{ success() }}
+ - name: Upload coverage results to Coveralls (normal)
+ if: ${{ success() && github.actor != 'dependabot[bot]' }}
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
COVERALLS_PARALLEL: true
COVERALLS_FLAG_NAME: php-${{ matrix.php }}-phpcs-${{ matrix.phpcs_version }}
run: php-coveralls -v -x build/logs/clover.xml
+ # Dependabot does not have access to secrets, other than the GH token.
+ # Ref: https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions
+ # Ref: https://github.com/lemurheavy/coveralls-public/issues/1721
+ - name: Upload coverage results to Coveralls (Dependabot)
+ if: ${{ success() && github.actor == 'dependabot[bot]' }}
+ env:
+ COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ COVERALLS_PARALLEL: true
+ COVERALLS_FLAG_NAME: php-${{ matrix.php }}-phpcs-${{ matrix.phpcs_version }}
+ run: php-coveralls -v -x build/logs/clover.xml
+
coveralls-finish:
needs: coverage
if: always() && needs.coverage.result == 'success'
@@ -196,8 +207,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- - name: Coveralls Finished
+ - name: Coveralls Finished (normal)
+ if: ${{ github.actor != 'dependabot[bot]' }}
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.COVERALLS_TOKEN }}
parallel-finished: true
+
+ # Dependabot does not have access to secrets, other than the GH token.
+ # Ref: https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions
+ # Ref: https://github.com/lemurheavy/coveralls-public/issues/1721
+ - name: Coveralls Finished (Dependabot)
+ if: ${{ github.actor == 'dependabot[bot]' }}
+ uses: coverallsapp/github-action@v2
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ parallel-finished: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 62e346e7..9e06a2e6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,13 +14,60 @@ This projects adheres to [Keep a CHANGELOG](http://keepachangelog.com/) and uses
_Nothing yet._
+## [1.1.0] - 2023-07-19
+
+### Added
+
+#### Universal
+
+* :wrench: :books: New `Universal.CodeAnalysis.NoEchoSprintf` sniff to detect use of the inefficient `echo [v]sprintf(...);` combi and recommends using `[v]printf()` instead. [#242]
+* :bar_chart: :books: New `Universal.FunctionDeclarations.NoLongClosures` sniff to detect "long" closures and recommend using a named function instead. [#240]
+ The sniff offers the following properties to influence its behaviour: `recommendedLines` (defaults to `5`), `maxLines` (defaults to `8`), `ignoreCommentLines` (defaults to `true`) and `ignoreEmptyLines` (defaults to `true`).
+* :wrench: :bar_chart: :books: New `Universal.FunctionDeclarations.RequireFinalMethodsInTraits` sniff to enforce non-private, non-abstract methods in traits to be declared as `final`. [#243], [#245]
+ There is a separate `NonFinalMagicMethodFound` error code for magic methods to allow those to be excluded from the check.
+* :wrench: :bar_chart: :books: New `Universal.UseStatements.DisallowMixedGroupUse` sniff to disallow group use statements which import a combination of namespace/OO construct, functions and/or constants in one statement. [#241], [#246]
+ Note: the fixer will use a semi-standardized format for group use statements. If there are more specific requirements for the formatting of group use statements, the ruleset configurator should ensure that additional sniffs are included in the ruleset to enforce the required format.
+* :wrench: :bar_chart: :books: New `Universal.UseStatements.KeywordSpacing` sniff to enforce the use of a single space after the `use`, `function`, `const` keywords and both before and after the `as` keyword in import `use` statements. [#247]
+ The sniff has modular error codes to allow for disabling individual checks.
+* :wrench: :books: New `Universal.UseStatements.NoUselessAliases` sniff to detect useless aliases (aliasing something to its original name) in import use statements. [#244]
+ Note: as OO and function names in PHP are case-insensitive, aliasing to the same name, using a different case is also considered useless.
+* :wrench: :bar_chart: :books: New `Universal.WhiteSpace.CommaSpacing` sniff to enforce that there is no space before a comma and exactly one space, or a new line, after a comma. [#254]
+ Additionally, the sniff also enforces that the comma should follow the code and not be placed after a trailing comment.
+ The sniff has modular error codes to allow for disabling individual checks and checks in certain contexts.
+ The sniff will respect a potentially set [`php_version` configuration option][php_version-config] when deciding how to handle the spacing after a heredoc/nowdoc closer.
+
+### Changed
+
+#### Universal
+
+* Minor performance improvements for the `Universal.Arrays.DuplicateArrayKey` and the `Universal.CodeAnalysis.ConstructorDestructorReturn` sniffs. [#251], [#252]
+
+#### Other
+
+* Composer: The minimum `PHPCSUtils` requirement has been updated to `^1.0.8` (was `^1.0.6`). [#249], [#254]
+* Various housekeeping.
+
+[#240]: https://github.com/PHPCSStandards/PHPCSExtra/pull/240
+[#241]: https://github.com/PHPCSStandards/PHPCSExtra/pull/241
+[#242]: https://github.com/PHPCSStandards/PHPCSExtra/pull/242
+[#243]: https://github.com/PHPCSStandards/PHPCSExtra/pull/243
+[#244]: https://github.com/PHPCSStandards/PHPCSExtra/pull/244
+[#245]: https://github.com/PHPCSStandards/PHPCSExtra/pull/245
+[#246]: https://github.com/PHPCSStandards/PHPCSExtra/pull/246
+[#247]: https://github.com/PHPCSStandards/PHPCSExtra/pull/247
+[#249]: https://github.com/PHPCSStandards/PHPCSExtra/pull/249
+[#251]: https://github.com/PHPCSStandards/PHPCSExtra/pull/251
+[#252]: https://github.com/PHPCSStandards/PHPCSExtra/pull/252
+[#254]: https://github.com/PHPCSStandards/PHPCSExtra/pull/254
+
+
## [1.0.4] - 2023-06-18
### Changed
#### Other
-* Composer: The minimum `PHPCSUtils` requirement has been updated to `^1.0.6` (was ^1.0.0). [#237]
+* Composer: The minimum `PHPCSUtils` requirement has been updated to `^1.0.6` (was `^1.0.0`). [#237]
* Various housekeeping.
### Fixed
@@ -183,7 +230,7 @@ For the full list of features, please see the changelogs of the alpha/rc release
* Updated the sniffs for compatibility with PHPCSUtils 1.0.0-alpha4. [#134]
* Updated the sniffs to correctly handle PHP 8.0/8.1/8.2 features whenever relevant.
* Readme: Updated installation instructions for compatibility with Composer 2.2+. [#101]
-* Composer: The minimum `PHP_CodeSniffer` requirement has been updated to `^3.7.1` (was ^3.3.1). [#115], [#130]
+* Composer: The minimum `PHP_CodeSniffer` requirement has been updated to `^3.7.1` (was `^3.3.1`). [#115], [#130]
* Composer: The package will now identify itself as a static analysis tool. Thanks [@GaryJones]! [#126]
* All non-`abstract` classes in this package are now `final`. [#121]
* All XML documentation now has a schema annotation. [#128]
@@ -441,6 +488,7 @@ This initial alpha release contains the following sniffs:
[php_version-config]: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Configuration-Options#setting-the-php-version
[Unreleased]: https://github.com/PHPCSStandards/PHPCSExtra/compare/stable...HEAD
+[1.1.0]: https://github.com/PHPCSStandards/PHPCSExtra/compare/1.0.4...1.1.0
[1.0.4]: https://github.com/PHPCSStandards/PHPCSExtra/compare/1.0.3...1.0.4
[1.0.3]: https://github.com/PHPCSStandards/PHPCSExtra/compare/1.0.2...1.0.3
[1.0.2]: https://github.com/PHPCSStandards/PHPCSExtra/compare/1.0.1...1.0.2
diff --git a/README.md b/README.md
index 2120ce3d..51c90888 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,7 @@ Minimum Requirements
* PHP 5.4 or higher.
* [PHP_CodeSniffer][phpcs-gh] version **3.7.1** or higher.
-* [PHPCSUtils][phpcsutils-gh] version **1.0.0** or higher.
+* [PHPCSUtils][phpcsutils-gh] version **1.0.8** or higher.
Installation
@@ -61,7 +61,7 @@ Installing via Composer is highly recommended.
Run the following from the root of your project:
```bash
composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
-composer require --dev phpcsstandards/phpcsextra:"^1.0"
+composer require --dev phpcsstandards/phpcsextra:"^1.1.0"
```
### Composer Global Installation
@@ -69,7 +69,7 @@ composer require --dev phpcsstandards/phpcsextra:"^1.0"
Alternatively, you may want to install this standard globally:
```bash
composer global config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
-composer global require --dev phpcsstandards/phpcsextra:"^1.0"
+composer global require --dev phpcsstandards/phpcsextra:"^1.1.0"
```
### Updating to a newer version
@@ -220,6 +220,10 @@ Require a consistent modifier keyword order for class declarations.
If a [`php_version` configuration option][php_version-config] has been passed to PHPCS using either `--config-set` or `--runtime-set`, it will be respected by the sniff.
In effect, this means that the sniff will only report on PHP4-style constructors if the configured PHP version is less than 8.0.
+#### `Universal.CodeAnalysis.NoEchoSprintf` :wrench: :books:
+
+Detects use of the inefficient `echo [v]sprintf(...);` combi. Use `[v]printf()` instead.
+
#### `Universal.CodeAnalysis.ForeachUniqueAssignment` :wrench: :books:
Detects `foreach` control structures which use the same variable for both the key as well as the value assignment as this will lead to unexpected - and most likely unintended - behaviour.
@@ -278,6 +282,26 @@ Enforce for a file to either declare (global/namespaced) functions or declare OO
* Also note: This sniff has no opinion on multiple OO structures being declared in one file.
If you want to sniff for that, use the PHPCS native `Generic.Files.OneObjectStructurePerFile` sniff.
+#### `Universal.FunctionDeclarations.NoLongClosures` :bar_chart: :books:
+
+Detects "long" closures and recommends using a named function instead.
+
+The sniff is configurable by setting any of the following properties in a custom ruleset:
+* `recommendedLines` (int): determines when a warning will be thrown.
+ Defaults to `5`, meaning a warning with the errorcode `ExceedsRecommended` will be thrown if the closure is more than 5 lines long.
+* `maxLines` (int): determines when an error will be thrown.
+ Defaults to `8`, meaning that an error with the errorcode `ExceedsMaximum` will be thrown if the closure is more than 8 lines long.
+* `ignoreCommentLines` (bool): whether or not comment-only lines should be ignored for the lines count.
+ Defaults to `true`.
+* `ignoreEmptyLines` (bool): whether or not blank lines should be ignored for the lines count.
+ Defaults to `true`.
+
+#### `Universal.FunctionDeclarations.RequireFinalMethodsInTraits` :wrench: :bar_chart: :books:
+
+Enforce non-private, non-abstract methods in traits to be declared as `final`.
+
+The available error codes are: `NonFinalMethodFound` and `NonFinalMagicMethodFound`.
+
#### `Universal.Lists.DisallowLongListSyntax` :wrench: :books:
Disallow the use of long `list`s.
@@ -368,6 +392,13 @@ The available error codes are: `UnionTypeSpacesBefore`, `UnionTypeSpacesAfter`,
Disallow short open echo tags `=` containing more than one PHP statement.
+#### `Universal.UseStatements.DisallowMixedGroupUse` :wrench: :bar_chart: :books:
+
+Disallow group use statements which import a combination of namespace/OO construct, functions and/or constants in one statement.
+
+Note: the fixer will use a semi-standardized format for group use statements.
+If there are more specific requirements for the formatting of group use statements, the ruleset configurator should ensure that additional sniffs are included in the ruleset to enforce the required format.
+
#### `Universal.UseStatements.DisallowUseClass` :bar_chart: :books:
Forbid using import `use` statements for classes/traits/interfaces/enums.
@@ -394,6 +425,14 @@ Enforce that `function` and `const` keywords when used in an import `use` statem
Companion sniff to the PHPCS native `Generic.PHP.LowerCaseKeyword` sniff which doesn't cover these keywords when used in an import `use` statement.
+#### `Universal.UseStatements.KeywordSpacing` :wrench: :bar_chart: :books:
+
+Enforce the use of a single space after the `use`, `function`, `const` keywords and both before and after the `as` keyword in import `use` statements.
+
+Companion sniff to the PHPCS native `Generic.WhiteSpace.LanguageConstructSpacing` sniff which doesn't cover the `function`, `const` and `as` keywords when used in an import `use` statement.
+
+The sniff has modular error codes to allow for disabling individual checks. The error codes are: `SpaceAfterUse`, `SpaceAfterFunction`, `SpaceAfterConst`, `SpaceBeforeAs` and `SpaceAfterAs`.
+
#### `Universal.UseStatements.NoLeadingBackslash` :wrench: :bar_chart: :books:
Verify that a name being imported in an import `use` statement does not start with a leading backslash.
@@ -402,6 +441,13 @@ Names in import `use` statements should always be fully qualified, so a leading
This sniff handles all types of import use statements supported by PHP, in contrast to other sniffs for the same in, for instance, the PHPCS native `PSR12` or the Slevomat standard, which are incomplete.
+#### `Universal.UseStatements.NoUselessAliases` :wrench: :books:
+
+Detects useless aliases in import use statements.
+
+Aliasing something to the same name as the original construct is considered useless (though allowed in PHP).
+Note: as OO and function names in PHP are case-insensitive, aliasing to the same name, using a different case is also considered useless.
+
#### `Universal.WhiteSpace.AnonClassKeywordSpacing` :wrench: :bar_chart: :books:
Standardize the amount of spacing between the `class` keyword and the open parenthesis (if any) for anonymous class declarations.
@@ -409,6 +455,26 @@ Standardize the amount of spacing between the `class` keyword and the open paren
* This sniff contains an `spacing` property to set the amount of spaces the sniff should check for.
Accepted values: (int) number of spaces. Defaults to `0` (spaces).
+#### `Universal.WhiteSpace.CommaSpacing` :wrench: :bar_chart: :books:
+
+Enforce that there is no space before a comma and exactly one space, or a new line, after a comma.
+
+Additionally, the sniff also enforces that the comma should follow the code and not be placed after a trailing comment.
+
+For the spacing part, the sniff makes the following exceptions:
+1. A comma preceded or followed by a parenthesis, curly or square bracket.
+ These will not be flagged to prevent conflicts with sniffs handling spacing around braces.
+2. A comma preceded or followed by another comma, like for skipping items in a list assignment.
+ These will not be flagged.
+
+* The sniff has a separate error code - `TooMuchSpaceAfterCommaBeforeTrailingComment` - for when a comma is found with more than one space after it, followed by a trailing comment.
+ Exclude this error code to allow trailing comment alignment.
+* The other error codes the sniff uses, `SpaceBefore`, `TooMuchSpaceAfter` and `NoSpaceAfter`, may be suffixed with a context indicator - `*InFunctionDeclaration`, `*InFunctionCall`, `*InClosureUse` or `*InDeclare` -.
+ This allows for disabling the sniff in any of these contexts by excluding the specific suffixed error codes.
+* The sniff will respect a potentially set [`php_version` configuration option][php_version-config] when deciding how to handle the spacing after a heredoc/nowdoc closer.
+ In effect, this means that the sniff will enforce a new line between the closer and a comma if the configured PHP version is less than 7.3.
+ When no `php_version` is passed, the sniff will handle the spacing between a heredoc/nowdoc closer and a comma based on whether it is a cross-version compatible heredoc/nowdoc (enforce new line) or a flexible heredoc/nowdoc (enforce no space).
+
#### `Universal.WhiteSpace.DisallowInlineTabs` :wrench: :books:
Enforce using spaces for mid-line alignment.
diff --git a/Universal/Docs/CodeAnalysis/NoEchoSprintfStandard.xml b/Universal/Docs/CodeAnalysis/NoEchoSprintfStandard.xml
new file mode 100644
index 00000000..914eb65d
--- /dev/null
+++ b/Universal/Docs/CodeAnalysis/NoEchoSprintfStandard.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ printf('text %s text', $var);
+echo callMe('text %s text', $var);
+ ]]>
+
+
+ echo sprintf('text %s text', $var);
+echo vsprintf('text %s text', [$var]);
+ ]]>
+
+
+
diff --git a/Universal/Docs/FunctionDeclarations/NoLongClosuresStandard.xml b/Universal/Docs/FunctionDeclarations/NoLongClosuresStandard.xml
new file mode 100644
index 00000000..dc84e0b1
--- /dev/null
+++ b/Universal/Docs/FunctionDeclarations/NoLongClosuresStandard.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Universal/Docs/FunctionDeclarations/RequireFinalMethodsInTraitsStandard.xml b/Universal/Docs/FunctionDeclarations/RequireFinalMethodsInTraitsStandard.xml
new file mode 100644
index 00000000..4b2622de
--- /dev/null
+++ b/Universal/Docs/FunctionDeclarations/RequireFinalMethodsInTraitsStandard.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+ final public function bar() {}
+ final public static function baz() {}
+
+ // Also valid (out of scope):
+ protected abstract function overload() {}
+ private function okay() {}
+}
+ ]]>
+
+
+ public function bar() {}
+ protected static function baz() {}
+}
+ ]]>
+
+
+
diff --git a/Universal/Docs/UseStatements/DisallowMixedGroupUseStandard.xml b/Universal/Docs/UseStatements/DisallowMixedGroupUseStandard.xml
new file mode 100644
index 00000000..2b930f27
--- /dev/null
+++ b/Universal/Docs/UseStatements/DisallowMixedGroupUseStandard.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Universal/Docs/UseStatements/KeywordSpacingStandard.xml b/Universal/Docs/UseStatements/KeywordSpacingStandard.xml
new file mode 100644
index 00000000..55d430e2
--- /dev/null
+++ b/Universal/Docs/UseStatements/KeywordSpacingStandard.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+ function strpos;
+use const PHP_EOL as MY_EOL;
+ ]]>
+
+
+ function strpos;
+use
+ const
+ PHP_EOL
+ as
+ MY_EOL;
+ ]]>
+
+
+
diff --git a/Universal/Docs/UseStatements/NoUselessAliasesStandard.xml b/Universal/Docs/UseStatements/NoUselessAliasesStandard.xml
new file mode 100644
index 00000000..39bb90d1
--- /dev/null
+++ b/Universal/Docs/UseStatements/NoUselessAliasesStandard.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Universal/Docs/WhiteSpace/CommaSpacingStandard.xml b/Universal/Docs/WhiteSpace/CommaSpacingStandard.xml
new file mode 100644
index 00000000..503ae6f7
--- /dev/null
+++ b/Universal/Docs/WhiteSpace/CommaSpacingStandard.xml
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+ , $param2, $param3);
+
+function_call(
+ $param1,
+ $param2,
+ $param3
+);
+
+$array = array($item1, $item2, $item3);
+$array = [
+ $item1,
+ $item2,
+];
+
+list(, $a, $b,,) = $array;
+list(
+ ,
+ $a,
+ $b,
+) = $array;
+ ]]>
+
+
+ , $param2,$param3);
+
+function_call(
+ $a
+ ,$b
+ ,$c
+);
+
+$array = array($item1,$item2 , $item3);
+$array = [
+ $item1,
+ $item2 ,
+];
+
+list( ,$a, $b ,,) = $array;
+list(
+ ,
+ $a,
+ $b ,
+) = $array;
+ ]]>
+
+
+
+
+
+
+
+ , // Comment.
+ $param2, /* Comment. */
+);
+ ]]>
+
+
+ ,
+ $param2 /* Comment. */,
+);
+ ]]>
+
+
+
diff --git a/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php b/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php
index 92532e5c..f9ed534b 100644
--- a/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php
+++ b/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php
@@ -69,7 +69,7 @@ final class DuplicateArrayKeySniff extends AbstractArrayDeclarationSniff
private $currentMaxIntKeyGt8;
/**
- * The current PHP version.
+ * PHP version as configured or -1 if unknown.
*
* @since 1.0.0
*
@@ -97,6 +97,9 @@ public function processArray(File $phpcsFile)
$this->keysSeenGt8 = [];
if (isset($this->phpVersion) === false) {
+ // Set default value to prevent this code from running every time the sniff is triggered.
+ $this->phpVersion = -1;
+
$phpVersion = Helper::getConfigData('php_version');
if ($phpVersion !== null) {
$this->phpVersion = (int) $phpVersion;
@@ -146,7 +149,7 @@ public function processKey(File $phpcsFile, $startPtr, $endPtr, $itemNr)
/*
* Check if we've seen the key before.
*/
- if ((isset($this->phpVersion) === false || $this->phpVersion < 80000)
+ if (($this->phpVersion === -1 || $this->phpVersion < 80000)
&& isset($this->keysSeenLt8[$key]) === true
) {
$errors['phplt8'] = [
@@ -166,7 +169,7 @@ public function processKey(File $phpcsFile, $startPtr, $endPtr, $itemNr)
$errors['phplt8']['data_subset'][] = $this->tokens[$firstNonEmptyFirstSeen]['line'];
}
- if ((isset($this->phpVersion) === false || $this->phpVersion >= 80000)
+ if (($this->phpVersion === -1 || $this->phpVersion >= 80000)
&& isset($this->keysSeenGt8[$key]) === true
) {
$errors['phpgt8'] = [
diff --git a/Universal/Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php b/Universal/Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php
index d199019e..ce66d4d1 100644
--- a/Universal/Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php
+++ b/Universal/Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php
@@ -31,6 +31,15 @@
final class ConstructorDestructorReturnSniff implements Sniff
{
+ /**
+ * PHP version as configured or 0 if unknown.
+ *
+ * @since 1.1.0
+ *
+ * @var int
+ */
+ private $phpVersion;
+
/**
* Registers the tokens that this sniff wants to listen for.
*
@@ -56,6 +65,16 @@ public function register()
*/
public function process(File $phpcsFile, $stackPtr)
{
+ if (isset($this->phpVersion) === false || \defined('PHP_CODESNIFFER_IN_TESTS')) {
+ // Set default value to prevent this code from running every time the sniff is triggered.
+ $this->phpVersion = 0;
+
+ $phpVersion = Helper::getConfigData('php_version');
+ if ($phpVersion !== null) {
+ $this->phpVersion = (int) $phpVersion;
+ }
+ }
+
$scopePtr = Scopes::validDirectScope($phpcsFile, $stackPtr, Tokens::$ooScopeTokens);
if ($scopePtr === false) {
// Not an OO method.
@@ -69,7 +88,7 @@ public function process(File $phpcsFile, $stackPtr)
$functionType = \sprintf('A "%s()" magic method', $functionNameLC);
} else {
// If the PHP version is explicitly set to PHP 8.0 or higher, ignore PHP 4-style constructors.
- if ((int) Helper::getConfigData('php_version') >= 80000) {
+ if ($this->phpVersion >= 80000) {
return;
}
diff --git a/Universal/Sniffs/CodeAnalysis/NoEchoSprintfSniff.php b/Universal/Sniffs/CodeAnalysis/NoEchoSprintfSniff.php
new file mode 100644
index 00000000..3273e228
--- /dev/null
+++ b/Universal/Sniffs/CodeAnalysis/NoEchoSprintfSniff.php
@@ -0,0 +1,131 @@
+
+ */
+ private $targetFunctions = [
+ 'sprintf' => 'printf',
+ 'vsprintf' => 'vprintf',
+ ];
+
+ /**
+ * Returns an array of tokens this test wants to listen for.
+ *
+ * @since 1.1.0
+ *
+ * @return array
+ */
+ public function register()
+ {
+ return [\T_ECHO];
+ }
+
+ /**
+ * Processes this test, when one of its tokens is encountered.
+ *
+ * @since 1.1.0
+ *
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
+ * @param int $stackPtr The position of the current token
+ * in the stack passed in $tokens.
+ *
+ * @return void
+ */
+ public function process(File $phpcsFile, $stackPtr)
+ {
+ $tokens = $phpcsFile->getTokens();
+
+ $skip = Tokens::$emptyTokens;
+ $skip[] = \T_NS_SEPARATOR;
+
+ $next = $phpcsFile->findNext($skip, ($stackPtr + 1), null, true);
+ if ($next === false
+ || $tokens[$next]['code'] !== \T_STRING
+ || isset($this->targetFunctions[\strtolower($tokens[$next]['content'])]) === false
+ ) {
+ // Not our target.
+ return;
+ }
+
+ $detectedFunction = \strtolower($tokens[$next]['content']);
+
+ $openParens = $phpcsFile->findNext(Tokens::$emptyTokens, ($next + 1), null, true);
+ if ($next === false
+ || $tokens[$openParens]['code'] !== \T_OPEN_PARENTHESIS
+ || isset($tokens[$openParens]['parenthesis_closer']) === false
+ ) {
+ // Live coding/parse error.
+ return;
+ }
+
+ $closeParens = $tokens[$openParens]['parenthesis_closer'];
+ $afterFunctionCall = $phpcsFile->findNext(Tokens::$emptyTokens, ($closeParens + 1), null, true);
+ if ($afterFunctionCall === false
+ || ($tokens[$afterFunctionCall]['code'] !== \T_SEMICOLON
+ && $tokens[$afterFunctionCall]['code'] !== \T_CLOSE_TAG)
+ ) {
+ // Live coding/parse error or compound echo statement.
+ return;
+ }
+
+ $fix = $phpcsFile->addFixableError(
+ 'Unnecessary "echo %s(...)" found. Use "%s(...)" instead.',
+ $next,
+ 'Found',
+ [
+ $tokens[$next]['content'],
+ $this->targetFunctions[$detectedFunction],
+ ]
+ );
+
+ if ($fix === true) {
+ $phpcsFile->fixer->beginChangeset();
+
+ // Remove echo and whitespace.
+ $phpcsFile->fixer->replaceToken($stackPtr, '');
+
+ for ($i = ($stackPtr + 1); $i < $next; $i++) {
+ if ($tokens[$i]['code'] !== \T_WHITESPACE) {
+ break;
+ }
+
+ $phpcsFile->fixer->replaceToken($i, '');
+ }
+
+ $phpcsFile->fixer->replaceToken($next, $this->targetFunctions[$detectedFunction]);
+
+ $phpcsFile->fixer->endChangeset();
+ }
+ }
+}
diff --git a/Universal/Sniffs/FunctionDeclarations/NoLongClosuresSniff.php b/Universal/Sniffs/FunctionDeclarations/NoLongClosuresSniff.php
new file mode 100644
index 00000000..13abb9fe
--- /dev/null
+++ b/Universal/Sniffs/FunctionDeclarations/NoLongClosuresSniff.php
@@ -0,0 +1,233 @@
+recommendedLines = (int) $this->recommendedLines;
+ $this->maxLines = (int) $this->maxLines;
+
+ $tokens = $phpcsFile->getTokens();
+
+ if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) {
+ // Live coding/parse error. Shouldn't be possible as in that case tokenizer won't retokenize to T_CLOSURE.
+ return; // @codeCoverageIgnore
+ }
+
+ $opener = $tokens[$stackPtr]['scope_opener'];
+ $closer = $tokens[$stackPtr]['scope_closer'];
+
+ $currentLine = $tokens[$opener]['line'];
+ $closerLine = $tokens[$closer]['line'];
+
+ $codeLines = 0;
+ $commentLines = 0;
+ $blankLines = 0;
+
+ // Check whether the line of the scope opener needs to be counted, but ignore trailing comments on that line.
+ $firstNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($opener + 1), $closer, true);
+ if ($firstNonEmpty !== false && $tokens[$firstNonEmpty]['line'] === $currentLine) {
+ ++$codeLines;
+ }
+
+ // Check whether the line of the scope closer needs to be counted.
+ if ($closerLine !== $currentLine) {
+ $hasCommentTokens = false;
+ $hasCodeTokens = false;
+ for ($i = ($closer - 1); $tokens[$i]['line'] === $closerLine && $i > $opener; $i--) {
+ if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === false) {
+ $hasCodeTokens = true;
+ } elseif (isset(Tokens::$commentTokens[$tokens[$i]['code']]) === true) {
+ $hasCommentTokens = true;
+ }
+ }
+
+ if ($hasCodeTokens === true) {
+ ++$codeLines;
+ } elseif ($hasCommentTokens === true) {
+ ++$commentLines;
+ }
+ }
+
+ // We've already examined the opener line, so move to the next line.
+ for ($i = ($opener + 1); $tokens[$i]['line'] === $currentLine && $i < $closer; $i++);
+ $currentLine = $tokens[$i]['line'];
+
+ // Walk tokens.
+ while ($currentLine !== $closerLine) {
+ $hasCommentTokens = false;
+ $hasCodeTokens = false;
+
+ while ($tokens[$i]['line'] === $currentLine) {
+ if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === false) {
+ $hasCodeTokens = true;
+ } elseif (isset(Tokens::$commentTokens[$tokens[$i]['code']]) === true) {
+ $hasCommentTokens = true;
+ }
+
+ ++$i;
+ }
+
+ if ($hasCodeTokens === true) {
+ ++$codeLines;
+ } elseif ($hasCommentTokens === true) {
+ ++$commentLines;
+ } else {
+ // Only option left is that this is an empty line.
+ ++$blankLines;
+ }
+
+ $currentLine = $tokens[$i]['line'];
+ }
+
+ $nonBlankLines = ($codeLines + $commentLines);
+ $totalLines = ($codeLines + $commentLines + $blankLines);
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME_CODE, $codeLines . ' lines');
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME_COMMENTS, $nonBlankLines . ' lines');
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME_ALL, $totalLines . ' lines');
+
+ $lines = $codeLines;
+ if ($this->ignoreCommentLines === false) {
+ $lines += $commentLines;
+ }
+ if ($this->ignoreEmptyLines === false) {
+ $lines += $blankLines;
+ }
+
+ $errorSuffix = ' Declare a named function instead. Found closure containing %s lines';
+
+ if ($lines > $this->maxLines) {
+ $phpcsFile->addError(
+ 'Closures which are longer than %s lines are forbidden.' . $errorSuffix,
+ $stackPtr,
+ 'ExceedsMaximum',
+ [$this->maxLines, $lines]
+ );
+
+ return;
+ }
+
+ if ($lines > $this->recommendedLines) {
+ $phpcsFile->addWarning(
+ 'It is recommended for closures to contain %s lines or less.' . $errorSuffix,
+ $stackPtr,
+ 'ExceedsRecommended',
+ [$this->recommendedLines, $lines]
+ );
+ }
+ }
+}
diff --git a/Universal/Sniffs/FunctionDeclarations/RequireFinalMethodsInTraitsSniff.php b/Universal/Sniffs/FunctionDeclarations/RequireFinalMethodsInTraitsSniff.php
new file mode 100644
index 00000000..da9e2415
--- /dev/null
+++ b/Universal/Sniffs/FunctionDeclarations/RequireFinalMethodsInTraitsSniff.php
@@ -0,0 +1,120 @@
+getTokens();
+ if (isset($tokens[$stackPtr]['parenthesis_opener']) === false) {
+ // Parse error/live coding.
+ return;
+ }
+
+ $scopePtr = Scopes::validDirectScope($phpcsFile, $stackPtr, \T_TRAIT);
+ if ($scopePtr === false) {
+ // Not a trait method.
+ return;
+ }
+
+ $methodProps = FunctionDeclarations::getProperties($phpcsFile, $stackPtr);
+ if ($methodProps['scope'] === 'private') {
+ // Private methods can't be final.
+ return;
+ }
+
+ if ($methodProps['is_final'] === true) {
+ // Already final, nothing to do.
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'final');
+ return;
+ }
+
+ if ($methodProps['is_abstract'] === true) {
+ // Abstract classes can't be final.
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'abstract');
+ return;
+ }
+
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'not abstract, not final');
+
+ $methodName = FunctionDeclarations::getName($phpcsFile, $stackPtr);
+ $magic = '';
+ $code = 'NonFinalMethodFound';
+ if (FunctionDeclarations::isMagicMethodName($methodName) === true) {
+ // Use separate error code for magic methods.
+ $magic = 'magic ';
+ $code = 'NonFinalMagicMethodFound';
+ }
+
+ $data = [
+ $methodProps['scope'],
+ $magic,
+ $methodName,
+ ObjectDeclarations::getName($phpcsFile, $scopePtr),
+ ];
+
+ $fix = $phpcsFile->addFixableError(
+ 'The non-abstract, %s %smethod "%s()" in trait %s should be declared as final.',
+ $stackPtr,
+ $code,
+ $data
+ );
+
+ if ($fix === true) {
+ $phpcsFile->fixer->addContentBefore($stackPtr, 'final ');
+ }
+ }
+}
diff --git a/Universal/Sniffs/UseStatements/DisallowMixedGroupUseSniff.php b/Universal/Sniffs/UseStatements/DisallowMixedGroupUseSniff.php
new file mode 100644
index 00000000..3303eac1
--- /dev/null
+++ b/Universal/Sniffs/UseStatements/DisallowMixedGroupUseSniff.php
@@ -0,0 +1,248 @@
+findNext([\T_SEMICOLON, \T_CLOSE_TAG], ($stackPtr + 1));
+ $groupStart = $phpcsFile->findNext(\T_OPEN_USE_GROUP, ($stackPtr + 1), $endOfStatement);
+
+ if ($groupStart === false) {
+ // Not a group use statement. Just record the metric.
+ if ($totalCount === 1) {
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'single import');
+ } else {
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'multi import');
+ }
+
+ return;
+ }
+
+ if ($totalCount === 1
+ || ($ooCount !== 0 && $functionCount === 0 && $constantCount === 0)
+ || ($ooCount === 0 && $functionCount !== 0 && $constantCount === 0)
+ || ($ooCount === 0 && $functionCount === 0 && $constantCount !== 0)
+ ) {
+ // Not a *mixed* group use statement.
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'group use, single type');
+ return;
+ }
+
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'group use, multi type');
+
+ // Build up the error message.
+ $foundPhrases = [];
+ if ($ooCount > 1) {
+ $foundPhrases[] = \sprintf('%d namespaces/OO names', $ooCount);
+ } elseif ($ooCount === 1) {
+ $foundPhrases[] = \sprintf('%d namespace/OO name', $ooCount);
+ }
+
+ if ($functionCount > 1) {
+ $foundPhrases[] = \sprintf('%d functions', $functionCount);
+ } elseif ($functionCount === 1) {
+ $foundPhrases[] = \sprintf('%d function', $functionCount);
+ }
+
+ if ($constantCount > 1) {
+ $foundPhrases[] = \sprintf('%d constants', $constantCount);
+ } elseif ($constantCount === 1) {
+ $foundPhrases[] = \sprintf('%d constant', $constantCount);
+ }
+
+ if (\count($foundPhrases) === 2) {
+ $found = \implode(' and ', $foundPhrases);
+ } else {
+ $found = \array_shift($foundPhrases) . ', ';
+ $found .= \implode(' and ', $foundPhrases);
+ }
+
+ $error = 'Group use statements should import one type of construct.'
+ . ' Mixed group use statement found importing %s.';
+ $code = 'Found';
+ $data = [$found];
+
+ $hasComment = $phpcsFile->findNext(Tokens::$commentTokens, ($stackPtr + 1), $endOfStatement);
+ if ($hasComment !== false) {
+ // Don't attempt to auto-fix is there are comments or PHPCS annotations in the statement.
+ $phpcsFile->addError($error, $stackPtr, $code, $data);
+ return;
+ }
+
+ $fix = $phpcsFile->addFixableError($error, $stackPtr, $code, $data);
+
+ if ($fix === false) {
+ return;
+ }
+
+ /*
+ * Fix it.
+ *
+ * This fixer complies with the following (arbitrary) requirements:
+ * - It will re-use the original base "group" name, i.e. the part before \{.
+ * - It take take aliases into account, but only when something is aliased to a different name.
+ * Aliases re-using the original name will be removed.
+ * - The fix will not add a trailing comma after the last group use sub-statement.
+ * This is a PHP 7.2+ feature.
+ * If a standard wants to enforce trailing commas, they should use a separate sniff for that.
+ * - If there is only 1 statement of a certain type, the replacement will be a single
+ * import use statement, not a group use statement.
+ */
+
+ $phpcsFile->fixer->beginChangeset();
+
+ // Ensure that a potential close PHP tag ending the statement is not removed.
+ $tokens = $phpcsFile->getTokens();
+ $endRemoval = $endOfStatement;
+ if ($tokens[$endOfStatement]['code'] !== \T_SEMICOLON) {
+ $endRemoval = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($endOfStatement - 1), null, true);
+ }
+
+ // Remove old statement with the exception of the `use` keyword.
+ for ($i = ($stackPtr + 1); $i <= $endRemoval; $i++) {
+ $phpcsFile->fixer->replaceToken($i, '');
+ }
+
+ // Build up the new use import statements.
+ $newStatements = [];
+
+ $useIndent = \str_repeat(' ', ($tokens[$stackPtr]['column'] - 1));
+ $insideIndent = $useIndent . \str_repeat(' ', 4);
+
+ $baseGroupName = GetTokensAsString::noEmpties($phpcsFile, ($stackPtr + 1), ($groupStart - 1));
+
+ foreach ($useStatements as $type => $statements) {
+ $count = \count($statements);
+ if ($count === 0) {
+ continue;
+ }
+
+ $typeName = $type . ' ';
+ if ($type === 'name') {
+ $typeName = '';
+ }
+
+ if ($count === 1) {
+ $fqName = \reset($statements);
+ $alias = \key($statements);
+
+ $newStatement = 'use ' . $typeName . $fqName;
+
+ $unqualifiedName = \ltrim(\substr($fqName, \strrpos($fqName, '\\')), '\\');
+ if ($unqualifiedName !== $alias) {
+ $newStatement .= ' as ' . $alias;
+ }
+
+ $newStatement .= ';';
+
+ $newStatements[] = $newStatement;
+ continue;
+ }
+
+ // Multiple statements, add a single-type group use statement.
+ $newStatement = 'use ' . $typeName . $baseGroupName . '{' . $phpcsFile->eolChar;
+
+ foreach ($statements as $alias => $fqName) {
+ $partialName = \str_replace($baseGroupName, '', $fqName);
+ $newStatement .= $insideIndent . $partialName;
+
+ $unqualifiedName = \ltrim(\substr($partialName, \strrpos($partialName, '\\')), '\\');
+ if ($unqualifiedName !== $alias) {
+ $newStatement .= ' as ' . $alias;
+ }
+
+ $newStatement .= ',' . $phpcsFile->eolChar;
+ }
+
+ // Remove trailing comma after last statement as that's PHP 7.2+.
+ $newStatement = \rtrim($newStatement, ',' . $phpcsFile->eolChar);
+
+ $newStatement .= $phpcsFile->eolChar . $useIndent . '};';
+ $newStatements[] = $newStatement;
+ }
+
+ $replacement = \implode($phpcsFile->eolChar . $useIndent, $newStatements);
+
+ $phpcsFile->fixer->replaceToken($stackPtr, $replacement);
+
+ $phpcsFile->fixer->endChangeset();
+ }
+}
diff --git a/Universal/Sniffs/UseStatements/KeywordSpacingSniff.php b/Universal/Sniffs/UseStatements/KeywordSpacingSniff.php
new file mode 100644
index 00000000..ffef7cb8
--- /dev/null
+++ b/Universal/Sniffs/UseStatements/KeywordSpacingSniff.php
@@ -0,0 +1,207 @@
+ string)
+ */
+ protected $keywords = [
+ 'const' => true,
+ 'function' => true,
+ ];
+
+ /**
+ * Returns an array of tokens this sniff wants to listen for.
+ *
+ * @since 1.1.0
+ *
+ * @return array
+ */
+ public function register()
+ {
+ return [\T_USE];
+ }
+
+ /**
+ * Processes this test, when one of its tokens is encountered.
+ *
+ * @since 1.1.0
+ *
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
+ * @param int $stackPtr The position of the current token in the
+ * stack passed in $tokens.
+ *
+ * @return void
+ */
+ public function process(File $phpcsFile, $stackPtr)
+ {
+ if (UseStatements::isImportUse($phpcsFile, $stackPtr) === false) {
+ // Trait or closure use statement.
+ return;
+ }
+
+ $tokens = $phpcsFile->getTokens();
+ $endOfStatement = $phpcsFile->findNext([\T_SEMICOLON, \T_CLOSE_TAG], ($stackPtr + 1));
+ if ($endOfStatement === false) {
+ // Live coding or parse error.
+ return;
+ }
+
+ // Check the spacing after the `use` keyword.
+ $this->checkSpacingAfterKeyword($phpcsFile, $stackPtr, $tokens[$stackPtr]['content']);
+
+ // Check the spacing before and after each `as` keyword.
+ $current = $stackPtr;
+ do {
+ $current = $phpcsFile->findNext(\T_AS, ($current + 1), $endOfStatement);
+ if ($current === false) {
+ break;
+ }
+
+ // Prevent false positives when "as" is used within a "name".
+ if (isset(Tokens::$emptyTokens[$tokens[($current - 1)]['code']]) === true) {
+ $this->checkSpacingBeforeKeyword($phpcsFile, $current, $tokens[$current]['content']);
+ $this->checkSpacingAfterKeyword($phpcsFile, $current, $tokens[$current]['content']);
+ }
+ } while (true);
+
+ /*
+ * Check the spacing after `function` and `const` keywords.
+ */
+ $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
+ if (isset($this->keywords[\strtolower($tokens[$nextNonEmpty]['content'])]) === true) {
+ // Keyword found at start of statement, applies to whole statement.
+ $this->checkSpacingAfterKeyword($phpcsFile, $nextNonEmpty, $tokens[$nextNonEmpty]['content']);
+ return;
+ }
+
+ // This may still be a group use statement with function/const substatements.
+ $openGroup = $phpcsFile->findNext(\T_OPEN_USE_GROUP, ($stackPtr + 1), $endOfStatement);
+ if ($openGroup === false) {
+ // Not a group use statement.
+ return;
+ }
+
+ $closeGroup = $phpcsFile->findNext(\T_CLOSE_USE_GROUP, ($openGroup + 1), $endOfStatement);
+
+ $current = $openGroup;
+ do {
+ $current = $phpcsFile->findNext(Tokens::$emptyTokens, ($current + 1), $closeGroup, true);
+ if ($current === false) {
+ return;
+ }
+
+ if (isset($this->keywords[\strtolower($tokens[$current]['content'])]) === true) {
+ $this->checkSpacingAfterKeyword($phpcsFile, $current, $tokens[$current]['content']);
+ }
+
+ // We're within the use group, so find the next comma.
+ $current = $phpcsFile->findNext(\T_COMMA, ($current + 1), $closeGroup);
+ } while ($current !== false);
+ }
+
+ /**
+ * Check the spacing before a found keyword.
+ *
+ * @since 1.1.0
+ *
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
+ * @param int $stackPtr The position of the keyword in the token stack.
+ * @param string $content The keyword as found.
+ *
+ * @return void
+ */
+ public function checkSpacingBeforeKeyword(File $phpcsFile, $stackPtr, $content)
+ {
+ $contentLC = \strtolower($content);
+ $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
+
+ SpacesFixer::checkAndFix(
+ $phpcsFile,
+ $stackPtr,
+ $prevNonEmpty,
+ 1, // Expected spaces.
+ 'Expected %s before the "' . $contentLC . '" keyword. Found: %s',
+ 'SpaceBefore' . \ucfirst($contentLC),
+ 'error',
+ 0, // Severity.
+ \sprintf(self::METRIC_NAME_BEFORE, $contentLC)
+ );
+ }
+
+ /**
+ * Check the spacing after a found keyword.
+ *
+ * @since 1.1.0
+ *
+ * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
+ * @param int $stackPtr The position of the keyword in the token stack.
+ * @param string $content The keyword as found.
+ *
+ * @return void
+ */
+ public function checkSpacingAfterKeyword(File $phpcsFile, $stackPtr, $content)
+ {
+ $contentLC = \strtolower($content);
+ $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
+
+ SpacesFixer::checkAndFix(
+ $phpcsFile,
+ $stackPtr,
+ $nextNonEmpty,
+ 1, // Expected spaces.
+ 'Expected %s after the "' . $contentLC . '" keyword. Found: %s',
+ 'SpaceAfter' . \ucfirst($contentLC),
+ 'error',
+ 0, // Severity.
+ \sprintf(self::METRIC_NAME_AFTER, $contentLC)
+ );
+ }
+}
diff --git a/Universal/Sniffs/UseStatements/NoUselessAliasesSniff.php b/Universal/Sniffs/UseStatements/NoUselessAliasesSniff.php
new file mode 100644
index 00000000..ebf7fe27
--- /dev/null
+++ b/Universal/Sniffs/UseStatements/NoUselessAliasesSniff.php
@@ -0,0 +1,164 @@
+findNext([\T_SEMICOLON, \T_CLOSE_TAG], ($stackPtr + 1));
+ if ($endOfStatement === false) {
+ // Parse error or live coding.
+ return;
+ }
+
+ $hasAliases = $phpcsFile->findNext(\T_AS, ($stackPtr + 1), $endOfStatement);
+ if ($hasAliases === false) {
+ // This use import statement does not alias anything, bow out.
+ return;
+ }
+
+ $useStatements = UseStatements::splitImportUseStatement($phpcsFile, $stackPtr);
+ if (\count($useStatements, \COUNT_RECURSIVE) <= 3) {
+ // No statements found. Shouldn't be possible, but still. Bow out.
+ return;
+ }
+
+ $tokens = $phpcsFile->getTokens();
+
+ // Collect all places where aliases are used in this use statement.
+ $aliasPtrs = [];
+ $currentAs = $hasAliases;
+ do {
+ $aliasPtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($currentAs + 1), null, true);
+ if ($aliasPtr !== false && $tokens[$aliasPtr]['code'] === \T_STRING) {
+ $aliasPtrs[$currentAs] = $aliasPtr;
+ }
+
+ $currentAs = $phpcsFile->findNext(\T_AS, ($currentAs + 1), $endOfStatement);
+ } while ($currentAs !== false);
+
+ // Now check the names in each use statement for useless aliases.
+ foreach ($useStatements as $type => $statements) {
+ foreach ($statements as $alias => $fqName) {
+ $unqualifiedName = \ltrim(\substr($fqName, \strrpos($fqName, '\\')), '\\');
+
+ $uselessAlias = false;
+ if ($type === 'const') {
+ // Do a case-sensitive comparison for constants.
+ if ($unqualifiedName === $alias) {
+ $uselessAlias = true;
+ }
+ } elseif (NamingConventions::isEqual($unqualifiedName, $alias)) {
+ $uselessAlias = true;
+ }
+
+ if ($uselessAlias === false) {
+ continue;
+ }
+
+ // Now check if this is actually used as an alias or just the actual name.
+ foreach ($aliasPtrs as $asPtr => $aliasPtr) {
+ if ($tokens[$aliasPtr]['content'] !== $alias) {
+ continue;
+ }
+
+ // Make sure this is really the right one.
+ $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($asPtr - 1), null, true);
+ if ($tokens[$prev]['code'] !== \T_STRING
+ || $tokens[$prev]['content'] !== $unqualifiedName
+ ) {
+ continue;
+ }
+
+ $error = 'Useless alias "%s" found for import of "%s"';
+ $code = 'Found';
+ $data = [$alias, $fqName];
+
+ // Okay, so this is the one which should be flagged.
+ $hasComments = $phpcsFile->findNext(Tokens::$commentTokens, ($prev + 1), $aliasPtr);
+ if ($hasComments !== false) {
+ // Don't auto-fix if there are comments.
+ $phpcsFile->addError($error, $aliasPtr, $code, $data);
+ break;
+ }
+
+ $fix = $phpcsFile->addFixableError($error, $aliasPtr, $code, $data);
+
+ if ($fix === true) {
+ $phpcsFile->fixer->beginChangeset();
+
+ for ($i = ($prev + 1); $i <= $aliasPtr; $i++) {
+ $phpcsFile->fixer->replaceToken($i, '');
+ }
+
+ $phpcsFile->fixer->endChangeset();
+ }
+
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/Universal/Sniffs/WhiteSpace/CommaSpacingSniff.php b/Universal/Sniffs/WhiteSpace/CommaSpacingSniff.php
new file mode 100644
index 00000000..29e5b335
--- /dev/null
+++ b/Universal/Sniffs/WhiteSpace/CommaSpacingSniff.php
@@ -0,0 +1,408 @@
+phpVersion) === false || \defined('PHP_CODESNIFFER_IN_TESTS')) {
+ // Set default value to prevent this code from running every time the sniff is triggered.
+ $this->phpVersion = 0;
+
+ $phpVersion = Helper::getConfigData('php_version');
+ if ($phpVersion !== null) {
+ $this->phpVersion = (int) $phpVersion;
+ }
+ }
+
+ $this->processSpacingBefore($phpcsFile, $stackPtr);
+ $this->processSpacingAfter($phpcsFile, $stackPtr);
+ }
+
+ /**
+ * Check the spacing before the comma.
+ *
+ * @since 1.1.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
+ */
+ protected function processSpacingBefore(File $phpcsFile, $stackPtr)
+ {
+ $tokens = $phpcsFile->getTokens();
+
+ $prevNonWhitespace = $phpcsFile->findPrevious(\T_WHITESPACE, ($stackPtr - 1), null, true);
+ $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
+ $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
+
+ if ($prevNonWhitespace !== $prevNonEmpty
+ && $tokens[$prevNonEmpty]['code'] !== \T_COMMA
+ && $tokens[$prevNonEmpty]['line'] !== $tokens[$nextNonEmpty]['line']
+ ) {
+ // Special case: comma after a trailing comment - the comma should be moved to before the comment.
+ $fix = $phpcsFile->addFixableError(
+ 'Comma found after comment, expected the comma after the end of the code',
+ $stackPtr,
+ 'CommaAfterComment'
+ );
+
+ if ($fix === true) {
+ $phpcsFile->fixer->beginChangeset();
+
+ $phpcsFile->fixer->replaceToken($stackPtr, '');
+ $phpcsFile->fixer->addContent($prevNonEmpty, ',');
+
+ // Clean up potential trailing whitespace left behind, but don't remove blank lines.
+ $nextNonWhitespace = $phpcsFile->findNext(\T_WHITESPACE, ($stackPtr + 1), null, true);
+ if ($tokens[($stackPtr - 1)]['code'] === \T_WHITESPACE
+ && $tokens[($stackPtr - 1)]['line'] === $tokens[$stackPtr]['line']
+ && $tokens[$stackPtr]['line'] !== $tokens[$nextNonWhitespace]['line']
+ ) {
+ $phpcsFile->fixer->replaceToken(($stackPtr - 1), '');
+ }
+
+ $phpcsFile->fixer->endChangeset();
+ }
+ return;
+ }
+
+ if ($tokens[$prevNonWhitespace]['code'] === \T_COMMA) {
+ // This must be a list assignment with ignored items. Ignore.
+ return;
+ }
+
+ if (isset(Tokens::$blockOpeners[$tokens[$prevNonWhitespace]['code']]) === true
+ || $tokens[$prevNonWhitespace]['code'] === \T_OPEN_SHORT_ARRAY
+ || $tokens[$prevNonWhitespace]['code'] === \T_OPEN_USE_GROUP
+ ) {
+ // Should only realistically be possible for lists. Leave for a block brace spacing sniff to sort out.
+ return;
+ }
+
+ $expectedSpaces = 0;
+
+ if ($tokens[$prevNonEmpty]['code'] === \T_END_HEREDOC
+ || $tokens[$prevNonEmpty]['code'] === \T_END_NOWDOC
+ ) {
+ /*
+ * If php_version is explicitly set to PHP < 7.3, enforce a new line between the closer and the comma.
+ *
+ * If php_version is *not* explicitly set, let the indent be leading and only enforce
+ * a new line between the closer and the comma when this is an old-style heredoc/nowdoc.
+ */
+ if ($this->phpVersion !== 0 && $this->phpVersion < 70300) {
+ $expectedSpaces = 'newline';
+ }
+
+ if ($this->phpVersion === 0
+ && \ltrim($tokens[$prevNonEmpty]['content']) === $tokens[$prevNonEmpty]['content']
+ ) {
+ $expectedSpaces = 'newline';
+ }
+ }
+
+ $error = 'Expected %1$s between "' . $this->escapePlaceholders($tokens[$prevNonWhitespace]['content'])
+ . '" and the comma. Found: %2$s';
+ $codeSuffix = $this->getSuffix($phpcsFile, $stackPtr);
+ $metricSuffix = $this->codeSuffixToMetric($codeSuffix);
+
+ SpacesFixer::checkAndFix(
+ $phpcsFile,
+ $stackPtr,
+ $prevNonWhitespace,
+ $expectedSpaces,
+ $error,
+ 'SpaceBefore' . $codeSuffix,
+ 'error',
+ 0,
+ self::METRIC_NAME_BEFORE . $metricSuffix
+ );
+ }
+
+ /**
+ * Check the spacing after the comma.
+ *
+ * @since 1.1.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
+ */
+ protected function processSpacingAfter(File $phpcsFile, $stackPtr)
+ {
+ $tokens = $phpcsFile->getTokens();
+
+ $nextNonWhitespace = $phpcsFile->findNext(\T_WHITESPACE, ($stackPtr + 1), null, true);
+ if ($nextNonWhitespace === false) {
+ // Live coding/parse error. Ignore.
+ return;
+ }
+
+ if ($tokens[$nextNonWhitespace]['code'] === \T_COMMA) {
+ // This must be a list assignment with ignored items. Ignore.
+ return;
+ }
+
+ if ($tokens[$nextNonWhitespace]['code'] === \T_CLOSE_CURLY_BRACKET
+ || $tokens[$nextNonWhitespace]['code'] === \T_CLOSE_SQUARE_BRACKET
+ || $tokens[$nextNonWhitespace]['code'] === \T_CLOSE_PARENTHESIS
+ || $tokens[$nextNonWhitespace]['code'] === \T_CLOSE_SHORT_ARRAY
+ || $tokens[$nextNonWhitespace]['code'] === \T_CLOSE_USE_GROUP
+ ) {
+ // Ignore. Leave for a block spacing sniff to sort out.
+ return;
+ }
+
+ $nextToken = $tokens[($stackPtr + 1)];
+
+ $error = 'Expected %1$s between the comma and "'
+ . $this->escapePlaceholders($tokens[$nextNonWhitespace]['content']) . '". Found: %2$s';
+
+ $codeSuffix = $this->getSuffix($phpcsFile, $stackPtr);
+ $metricSuffix = $this->codeSuffixToMetric($codeSuffix);
+
+ if ($nextToken['code'] === \T_WHITESPACE) {
+ if ($nextToken['content'] === ' ') {
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME_AFTER . $metricSuffix, '1 space');
+ return;
+ }
+
+ // Note: this check allows for trailing whitespace between the comma and a new line char.
+ // The trailing whitespace is not the concern of this sniff.
+ if (\ltrim($nextToken['content'], ' ') === $phpcsFile->eolChar) {
+ $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME_AFTER . $metricSuffix, 'a new line');
+ return;
+ }
+
+ $errorCode = 'TooMuchSpaceAfter' . $codeSuffix;
+
+ $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
+ if (isset(Tokens::$commentTokens[$tokens[$nextNonWhitespace]['code']]) === true
+ && ($nextNonEmpty === false || $tokens[$stackPtr]['line'] !== $tokens[$nextNonEmpty]['line'])
+ ) {
+ // Separate error code to allow for aligning trailing comments.
+ $errorCode = 'TooMuchSpaceAfterCommaBeforeTrailingComment';
+ }
+
+ SpacesFixer::checkAndFix(
+ $phpcsFile,
+ $stackPtr,
+ $nextNonWhitespace,
+ 1,
+ $error,
+ $errorCode,
+ 'error',
+ 0,
+ self::METRIC_NAME_AFTER . $metricSuffix
+ );
+ return;
+ }
+
+ SpacesFixer::checkAndFix(
+ $phpcsFile,
+ $stackPtr,
+ $nextNonWhitespace,
+ 1,
+ $error,
+ 'NoSpaceAfter' . $codeSuffix,
+ 'error',
+ 0,
+ self::METRIC_NAME_AFTER . $metricSuffix
+ );
+ }
+
+ /**
+ * Escape arbitrary token content for *printf() placeholders.
+ *
+ * @since 1.1.0
+ *
+ * @param string $text Arbitrary text string.
+ *
+ * @return string
+ */
+ private function escapePlaceholders($text)
+ {
+ return \preg_replace('`(?:^|[^%])(%)(?:[^%]|$)`', '%%', \trim($text));
+ }
+
+ /**
+ * Retrieve a text string for use as a suffix to an error code.
+ *
+ * This allows for modular error codes, which in turn allow for selectively excluding
+ * error codes.
+ *
+ * {@internal Closure use will be parentheses owner in PHPCS 4.x, this code will
+ * need an update for that in due time.}
+ *
+ * @since 1.1.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 string
+ */
+ private function getSuffix($phpcsFile, $stackPtr)
+ {
+ $opener = Parentheses::getLastOpener($phpcsFile, $stackPtr);
+ if ($opener === false) {
+ return '';
+ }
+
+ $tokens = $phpcsFile->getTokens();
+
+ $owner = Parentheses::getOwner($phpcsFile, $opener);
+ if ($owner !== false) {
+ switch ($tokens[$owner]['code']) {
+ case \T_FUNCTION:
+ case \T_CLOSURE:
+ case \T_FN:
+ return 'InFunctionDeclaration';
+
+ case \T_DECLARE:
+ return 'InDeclare';
+
+ case \T_ANON_CLASS:
+ case \T_ISSET:
+ case \T_UNSET:
+ return 'InFunctionCall';
+
+ // Long array, long list, isset, unset, empty, exit, eval, control structures.
+ default:
+ return '';
+ }
+ }
+
+ $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($opener - 1), null, true);
+
+ if (isset(Collections::nameTokens()[$tokens[$prevNonEmpty]['code']]) === true) {
+ return 'InFunctionCall';
+ }
+
+ switch ($tokens[$prevNonEmpty]['code']) {
+ case \T_USE:
+ return 'InClosureUse';
+
+ case \T_VARIABLE:
+ case \T_SELF:
+ case \T_STATIC:
+ case \T_PARENT:
+ return 'InFunctionCall';
+
+ default:
+ return '';
+ }
+ }
+
+ /**
+ * Transform a suffix for an error code into a suffix for a metric.
+ *
+ * @since 1.1.0
+ *
+ * @param string $suffix Error code suffix.
+ *
+ * @return string
+ */
+ private function codeSuffixToMetric($suffix)
+ {
+ return \strtolower(\preg_replace('`([A-Z])`', ' $1', $suffix));
+ }
+}
diff --git a/Universal/Tests/CodeAnalysis/NoEchoSprintfUnitTest.1.inc b/Universal/Tests/CodeAnalysis/NoEchoSprintfUnitTest.1.inc
new file mode 100644
index 00000000..f575d4a6
--- /dev/null
+++ b/Universal/Tests/CodeAnalysis/NoEchoSprintfUnitTest.1.inc
@@ -0,0 +1,35 @@
+' . sprintf('%s - %d', $string, $number) . '';
+
+echo \sprintf('%s - %d', $string, $number), 'text', sprintf('%s - %d', $string, $number);
+
+echo 'text' . sprintf('%s - %d', $string, $number);
+
+echo sprintf('%s - %d', $string, $number), \sprintf('%s - %d', $string, $number);
+
+/*
+ * The issue.
+ */
+echo sprintf('%s - %d', $string, $number);
+echo \sprintf(
+ '%s',
+ $string,
+) ?>
+' . sprintf('%s - %d', $string, $number) . '';
+
+echo \sprintf('%s - %d', $string, $number), 'text', sprintf('%s - %d', $string, $number);
+
+echo 'text' . sprintf('%s - %d', $string, $number);
+
+echo sprintf('%s - %d', $string, $number), \sprintf('%s - %d', $string, $number);
+
+/*
+ * The issue.
+ */
+printf('%s - %d', $string, $number);
+\printf(
+ '%s',
+ $string,
+) ?>
+ =>
+ */
+ public function getErrorList($testFile = '')
+ {
+ switch ($testFile) {
+ case 'NoEchoSprintfUnitTest.1.inc':
+ return [
+ 19 => 1,
+ 20 => 1,
+ 26 => 1,
+ 28 => 1,
+ 30 => 1,
+ 31 => 1,
+ ];
+
+ default:
+ return [];
+ }
+ }
+
+ /**
+ * Returns the lines where warnings should occur.
+ *
+ * @return array =>
+ */
+ public function getWarningList()
+ {
+ return [];
+ }
+}
diff --git a/Universal/Tests/FunctionDeclarations/NoLongClosuresUnitTest.1.inc b/Universal/Tests/FunctionDeclarations/NoLongClosuresUnitTest.1.inc
new file mode 100644
index 00000000..9fd39d33
--- /dev/null
+++ b/Universal/Tests/FunctionDeclarations/NoLongClosuresUnitTest.1.inc
@@ -0,0 +1,110 @@
+ =>
+ */
+ public function getErrorList($testFile = '')
+ {
+ switch ($testFile) {
+ case 'NoLongClosuresUnitTest.1.inc':
+ return [
+ 102 => 1,
+ 105 => 1,
+ 108 => 1,
+ ];
+
+ case 'NoLongClosuresUnitTest.2.inc':
+ return [
+ 22 => 1,
+ 31 => 1,
+ 45 => 1,
+ 57 => 1,
+ ];
+
+ case 'NoLongClosuresUnitTest.3.inc':
+ case 'NoLongClosuresUnitTest.13.inc':
+ case 'NoLongClosuresUnitTest.17.inc':
+ return [
+ 57 => 1,
+ ];
+
+ case 'NoLongClosuresUnitTest.5.inc':
+ case 'NoLongClosuresUnitTest.6.inc':
+ case 'NoLongClosuresUnitTest.10.inc':
+ case 'NoLongClosuresUnitTest.11.inc':
+ case 'NoLongClosuresUnitTest.14.inc':
+ case 'NoLongClosuresUnitTest.16.inc':
+ case 'NoLongClosuresUnitTest.18.inc':
+ case 'NoLongClosuresUnitTest.19.inc':
+ case 'NoLongClosuresUnitTest.20.inc':
+ case 'NoLongClosuresUnitTest.21.inc':
+ return [
+ 45 => 1,
+ 57 => 1,
+ ];
+
+ case 'NoLongClosuresUnitTest.7.inc':
+ case 'NoLongClosuresUnitTest.8.inc':
+ case 'NoLongClosuresUnitTest.9.inc':
+ case 'NoLongClosuresUnitTest.12.inc':
+ case 'NoLongClosuresUnitTest.15.inc':
+ default:
+ return [];
+ }
+ }
+
+ /**
+ * Returns the lines where warnings should occur.
+ *
+ * @param string $testFile The name of the file being tested.
+ *
+ * @return array =>
+ */
+ public function getWarningList($testFile = '')
+ {
+ switch ($testFile) {
+ case 'NoLongClosuresUnitTest.1.inc':
+ return [
+ 42 => 1,
+ 50 => 1,
+ 58 => 1,
+ 83 => 1,
+ 91 => 1,
+ ];
+
+ case 'NoLongClosuresUnitTest.2.inc':
+ return [
+ 11 => 1,
+ ];
+
+ case 'NoLongClosuresUnitTest.3.inc':
+ case 'NoLongClosuresUnitTest.17.inc':
+ return [
+ 45 => 1,
+ ];
+
+ case 'NoLongClosuresUnitTest.4.inc':
+ return [
+ 22 => 1,
+ 31 => 1,
+ 45 => 1,
+ 57 => 1,
+ ];
+
+ case 'NoLongClosuresUnitTest.6.inc':
+ case 'NoLongClosuresUnitTest.10.inc':
+ case 'NoLongClosuresUnitTest.11.inc':
+ case 'NoLongClosuresUnitTest.14.inc':
+ case 'NoLongClosuresUnitTest.16.inc':
+ case 'NoLongClosuresUnitTest.18.inc':
+ case 'NoLongClosuresUnitTest.19.inc':
+ case 'NoLongClosuresUnitTest.20.inc':
+ case 'NoLongClosuresUnitTest.21.inc':
+ return [
+ 22 => 1,
+ 31 => 1,
+ ];
+
+ case 'NoLongClosuresUnitTest.13.inc':
+ return [
+ 31 => 1,
+ 45 => 1,
+ ];
+
+ case 'NoLongClosuresUnitTest.5.inc':
+ case 'NoLongClosuresUnitTest.7.inc':
+ case 'NoLongClosuresUnitTest.8.inc':
+ case 'NoLongClosuresUnitTest.9.inc':
+ case 'NoLongClosuresUnitTest.12.inc':
+ case 'NoLongClosuresUnitTest.15.inc':
+ default:
+ return [];
+ }
+ }
+}
diff --git a/Universal/Tests/FunctionDeclarations/RequireFinalMethodsInTraitsUnitTest.inc b/Universal/Tests/FunctionDeclarations/RequireFinalMethodsInTraitsUnitTest.inc
new file mode 100644
index 00000000..6c388b05
--- /dev/null
+++ b/Universal/Tests/FunctionDeclarations/RequireFinalMethodsInTraitsUnitTest.inc
@@ -0,0 +1,97 @@
+ =>
+ */
+ public function getErrorList()
+ {
+ return [
+ 62 => 1,
+ 63 => 1,
+ 65 => 1,
+ 66 => 1,
+ 68 => 1,
+ 73 => 1,
+ 75 => 1,
+ 77 => 1,
+ 81 => 1,
+ 82 => 1,
+ 83 => 1,
+ 84 => 1,
+ 85 => 1,
+ 86 => 1,
+ 87 => 1,
+ 88 => 1,
+ 89 => 1,
+ 90 => 1,
+ 91 => 1,
+ 92 => 1,
+ 93 => 1,
+ 94 => 1,
+ 95 => 1,
+ 96 => 1,
+ ];
+ }
+
+ /**
+ * Returns the lines where warnings should occur.
+ *
+ * @return array =>
+ */
+ public function getWarningList()
+ {
+ return [];
+ }
+}
diff --git a/Universal/Tests/UseStatements/DisallowMixedGroupUseUnitTest.inc b/Universal/Tests/UseStatements/DisallowMixedGroupUseUnitTest.inc
new file mode 100644
index 00000000..822208a6
--- /dev/null
+++ b/Universal/Tests/UseStatements/DisallowMixedGroupUseUnitTest.inc
@@ -0,0 +1,123 @@
+
+
+tabWidth = 4;
+ }
+
+ /**
+ * Returns the lines where errors should occur.
+ *
+ * @return array =>
+ */
+ public function getErrorList()
+ {
+ return [
+ 47 => 1,
+ 54 => 1,
+ 62 => 1,
+ 73 => 1,
+ 83 => 1,
+ 93 => 1,
+ 100 => 1,
+ 107 => 1,
+ 113 => 1,
+ ];
+ }
+
+ /**
+ * Returns the lines where warnings should occur.
+ *
+ * @return array =>
+ */
+ public function getWarningList()
+ {
+ return [];
+ }
+}
diff --git a/Universal/Tests/UseStatements/KeywordSpacingUnitTest.1.inc b/Universal/Tests/UseStatements/KeywordSpacingUnitTest.1.inc
new file mode 100644
index 00000000..449e0439
--- /dev/null
+++ b/Universal/Tests/UseStatements/KeywordSpacingUnitTest.1.inc
@@ -0,0 +1,79 @@
+ $v) {}
+
+// Ignore, spacing is already correct.
+use Vendor\Package\Name as OtherName;
+use function Vendor\Package\functionName as otherFunction;
+use const Vendor\Package\CONSTANT_NAME as OTHER_CONSTANT;
+
+use Vendor\Package\MultiStatement as Multi,
+ DateTime as dateT;
+
+use function Vendor\Package\MultiFunction as MFunction,
+ strpos as pos;
+
+USE Some\NS\ {
+ ClassName As OtherClassName,
+ Function SubLevel\functionName AS OtherFunctionName,
+ CONST Constants\MYCONSTANT as OTHERCONSTANT,
+};
+
+// Ignore, "function", "const", "as" are used as part of a name (PHP 8.0+), not as the keyword.
+use Vendor\Package\As\Name as Something;
+use function Vendor\Function\Name as some_function;
+use Vendor\Const\Name as CONSTANT_HANDLER;
+
+/*
+ * Error.
+ */
+use Vendor\Package\Name as OtherName;
+use
+
+ Function
+
+ Vendor\Package\functionName
+
+ as
+
+ otherFunction;
+use FuncTion\Util\functionB;
+use CONST Vendor\Package\CONSTANT_NAME as /*comment*/ OTHER_CONSTANT;
+use Const\Util\MyClass\CONSTANT_Y;
+
+use Vendor\Package\MultiStatement as Multi,
+ DateTime AS dateT;
+
+Use function Vendor\Package\MultiFunction as MFunction,
+ strpos as pos;
+
+use Some\NS\{
+ ClassName
+ // phpcs:ignore Stnd.Cat.Sniff --for reasons.
+ as OtherClassName,
+ function/*comment*/ SubLevel\functionName
+ as OtherFunctionName,
+ const Constants\MYCONSTANT
+ as OTHERCONSTANT,
+};
+
+// Invalid code, but will still be handled.
+use;
diff --git a/Universal/Tests/UseStatements/KeywordSpacingUnitTest.1.inc.fixed b/Universal/Tests/UseStatements/KeywordSpacingUnitTest.1.inc.fixed
new file mode 100644
index 00000000..f7aae45f
--- /dev/null
+++ b/Universal/Tests/UseStatements/KeywordSpacingUnitTest.1.inc.fixed
@@ -0,0 +1,69 @@
+ $v) {}
+
+// Ignore, spacing is already correct.
+use Vendor\Package\Name as OtherName;
+use function Vendor\Package\functionName as otherFunction;
+use const Vendor\Package\CONSTANT_NAME as OTHER_CONSTANT;
+
+use Vendor\Package\MultiStatement as Multi,
+ DateTime as dateT;
+
+use function Vendor\Package\MultiFunction as MFunction,
+ strpos as pos;
+
+USE Some\NS\ {
+ ClassName As OtherClassName,
+ Function SubLevel\functionName AS OtherFunctionName,
+ CONST Constants\MYCONSTANT as OTHERCONSTANT,
+};
+
+// Ignore, "function", "const", "as" are used as part of a name (PHP 8.0+), not as the keyword.
+use Vendor\Package\As\Name as Something;
+use function Vendor\Function\Name as some_function;
+use Vendor\Const\Name as CONSTANT_HANDLER;
+
+/*
+ * Error.
+ */
+use Vendor\Package\Name as OtherName;
+use Function Vendor\Package\functionName as otherFunction;
+use FuncTion \Util\functionB;
+use CONST Vendor\Package\CONSTANT_NAME as /*comment*/ OTHER_CONSTANT;
+use Const \Util\MyClass\CONSTANT_Y;
+
+use Vendor\Package\MultiStatement as Multi,
+ DateTime AS dateT;
+
+Use function Vendor\Package\MultiFunction as MFunction,
+ strpos as pos;
+
+use Some\NS\{
+ ClassName
+ // phpcs:ignore Stnd.Cat.Sniff --for reasons.
+ as OtherClassName,
+ function/*comment*/ SubLevel\functionName as OtherFunctionName,
+ const Constants\MYCONSTANT as OTHERCONSTANT,
+};
+
+// Invalid code, but will still be handled.
+use ;
diff --git a/Universal/Tests/UseStatements/KeywordSpacingUnitTest.2.inc b/Universal/Tests/UseStatements/KeywordSpacingUnitTest.2.inc
new file mode 100644
index 00000000..162e98b3
--- /dev/null
+++ b/Universal/Tests/UseStatements/KeywordSpacingUnitTest.2.inc
@@ -0,0 +1,11 @@
+
+ */
+ public function getErrorList($testFile = '')
+ {
+ switch ($testFile) {
+ case 'KeywordSpacingUnitTest.1.inc':
+ return [
+ 48 => 3,
+ 49 => 1,
+ 51 => 1,
+ 55 => 2,
+ 58 => 1,
+ 59 => 4,
+ 60 => 1,
+ 62 => 1,
+ 63 => 2,
+ 65 => 4,
+ 66 => 2,
+ 68 => 1,
+ 71 => 2,
+ 72 => 1,
+ 73 => 2,
+ 74 => 1,
+ 75 => 2,
+ 79 => 1,
+ ];
+
+ case 'KeywordSpacingUnitTest.2.inc':
+ if (\PHP_VERSION_ID >= 80000) {
+ return [];
+ }
+
+ return [
+ 11 => 1,
+ ];
+
+ default:
+ return [];
+ }
+ }
+
+ /**
+ * Returns the lines where warnings should occur.
+ *
+ * @return array
+ */
+ public function getWarningList()
+ {
+ return [];
+ }
+}
diff --git a/Universal/Tests/UseStatements/NoUselessAliasesUnitTest.inc b/Universal/Tests/UseStatements/NoUselessAliasesUnitTest.inc
new file mode 100644
index 00000000..6652d254
--- /dev/null
+++ b/Universal/Tests/UseStatements/NoUselessAliasesUnitTest.inc
@@ -0,0 +1,84 @@
+ =>
+ */
+ public function getErrorList()
+ {
+ return [
+ 26 => 1,
+ 30 => 1,
+ 32 => 1,
+ 33 => 1,
+ 34 => 1,
+ 36 => 1,
+ 39 => 1,
+ 41 => 1,
+ 45 => 1,
+ 51 => 1,
+ 56 => 1,
+ 59 => 1,
+ 60 => 1,
+ 61 => 1,
+ 68 => 1,
+ 69 => 1,
+ 72 => 1,
+ 76 => 1,
+ ];
+ }
+
+ /**
+ * Returns the lines where warnings should occur.
+ *
+ * @return array =>
+ */
+ public function getWarningList()
+ {
+ return [];
+ }
+}
diff --git a/Universal/Tests/WhiteSpace/CommaSpacingUnitTest.1.inc b/Universal/Tests/WhiteSpace/CommaSpacingUnitTest.1.inc
new file mode 100644
index 00000000..538270e6
--- /dev/null
+++ b/Universal/Tests/WhiteSpace/CommaSpacingUnitTest.1.inc
@@ -0,0 +1,203 @@
+ $param1 * $param2 + $param3;
+class Foo {
+ public function name($param1 , $param2,$param3) {}
+}
+
+// *InClosureUse suffix.
+$closure = function () use($param1 , $param2,$param3) {};
+
+// *InFunctionCall suffix.
+do_something($param1 , $param2,$param3);
+$obj = new Foo($param1 , $param2,$param3);
+$obj = new self($param1 , $param2,$param3);
+$obj = new parent($param1 , $param2,$param3);
+$obj = new static($param1 , $param2,$param3);
+$anonClass = new class($param1 , $param2,$param3) {};
+$var($param1 , $param2,$param3);
+#[MyAttribute(1 , 'foo',false)]
+function name() {}
+isset($item1 , $item2,$item3);
+unset($item1 , $item2,$item3);
+
+// No suffix.
+$a = array($item1 , $item2,$item3);
+$a = [$item1 , $item2,$item3];
+
+list($item1 , $item2,$item3) = $array;
+[$item1 , $item2,$item3] = $array;
+
+for ($i = 0 , $j = 1,$k = 2; $i < $j && $j < $k; $i++ , $j++,$k++) {}
+
+echo $item1 , $item2,$item3;
+
+use Vendor\Package\{NameA , NameB,NameC};
+
+class Multi {
+ const CONST_A = 1 , CONST_B = 2,CONST_C = 3;
+ public $propA = 1 , $propB = 2,$propC = 3;
+
+ public function name() {
+ global $var1 , $var2,$var3;
+ static $localA = 1 , $localB,$localC = 'foo';
+ }
+}
+
+$value = match($value) {
+ 1 , 2,3 => 123,
+};
+
+// Parse error, but not our concern.
+print ($item1 , $item2,$item3);
diff --git a/Universal/Tests/WhiteSpace/CommaSpacingUnitTest.2.inc.fixed b/Universal/Tests/WhiteSpace/CommaSpacingUnitTest.2.inc.fixed
new file mode 100644
index 00000000..4c4d4bf2
--- /dev/null
+++ b/Universal/Tests/WhiteSpace/CommaSpacingUnitTest.2.inc.fixed
@@ -0,0 +1,64 @@
+ $param1 * $param2 + $param3;
+class Foo {
+ public function name($param1, $param2, $param3) {}
+}
+
+// *InClosureUse suffix.
+$closure = function () use($param1, $param2, $param3) {};
+
+// *InFunctionCall suffix.
+do_something($param1, $param2, $param3);
+$obj = new Foo($param1, $param2, $param3);
+$obj = new self($param1, $param2, $param3);
+$obj = new parent($param1, $param2, $param3);
+$obj = new static($param1, $param2, $param3);
+$anonClass = new class($param1, $param2, $param3) {};
+$var($param1, $param2, $param3);
+#[MyAttribute(1, 'foo', false)]
+function name() {}
+isset($item1, $item2, $item3);
+unset($item1, $item2, $item3);
+
+// No suffix.
+$a = array($item1, $item2, $item3);
+$a = [$item1, $item2, $item3];
+
+list($item1, $item2, $item3) = $array;
+[$item1, $item2, $item3] = $array;
+
+for ($i = 0, $j = 1, $k = 2; $i < $j && $j < $k; $i++, $j++, $k++) {}
+
+echo $item1, $item2, $item3;
+
+use Vendor\Package\{NameA, NameB, NameC};
+
+class Multi {
+ const CONST_A = 1, CONST_B = 2, CONST_C = 3;
+ public $propA = 1, $propB = 2, $propC = 3;
+
+ public function name() {
+ global $var1, $var2, $var3;
+ static $localA = 1, $localB, $localC = 'foo';
+ }
+}
+
+$value = match($value) {
+ 1, 2, 3 => 123,
+};
+
+// Parse error, but not our concern.
+print ($item1, $item2, $item3);
diff --git a/Universal/Tests/WhiteSpace/CommaSpacingUnitTest.3.inc b/Universal/Tests/WhiteSpace/CommaSpacingUnitTest.3.inc
new file mode 100644
index 00000000..d071f45c
--- /dev/null
+++ b/Universal/Tests/WhiteSpace/CommaSpacingUnitTest.3.inc
@@ -0,0 +1,25 @@
+ $path) {
+ if (\substr($path, -6) === '.5.inc'
+ || \substr($path, -6) === '.6.inc'
+ || \substr($path, -6) === '.7.inc'
+ ) {
+ unset($testFiles[$key]);
+ }
+ }
+ }
+
+ return $testFiles;
+ }
+
+ /**
+ * Returns the lines where errors should occur.
+ *
+ * @param string $testFile The name of the file being tested.
+ *
+ * @return array
+ */
+ public function getErrorList($testFile = '')
+ {
+ switch ($testFile) {
+ case 'CommaSpacingUnitTest.1.inc':
+ return [
+ 85 => 1,
+ 86 => 1,
+ 87 => 1,
+ 88 => 1,
+ 91 => 1,
+ 92 => 1,
+ 93 => 1,
+ 99 => 3,
+ 101 => 1,
+ 106 => 1,
+ 107 => 1,
+ 111 => 2,
+ 115 => 2,
+ 116 => 2,
+ 117 => 2,
+ 122 => 1,
+ 124 => 1,
+ 126 => 1,
+ 131 => 1,
+ 132 => 1,
+ 137 => 2,
+ 138 => 2,
+ 139 => 3,
+ 143 => 1,
+ 144 => 1,
+ 145 => 1,
+ 146 => 1,
+ 149 => 3,
+ 150 => 4,
+ 154 => 1,
+ 157 => 2,
+ 158 => 2,
+ 161 => 4,
+ 162 => 4,
+ 166 => 1,
+ 168 => 1,
+ 174 => 2,
+ 178 => 1,
+ 182 => 2,
+ 186 => 1,
+ 189 => 1,
+ 195 => 1,
+ 196 => 1,
+ 197 => 1,
+ 201 => 1,
+ 202 => 1,
+ ];
+
+ // Modular error code check.
+ case 'CommaSpacingUnitTest.2.inc':
+ return [
+ 10 => 3,
+ 13 => 3,
+ 14 => 3,
+ 15 => 3,
+ 17 => 3,
+ 21 => 3,
+ 24 => 3,
+ 25 => 3,
+ 26 => 3,
+ 27 => 3,
+ 28 => 3,
+ 29 => 3,
+ 30 => 3,
+ 31 => 3,
+ 33 => 3,
+ 34 => 3,
+ 37 => 3,
+ 38 => 3,
+ 40 => 3,
+ 41 => 3,
+ 43 => 6,
+ 45 => 3,
+ 47 => 3,
+ 50 => 3,
+ 51 => 3,
+ 54 => 3,
+ 55 => 3,
+ 60 => 3,
+ 64 => 3,
+ ];
+
+ // Comma before trailing comment.
+ case 'CommaSpacingUnitTest.3.inc':
+ return [
+ 6 => 2,
+ 10 => 1,
+ 11 => 1,
+ 13 => 1,
+ 18 => 1,
+ 20 => 1,
+ 23 => 1,
+ 24 => 1,
+ ];
+
+ /*
+ * PHP 7.3+ flexible heredoc/nowdoc tests.
+ * These tests will not be run on PHP < 7.3 as the results will be unreliable.
+ * The tests files will be removed from the tests to be run via the overloaded getTestFiles() method.
+ */
+ case 'CommaSpacingUnitTest.5.inc':
+ return [
+ 43 => 1,
+ 46 => 1,
+ 53 => 2,
+ 56 => 1,
+ ];
+
+ case 'CommaSpacingUnitTest.6.inc':
+ return [
+ 46 => 1,
+ 49 => 1,
+ 56 => 2,
+ 58 => 1,
+ ];
+
+ case 'CommaSpacingUnitTest.7.inc':
+ return [
+ 47 => 2,
+ 49 => 1,
+ 56 => 2,
+ 59 => 1,
+ ];
+
+ // Parse error test.
+ case 'CommaSpacingUnitTest.9.inc':
+ return [
+ 5 => 1,
+ ];
+
+ default:
+ return [];
+ }
+ }
+
+ /**
+ * Returns the lines where warnings should occur.
+ *
+ * @return array
+ */
+ public function getWarningList()
+ {
+ return [];
+ }
+}
diff --git a/composer.json b/composer.json
index 9e56a009..f4bbf2b9 100644
--- a/composer.json
+++ b/composer.json
@@ -22,7 +22,7 @@
"require" : {
"php" : ">=5.4",
"squizlabs/php_codesniffer" : "^3.7.1",
- "phpcsstandards/phpcsutils" : "^1.0.6"
+ "phpcsstandards/phpcsutils" : "^1.0.8"
},
"require-dev" : {
"php-parallel-lint/php-parallel-lint": "^1.3.2",