From a70635c023c63cbf366272c8df6a235e240ab99f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=BDoljom?= Date: Sat, 27 Aug 2022 18:48:28 +0200 Subject: [PATCH 001/125] Add new sniff that checks the declare statements --- .../DeclareStatementsStyleStandard.xml | 50 ++ .../DeclareStatementsStyleSniff.php | 241 +++++++++ .../DeclareStatementsStyleUnitTest.inc | 481 ++++++++++++++++++ .../DeclareStatementsStyleUnitTest.php | 78 +++ 4 files changed, 850 insertions(+) create mode 100644 Universal/Docs/DeclareStatements/DeclareStatementsStyleStandard.xml create mode 100644 Universal/Sniffs/DeclareStatements/DeclareStatementsStyleSniff.php create mode 100644 Universal/Tests/DeclareStatements/DeclareStatementsStyleUnitTest.inc create mode 100644 Universal/Tests/DeclareStatements/DeclareStatementsStyleUnitTest.php diff --git a/Universal/Docs/DeclareStatements/DeclareStatementsStyleStandard.xml b/Universal/Docs/DeclareStatements/DeclareStatementsStyleStandard.xml new file mode 100644 index 00000000..9adfd583 --- /dev/null +++ b/Universal/Docs/DeclareStatements/DeclareStatementsStyleStandard.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Universal/Sniffs/DeclareStatements/DeclareStatementsStyleSniff.php b/Universal/Sniffs/DeclareStatements/DeclareStatementsStyleSniff.php new file mode 100644 index 00000000..64488202 --- /dev/null +++ b/Universal/Sniffs/DeclareStatements/DeclareStatementsStyleSniff.php @@ -0,0 +1,241 @@ +getTokens(); + + $openParenPtr = $tokens[$stackPtr]['parenthesis_opener']; + $closeParenPtr = $tokens[$stackPtr]['parenthesis_closer']; + + if (isset($openParenPtr, $closeParenPtr) === false) { + // Parse error or live coding, bow out. + return; + } + + $directiveStrings = []; + // Get the next string and check if it's an allowed directive. + // Find all the directive strings inside the declare statement. + for ($i = $openParenPtr; $i <= $closeParenPtr; $i++) { + if ($tokens[$i]['code'] === \T_STRING) { + $directiveStrings[] = $tokens[$i]['content']; + } + } + + foreach ($directiveStrings as $directiveString) { + if (!in_array($directiveString, $this->allowedDirectives, true)) { + $phpcsFile->addError( + sprintf( + 'Declare directives can be one of: %1$s. "%2$s" found.', + implode(', ', $this->allowedDirectives), + $directiveString + ), + $stackPtr, + 'WrongDeclareDirective' + ); + return; + } + unset($directiveString); + } + + // Curly braces. + $hasScopeOpenerCloser = isset($tokens[$stackPtr]['scope_opener']); + + // If strict types is defined using curly brace, throw error. + if ($hasScopeOpenerCloser !== false && in_array('strict_types', $directiveStrings, true)) { + $phpcsFile->addError( + sprintf( + 'strict_types declaration must not use block mode. Opening brace found on line %d', + $tokens[$stackPtr]['line'] + ), + $stackPtr, + 'Forbidden' + ); + return; + } + + // Fix for the case when the code is between the curly braces for the strict_types. + $codePtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($closeParenPtr + 1), null, true); + + /* If the code pointer is not one of: + * \T_SEMICOLON, \T_CLOSE_TAG or \T_OPEN_CURLY_BRACKET, \T_COLON + * throw an error. + */ + if (!in_array($tokens[$codePtr]['code'], [\T_SEMICOLON, \T_CLOSE_TAG, \T_OPEN_CURLY_BRACKET, \T_COLON], true)) { + $phpcsFile->addError( + 'Unexpected code found after opening the declare statement without closing it.', + $stackPtr, + 'UnexpectedCodeFound' + ); + return; + } + + if ($this->declareStyle === 'requireBraces' && + $hasScopeOpenerCloser === false && + in_array('strict_types', $directiveStrings, true) + ) { + $phpcsFile->addError( + 'strict_types declaration is not compatible with requireBraces option.', + $stackPtr, + 'IncompatibleCurlyBracesRequirement' + ); + return; + } + + if ($this->declareStyle === 'disallowBraces' && $hasScopeOpenerCloser !== false) { + $phpcsFile->addError('Declare statement found using curly braces', $stackPtr, 'DisallowedCurlyBraces'); + return; + } + + if (in_array('ticks', $this->directiveType, true)) { + if ($this->declareStyle === 'requireBraces' && + $hasScopeOpenerCloser === false && + in_array('ticks', $directiveStrings, true) + ) { + $phpcsFile->addError( + 'Declare statement for found without curly braces', + $stackPtr, + 'MissingCurlyBracesTicks' + ); + return; + } + + if ($this->declareStyle === 'disallowBraces' && $hasScopeOpenerCloser !== false) { + $phpcsFile->addError( + 'Declare statement found using curly braces', + $stackPtr, + 'DisallowedCurlyBracesTicks' + ); + return; + } + } + + if (in_array('encoding', $this->directiveType, true)) { + if ($this->declareStyle === 'requireBraces' && + $hasScopeOpenerCloser === false && + in_array('encoding', $directiveStrings, true) + ) { + $phpcsFile->addError( + 'Declare statement found without curly braces', + $stackPtr, + 'MissingCurlyBracesEncoding' + ); + return; + } + + if ($this->declareStyle === 'disallowBraces' && $hasScopeOpenerCloser !== false) { + $phpcsFile->addError( + 'Declare statement found using curly braces', + $stackPtr, + 'DisallowedCurlyBracesEncoding' + ); + return; + } + } + + if ($this->declareStyle === 'requireBraces' && + $hasScopeOpenerCloser === false && + empty($this->directiveType) && + !in_array('strict_types', $directiveStrings, true) + ) { + $phpcsFile->addError('Declare statement found without curly braces', $stackPtr, 'MissingCurlyBraces'); + return; + } + } +} diff --git a/Universal/Tests/DeclareStatements/DeclareStatementsStyleUnitTest.inc b/Universal/Tests/DeclareStatements/DeclareStatementsStyleUnitTest.inc new file mode 100644 index 00000000..6d9b84dd --- /dev/null +++ b/Universal/Tests/DeclareStatements/DeclareStatementsStyleUnitTest.inc @@ -0,0 +1,481 @@ + => + */ + public function getErrorList() + { + return [ + 15 => 1, + 19 => 1, + 23 => 1, + 32 => 1, + 41 => 1, + 45 => 1, + 49 => 1, + 50 => 1, + 55 => 1, + 56 => 1, + 63 => 1, + 67 => 1, + 76 => 1, + 85 => 1, + 89 => 1, + 95 => 1, + 104 => 1, + 185 => 1, + 187 => 1, + 189 => 1, + 191 => 1, + 217 => 1, + 226 => 1, + 261 => 1, + 271 => 1, + 331 => 1, + 340 => 1, + 375 => 1, + 385 => 1, + 445 => 1, + 454 => 1, + 463 => 1, + 472 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return []; + } +} From 7e27d1b45bf354feeb4796405733fecc29bca52b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=BDoljom?= Date: Sun, 25 Sep 2022 11:46:56 +0200 Subject: [PATCH 002/125] Remove renamed files --- .../DeclareStatementsStyleStandard.xml | 50 ---- .../DeclareStatementsStyleSniff.php | 241 ------------------ ...leUnitTest.inc => BlockModeUnitTest.1.inc} | 0 ...tyleUnitTest.php => BlockModeUnitTest.php} | 63 ++--- 4 files changed, 27 insertions(+), 327 deletions(-) delete mode 100644 Universal/Docs/DeclareStatements/DeclareStatementsStyleStandard.xml delete mode 100644 Universal/Sniffs/DeclareStatements/DeclareStatementsStyleSniff.php rename Universal/Tests/DeclareStatements/{DeclareStatementsStyleUnitTest.inc => BlockModeUnitTest.1.inc} (100%) rename Universal/Tests/DeclareStatements/{DeclareStatementsStyleUnitTest.php => BlockModeUnitTest.php} (56%) diff --git a/Universal/Docs/DeclareStatements/DeclareStatementsStyleStandard.xml b/Universal/Docs/DeclareStatements/DeclareStatementsStyleStandard.xml deleted file mode 100644 index 9adfd583..00000000 --- a/Universal/Docs/DeclareStatements/DeclareStatementsStyleStandard.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Universal/Sniffs/DeclareStatements/DeclareStatementsStyleSniff.php b/Universal/Sniffs/DeclareStatements/DeclareStatementsStyleSniff.php deleted file mode 100644 index 64488202..00000000 --- a/Universal/Sniffs/DeclareStatements/DeclareStatementsStyleSniff.php +++ /dev/null @@ -1,241 +0,0 @@ -getTokens(); - - $openParenPtr = $tokens[$stackPtr]['parenthesis_opener']; - $closeParenPtr = $tokens[$stackPtr]['parenthesis_closer']; - - if (isset($openParenPtr, $closeParenPtr) === false) { - // Parse error or live coding, bow out. - return; - } - - $directiveStrings = []; - // Get the next string and check if it's an allowed directive. - // Find all the directive strings inside the declare statement. - for ($i = $openParenPtr; $i <= $closeParenPtr; $i++) { - if ($tokens[$i]['code'] === \T_STRING) { - $directiveStrings[] = $tokens[$i]['content']; - } - } - - foreach ($directiveStrings as $directiveString) { - if (!in_array($directiveString, $this->allowedDirectives, true)) { - $phpcsFile->addError( - sprintf( - 'Declare directives can be one of: %1$s. "%2$s" found.', - implode(', ', $this->allowedDirectives), - $directiveString - ), - $stackPtr, - 'WrongDeclareDirective' - ); - return; - } - unset($directiveString); - } - - // Curly braces. - $hasScopeOpenerCloser = isset($tokens[$stackPtr]['scope_opener']); - - // If strict types is defined using curly brace, throw error. - if ($hasScopeOpenerCloser !== false && in_array('strict_types', $directiveStrings, true)) { - $phpcsFile->addError( - sprintf( - 'strict_types declaration must not use block mode. Opening brace found on line %d', - $tokens[$stackPtr]['line'] - ), - $stackPtr, - 'Forbidden' - ); - return; - } - - // Fix for the case when the code is between the curly braces for the strict_types. - $codePtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($closeParenPtr + 1), null, true); - - /* If the code pointer is not one of: - * \T_SEMICOLON, \T_CLOSE_TAG or \T_OPEN_CURLY_BRACKET, \T_COLON - * throw an error. - */ - if (!in_array($tokens[$codePtr]['code'], [\T_SEMICOLON, \T_CLOSE_TAG, \T_OPEN_CURLY_BRACKET, \T_COLON], true)) { - $phpcsFile->addError( - 'Unexpected code found after opening the declare statement without closing it.', - $stackPtr, - 'UnexpectedCodeFound' - ); - return; - } - - if ($this->declareStyle === 'requireBraces' && - $hasScopeOpenerCloser === false && - in_array('strict_types', $directiveStrings, true) - ) { - $phpcsFile->addError( - 'strict_types declaration is not compatible with requireBraces option.', - $stackPtr, - 'IncompatibleCurlyBracesRequirement' - ); - return; - } - - if ($this->declareStyle === 'disallowBraces' && $hasScopeOpenerCloser !== false) { - $phpcsFile->addError('Declare statement found using curly braces', $stackPtr, 'DisallowedCurlyBraces'); - return; - } - - if (in_array('ticks', $this->directiveType, true)) { - if ($this->declareStyle === 'requireBraces' && - $hasScopeOpenerCloser === false && - in_array('ticks', $directiveStrings, true) - ) { - $phpcsFile->addError( - 'Declare statement for found without curly braces', - $stackPtr, - 'MissingCurlyBracesTicks' - ); - return; - } - - if ($this->declareStyle === 'disallowBraces' && $hasScopeOpenerCloser !== false) { - $phpcsFile->addError( - 'Declare statement found using curly braces', - $stackPtr, - 'DisallowedCurlyBracesTicks' - ); - return; - } - } - - if (in_array('encoding', $this->directiveType, true)) { - if ($this->declareStyle === 'requireBraces' && - $hasScopeOpenerCloser === false && - in_array('encoding', $directiveStrings, true) - ) { - $phpcsFile->addError( - 'Declare statement found without curly braces', - $stackPtr, - 'MissingCurlyBracesEncoding' - ); - return; - } - - if ($this->declareStyle === 'disallowBraces' && $hasScopeOpenerCloser !== false) { - $phpcsFile->addError( - 'Declare statement found using curly braces', - $stackPtr, - 'DisallowedCurlyBracesEncoding' - ); - return; - } - } - - if ($this->declareStyle === 'requireBraces' && - $hasScopeOpenerCloser === false && - empty($this->directiveType) && - !in_array('strict_types', $directiveStrings, true) - ) { - $phpcsFile->addError('Declare statement found without curly braces', $stackPtr, 'MissingCurlyBraces'); - return; - } - } -} diff --git a/Universal/Tests/DeclareStatements/DeclareStatementsStyleUnitTest.inc b/Universal/Tests/DeclareStatements/BlockModeUnitTest.1.inc similarity index 100% rename from Universal/Tests/DeclareStatements/DeclareStatementsStyleUnitTest.inc rename to Universal/Tests/DeclareStatements/BlockModeUnitTest.1.inc diff --git a/Universal/Tests/DeclareStatements/DeclareStatementsStyleUnitTest.php b/Universal/Tests/DeclareStatements/BlockModeUnitTest.php similarity index 56% rename from Universal/Tests/DeclareStatements/DeclareStatementsStyleUnitTest.php rename to Universal/Tests/DeclareStatements/BlockModeUnitTest.php index cceedfd9..30f40b0d 100644 --- a/Universal/Tests/DeclareStatements/DeclareStatementsStyleUnitTest.php +++ b/Universal/Tests/DeclareStatements/BlockModeUnitTest.php @@ -25,45 +25,36 @@ class DeclareStatementsStyleUnitTest extends AbstractSniffUnitTest /** * Returns the lines where errors should occur. * + * @param string $testFile The name of the file being tested. + * * @return array => */ - public function getErrorList() + public function getErrorList($testFile = '') { - return [ - 15 => 1, - 19 => 1, - 23 => 1, - 32 => 1, - 41 => 1, - 45 => 1, - 49 => 1, - 50 => 1, - 55 => 1, - 56 => 1, - 63 => 1, - 67 => 1, - 76 => 1, - 85 => 1, - 89 => 1, - 95 => 1, - 104 => 1, - 185 => 1, - 187 => 1, - 189 => 1, - 191 => 1, - 217 => 1, - 226 => 1, - 261 => 1, - 271 => 1, - 331 => 1, - 340 => 1, - 375 => 1, - 385 => 1, - 445 => 1, - 454 => 1, - 463 => 1, - 472 => 1, - ]; + switch ($testFile) { + case 'DeclareStatementsStyleUnitTest.1.inc': + return [ + ]; + + case 'DeclareStatementsStyleUnitTest.2.inc': + return [ + ]; + + case 'DeclareStatementsStyleUnitTest.3.inc': + return [ + ]; + + case 'DeclareStatementsStyleUnitTest.4.inc': + return [ + ]; + + case 'DeclareStatementsStyleUnitTest.5.inc': + return [ + ]; + + default: + return []; + } } /** From 98925a945aaff44470186dddf46d0f25b5cf9edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=BDoljom?= Date: Sun, 25 Sep 2022 11:47:42 +0200 Subject: [PATCH 003/125] Rewrite the sniff according to the PR suggestions To do: add fixers and metric --- .../DeclareStatements/BlockModeSniff.php | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 Universal/Sniffs/DeclareStatements/BlockModeSniff.php diff --git a/Universal/Sniffs/DeclareStatements/BlockModeSniff.php b/Universal/Sniffs/DeclareStatements/BlockModeSniff.php new file mode 100644 index 00000000..37ed405e --- /dev/null +++ b/Universal/Sniffs/DeclareStatements/BlockModeSniff.php @@ -0,0 +1,239 @@ + true, + 'ticks' => true, + 'encoding' => true, + ]; + + /** + * Returns an array of tokens this test wants to listen for. + * + * @since 1.0.0 + * + * @return array + */ + public function register() + { + return [ + T_DECLARE + ]; + } + + /** + * Processes this test, when one of its tokens is encountered. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. + * + * @return void + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]['parenthesis_opener'], $tokens[$stackPtr]['parenthesis_closer']) === false) { + // Parse error or live coding, bow out. + return; + } + + $openParenPtr = $tokens[$stackPtr]['parenthesis_opener']; + $closeParenPtr = $tokens[$stackPtr]['parenthesis_closer']; + + $directiveStrings = []; + // Get the next string and check if it's an allowed directive. + // Find all the directive strings inside the declare statement. + for ($i = $openParenPtr; $i <= $closeParenPtr; $i++) { + if ($tokens[$i]['code'] === \T_STRING + && isset($this->allowedDirectives[\strtolower($tokens[$i]['content'])]) + ) { + $directiveStrings[$tokens[$i]['content']] = true; + } + } + + unset($i); + + if (empty($directiveStrings)) { + // No valid directives were found, this is outside the scope of this sniff. + return; + } + + $usesBlockMode = isset($tokens[$stackPtr]['scope_opener']); + + // If strict types is defined using block mode, throw error. + if ($usesBlockMode && isset($directiveStrings['strict_types'])) { + $phpcsFile->addError( + 'strict_types declaration must not use block mode.', + $stackPtr, + 'Forbidden' + ); + return; + } + // To do: Add a fixer! + + // Check if there is a code between the declare statement and opening brace/alternative syntax. + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($closeParenPtr + 1), null, true); + $directiveCloserTokens = [\T_SEMICOLON, \T_CLOSE_TAG, \T_OPEN_CURLY_BRACKET, \T_COLON]; + + if (!in_array($tokens[$nextNonEmpty]['code'], $directiveCloserTokens, true)) { + $phpcsFile->addError( + 'Unexpected code found after opening the declare statement without closing it.', + $stackPtr, + 'UnexpectedCodeFound' + ); + return; + } + + // Multiple directives - if one requires block mode usage, other has to as well. + if (count($directiveStrings) > 1 + && (($this->encodingBlockMode === 'disallow' && $this->ticksBlockMode !== 'disallow') + || ($this->ticksBlockMode === 'disallow' && $this->encodingBlockMode !== 'disallow')) + ) { + $phpcsFile->addError( + 'Multiple directives found, but one of them is disallowing the use of block mode.', + $stackPtr, + 'Forbidden' + ); + return; + } + + if (($this->encodingBlockMode === 'allow' || $this->encodingBlockMode === 'require') + && $this->ticksBlockMode === 'disallow' + && $usesBlockMode && isset($directiveStrings['ticks']) + ) { + $phpcsFile->addError( + 'Block mode is not allowed for ticks directive.', + $stackPtr, + 'DisallowedTicksBlockMode' + ); + return; + } + + if ($this->ticksBlockMode === 'require' + && !$usesBlockMode && isset($directiveStrings['ticks']) + ) { + $phpcsFile->addError( + 'Block mode is required for ticks directive.', + $stackPtr, + 'RequiredTicksBlockMode' + ); + return; + } + + if ($this->encodingBlockMode === 'disallow' + && ($this->ticksBlockMode === 'allow' || $this->ticksBlockMode === 'require') + && $usesBlockMode && isset($directiveStrings['encoding']) + ) { + $phpcsFile->addError( + 'Block mode is not allowed for encoding directive.', + $stackPtr, + 'DisallowedEncodingBlockMode' + ); + return; + } + + if ($this->encodingBlockMode === 'disallow' && $this->ticksBlockMode === 'disallow' && $usesBlockMode) { + $phpcsFile->addError( + 'Block mode is not allowed for any declare directive.', + $stackPtr, + 'DisallowedBlockMode' + ); + return; + } + + if ($this->encodingBlockMode === 'require' + && !$usesBlockMode && isset($directiveStrings['encoding']) + ) { + $phpcsFile->addError( + 'Block mode is required for encoding directive.', + $stackPtr, + 'RequiredEncodingBlockMode' + ); + return; + } + } +} From 74ee3345a1a00b7fd2576ee736e1be5529c117ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=BDoljom?= Date: Sun, 25 Sep 2022 11:48:05 +0200 Subject: [PATCH 004/125] Update docs for the sniff Will probably need some fixup. --- .../DeclareStatements/BlockModeStandard.xml | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 Universal/Docs/DeclareStatements/BlockModeStandard.xml diff --git a/Universal/Docs/DeclareStatements/BlockModeStandard.xml b/Universal/Docs/DeclareStatements/BlockModeStandard.xml new file mode 100644 index 00000000..a64c7549 --- /dev/null +++ b/Universal/Docs/DeclareStatements/BlockModeStandard.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + { + // Code. +} + ]]> + + + From 0acf7a2a0b95fabe85b2a8e058230f5941fb5a3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=BDoljom?= Date: Sun, 25 Sep 2022 11:48:23 +0200 Subject: [PATCH 005/125] Update test file --- .../DeclareStatements/BlockModeUnitTest.php | 76 ++++++++++++++++--- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/Universal/Tests/DeclareStatements/BlockModeUnitTest.php b/Universal/Tests/DeclareStatements/BlockModeUnitTest.php index 30f40b0d..1be462b0 100644 --- a/Universal/Tests/DeclareStatements/BlockModeUnitTest.php +++ b/Universal/Tests/DeclareStatements/BlockModeUnitTest.php @@ -15,11 +15,11 @@ /** * Unit test class for the DeclareStatementsStyleUnitTest sniff. * - * @covers PHPCSExtra\Universal\Sniffs\DeclareStatements\DeclareStatementsStyleSniff + * @covers PHPCSExtra\Universal\Sniffs\DeclareStatements\BlockModeSniff * * @since 1.0.0 */ -class DeclareStatementsStyleUnitTest extends AbstractSniffUnitTest +class BlockModeUnitTest extends AbstractSniffUnitTest { /** @@ -32,24 +32,80 @@ class DeclareStatementsStyleUnitTest extends AbstractSniffUnitTest public function getErrorList($testFile = '') { switch ($testFile) { - case 'DeclareStatementsStyleUnitTest.1.inc': + case 'BlockModeUnitTest.1.inc': return [ + 21 => 1, + 25 => 1, + 29 => 1, + 38 => 1, + 47 => 1, + 51 => 1, + 55 => 1, + 56 => 1, + 60 => 1, + 69 => 1, ]; - case 'DeclareStatementsStyleUnitTest.2.inc': + case 'BlockModeUnitTest.3.inc': return [ + 7 => 1, + 11 => 1, + 19 => 1, + 20 => 1, + 31 => 1, + 38 => 1, + 42 => 1, + 43 => 1, ]; - case 'DeclareStatementsStyleUnitTest.3.inc': + case 'BlockModeUnitTest.4.inc': return [ + 27 => 1, + 31 => 1, + 56 => 1, + 75 => 1, + 79 => 1, + 86 => 1, + 90 => 1, + 98 => 1, + 102 => 1, + 117 => 1, + 121 => 1, + 125 => 1, + 144 => 1, + 157 => 1, + 161 => 1, + 167 => 1, + 188 => 1, + 190 => 1, ]; - case 'DeclareStatementsStyleUnitTest.4.inc': - return [ - ]; - - case 'DeclareStatementsStyleUnitTest.5.inc': + case 'BlockModeUnitTest.5.inc': return [ + 51 => 1, + 55 => 1, + 56 => 1, + 66 => 1, + 86 => 1, + 111 => 1, + 117 => 1, + 126 => 1, + 134 => 1, + 135 => 1, + 144 => 1, + 148 => 1, + 172 => 1, + 181 => 1, + 185 => 1, + 192 => 1, + 213 => 1, + 229 => 1, + 239 => 1, + 245 => 1, + 248 => 1, + 268 => 1, + 276 => 1, + 281 => 1, ]; default: From bfcf2b49ebb6872004dba3b96afa710334455672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=BDoljom?= Date: Sun, 25 Sep 2022 11:48:49 +0200 Subject: [PATCH 006/125] Split code examples for the test in multiple files --- .../DeclareStatements/BlockModeUnitTest.1.inc | 427 +----------------- .../DeclareStatements/BlockModeUnitTest.2.inc | 12 + .../DeclareStatements/BlockModeUnitTest.3.inc | 50 ++ .../DeclareStatements/BlockModeUnitTest.4.inc | 208 +++++++++ .../DeclareStatements/BlockModeUnitTest.5.inc | 301 ++++++++++++ 5 files changed, 585 insertions(+), 413 deletions(-) create mode 100644 Universal/Tests/DeclareStatements/BlockModeUnitTest.2.inc create mode 100644 Universal/Tests/DeclareStatements/BlockModeUnitTest.3.inc create mode 100644 Universal/Tests/DeclareStatements/BlockModeUnitTest.4.inc create mode 100644 Universal/Tests/DeclareStatements/BlockModeUnitTest.5.inc diff --git a/Universal/Tests/DeclareStatements/BlockModeUnitTest.1.inc b/Universal/Tests/DeclareStatements/BlockModeUnitTest.1.inc index 6d9b84dd..c97e8e79 100644 --- a/Universal/Tests/DeclareStatements/BlockModeUnitTest.1.inc +++ b/Universal/Tests/DeclareStatements/BlockModeUnitTest.1.inc @@ -1,6 +1,6 @@ Date: Sat, 3 Dec 2022 10:44:42 +0100 Subject: [PATCH 007/125] Initial attempt at adding metrics --- .../DeclareStatements/BlockModeSniff.php | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/Universal/Sniffs/DeclareStatements/BlockModeSniff.php b/Universal/Sniffs/DeclareStatements/BlockModeSniff.php index 37ed405e..31110852 100644 --- a/Universal/Sniffs/DeclareStatements/BlockModeSniff.php +++ b/Universal/Sniffs/DeclareStatements/BlockModeSniff.php @@ -38,6 +38,24 @@ class BlockModeSniff implements Sniff { + /** + * Name of the metric. + * + * @since 1.0.0 + * + * @var string + */ + const DECLARE_SCOPE_METRIC = 'Declare directive scope'; + + /** + * Name of the metric. + * + * @since 1.0.0 + * + * @var string + */ + const DECLARE_TYPE_METRIC = 'Declare directive type'; + /** * The option for the encoding directive. * @@ -144,6 +162,12 @@ public function process(File $phpcsFile, $stackPtr) $usesBlockMode = isset($tokens[$stackPtr]['scope_opener']); + if ($usesBlockMode) { + $phpcsFile->recordMetric($stackPtr, self::DECLARE_SCOPE_METRIC, 'Block'); + } else { + $phpcsFile->recordMetric($stackPtr, self::DECLARE_SCOPE_METRIC, 'Global'); + } + // If strict types is defined using block mode, throw error. if ($usesBlockMode && isset($directiveStrings['strict_types'])) { $phpcsFile->addError( @@ -153,7 +177,14 @@ public function process(File $phpcsFile, $stackPtr) ); return; } - // To do: Add a fixer! + /* + * To do: Add a fixer + * + * But only if strict_types is on its own. In this case we should remove the last brace, + * remove the first one, and after the closing parenthesis add a comma. + * + * Add a fixable test for this case! + */ // Check if there is a code between the declare statement and opening brace/alternative syntax. $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($closeParenPtr + 1), null, true); @@ -168,6 +199,12 @@ public function process(File $phpcsFile, $stackPtr) return; } + foreach (\array_keys($directiveStrings) as $directiveString) { + if (isset($this->allowedDirectives[$directiveString])) { + $phpcsFile->recordMetric($stackPtr, self::DECLARE_TYPE_METRIC, $directiveString); + } + } + // Multiple directives - if one requires block mode usage, other has to as well. if (count($directiveStrings) > 1 && (($this->encodingBlockMode === 'disallow' && $this->ticksBlockMode !== 'disallow') From a8d53eb6ea037ef444384621d497968e9075ffb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=BDoljom?= Date: Sat, 3 Dec 2022 12:00:55 +0100 Subject: [PATCH 008/125] Remove fixer comments --- Universal/Sniffs/DeclareStatements/BlockModeSniff.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Universal/Sniffs/DeclareStatements/BlockModeSniff.php b/Universal/Sniffs/DeclareStatements/BlockModeSniff.php index 31110852..d3cdea43 100644 --- a/Universal/Sniffs/DeclareStatements/BlockModeSniff.php +++ b/Universal/Sniffs/DeclareStatements/BlockModeSniff.php @@ -177,14 +177,6 @@ public function process(File $phpcsFile, $stackPtr) ); return; } - /* - * To do: Add a fixer - * - * But only if strict_types is on its own. In this case we should remove the last brace, - * remove the first one, and after the closing parenthesis add a comma. - * - * Add a fixable test for this case! - */ // Check if there is a code between the declare statement and opening brace/alternative syntax. $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($closeParenPtr + 1), null, true); From 5834c6426084817b624c9c412f6229db2a3b7f87 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 5 Dec 2022 12:01:38 +0100 Subject: [PATCH 009/125] WIP / review - TODO: rest of sniff + tests reviewen --- .../DeclareStatements/BlockModeStandard.xml | 16 +-- .../DeclareStatements/BlockModeSniff.php | 97 +++++++++++-------- .../DeclareStatements/BlockModeUnitTest.1.inc | 4 - .../DeclareStatements/BlockModeUnitTest.2.inc | 4 +- .../DeclareStatements/BlockModeUnitTest.3.inc | 2 +- .../DeclareStatements/BlockModeUnitTest.4.inc | 2 +- .../DeclareStatements/BlockModeUnitTest.5.inc | 2 +- 7 files changed, 72 insertions(+), 55 deletions(-) diff --git a/Universal/Docs/DeclareStatements/BlockModeStandard.xml b/Universal/Docs/DeclareStatements/BlockModeStandard.xml index a64c7549..5c9be11d 100644 --- a/Universal/Docs/DeclareStatements/BlockModeStandard.xml +++ b/Universal/Docs/DeclareStatements/BlockModeStandard.xml @@ -16,7 +16,7 @@ declare(strict_types=1); declare(encoding='utf-8'); declare(ticks=10): - // Code. + // Code. enddeclare; ]]> @@ -36,20 +36,24 @@ enddeclare; - + - + { - // Code. +declare(strict_types=1) { + // Code. } + +declare(strict_types=1) : + // Code. +enddeclare; ]]> diff --git a/Universal/Sniffs/DeclareStatements/BlockModeSniff.php b/Universal/Sniffs/DeclareStatements/BlockModeSniff.php index d3cdea43..6a152ebe 100644 --- a/Universal/Sniffs/DeclareStatements/BlockModeSniff.php +++ b/Universal/Sniffs/DeclareStatements/BlockModeSniff.php @@ -18,13 +18,13 @@ /** * Checks the style of the declare statement. * - * `declare` directives can be written in two different styles: + * Declare statements can be written in two different styles: * 1. Applied to the rest of the file, usually written at the top of a file like `declare(strict_types=1);`. * 2. Applied to a limited scope using curly braces or using alternative control structure syntax - * (the exception to this rule is the `strict_types` directive). This is known as a block mode. + * (the exception to this rule is the `strict_types` directive). This is known as block mode. * - * You can also have multiple directives written inside the `declare` directive. - * This sniff will check the preferred mode of the `declare` directive. + * There can be multiple directives inside a `declare` statement. + * This sniff checks the preferred mode for the `declare` statements. * * You can modify the sniff by changing the whether the block mode of the encoding and the ticks directives * is allowed, disallowed or required. By default, the ticks directive, if written in @@ -45,7 +45,7 @@ class BlockModeSniff implements Sniff * * @var string */ - const DECLARE_SCOPE_METRIC = 'Declare directive scope'; + const DECLARE_SCOPE_METRIC = 'Declare statement scope'; /** * Name of the metric. @@ -57,10 +57,10 @@ class BlockModeSniff implements Sniff const DECLARE_TYPE_METRIC = 'Declare directive type'; /** - * The option for the encoding directive. + * Whether block mode is allowed for `encoding` directives. * - * Can be one of: 'disallow', 'allow' (no preference), 'require'. - * By default it's disallowed. + * Can be one of: 'disallow', 'allow' (no preference), or 'require'. + * Defaults to: 'disallow'. * * @since 1.0.0 * @@ -69,10 +69,10 @@ class BlockModeSniff implements Sniff public $encodingBlockMode = 'disallow'; /** - * The option for the ticks directive. + * Whether block mode is allowed for `ticks` directives. * - * Can be one of: 'disallow', 'allow' (no preference), 'require'. - * By default it's allowed. + * Can be one of: 'disallow', 'allow' (no preference), or 'require'. + * Defaults to: 'allow'. * * @since 1.0.0 * @@ -83,7 +83,7 @@ class BlockModeSniff implements Sniff /** * The default option for the strict_types directive. * - * Only directive that cannot be written in block mode is strict_types. + * Block mode is not allowed for the `strict_types` directive. * Using it in block mode will throw a PHP fatal error. * * @since 1.0.0 @@ -114,9 +114,7 @@ class BlockModeSniff implements Sniff */ public function register() { - return [ - T_DECLARE - ]; + return [T_DECLARE]; } /** @@ -146,10 +144,12 @@ public function process(File $phpcsFile, $stackPtr) // Get the next string and check if it's an allowed directive. // Find all the directive strings inside the declare statement. for ($i = $openParenPtr; $i <= $closeParenPtr; $i++) { - if ($tokens[$i]['code'] === \T_STRING - && isset($this->allowedDirectives[\strtolower($tokens[$i]['content'])]) - ) { - $directiveStrings[$tokens[$i]['content']] = true; + if ($tokens[$i]['code'] === \T_STRING) { + $contentsLC = \strtolower($tokens[$i]['content']); + if (isset($this->allowedDirectives[$contentsLC])) { + $phpcsFile->recordMetric($i, self::DECLARE_TYPE_METRIC, $contentsLC); + $directiveStrings[$contentsLC] = true; + } } } @@ -163,40 +163,59 @@ public function process(File $phpcsFile, $stackPtr) $usesBlockMode = isset($tokens[$stackPtr]['scope_opener']); if ($usesBlockMode) { - $phpcsFile->recordMetric($stackPtr, self::DECLARE_SCOPE_METRIC, 'Block'); + $phpcsFile->recordMetric($stackPtr, self::DECLARE_SCOPE_METRIC, 'Block mode'); } else { - $phpcsFile->recordMetric($stackPtr, self::DECLARE_SCOPE_METRIC, 'Global'); + $phpcsFile->recordMetric($stackPtr, self::DECLARE_SCOPE_METRIC, 'File mode'); } // If strict types is defined using block mode, throw error. if ($usesBlockMode && isset($directiveStrings['strict_types'])) { - $phpcsFile->addError( - 'strict_types declaration must not use block mode.', - $stackPtr, - 'Forbidden' - ); + $error = 'strict_types declaration must not use block mode.'; + $code = 'Forbidden'; + + if (isset($tokens[$stackPtr]['scope_closer'])) { + // If there is no scope closer, we cannot auto-fix. + $phpcsFile->addError($error, $stackPtr, $code); + return; + } + + $fix = $phpcsFile->addFixableError($error, $stackPtr, $code); + + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->addContent($closeParenPtr, ';'); + $phpcsFile->fixer->replaceToken($tokens[$stackPtr]['scope_opener'], ''); + + // Remove potential whitespace between parenthesis closer and the brace. + for ($i = ($tokens[$stackPtr]['scope_opener'] - 1); $i > 0; $i--) { + if ($tokens[$i]['code'] !== \T_WHITESPACE) { + break; + } + + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->replaceToken($tokens[$stackPtr]['scope_closer'], ''); + $phpcsFile->fixer->endChangeset(); + } return; } - // Check if there is a code between the declare statement and opening brace/alternative syntax. - $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($closeParenPtr + 1), null, true); - $directiveCloserTokens = [\T_SEMICOLON, \T_CLOSE_TAG, \T_OPEN_CURLY_BRACKET, \T_COLON]; - - if (!in_array($tokens[$nextNonEmpty]['code'], $directiveCloserTokens, true)) { + // Check if there is code between the declare statement and opening brace/alternative syntax. + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($closeParenPtr + 1), null, true); + if ($tokens[$nextNonEmpty]['code'] !== \T_SEMICOLON + && $tokens[$nextNonEmpty]['code'] !== \T_CLOSE_TAG + && $tokens[$nextNonEmpty]['code'] !== \T_OPEN_CURLY_BRACKET + && $tokens[$nextNonEmpty]['code'] !== \T_COLON + ) { $phpcsFile->addError( - 'Unexpected code found after opening the declare statement without closing it.', + 'Unexpected code found after the declare statement.', $stackPtr, 'UnexpectedCodeFound' ); return; } - foreach (\array_keys($directiveStrings) as $directiveString) { - if (isset($this->allowedDirectives[$directiveString])) { - $phpcsFile->recordMetric($stackPtr, self::DECLARE_TYPE_METRIC, $directiveString); - } - } - // Multiple directives - if one requires block mode usage, other has to as well. if (count($directiveStrings) > 1 && (($this->encodingBlockMode === 'disallow' && $this->ticksBlockMode !== 'disallow') @@ -205,7 +224,7 @@ public function process(File $phpcsFile, $stackPtr) $phpcsFile->addError( 'Multiple directives found, but one of them is disallowing the use of block mode.', $stackPtr, - 'Forbidden' + 'Forbidden' // <= Duplicate error code for different message (line 175) ); return; } diff --git a/Universal/Tests/DeclareStatements/BlockModeUnitTest.1.inc b/Universal/Tests/DeclareStatements/BlockModeUnitTest.1.inc index c97e8e79..0a463018 100644 --- a/Universal/Tests/DeclareStatements/BlockModeUnitTest.1.inc +++ b/Universal/Tests/DeclareStatements/BlockModeUnitTest.1.inc @@ -76,7 +76,3 @@ echo 'hi!'; } declare ticks=1; // Intentional live coding error. Ignore. - -/* Rest to the default directives for the next run */ -// phpcs:set Universal.DeclareStatements.BlockMode encodingBlockMode disallow -// phpcs:set Universal.DeclareStatements.BlockMode ticksBlockMode allow diff --git a/Universal/Tests/DeclareStatements/BlockModeUnitTest.2.inc b/Universal/Tests/DeclareStatements/BlockModeUnitTest.2.inc index 484db4c1..4503293b 100644 --- a/Universal/Tests/DeclareStatements/BlockModeUnitTest.2.inc +++ b/Universal/Tests/DeclareStatements/BlockModeUnitTest.2.inc @@ -7,6 +7,4 @@ declare(enCodINg='1'); declare(STRICT_TYPES=1); declare(random_directive=false); -/* Rest to the default directives for the next run */ -// phpcs:set Universal.DeclareStatements.BlockMode encodingBlockMode disallow -// phpcs:set Universal.DeclareStatements.BlockMode ticksBlockMode allow +// What's invalid about these ? (safe for line 4 and 8) Directives are case-INsensitive. diff --git a/Universal/Tests/DeclareStatements/BlockModeUnitTest.3.inc b/Universal/Tests/DeclareStatements/BlockModeUnitTest.3.inc index 3b1ec74e..c0d34718 100644 --- a/Universal/Tests/DeclareStatements/BlockModeUnitTest.3.inc +++ b/Universal/Tests/DeclareStatements/BlockModeUnitTest.3.inc @@ -45,6 +45,6 @@ declare(strict_types=1, encoding='ISO-8859-1'): // Error. enddeclare; enddeclare; -/* Rest to the default directives for the next run */ +/* Reset to the default directives for the next run */ // phpcs:set Universal.DeclareStatements.BlockMode encodingBlockMode disallow // phpcs:set Universal.DeclareStatements.BlockMode ticksBlockMode allow diff --git a/Universal/Tests/DeclareStatements/BlockModeUnitTest.4.inc b/Universal/Tests/DeclareStatements/BlockModeUnitTest.4.inc index 2939177d..845696d2 100644 --- a/Universal/Tests/DeclareStatements/BlockModeUnitTest.4.inc +++ b/Universal/Tests/DeclareStatements/BlockModeUnitTest.4.inc @@ -203,6 +203,6 @@ declare(ticks=1): // Ok. enddeclare; enddeclare; -/* Rest to the default directives for the next run */ +/* Reset to the default directives for the next run */ // phpcs:set Universal.DeclareStatements.BlockMode encodingBlockMode disallow // phpcs:set Universal.DeclareStatements.BlockMode ticksBlockMode allow diff --git a/Universal/Tests/DeclareStatements/BlockModeUnitTest.5.inc b/Universal/Tests/DeclareStatements/BlockModeUnitTest.5.inc index 4c4ed66b..2a8d2716 100644 --- a/Universal/Tests/DeclareStatements/BlockModeUnitTest.5.inc +++ b/Universal/Tests/DeclareStatements/BlockModeUnitTest.5.inc @@ -296,6 +296,6 @@ declare(ticks=1) { // Ok. // Code. } -/* Rest to the default directives for the next run */ +/* Reset to the default directives for the next run */ // phpcs:set Universal.DeclareStatements.BlockMode encodingBlockMode disallow // phpcs:set Universal.DeclareStatements.BlockMode ticksBlockMode allow From 9a6284e53cc3003e6927b729457097ea0bf40186 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 24 Apr 2022 06:04:00 +0200 Subject: [PATCH 010/125] :sparkles: New `Universal.CodeAnalysis.StaticInFinalClass` sniff New sniff to detect using `static` instead of `self` in OO constructs which are `final`. Includes fixer. Includes unit tests. Includes documentation. --- .../StaticInFinalClassStandard.xml | 40 ++++ .../CodeAnalysis/StaticInFinalClassSniff.php | 205 ++++++++++++++++++ .../StaticInFinalClassUnitTest.inc | 171 +++++++++++++++ .../StaticInFinalClassUnitTest.inc.fixed | 171 +++++++++++++++ .../StaticInFinalClassUnitTest.php | 65 ++++++ 5 files changed, 652 insertions(+) create mode 100644 Universal/Docs/CodeAnalysis/StaticInFinalClassStandard.xml create mode 100644 Universal/Sniffs/CodeAnalysis/StaticInFinalClassSniff.php create mode 100644 Universal/Tests/CodeAnalysis/StaticInFinalClassUnitTest.inc create mode 100644 Universal/Tests/CodeAnalysis/StaticInFinalClassUnitTest.inc.fixed create mode 100644 Universal/Tests/CodeAnalysis/StaticInFinalClassUnitTest.php diff --git a/Universal/Docs/CodeAnalysis/StaticInFinalClassStandard.xml b/Universal/Docs/CodeAnalysis/StaticInFinalClassStandard.xml new file mode 100644 index 00000000..c52604fd --- /dev/null +++ b/Universal/Docs/CodeAnalysis/StaticInFinalClassStandard.xml @@ -0,0 +1,40 @@ + + + + + + + + self + { + $var = self::functionCall(); + $var = $obj instanceof self; + $var = new self; + } +} + ]]> + + + static|false { + $var = static::$prop; + $var = $obj instanceof static; + $var = new static(); + } +}; + ]]> + + + diff --git a/Universal/Sniffs/CodeAnalysis/StaticInFinalClassSniff.php b/Universal/Sniffs/CodeAnalysis/StaticInFinalClassSniff.php new file mode 100644 index 00000000..b46fff5c --- /dev/null +++ b/Universal/Sniffs/CodeAnalysis/StaticInFinalClassSniff.php @@ -0,0 +1,205 @@ +getTokens(); + + if ($tokens[$stackPtr]['code'] === \T_STRING + && \strtolower($tokens[$stackPtr]['content']) !== 'static' + ) { + return; + } + + if ($tokens[$stackPtr]['code'] === \T_FUNCTION) { + /* + * Check return types for methods in final classes, anon classes and enums. + * + * Will return the scope opener of the function to prevent potential duplicate notifications. + */ + $scopeOpener = $stackPtr; + if (isset($tokens[$stackPtr]['scope_opener']) === true) { + $scopeOpener = $tokens[$stackPtr]['scope_opener']; + } + + $ooPtr = Scopes::validDirectScope($phpcsFile, $stackPtr, $this->validOOScopes); + if ($ooPtr === false) { + // Method in a trait (not known where it is used), interface (never final) or not in an OO scope. + return $scopeOpener; + } + + if ($tokens[$ooPtr]['code'] === \T_CLASS) { + $classProps = ObjectDeclarations::getClassProperties($phpcsFile, $ooPtr); + if ($classProps['is_final'] === false) { + // Method in a non-final class. + return $scopeOpener; + } + } + + $functionProps = FunctionDeclarations::getProperties($phpcsFile, $stackPtr); + if ($functionProps['return_type'] === '') { + return $scopeOpener; + } + + $staticPtr = $phpcsFile->findNext( + \T_STATIC, + $functionProps['return_type_token'], + ($functionProps['return_type_end_token'] + 1) + ); + + if ($staticPtr === false) { + return $scopeOpener; + } + + // Found a return type containing the `static` type. + $this->handleError($phpcsFile, $staticPtr, 'ReturnType', '"static" return type'); + + return $scopeOpener; + } + + /* + * Check other uses of static. + */ + $functionPtr = Conditions::getLastCondition($phpcsFile, $stackPtr, [\T_FUNCTION, \T_CLOSURE]); + if ($functionPtr === false || $tokens[$functionPtr]['code'] === \T_CLOSURE) { + /* + * When `false`, this code is absolutely invalid, but not something to be addressed via this sniff. + * When a closure, we're not interested in it. The closure class is final, but closures + * can be bound to other classes. This needs further research and should maybe get its own sniff. + */ + return; + } + + $ooPtr = Scopes::validDirectScope($phpcsFile, $functionPtr, $this->validOOScopes); + if ($ooPtr === false) { + // Not in an OO context. + return; + } + + if ($tokens[$ooPtr]['code'] === \T_CLASS) { + $classProps = ObjectDeclarations::getClassProperties($phpcsFile, $ooPtr); + if ($classProps['is_final'] === false) { + // Token in a non-final class. + return; + } + } + + $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + if ($prevNonEmpty !== false) { + if ($tokens[$prevNonEmpty]['code'] === \T_INSTANCEOF) { + $prevPrevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevNonEmpty - 1), null, true); + $extraMsg = GetTokensAsString::compact($phpcsFile, $prevPrevNonEmpty, $stackPtr, true); + $this->handleError($phpcsFile, $stackPtr, 'InstanceOf', '"' . $extraMsg . '"'); + return; + } + + if ($tokens[$prevNonEmpty]['code'] === \T_NEW) { + $this->handleError($phpcsFile, $stackPtr, 'NewInstance', '"new static"'); + return; + } + } + + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if ($nextNonEmpty !== false && $tokens[$nextNonEmpty]['code'] === \T_DOUBLE_COLON) { + $nextNextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), null, true); + $extraMsg = GetTokensAsString::compact($phpcsFile, $stackPtr, $nextNextNonEmpty, true); + $this->handleError($phpcsFile, $stackPtr, 'ScopeResolution', '"' . $extraMsg . '"'); + return; + } + } + + /** + * Throw and potentially fix the error. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of erroneous `T_STATIC` token. + * @param string $errorCode The error code for the message. + * @param string $extraMsg Addition to the error message. + * + * @return void + */ + private function handleError($phpcsFile, $stackPtr, $errorCode, $extraMsg) + { + $fix = $phpcsFile->addFixableError( + 'Use "self" instead of "static" when using late static binding in a final OO construct. Found: %s', + $stackPtr, + $errorCode, + [$extraMsg] + ); + + if ($fix === true) { + $phpcsFile->fixer->replaceToken($stackPtr, 'self'); + } + } +} diff --git a/Universal/Tests/CodeAnalysis/StaticInFinalClassUnitTest.inc b/Universal/Tests/CodeAnalysis/StaticInFinalClassUnitTest.inc new file mode 100644 index 00000000..2dbef3e6 --- /dev/null +++ b/Universal/Tests/CodeAnalysis/StaticInFinalClassUnitTest.inc @@ -0,0 +1,171 @@ + => + */ + public function getErrorList() + { + return [ + 105 => 1, + 107 => 1, + 108 => 1, + 110 => 1, + 112 => 1, + 119 => 1, + 121 => 1, + 122 => 1, + 127 => 1, + 129 => 1, + 138 => 1, + 140 => 1, + 141 => 1, + 146 => 1, + 148 => 1, + 155 => 1, + 156 => 1, + 157 => 1, + 159 => 1, + 161 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return []; + } +} From e573d36977097944fdb9ea612a4ad8dd079325b0 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 6 May 2022 15:41:48 +0200 Subject: [PATCH 011/125] :sparkles: New `Universal.Operators.TypeSeparatorSpacing` sniff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New sniff to enforce no spaces around union type and intersection type separator operators. Includes fixer. Includes unit tests. Includes documentation. Includes metrics. Co-authored-by: Denis Žoljom --- .../TypeSeparatorSpacingStandard.xml | 30 +++++++ .../Operators/TypeSeparatorSpacingSniff.php | 85 +++++++++++++++++++ .../TypeSeparatorSpacingUnitTest.inc | 41 +++++++++ .../TypeSeparatorSpacingUnitTest.inc.fixed | 37 ++++++++ .../TypeSeparatorSpacingUnitTest.php | 53 ++++++++++++ 5 files changed, 246 insertions(+) create mode 100644 Universal/Docs/Operators/TypeSeparatorSpacingStandard.xml create mode 100644 Universal/Sniffs/Operators/TypeSeparatorSpacingSniff.php create mode 100644 Universal/Tests/Operators/TypeSeparatorSpacingUnitTest.inc create mode 100644 Universal/Tests/Operators/TypeSeparatorSpacingUnitTest.inc.fixed create mode 100644 Universal/Tests/Operators/TypeSeparatorSpacingUnitTest.php diff --git a/Universal/Docs/Operators/TypeSeparatorSpacingStandard.xml b/Universal/Docs/Operators/TypeSeparatorSpacingStandard.xml new file mode 100644 index 00000000..11462e7d --- /dev/null +++ b/Universal/Docs/Operators/TypeSeparatorSpacingStandard.xml @@ -0,0 +1,30 @@ + + + + + + + + |string $paramA, + TypeA&TypeB $paramB +): int|false {} + ]]> + + + | string $paramA, + TypeA & TypeB $paramB +): int + | + false {} + ]]> + + + diff --git a/Universal/Sniffs/Operators/TypeSeparatorSpacingSniff.php b/Universal/Sniffs/Operators/TypeSeparatorSpacingSniff.php new file mode 100644 index 00000000..b5f9d18c --- /dev/null +++ b/Universal/Sniffs/Operators/TypeSeparatorSpacingSniff.php @@ -0,0 +1,85 @@ +getTokens(); + + $type = ($tokens[$stackPtr]['code'] === \T_TYPE_UNION) ? 'union' : 'intersection'; + $code = \ucfirst($type) . 'Type'; + + $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + SpacesFixer::checkAndFix( + $phpcsFile, + $stackPtr, + $prevNonEmpty, + 0, // Expected spaces. + 'Expected %s before the ' . $type . ' type separator. Found: %s', + $code . 'SpacesBefore', + 'error', + 0, + 'Space before ' . $type . ' type separator' + ); + + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + SpacesFixer::checkAndFix( + $phpcsFile, + $stackPtr, + $nextNonEmpty, + 0, // Expected spaces. + 'Expected %s after the ' . $type . ' type separator. Found: %s', + $code . 'SpacesAfter', + 'error', + 0, + 'Space after ' . $type . ' type separator' + ); + } +} diff --git a/Universal/Tests/Operators/TypeSeparatorSpacingUnitTest.inc b/Universal/Tests/Operators/TypeSeparatorSpacingUnitTest.inc new file mode 100644 index 00000000..530a14a5 --- /dev/null +++ b/Universal/Tests/Operators/TypeSeparatorSpacingUnitTest.inc @@ -0,0 +1,41 @@ + $p * $q; + +/* + * Incorrect spacing. + */ +class IncorrectSpacing { + + public int | bool | + null $property; + + protected function foo(int | bool $paramA, Foo + & + Bar $paramB): InterFaceName /*comment*/ | false /*comment */ & InvalidIntersection {} +} + +function namedFunction(?string | null &...$p, TypeA& namespace\TypeB $q): string + |int {}; + +$closure = function (TypeA | TypeB $p, TypeA &namespace\TypeB $q): \Fully\Qualified & Partially\Qualified {}; + +$arrow = fn (TypeA | TypeB $p, TypeA & namespace\TypeB $q): string | int => $p * $q; + +// Live coding. +// This test should be the last test in the file. +function foo(int| diff --git a/Universal/Tests/Operators/TypeSeparatorSpacingUnitTest.inc.fixed b/Universal/Tests/Operators/TypeSeparatorSpacingUnitTest.inc.fixed new file mode 100644 index 00000000..a1ec0ebb --- /dev/null +++ b/Universal/Tests/Operators/TypeSeparatorSpacingUnitTest.inc.fixed @@ -0,0 +1,37 @@ + $p * $q; + +/* + * Incorrect spacing. + */ +class IncorrectSpacing { + + public int|bool|null $property; + + protected function foo(int|bool $paramA, Foo&Bar $paramB): InterFaceName /*comment*/ |false /*comment */ &InvalidIntersection {} +} + +function namedFunction(?string|null &...$p, TypeA&namespace\TypeB $q): string|int {}; + +$closure = function (TypeA|TypeB $p, TypeA&namespace\TypeB $q): \Fully\Qualified&Partially\Qualified {}; + +$arrow = fn (TypeA|TypeB $p, TypeA&namespace\TypeB $q): string|int => $p * $q; + +// Live coding. +// This test should be the last test in the file. +function foo(int| diff --git a/Universal/Tests/Operators/TypeSeparatorSpacingUnitTest.php b/Universal/Tests/Operators/TypeSeparatorSpacingUnitTest.php new file mode 100644 index 00000000..29427848 --- /dev/null +++ b/Universal/Tests/Operators/TypeSeparatorSpacingUnitTest.php @@ -0,0 +1,53 @@ + + */ + public function getErrorList() + { + return [ + 24 => 4, + 27 => 2, + 28 => 2, + 29 => 4, + 32 => 3, + 33 => 1, + 35 => 5, + 37 => 6, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array + */ + public function getWarningList() + { + return []; + } +} From c1f843685b4e11169b83b3224bd3538b5f69907c Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 1 Jul 2022 14:16:10 +0200 Subject: [PATCH 012/125] NormalizedArrays/ArrayBraceSpacing: allow for trailing comments after array opener ... when a new line is required after the opener of a multi-line array. This sniff should not have an opinion on whether or not trailing comments are allowed. There are other sniffs around to forbid those, if so desired. Includes extra unit tests to safeguard this change and to safeguard handling of comments after the array opener regardless of this change. --- .../Sniffs/Arrays/ArrayBraceSpacingSniff.php | 18 +++- .../Arrays/ArrayBraceSpacingUnitTest.inc | 96 +++++++++++++++++- .../ArrayBraceSpacingUnitTest.inc.fixed | 98 ++++++++++++++++++- .../Arrays/ArrayBraceSpacingUnitTest.php | 26 +++-- 4 files changed, 225 insertions(+), 13 deletions(-) diff --git a/NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php b/NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php index bc0f0c5e..9fab7a8f 100644 --- a/NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php +++ b/NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php @@ -12,6 +12,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Tokens; use PHPCSUtils\Fixers\SpacesFixer; use PHPCSUtils\Utils\Arrays; @@ -156,7 +157,7 @@ public function process(File $phpcsFile, $stackPtr) $openClose = Arrays::getOpenClose($phpcsFile, $stackPtr); if ($openClose === false) { - // Short list or real square brackets. + // Live coding, short list or real square brackets. return; } @@ -264,10 +265,23 @@ public function process(File $phpcsFile, $stackPtr) $error = 'Expected %s after the array opener in a multi line array. Found: %s'; $code = 'SpaceAfterArrayOpenerMultiLine'; + $nextNonWhitespace = $phpcsFile->findNext(\T_WHITESPACE, ($opener + 1), null, true); + if ($this->spacesMultiLine === 'newline') { + // Check for a trailing comment after the array opener and allow for it. + if (($tokens[$nextNonWhitespace]['code'] === \T_COMMENT + || isset(Tokens::$phpcsCommentTokens[$tokens[$nextNonWhitespace]['code']]) === true) + && $tokens[$nextNonWhitespace]['line'] === $tokens[$opener]['line'] + ) { + // We found a trailing comment after array opener. Treat that as the opener instead. + $opener = $nextNonWhitespace; + $nextNonWhitespace = $phpcsFile->findNext(\T_WHITESPACE, ($opener + 1), null, true); + } + } + SpacesFixer::checkAndFix( $phpcsFile, $opener, - $phpcsFile->findNext(\T_WHITESPACE, ($opener + 1), null, true), + $nextNonWhitespace, $this->spacesMultiLine, $error, $code, diff --git a/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc b/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc index 61a3dd1b..bd79198a 100644 --- a/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc +++ b/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc @@ -134,12 +134,58 @@ $array = [ $a, - $b]; // Error x 2. + $b]; // Error x 1. $array = array($a, $b ); // Error x 1. +$array = array( + // Comment at the first line is always fine. + $a, + $b +); + +$array = array( // Allow trailing comment after opener. + $a, + $b +); + +$array = array( /* Allow trailing comment after opener. */ + $a, + $b +); + +$array = array( // phpcs:ignore Stdn.Cat.Sniff -- Allow trailing annotation after opener. + $a, + $b +); + +$array = array( /* Allow trailing comment after opener. */ /* But don't allow a second one */ + $a, + $b +); // Error x 1. + +$array = array( /* This is still an error */ $a, + $b +); // Error x 1. + +$array = [ // Allow trailing comment after opener. + + + + $a, + $b +]; + +$array = [ /* Allow trailing comment after opener */ + + + + $a, + $b +]; + // phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesMultiLine 0 $array = [$a, @@ -163,6 +209,30 @@ $array = array($a, $b ); // Error x 1. +$array = array( // Comment. Error x 1. + $a, + $b); + +$array = array( /* Comment. Error x 1. */ + $a, + $b); + +$array = array( // phpcs:ignore Stdn.Cat.Sniff -- Error x 1. + $a, + $b); + +$array = array(// Comment. No space, this is fine. + $a, + $b); + +$array = array(/* Comment. No space, this is fine. */ + $a, + $b); + +$array = array(// phpcs:ignore Stdn.Cat.Sniff -- No space, this is fine. + $a, + $b); + // phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesMultiLine 1 $array = [ $a, @@ -186,6 +256,30 @@ $array = array($a, $b ); // Error x 2. +$array = array( // Comment. One space, this is fine. + $a, + $b ); + +$array = array( /* Comment. One space, this is fine. */ + $a, + $b ); + +$array = array( // phpcs:ignore Stdn.Cat.Sniff -- One space, this is fine. + $a, + $b ); + +$array = array(// Comment. No space, error x 1. + $a, + $b ); + +$array = array(/* Comment. No space, error x 1. */ + $a, + $b ); + +$array = array(// phpcs:ignore Stdn.Cat.Sniff -- No space, error x 1. + $a, + $b ); + // phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesMultiLine false $array = array( diff --git a/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc.fixed b/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc.fixed index 583ac9f1..4445012b 100644 --- a/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc.fixed +++ b/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc.fixed @@ -135,13 +135,61 @@ $array = [ $a, $b -]; // Error x 2. +]; // Error x 1. $array = array( $a, $b ); // Error x 1. +$array = array( + // Comment at the first line is always fine. + $a, + $b +); + +$array = array( // Allow trailing comment after opener. + $a, + $b +); + +$array = array( /* Allow trailing comment after opener. */ + $a, + $b +); + +$array = array( // phpcs:ignore Stdn.Cat.Sniff -- Allow trailing annotation after opener. + $a, + $b +); + +$array = array( /* Allow trailing comment after opener. */ +/* But don't allow a second one */ + $a, + $b +); // Error x 1. + +$array = array( /* This is still an error */ +$a, + $b +); // Error x 1. + +$array = [ // Allow trailing comment after opener. + + + + $a, + $b +]; + +$array = [ /* Allow trailing comment after opener */ + + + + $a, + $b +]; + // phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesMultiLine 0 $array = [$a, @@ -158,6 +206,30 @@ $array = [$a, $array = array($a, $b); // Error x 1. +$array = array(// Comment. Error x 1. + $a, + $b); + +$array = array(/* Comment. Error x 1. */ + $a, + $b); + +$array = array(// phpcs:ignore Stdn.Cat.Sniff -- Error x 1. + $a, + $b); + +$array = array(// Comment. No space, this is fine. + $a, + $b); + +$array = array(/* Comment. No space, this is fine. */ + $a, + $b); + +$array = array(// phpcs:ignore Stdn.Cat.Sniff -- No space, this is fine. + $a, + $b); + // phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesMultiLine 1 $array = [ $a, @@ -174,6 +246,30 @@ $array = [ $a, $array = array( $a, $b ); // Error x 2. +$array = array( // Comment. One space, this is fine. + $a, + $b ); + +$array = array( /* Comment. One space, this is fine. */ + $a, + $b ); + +$array = array( // phpcs:ignore Stdn.Cat.Sniff -- One space, this is fine. + $a, + $b ); + +$array = array( // Comment. No space, error x 1. + $a, + $b ); + +$array = array( /* Comment. No space, error x 1. */ + $a, + $b ); + +$array = array( // phpcs:ignore Stdn.Cat.Sniff -- No space, error x 1. + $a, + $b ); + // phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesMultiLine false $array = array( diff --git a/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.php b/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.php index 5f325807..d542a685 100644 --- a/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.php +++ b/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.php @@ -58,16 +58,24 @@ public function getErrorList() 130 => 1, 137 => 1, 139 => 1, - 150 => 1, - 153 => 1, - 155 => 1, 164 => 1, - 173 => 1, - 176 => 1, - 178 => 1, - 183 => 1, - 185 => 1, - 187 => 1, + 169 => 1, + 196 => 1, + 199 => 1, + 201 => 1, + 210 => 1, + 212 => 1, + 216 => 1, + 220 => 1, + 243 => 1, + 246 => 1, + 248 => 1, + 253 => 1, + 255 => 1, + 257 => 1, + 271 => 1, + 275 => 1, + 279 => 1, ]; } From 772e7d80a7f81b6c18549406d57aecfdcf1af085 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 20 Jul 2022 03:15:08 +0200 Subject: [PATCH 013/125] :sparkles: New `Universal.WhiteSpace.PrecisionAlignment` sniff New sniff to enforce indentation to always be a multiple of a tabstop, i.e. disallow precision alignment. For space-based standards, the `Generic.WhiteSpace.DisallowTabIndent` sniff (unintentionally/incorrectly) already removes precision alignment. For tab-based standards, the `Generic.WhiteSpace.DisallowSpaceIndent` sniff, however, does not remove precision alignment. With that in mind, this sniff is mostly intended for standards which demand tabs for indentation. In rare cases, spaces for precision alignment can be intentional and acceptable, but more often than not, precision alignment is a typo. Notes: * This sniff does not concern itself with tabs versus spaces. Use the PHPCS native `Generic.WhiteSpace.DisallowTabIndent` or the `Generic.WhiteSpace.DisallowSpaceIndent` sniffs for that. * When using this sniff with tab-based standards, please ensure that the `tab-width` is set and either don't set the `$indent` property or set it to the tab-width (or a multiple thereof). * Precision alignment *within* text strings (multi-line text strings, heredocs, nowdocs) will NOT be flagged by this sniff. * Precision alignment in (trailing) whitespace on otherwise blank lines will not be flagged by default. Most standards will automatically remove trailing whitespace anyway using the `Squiz.WhiteSpace.SuperfluousWhitespace` sniff. If the Squiz sniff is not used, set the `ignoreBlankLines` property to `false` to enable reporting for this. * The sniff also supports an option to ignore precision alignment in specific situations, like for multi-line chained method calls. **Note**: the fixer works based on "best guess" and may not always result in the desired indentation. Combine this sniff with the `Generic.WhiteSpace.ScopeIndent` sniff for more precise indentation fixes. If so desired, the fixer can be turned off by including the rule in a standard like so: `` As it is, the sniff supports both PHP, as well as JS and CSS, though the support for JS and CSS should be considered incidental and will be removed once PHPCS 4.0 will be released. Includes fixer. Includes unit tests. Includes documentation. --- .../WhiteSpace/PrecisionAlignmentStandard.xml | 26 ++ .../WhiteSpace/PrecisionAlignmentSniff.php | 381 ++++++++++++++++++ .../PrecisionAlignmentUnitTest.1.css | 6 + .../PrecisionAlignmentUnitTest.1.css.fixed | 6 + .../PrecisionAlignmentUnitTest.1.inc | 84 ++++ .../PrecisionAlignmentUnitTest.1.inc.fixed | 84 ++++ .../PrecisionAlignmentUnitTest.1.js | 8 + .../PrecisionAlignmentUnitTest.1.js.fixed | 8 + .../PrecisionAlignmentUnitTest.10.inc | 6 + .../PrecisionAlignmentUnitTest.10.inc.fixed | 6 + .../PrecisionAlignmentUnitTest.11.inc | 60 +++ .../PrecisionAlignmentUnitTest.11.inc.fixed | 60 +++ .../PrecisionAlignmentUnitTest.12.inc | 83 ++++ .../PrecisionAlignmentUnitTest.12.inc.fixed | 83 ++++ .../PrecisionAlignmentUnitTest.13.inc | 84 ++++ .../PrecisionAlignmentUnitTest.13.inc.fixed | 84 ++++ .../PrecisionAlignmentUnitTest.14.inc | 86 ++++ .../PrecisionAlignmentUnitTest.14.inc.fixed | 86 ++++ .../PrecisionAlignmentUnitTest.15.inc | 86 ++++ .../PrecisionAlignmentUnitTest.15.inc.fixed | 86 ++++ .../PrecisionAlignmentUnitTest.16.inc | 86 ++++ .../PrecisionAlignmentUnitTest.16.inc.fixed | 86 ++++ .../PrecisionAlignmentUnitTest.17.inc | 86 ++++ .../PrecisionAlignmentUnitTest.17.inc.fixed | 86 ++++ .../PrecisionAlignmentUnitTest.18.inc | 5 + .../PrecisionAlignmentUnitTest.18.inc.fixed | 5 + .../PrecisionAlignmentUnitTest.19a.inc | 15 + .../PrecisionAlignmentUnitTest.19b.inc | 15 + .../PrecisionAlignmentUnitTest.19c.inc | 17 + .../PrecisionAlignmentUnitTest.2.css | 6 + .../PrecisionAlignmentUnitTest.2.css.fixed | 6 + .../PrecisionAlignmentUnitTest.2.inc | 86 ++++ .../PrecisionAlignmentUnitTest.2.inc.fixed | 86 ++++ .../PrecisionAlignmentUnitTest.2.js | 8 + .../PrecisionAlignmentUnitTest.2.js.fixed | 8 + .../PrecisionAlignmentUnitTest.3.inc | 86 ++++ .../PrecisionAlignmentUnitTest.3.inc.fixed | 86 ++++ .../PrecisionAlignmentUnitTest.4.inc | 86 ++++ .../PrecisionAlignmentUnitTest.4.inc.fixed | 86 ++++ .../PrecisionAlignmentUnitTest.5.inc | 84 ++++ .../PrecisionAlignmentUnitTest.5.inc.fixed | 84 ++++ .../PrecisionAlignmentUnitTest.6.inc | 86 ++++ .../PrecisionAlignmentUnitTest.6.inc.fixed | 86 ++++ .../PrecisionAlignmentUnitTest.7.inc | 86 ++++ .../PrecisionAlignmentUnitTest.7.inc.fixed | 86 ++++ .../PrecisionAlignmentUnitTest.8.inc | 86 ++++ .../PrecisionAlignmentUnitTest.8.inc.fixed | 86 ++++ .../PrecisionAlignmentUnitTest.9.inc | 6 + .../PrecisionAlignmentUnitTest.9.inc.fixed | 6 + .../WhiteSpace/PrecisionAlignmentUnitTest.php | 214 ++++++++++ 50 files changed, 3268 insertions(+) create mode 100644 Universal/Docs/WhiteSpace/PrecisionAlignmentStandard.xml create mode 100644 Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.1.css create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.1.css.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.1.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.1.inc.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.1.js create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.1.js.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.10.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.10.inc.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.11.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.11.inc.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.12.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.12.inc.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.13.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.13.inc.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.14.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.14.inc.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.15.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.15.inc.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.16.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.16.inc.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.17.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.17.inc.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.18.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.18.inc.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.19a.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.19b.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.19c.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.css create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.css.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.inc.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.js create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.js.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.3.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.3.inc.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.4.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.4.inc.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.5.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.5.inc.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.6.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.6.inc.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.7.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.7.inc.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.8.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.8.inc.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.9.inc create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.9.inc.fixed create mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.php diff --git a/Universal/Docs/WhiteSpace/PrecisionAlignmentStandard.xml b/Universal/Docs/WhiteSpace/PrecisionAlignmentStandard.xml new file mode 100644 index 00000000..a615f0f7 --- /dev/null +++ b/Universal/Docs/WhiteSpace/PrecisionAlignmentStandard.xml @@ -0,0 +1,26 @@ + + + + + + + + [space][space][space][space]$foo = 'bar'; + +[tab]$foo = 'bar'; + ]]> + + + [space][space]$foo = 'bar'; + +[tab][space]$foo = 'bar'; + ]]> + + + diff --git a/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php b/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php new file mode 100644 index 00000000..2a4a4ab1 --- /dev/null +++ b/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php @@ -0,0 +1,381 @@ + + * + * + * + * + * + * + * + * + * + * ``` + * + * @since 1.0.0 + * + * @var array + */ + public $ignoreAlignmentBefore = []; + + /** + * Whether or not potential trailing whitespace on otherwise blank lines should be examined or ignored. + * + * Defaults to `true`, i.e. ignore blank lines. + * + * It is recommended to only set this to `false` if the standard including this sniff does not + * include the `Squiz.WhiteSpace.SuperfluousWhitespace` sniff (which is included in most standards). + * + * @since 1.0.0 + * + * @var bool + */ + public $ignoreBlankLines = true; + + /** + * The --tab-width CLI value that is being used. + * + * @since 1.0.0 + * + * @var int + */ + private $tabWidth; + + /** + * Whitespace tokens and tokens which can contain leading whitespace. + * + * A few additional tokens will be added to this list in the register() method. + * + * @since 1.0.0 + * + * @var array + */ + private $tokensToCheck = [ + \T_WHITESPACE => \T_WHITESPACE, + \T_INLINE_HTML => \T_INLINE_HTML, + \T_DOC_COMMENT_WHITESPACE => \T_DOC_COMMENT_WHITESPACE, + \T_COMMENT => \T_COMMENT, + \T_END_HEREDOC => \T_END_HEREDOC, + \T_END_NOWDOC => \T_END_NOWDOC, + ]; + + /** + * Returns an array of tokens this test wants to listen for. + * + * @since 1.0.0 + * + * @return array + */ + public function register() + { + // Add the ignore annotation tokens to the list of tokens to check. + $this->tokensToCheck += Tokens::$phpcsCommentTokens; + + return [ + \T_OPEN_TAG, + \T_OPEN_TAG_WITH_ECHO, + ]; + } + + /** + * Processes this test, when one of its tokens is encountered. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. + * + * @return int Integer stack pointer to skip the rest of the file. + */ + public function process(File $phpcsFile, $stackPtr) + { + /* + * Handle the properties. + */ + if (isset($this->tabWidth) === false || \defined('PHP_CODESNIFFER_IN_TESTS') === true) { + $this->tabWidth = Helper::getTabWidth($phpcsFile); + } + + if (isset($this->indent) === true) { + $indent = (int) $this->indent; + } else { + $indent = $this->tabWidth; + } + + $ignoreTokens = (array) $this->ignoreAlignmentBefore; + if (empty($ignoreTokens) === false) { + $ignoreTokens = \array_flip($ignoreTokens); + } + + /* + * Check the whole file in one go. + */ + $tokens = $phpcsFile->getTokens(); + + for ($i = 0; $i < $phpcsFile->numTokens; $i++) { + if ($tokens[$i]['column'] !== 1) { + // Only interested in the first token on each line. + continue; + } + + if (isset($this->tokensToCheck[$tokens[$i]['code']]) === false) { + // Not one of the target tokens. + continue; + } + + if ($tokens[$i]['content'] === $phpcsFile->eolChar) { + // Skip completely blank lines. + continue; + } + + if (isset($ignoreTokens[$tokens[$i]['type']]) === true + || (isset($tokens[($i + 1)]) && isset($ignoreTokens[$tokens[($i + 1)]['type']])) + ) { + // This is one of the tokens being ignored. + continue; + } + + $spaces = 0; + switch ($tokens[$i]['code']) { + case \T_WHITESPACE: + if ($this->ignoreBlankLines === true + && isset($tokens[($i + 1)]) + && $tokens[$i]['line'] !== $tokens[($i + 1)]['line'] + ) { + // Skip blank lines which only contain trailing whitespace. + continue 2; + } + + $spaces = ($tokens[$i]['length'] % $indent); + break; + + case \T_DOC_COMMENT_WHITESPACE: + /* + * Blank lines with trailing whitespace in docblocks are tokenized as + * two T_DOC_COMMENT_WHITESPACE tokens: one for the trailing whitespace, + * one for the new line character. + */ + if ($this->ignoreBlankLines === true + && isset($tokens[($i + 1)]) + && $tokens[($i + 1)]['content'] === $phpcsFile->eolChar + && isset($tokens[($i + 2)]) + && $tokens[$i]['line'] !== $tokens[($i + 2)]['line'] + ) { + // Skip blank lines which only contain trailing whitespace. + continue 2; + } + + $spaces = ($tokens[$i]['length'] % $indent); + + if (isset($tokens[($i + 1)]) === true + && ($tokens[($i + 1)]['code'] === \T_DOC_COMMENT_STAR + || $tokens[($i + 1)]['code'] === \T_DOC_COMMENT_CLOSE_TAG) + && $spaces !== 0 + ) { + // One alignment space expected before the *. + --$spaces; + } + break; + + case \T_COMMENT: + case \T_INLINE_HTML: + if ($this->ignoreBlankLines === true + && \trim($tokens[$i]['content']) === '' + && isset($tokens[($i + 1)]) + && $tokens[$i]['line'] !== $tokens[($i + 1)]['line'] + ) { + // Skip blank lines which only contain trailing whitespace. + continue 2; + } + + // Deliberate fall-through. + + case \T_PHPCS_ENABLE: + case \T_PHPCS_DISABLE: + case \T_PHPCS_SET: + case \T_PHPCS_IGNORE: + case \T_PHPCS_IGNORE_FILE: + case \T_END_HEREDOC: + case \T_END_NOWDOC: + /* + * Indentation is included in the contents of the token for: + * - inline HTML + * - PHP 7.3+ flexible heredoc/nowdoc closer identifiers; + * - subsequent lines of multi-line comments; + * - PHPCS native annotations when part of a multi-line comment. + */ + $content = \ltrim($tokens[$i]['content']); + $whitespace = \str_replace($content, '', $tokens[$i]['content']); + + /* + * If there is no content, this is a blank line in a comment or in inline HTML. + * In that case, use the predetermined length as otherwise the new line character + * at the end of the whitespace will throw the count off. + */ + $length = ($content === '') ? $tokens[$i]['length'] : \strlen($whitespace); + $spaces = ($length % $indent); + + /* + * For multi-line star-comments, which use (aligned) stars on subsequent + * lines, we don't want to trigger on the one extra space before the star. + * + * While not 100% correct, don't exclude inline HTML from this check as + * otherwise the sniff would trigger on multi-line /*-style inline javascript comments. + * This may cause false negatives as there is no check for being in a + * + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Warning: [4 spaces][4 spaces][extra space][extra space][extra space]. + -> anothermethod(); // Warning: [4 spaces][4 spaces][extra space][extra space][extra space]. + + ?> + +

+ Warning: Some inline HTML with precision alignment. +

+ + + + + + +
+

+ +

+
diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.10.inc.fixed b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.10.inc.fixed new file mode 100644 index 00000000..1f9ed166 --- /dev/null +++ b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.10.inc.fixed @@ -0,0 +1,6 @@ + +
+

+ +

+
diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.11.inc b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.11.inc new file mode 100644 index 00000000..e115df93 --- /dev/null +++ b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.11.inc @@ -0,0 +1,60 @@ += 7.3: flexible heredoc/nowdocs. + * + * In this case, the indentation before the closing identifier will be checked. + * As before, the text lines will not be checked. + */ +$heredoc = <<= 7.3: flexible heredoc/nowdocs. + * + * In this case, the indentation before the closing identifier will be checked. + * As before, the text lines will not be checked. + */ +$heredoc = << +The line below only contains a new line char. OK. + +The line below has 4 spaces trailing whitespace! Ignore. + +The line below has 5 spaces trailing whitespace! Ignore, even though not matching tab width. + + + The line below only contains a new line char. OK. + + The line below has 4 spaces trailing whitespace! Ignore. + + The line below has 5 spaces trailing whitespace! Ignore, even though not matching tab width. + + +The line below only contains a new line char. OK. + +The line below has 4 spaces trailing whitespace! Ignore. + +The line below has 5 spaces trailing whitespace! Ignore, even though not matching tab width. + + + The line below only contains a new line char. OK. + + The line below has 4 spaces trailing whitespace! Ignore. + + The line below has 5 spaces trailing whitespace! Ignore, even though not matching tab width. + + +The line below only contains a new line char. OK. + +The line below has 4 spaces trailing whitespace! Ignore. + +The line below has 5 spaces trailing whitespace! Ignore, even though not matching tab width. + + + The line below only contains a new line char. OK. + + The line below has 4 spaces trailing whitespace! Ignore. + + The line below has 5 spaces trailing whitespace! Ignore, even though not matching tab width. + + +The line below only contains a new line char. OK. + +The line below has 4 spaces trailing whitespace! Ignore. + +The line below has 5 spaces trailing whitespace! Ignore, even though not matching tab width. + + + The line below only contains a new line char. OK. + + The line below has 4 spaces trailing whitespace! Ignore. + + The line below has 5 spaces trailing whitespace! Ignore, even though not matching tab width. + + functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Ignored. + -> anothermethod(); // Ignored. + + ?> + +

+ Warning: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Ignored. + -> anothermethod(); // Ignored. + + ?> + +

+ Warning: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Ignored. + -> anothermethod(); // Ignored. + + ?> + +

+ Warning: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Ignored. + -> anothermethod(); // Ignored. + + ?> + +

+ Warning: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Warning: [4 spaces][4 spaces][extra space][extra space][extra space]. + -> anothermethod(); // Warning: [4 spaces][4 spaces][extra space][extra space][extra space]. + + ?> + +

+ Ignored: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Warning: [4 spaces][4 spaces][extra space][extra space][extra space]. + -> anothermethod(); // Warning: [4 spaces][4 spaces][extra space][extra space][extra space]. + + ?> + +

+ Ignored: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Warning: [tab][tab][space][space][space]. + -> anothermethod(); // Warning: [tab][tab][space][space][space]. + + ?> + +

+ Ignored: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Warning: [tab][tab][space][space][space]. + -> anothermethod(); // Warning: [tab][tab][space][space][space]. + + ?> + +

+ Ignored: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Warning: [4 spaces][4 spaces][extra space][extra space][extra space]. + -> anothermethod(); // Warning: [4 spaces][4 spaces][extra space][extra space][extra space]. + + ?> + +

+ Warning: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Warning: [4 spaces][4 spaces][extra space][extra space][extra space]. + -> anothermethod(); // Warning: [4 spaces][4 spaces][extra space][extra space][extra space]. + + ?> + +

+ Warning: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Warning: [3 spaces][3 spaces][extra space]. + -> anothermethod(); // Warning: [3 spaces][3 spaces][extra space]. + + ?> + +

+ Warning: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Warning: [3 spaces][3 spaces][extra space]. + -> anothermethod(); // Warning: [3 spaces][3 spaces][extra space]. + + ?> + +

+ Warning: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Warning: [2 spaces][2 spaces][2 spaces][2 spaces][extra space]. + -> anothermethod(); // Warning: [2 spaces][2 spaces][2 spaces][2 spaces][extra space]. + + ?> + +

+ Warning: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Warning: [2 spaces][2 spaces][2 spaces][2 spaces][extra space]. + -> anothermethod(); // Warning: [2 spaces][2 spaces][2 spaces][2 spaces][extra space]. + + ?> + +

+ Warning: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Warning: [tab][tab][space][space][space]. + -> anothermethod(); // Warning: [tab][tab][space][space][space]. + + ?> + +

+ Warning: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Warning: [tab][tab][space][space][space]. + -> anothermethod(); // Warning: [tab][tab][space][space][space]. + + ?> + +

+ Warning: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Warning: [tab][tab][space][space][space]. + -> anothermethod(); // Warning: [tab][tab][space][space][space]. + + ?> + +

+ Warning: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Warning: [tab][tab][space][space][space]. + -> anothermethod(); // Warning: [tab][tab][space][space][space]. + + ?> + +

+ Warning: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Warning: [tab][tab][space]. + -> anothermethod(); // Warning: [tab][tab][space][space]. + + ?> + +

+ Warning: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Warning: [tab][tab][space]. + -> anothermethod(); // Warning: [tab][tab][space][space]. + + ?> + +

+ Warning: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Warning: [tab][tab][space][space][space]. + -> anothermethod(); // Warning: [tab][tab][space][space][space]. + + ?> + +

+ Warning: Some inline HTML with precision alignment. +

+ + + + + +functioncall() + -> chained() + -> anothermethod(); + + $object->functioncall() + -> chained() // Warning: [tab][tab][space][space][space]. + -> anothermethod(); // Warning: [tab][tab][space][space][space]. + + ?> + +

+ Warning: Some inline HTML with precision alignment. +

+ + + + + + +
+

+ +

+
diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.9.inc.fixed b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.9.inc.fixed new file mode 100644 index 00000000..79df0922 --- /dev/null +++ b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.9.inc.fixed @@ -0,0 +1,6 @@ + +
+

+ +

+
diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.php b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.php new file mode 100644 index 00000000..664abe44 --- /dev/null +++ b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.php @@ -0,0 +1,214 @@ + File name => tab width to set. + */ + private $tabBasedFiles = [ + 'PrecisionAlignmentUnitTest.5.inc' => 4, + 'PrecisionAlignmentUnitTest.6.inc' => 4, + 'PrecisionAlignmentUnitTest.7.inc' => 3, + 'PrecisionAlignmentUnitTest.8.inc' => 2, + 'PrecisionAlignmentUnitTest.10.inc' => 4, + 'PrecisionAlignmentUnitTest.15.inc' => 4, + 'PrecisionAlignmentUnitTest.17.inc' => 4, + 'PrecisionAlignmentUnitTest.2.css' => 4, + 'PrecisionAlignmentUnitTest.2.js' => 4, + ]; + + /** + * Set CLI values before the file is tested. + * + * @param string $testFile The name of the file being tested. + * @param \PHP_CodeSniffer\Config $config The config data for the test run. + * + * @return void + */ + public function setCliValues($testFile, $config) + { + if (isset($this->tabBasedFiles[$testFile]) === true) { + $config->tabWidth = $this->tabBasedFiles[$testFile]; + } else { + $config->tabWidth = 0; + } + + // Testing a file with "--ignore-annotations". + if ($testFile === 'PrecisionAlignmentUnitTest.18.inc') { + $config->annotations = false; + } else { + $config->annotations = true; + } + } + + /** + * Returns the lines where errors should occur. + * + * @return array + */ + public function getErrorList() + { + return []; + } + + /** + * Returns the lines where warnings should occur. + * + * @param string $testFile The name of the file being tested. + * + * @return array + */ + public function getWarningList($testFile = '') + { + $phpcsVersion = Helper::getVersion(); + $isPhpcs4 = \version_compare($phpcsVersion, '3.99.99', '>'); + + switch ($testFile) { + case 'PrecisionAlignmentUnitTest.1.inc': // Space-based, default indent. + case 'PrecisionAlignmentUnitTest.2.inc': // Space-based, custom indent 4. + case 'PrecisionAlignmentUnitTest.3.inc': // Space-based, custom indent 3. + case 'PrecisionAlignmentUnitTest.5.inc': // Tab-based, default indent, tab-width 4. + case 'PrecisionAlignmentUnitTest.6.inc': // Tab-based, custom indent 4, tab-width 4. + case 'PrecisionAlignmentUnitTest.7.inc': // Tab-based, custom indent 3, tab-width 3. + return [ + 23 => 1, + 29 => 1, + 36 => 1, + 39 => 1, + 40 => 1, + 41 => 1, + 48 => 1, + 49 => 1, + 51 => 1, + 54 => 1, + 57 => 1, + 81 => 1, + ]; + + case 'PrecisionAlignmentUnitTest.4.inc': // Space-based, custom indent 2. + case 'PrecisionAlignmentUnitTest.8.inc': // Tab-based, custom indent 2, tab-width 2. + return [ + 29 => 1, + 39 => 1, + 40 => 1, + 41 => 1, + 48 => 1, + 49 => 1, + 51 => 1, + 54 => 1, + 57 => 1, + 81 => 1, + ]; + + // Verify handling of files only containing short open echo tags. + case 'PrecisionAlignmentUnitTest.9.inc': // Space-based. + case 'PrecisionAlignmentUnitTest.10.inc': // Tab-based. + return [ + 2 => 1, + 3 => 1, + 4 => 1, + 5 => 1, + 6 => 1, + ]; + + // Verify handling of hereodc/nowdocs closing identifiers. + case 'PrecisionAlignmentUnitTest.11.inc': + if (\PHP_VERSION_ID >= 70300) { + return [ + 54 => 1, + 60 => 1, + ]; + } + + // PHP 7.2 or lower: PHP version which doesn't support flexible heredocs/nowdocs yet. + return []; + + case 'PrecisionAlignmentUnitTest.12.inc': + // Testing that precision alignment on blank lines is ignored. + return []; + + case 'PrecisionAlignmentUnitTest.13.inc': + // Testing that precision alignment on blank lines is NOT ignored. + return [ + 19 => 1, + 26 => 1, + 35 => 1, + 44 => 1, + 57 => 1, + 64 => 1, + 73 => 1, + 82 => 1, + ]; + + // Testing ignoring the indentation before certain tokens. + case 'PrecisionAlignmentUnitTest.14.inc': // Space-based. + case 'PrecisionAlignmentUnitTest.15.inc': // Tab-based. + return [ + 23 => 1, + 29 => 1, + 36 => 1, + 51 => 1, + 54 => 1, + 81 => 1, + ]; + + // Testing ignoring the indentation before certain tokens. + case 'PrecisionAlignmentUnitTest.16.inc': // Space-based. + case 'PrecisionAlignmentUnitTest.17.inc': // Tab-based. + return [ + 39 => 1, + 40 => 1, + 41 => 1, + 48 => 1, + 49 => 1, + 51 => 1, + ]; + + // Verify detection of precision alignment for ignore annotation lines. + case 'PrecisionAlignmentUnitTest.18.inc': + return [ + 4 => 1, + ]; + + case 'PrecisionAlignmentUnitTest.1.css': // Space-based. + case 'PrecisionAlignmentUnitTest.2.css': // Tab-based. + return [ + 5 => ($isPhpcs4 === true ? 0 : 1), + ]; + + case 'PrecisionAlignmentUnitTest.1.js': // Space-based. + case 'PrecisionAlignmentUnitTest.2.js': // Tab-based. + return [ + 5 => ($isPhpcs4 === true ? 0 : 1), + 6 => ($isPhpcs4 === true ? 0 : 1), + 7 => ($isPhpcs4 === true ? 0 : 1), + ]; + + default: + return []; + } + } +} From 1e78d8e92da371d7f40f7ef75fec3eb24f2624fd Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 22 Jul 2022 22:42:52 +0200 Subject: [PATCH 014/125] :sparkles: New `Universal.WhiteSpace.AnonClassKeywordSpacing` sniff Checks the amount of spacing between the `class` keyword and the open parenthesis (if any) for anonymous class declarations. The expected amount of spacing is configurable and defaults to `0`. The default value of `0` has been decided upon based on scanning a not insignificant number of codebases and determining the prevailing style used for anonymous classes. It is also in line with the previously proposed [errata for PSR12](https://github.com/php-fig/fig-standards/pull/1206). Includes fixer. Includes unit tests. Includes documentation. Includes metrics. --- .../AnonClassKeywordSpacingStandard.xml | 28 +++++ .../AnonClassKeywordSpacingSniff.php | 79 ++++++++++++++ .../AnonClassKeywordSpacingUnitTest.inc | 103 ++++++++++++++++++ .../AnonClassKeywordSpacingUnitTest.inc.fixed | 100 +++++++++++++++++ .../AnonClassKeywordSpacingUnitTest.php | 63 +++++++++++ 5 files changed, 373 insertions(+) create mode 100644 Universal/Docs/WhiteSpace/AnonClassKeywordSpacingStandard.xml create mode 100644 Universal/Sniffs/WhiteSpace/AnonClassKeywordSpacingSniff.php create mode 100644 Universal/Tests/WhiteSpace/AnonClassKeywordSpacingUnitTest.inc create mode 100644 Universal/Tests/WhiteSpace/AnonClassKeywordSpacingUnitTest.inc.fixed create mode 100644 Universal/Tests/WhiteSpace/AnonClassKeywordSpacingUnitTest.php diff --git a/Universal/Docs/WhiteSpace/AnonClassKeywordSpacingStandard.xml b/Universal/Docs/WhiteSpace/AnonClassKeywordSpacingStandard.xml new file mode 100644 index 00000000..4eff64c3 --- /dev/null +++ b/Universal/Docs/WhiteSpace/AnonClassKeywordSpacingStandard.xml @@ -0,0 +1,28 @@ + + + + + + + + class($param) +{ + public function __construct($p) {} +}; + ]]> + + + class ($param) +{ + public function __construct($p) {} +}; + ]]> + + + diff --git a/Universal/Sniffs/WhiteSpace/AnonClassKeywordSpacingSniff.php b/Universal/Sniffs/WhiteSpace/AnonClassKeywordSpacingSniff.php new file mode 100644 index 00000000..4cde4b26 --- /dev/null +++ b/Universal/Sniffs/WhiteSpace/AnonClassKeywordSpacingSniff.php @@ -0,0 +1,79 @@ +getTokens(); + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== \T_OPEN_PARENTHESIS) { + // No parentheses, nothing to do. + return; + } + + SpacesFixer::checkAndFix( + $phpcsFile, + $stackPtr, + $nextNonEmpty, + (int) $this->spacing, + 'There must be %1$s between the class keyword and the open parenthesis for an anonymous class. Found: %2$s', + 'Incorrect', + 'error', + 0, + 'Anon class: space between keyword and open parenthesis' + ); + } +} diff --git a/Universal/Tests/WhiteSpace/AnonClassKeywordSpacingUnitTest.inc b/Universal/Tests/WhiteSpace/AnonClassKeywordSpacingUnitTest.inc new file mode 100644 index 00000000..fc238705 --- /dev/null +++ b/Universal/Tests/WhiteSpace/AnonClassKeywordSpacingUnitTest.inc @@ -0,0 +1,103 @@ + + */ + public function getErrorList() + { + return [ + 28 => 1, + 29 => 1, + 30 => 1, + 37 => 1, + 38 => 1, + 39 => 1, + 56 => 1, + 57 => 1, + 60 => 1, + 61 => 1, + 68 => 1, + 69 => 1, + 70 => 1, + 87 => 1, + 88 => 1, + 91 => 1, + 92 => 1, + 93 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array + */ + public function getWarningList() + { + return []; + } +} From dacf3d82ba0dd9e7ad3fef383a537709fc141e70 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Tue, 29 Mar 2022 19:13:33 +0200 Subject: [PATCH 015/125] QA: make all classes `final` --- NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php | 2 +- NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php | 2 +- NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.php | 2 +- NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.php | 2 +- Universal/Helpers/DummyTokenizer.php | 2 +- Universal/Sniffs/Arrays/DisallowShortArraySyntaxSniff.php | 2 +- Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php | 2 +- Universal/Sniffs/Arrays/MixedArrayKeyTypesSniff.php | 2 +- Universal/Sniffs/Arrays/MixedKeyedUnkeyedArraySniff.php | 2 +- Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php | 2 +- Universal/Sniffs/Classes/DisallowFinalClassSniff.php | 2 +- Universal/Sniffs/Classes/RequireAnonClassParenthesesSniff.php | 2 +- Universal/Sniffs/Classes/RequireFinalClassSniff.php | 2 +- Universal/Sniffs/CodeAnalysis/ForeachUniqueAssignmentSniff.php | 2 +- .../Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php | 2 +- Universal/Sniffs/Constants/UppercaseMagicConstantsSniff.php | 2 +- .../Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php | 2 +- Universal/Sniffs/ControlStructures/DisallowLonelyIfSniff.php | 2 +- Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php | 2 +- Universal/Sniffs/Lists/DisallowLongListSyntaxSniff.php | 2 +- Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php | 2 +- Universal/Sniffs/Namespaces/DisallowCurlyBraceSyntaxSniff.php | 2 +- .../Sniffs/Namespaces/DisallowDeclarationWithoutNameSniff.php | 2 +- Universal/Sniffs/Namespaces/EnforceCurlyBraceSyntaxSniff.php | 2 +- Universal/Sniffs/Namespaces/OneDeclarationPerFileSniff.php | 2 +- .../NamingConventions/NoReservedKeywordParameterNamesSniff.php | 2 +- .../Sniffs/OOStructures/AlphabeticExtendsImplementsSniff.php | 2 +- Universal/Sniffs/Operators/DisallowLogicalAndOrSniff.php | 2 +- Universal/Sniffs/Operators/DisallowShortTernarySniff.php | 2 +- .../Operators/DisallowStandalonePostIncrementDecrementSniff.php | 2 +- Universal/Sniffs/Operators/StrictComparisonsSniff.php | 2 +- Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php | 2 +- Universal/Sniffs/UseStatements/DisallowUseClassSniff.php | 2 +- Universal/Sniffs/UseStatements/DisallowUseConstSniff.php | 2 +- Universal/Sniffs/UseStatements/DisallowUseFunctionSniff.php | 2 +- Universal/Sniffs/UseStatements/LowercaseFunctionConstSniff.php | 2 +- Universal/Sniffs/UseStatements/NoLeadingBackslashSniff.php | 2 +- Universal/Sniffs/WhiteSpace/DisallowInlineTabsSniff.php | 2 +- Universal/Tests/Arrays/DisallowShortArraySyntaxUnitTest.php | 2 +- Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.php | 2 +- Universal/Tests/Arrays/MixedArrayKeyTypesUnitTest.php | 2 +- Universal/Tests/Arrays/MixedKeyedUnkeyedArrayUnitTest.php | 2 +- .../Tests/Classes/DisallowAnonClassParenthesesUnitTest.php | 2 +- Universal/Tests/Classes/DisallowFinalClassUnitTest.php | 2 +- Universal/Tests/Classes/RequireAnonClassParenthesesUnitTest.php | 2 +- Universal/Tests/Classes/RequireFinalClassUnitTest.php | 2 +- .../Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.php | 2 +- .../Tests/Constants/LowercaseClassResolutionKeywordUnitTest.php | 2 +- Universal/Tests/Constants/UppercaseMagicConstantsUnitTest.php | 2 +- .../ControlStructures/DisallowAlternativeSyntaxUnitTest.php | 2 +- Universal/Tests/ControlStructures/DisallowLonelyIfUnitTest.php | 2 +- Universal/Tests/ControlStructures/IfElseDeclarationUnitTest.php | 2 +- Universal/Tests/Lists/DisallowLongListSyntaxUnitTest.php | 2 +- Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.php | 2 +- Universal/Tests/Namespaces/DisallowCurlyBraceSyntaxUnitTest.php | 2 +- .../Tests/Namespaces/DisallowDeclarationWithoutNameUnitTest.php | 2 +- Universal/Tests/Namespaces/EnforceCurlyBraceSyntaxUnitTest.php | 2 +- Universal/Tests/Namespaces/OneDeclarationPerFileUnitTest.php | 2 +- .../NoReservedKeywordParameterNamesUnitTest.php | 2 +- .../Tests/OOStructures/AlphabeticExtendsImplementsUnitTest.php | 2 +- Universal/Tests/Operators/DisallowLogicalAndOrUnitTest.php | 2 +- Universal/Tests/Operators/DisallowShortTernaryUnitTest.php | 2 +- .../DisallowStandalonePostIncrementDecrementUnitTest.php | 2 +- Universal/Tests/Operators/StrictComparisonsUnitTest.php | 2 +- Universal/Tests/PHP/OneStatementInShortEchoTagUnitTest.php | 2 +- Universal/Tests/UseStatements/DisallowUseClassUnitTest.php | 2 +- Universal/Tests/UseStatements/DisallowUseConstUnitTest.php | 2 +- Universal/Tests/UseStatements/DisallowUseFunctionUnitTest.php | 2 +- .../Tests/UseStatements/LowercaseFunctionConstUnitTest.php | 2 +- Universal/Tests/UseStatements/NoLeadingBackslashUnitTest.php | 2 +- Universal/Tests/WhiteSpace/DisallowInlineTabsUnitTest.php | 2 +- 71 files changed, 71 insertions(+), 71 deletions(-) diff --git a/NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php b/NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php index 9fab7a8f..db8e9489 100644 --- a/NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php +++ b/NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php @@ -31,7 +31,7 @@ * * @since 1.0.0 */ -class ArrayBraceSpacingSniff implements Sniff +final class ArrayBraceSpacingSniff implements Sniff { /** diff --git a/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php b/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php index 2adf4a1f..697ec620 100644 --- a/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php +++ b/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php @@ -22,7 +22,7 @@ * * @since 1.0.0 */ -class CommaAfterLastSniff implements Sniff +final class CommaAfterLastSniff implements Sniff { /** diff --git a/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.php b/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.php index d542a685..14d3713d 100644 --- a/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.php +++ b/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class ArrayBraceSpacingUnitTest extends AbstractSniffUnitTest +final class ArrayBraceSpacingUnitTest extends AbstractSniffUnitTest { /** diff --git a/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.php b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.php index 31d34634..b0e32f19 100644 --- a/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.php +++ b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class CommaAfterLastUnitTest extends AbstractSniffUnitTest +final class CommaAfterLastUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Helpers/DummyTokenizer.php b/Universal/Helpers/DummyTokenizer.php index a3710c65..29a58fe6 100644 --- a/Universal/Helpers/DummyTokenizer.php +++ b/Universal/Helpers/DummyTokenizer.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class DummyTokenizer extends Tokenizer +final class DummyTokenizer extends Tokenizer { /** diff --git a/Universal/Sniffs/Arrays/DisallowShortArraySyntaxSniff.php b/Universal/Sniffs/Arrays/DisallowShortArraySyntaxSniff.php index da16cce7..e0c076e4 100644 --- a/Universal/Sniffs/Arrays/DisallowShortArraySyntaxSniff.php +++ b/Universal/Sniffs/Arrays/DisallowShortArraySyntaxSniff.php @@ -26,7 +26,7 @@ * @since 1.0.0 This sniff is loosely based on and inspired by the upstream * `Generic.Arrays.DisallowShortArraySyntax` sniff. */ -class DisallowShortArraySyntaxSniff implements Sniff +final class DisallowShortArraySyntaxSniff implements Sniff { /** diff --git a/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php b/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php index 3c1c7d1d..db95f325 100644 --- a/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php +++ b/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php @@ -22,7 +22,7 @@ * * @since 1.0.0 */ -class DuplicateArrayKeySniff extends AbstractArrayDeclarationSniff +final class DuplicateArrayKeySniff extends AbstractArrayDeclarationSniff { /** diff --git a/Universal/Sniffs/Arrays/MixedArrayKeyTypesSniff.php b/Universal/Sniffs/Arrays/MixedArrayKeyTypesSniff.php index 516472ef..391844e4 100644 --- a/Universal/Sniffs/Arrays/MixedArrayKeyTypesSniff.php +++ b/Universal/Sniffs/Arrays/MixedArrayKeyTypesSniff.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class MixedArrayKeyTypesSniff extends AbstractArrayDeclarationSniff +final class MixedArrayKeyTypesSniff extends AbstractArrayDeclarationSniff { /** diff --git a/Universal/Sniffs/Arrays/MixedKeyedUnkeyedArraySniff.php b/Universal/Sniffs/Arrays/MixedKeyedUnkeyedArraySniff.php index 236bb686..f8236583 100644 --- a/Universal/Sniffs/Arrays/MixedKeyedUnkeyedArraySniff.php +++ b/Universal/Sniffs/Arrays/MixedKeyedUnkeyedArraySniff.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class MixedKeyedUnkeyedArraySniff extends AbstractArrayDeclarationSniff +final class MixedKeyedUnkeyedArraySniff extends AbstractArrayDeclarationSniff { /** diff --git a/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php b/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php index 2f57fbe1..afcf283a 100644 --- a/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php +++ b/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php @@ -20,7 +20,7 @@ * * @since 1.0.0 */ -class DisallowAnonClassParenthesesSniff implements Sniff +final class DisallowAnonClassParenthesesSniff implements Sniff { /** diff --git a/Universal/Sniffs/Classes/DisallowFinalClassSniff.php b/Universal/Sniffs/Classes/DisallowFinalClassSniff.php index 3704ddbc..326b2f7a 100644 --- a/Universal/Sniffs/Classes/DisallowFinalClassSniff.php +++ b/Universal/Sniffs/Classes/DisallowFinalClassSniff.php @@ -20,7 +20,7 @@ * * @since 1.0.0 */ -class DisallowFinalClassSniff implements Sniff +final class DisallowFinalClassSniff implements Sniff { /** diff --git a/Universal/Sniffs/Classes/RequireAnonClassParenthesesSniff.php b/Universal/Sniffs/Classes/RequireAnonClassParenthesesSniff.php index 7e947078..3a868a2b 100644 --- a/Universal/Sniffs/Classes/RequireAnonClassParenthesesSniff.php +++ b/Universal/Sniffs/Classes/RequireAnonClassParenthesesSniff.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class RequireAnonClassParenthesesSniff implements Sniff +final class RequireAnonClassParenthesesSniff implements Sniff { /** diff --git a/Universal/Sniffs/Classes/RequireFinalClassSniff.php b/Universal/Sniffs/Classes/RequireFinalClassSniff.php index 26d8b434..5e63e4f1 100644 --- a/Universal/Sniffs/Classes/RequireFinalClassSniff.php +++ b/Universal/Sniffs/Classes/RequireFinalClassSniff.php @@ -20,7 +20,7 @@ * * @since 1.0.0 */ -class RequireFinalClassSniff implements Sniff +final class RequireFinalClassSniff implements Sniff { /** diff --git a/Universal/Sniffs/CodeAnalysis/ForeachUniqueAssignmentSniff.php b/Universal/Sniffs/CodeAnalysis/ForeachUniqueAssignmentSniff.php index 0d6d4988..6275e76d 100644 --- a/Universal/Sniffs/CodeAnalysis/ForeachUniqueAssignmentSniff.php +++ b/Universal/Sniffs/CodeAnalysis/ForeachUniqueAssignmentSniff.php @@ -23,7 +23,7 @@ * * @since 1.0.0 */ -class ForeachUniqueAssignmentSniff implements Sniff +final class ForeachUniqueAssignmentSniff implements Sniff { /** diff --git a/Universal/Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php b/Universal/Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php index 71398d57..ff06bc97 100644 --- a/Universal/Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php +++ b/Universal/Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php @@ -22,7 +22,7 @@ * * @since 1.0.0 */ -class LowercaseClassResolutionKeywordSniff implements Sniff +final class LowercaseClassResolutionKeywordSniff implements Sniff { /** diff --git a/Universal/Sniffs/Constants/UppercaseMagicConstantsSniff.php b/Universal/Sniffs/Constants/UppercaseMagicConstantsSniff.php index ee19e3d0..ad5815b1 100644 --- a/Universal/Sniffs/Constants/UppercaseMagicConstantsSniff.php +++ b/Universal/Sniffs/Constants/UppercaseMagicConstantsSniff.php @@ -21,7 +21,7 @@ * * @since 1.0.0 */ -class UppercaseMagicConstantsSniff implements Sniff +final class UppercaseMagicConstantsSniff implements Sniff { /** diff --git a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php index ce74722b..0a2535ff 100644 --- a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php +++ b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php @@ -21,7 +21,7 @@ * * @since 1.0.0 */ -class DisallowAlternativeSyntaxSniff implements Sniff +final class DisallowAlternativeSyntaxSniff implements Sniff { /** diff --git a/Universal/Sniffs/ControlStructures/DisallowLonelyIfSniff.php b/Universal/Sniffs/ControlStructures/DisallowLonelyIfSniff.php index 397a9e13..e582801b 100644 --- a/Universal/Sniffs/ControlStructures/DisallowLonelyIfSniff.php +++ b/Universal/Sniffs/ControlStructures/DisallowLonelyIfSniff.php @@ -27,7 +27,7 @@ * * @since 1.0.0 */ -class DisallowLonelyIfSniff implements Sniff +final class DisallowLonelyIfSniff implements Sniff { /** diff --git a/Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php b/Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php index 85780f7d..9bdb829c 100644 --- a/Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php +++ b/Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php @@ -28,7 +28,7 @@ * * @since 1.0.0 */ -class IfElseDeclarationSniff implements Sniff +final class IfElseDeclarationSniff implements Sniff { /** diff --git a/Universal/Sniffs/Lists/DisallowLongListSyntaxSniff.php b/Universal/Sniffs/Lists/DisallowLongListSyntaxSniff.php index 19499268..595932b9 100644 --- a/Universal/Sniffs/Lists/DisallowLongListSyntaxSniff.php +++ b/Universal/Sniffs/Lists/DisallowLongListSyntaxSniff.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class DisallowLongListSyntaxSniff implements Sniff +final class DisallowLongListSyntaxSniff implements Sniff { /** diff --git a/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php b/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php index bcba3ba6..48620ce5 100644 --- a/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php +++ b/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class DisallowShortListSyntaxSniff implements Sniff +final class DisallowShortListSyntaxSniff implements Sniff { /** diff --git a/Universal/Sniffs/Namespaces/DisallowCurlyBraceSyntaxSniff.php b/Universal/Sniffs/Namespaces/DisallowCurlyBraceSyntaxSniff.php index b0eed5f3..f8e3bcae 100644 --- a/Universal/Sniffs/Namespaces/DisallowCurlyBraceSyntaxSniff.php +++ b/Universal/Sniffs/Namespaces/DisallowCurlyBraceSyntaxSniff.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class DisallowCurlyBraceSyntaxSniff implements Sniff +final class DisallowCurlyBraceSyntaxSniff implements Sniff { /** diff --git a/Universal/Sniffs/Namespaces/DisallowDeclarationWithoutNameSniff.php b/Universal/Sniffs/Namespaces/DisallowDeclarationWithoutNameSniff.php index bb14eeaa..f19601de 100644 --- a/Universal/Sniffs/Namespaces/DisallowDeclarationWithoutNameSniff.php +++ b/Universal/Sniffs/Namespaces/DisallowDeclarationWithoutNameSniff.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class DisallowDeclarationWithoutNameSniff implements Sniff +final class DisallowDeclarationWithoutNameSniff implements Sniff { /** diff --git a/Universal/Sniffs/Namespaces/EnforceCurlyBraceSyntaxSniff.php b/Universal/Sniffs/Namespaces/EnforceCurlyBraceSyntaxSniff.php index a5a9825d..2d844653 100644 --- a/Universal/Sniffs/Namespaces/EnforceCurlyBraceSyntaxSniff.php +++ b/Universal/Sniffs/Namespaces/EnforceCurlyBraceSyntaxSniff.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class EnforceCurlyBraceSyntaxSniff implements Sniff +final class EnforceCurlyBraceSyntaxSniff implements Sniff { /** diff --git a/Universal/Sniffs/Namespaces/OneDeclarationPerFileSniff.php b/Universal/Sniffs/Namespaces/OneDeclarationPerFileSniff.php index d18c59a0..c53cdf0b 100644 --- a/Universal/Sniffs/Namespaces/OneDeclarationPerFileSniff.php +++ b/Universal/Sniffs/Namespaces/OneDeclarationPerFileSniff.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class OneDeclarationPerFileSniff implements Sniff +final class OneDeclarationPerFileSniff implements Sniff { /** diff --git a/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php b/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php index d57be87d..373be90a 100644 --- a/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php +++ b/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php @@ -23,7 +23,7 @@ * * @since 1.0.0 */ -class NoReservedKeywordParameterNamesSniff implements Sniff +final class NoReservedKeywordParameterNamesSniff implements Sniff { /** diff --git a/Universal/Sniffs/OOStructures/AlphabeticExtendsImplementsSniff.php b/Universal/Sniffs/OOStructures/AlphabeticExtendsImplementsSniff.php index 5d99519e..a397a6a3 100644 --- a/Universal/Sniffs/OOStructures/AlphabeticExtendsImplementsSniff.php +++ b/Universal/Sniffs/OOStructures/AlphabeticExtendsImplementsSniff.php @@ -22,7 +22,7 @@ * * @since 1.0.0 */ -class AlphabeticExtendsImplementsSniff implements Sniff +final class AlphabeticExtendsImplementsSniff implements Sniff { /** diff --git a/Universal/Sniffs/Operators/DisallowLogicalAndOrSniff.php b/Universal/Sniffs/Operators/DisallowLogicalAndOrSniff.php index be8b2df1..a7a2c7ab 100644 --- a/Universal/Sniffs/Operators/DisallowLogicalAndOrSniff.php +++ b/Universal/Sniffs/Operators/DisallowLogicalAndOrSniff.php @@ -22,7 +22,7 @@ * * @since 1.0.0 */ -class DisallowLogicalAndOrSniff implements Sniff +final class DisallowLogicalAndOrSniff implements Sniff { /** diff --git a/Universal/Sniffs/Operators/DisallowShortTernarySniff.php b/Universal/Sniffs/Operators/DisallowShortTernarySniff.php index 64280be6..cfeeaf15 100644 --- a/Universal/Sniffs/Operators/DisallowShortTernarySniff.php +++ b/Universal/Sniffs/Operators/DisallowShortTernarySniff.php @@ -23,7 +23,7 @@ * * @since 1.0.0 */ -class DisallowShortTernarySniff implements Sniff +final class DisallowShortTernarySniff implements Sniff { /** diff --git a/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php b/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php index 094c75e9..90d28753 100644 --- a/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php +++ b/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php @@ -28,7 +28,7 @@ * * @since 1.0.0 */ -class DisallowStandalonePostIncrementDecrementSniff implements Sniff +final class DisallowStandalonePostIncrementDecrementSniff implements Sniff { /** diff --git a/Universal/Sniffs/Operators/StrictComparisonsSniff.php b/Universal/Sniffs/Operators/StrictComparisonsSniff.php index a2145324..2527d3ac 100644 --- a/Universal/Sniffs/Operators/StrictComparisonsSniff.php +++ b/Universal/Sniffs/Operators/StrictComparisonsSniff.php @@ -21,7 +21,7 @@ * * @since 1.0.0 */ -class StrictComparisonsSniff implements Sniff +final class StrictComparisonsSniff implements Sniff { /** diff --git a/Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php b/Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php index be053a12..399213ca 100644 --- a/Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php +++ b/Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php @@ -24,7 +24,7 @@ * * @since 1.0.0 */ -class OneStatementInShortEchoTagSniff implements Sniff +final class OneStatementInShortEchoTagSniff implements Sniff { /** diff --git a/Universal/Sniffs/UseStatements/DisallowUseClassSniff.php b/Universal/Sniffs/UseStatements/DisallowUseClassSniff.php index c767d307..fd16040f 100644 --- a/Universal/Sniffs/UseStatements/DisallowUseClassSniff.php +++ b/Universal/Sniffs/UseStatements/DisallowUseClassSniff.php @@ -26,7 +26,7 @@ * * @since 1.0.0 */ -class DisallowUseClassSniff implements Sniff +final class DisallowUseClassSniff implements Sniff { /** diff --git a/Universal/Sniffs/UseStatements/DisallowUseConstSniff.php b/Universal/Sniffs/UseStatements/DisallowUseConstSniff.php index 8584b305..cbcaa9a0 100644 --- a/Universal/Sniffs/UseStatements/DisallowUseConstSniff.php +++ b/Universal/Sniffs/UseStatements/DisallowUseConstSniff.php @@ -26,7 +26,7 @@ * * @since 1.0.0 */ -class DisallowUseConstSniff implements Sniff +final class DisallowUseConstSniff implements Sniff { /** diff --git a/Universal/Sniffs/UseStatements/DisallowUseFunctionSniff.php b/Universal/Sniffs/UseStatements/DisallowUseFunctionSniff.php index dc270966..11cb9c2a 100644 --- a/Universal/Sniffs/UseStatements/DisallowUseFunctionSniff.php +++ b/Universal/Sniffs/UseStatements/DisallowUseFunctionSniff.php @@ -26,7 +26,7 @@ * * @since 1.0.0 */ -class DisallowUseFunctionSniff implements Sniff +final class DisallowUseFunctionSniff implements Sniff { /** diff --git a/Universal/Sniffs/UseStatements/LowercaseFunctionConstSniff.php b/Universal/Sniffs/UseStatements/LowercaseFunctionConstSniff.php index 0780a689..6cfb8313 100644 --- a/Universal/Sniffs/UseStatements/LowercaseFunctionConstSniff.php +++ b/Universal/Sniffs/UseStatements/LowercaseFunctionConstSniff.php @@ -23,7 +23,7 @@ * * @since 1.0.0 */ -class LowercaseFunctionConstSniff implements Sniff +final class LowercaseFunctionConstSniff implements Sniff { /** diff --git a/Universal/Sniffs/UseStatements/NoLeadingBackslashSniff.php b/Universal/Sniffs/UseStatements/NoLeadingBackslashSniff.php index 1dfdeea3..cca2a498 100644 --- a/Universal/Sniffs/UseStatements/NoLeadingBackslashSniff.php +++ b/Universal/Sniffs/UseStatements/NoLeadingBackslashSniff.php @@ -24,7 +24,7 @@ * * @since 1.0.0 */ -class NoLeadingBackslashSniff implements Sniff +final class NoLeadingBackslashSniff implements Sniff { /** diff --git a/Universal/Sniffs/WhiteSpace/DisallowInlineTabsSniff.php b/Universal/Sniffs/WhiteSpace/DisallowInlineTabsSniff.php index e0a2b8bd..56153e60 100644 --- a/Universal/Sniffs/WhiteSpace/DisallowInlineTabsSniff.php +++ b/Universal/Sniffs/WhiteSpace/DisallowInlineTabsSniff.php @@ -38,7 +38,7 @@ * * @since 1.0.0 */ -class DisallowInlineTabsSniff implements Sniff +final class DisallowInlineTabsSniff implements Sniff { /** diff --git a/Universal/Tests/Arrays/DisallowShortArraySyntaxUnitTest.php b/Universal/Tests/Arrays/DisallowShortArraySyntaxUnitTest.php index c56651c9..5eaf044b 100644 --- a/Universal/Tests/Arrays/DisallowShortArraySyntaxUnitTest.php +++ b/Universal/Tests/Arrays/DisallowShortArraySyntaxUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class DisallowShortArraySyntaxUnitTest extends AbstractSniffUnitTest +final class DisallowShortArraySyntaxUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.php b/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.php index e1e7335c..d2c1f159 100644 --- a/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.php +++ b/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class DuplicateArrayKeyUnitTest extends AbstractSniffUnitTest +final class DuplicateArrayKeyUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/Arrays/MixedArrayKeyTypesUnitTest.php b/Universal/Tests/Arrays/MixedArrayKeyTypesUnitTest.php index 7ca45fa2..b8195c50 100644 --- a/Universal/Tests/Arrays/MixedArrayKeyTypesUnitTest.php +++ b/Universal/Tests/Arrays/MixedArrayKeyTypesUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class MixedArrayKeyTypesUnitTest extends AbstractSniffUnitTest +final class MixedArrayKeyTypesUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/Arrays/MixedKeyedUnkeyedArrayUnitTest.php b/Universal/Tests/Arrays/MixedKeyedUnkeyedArrayUnitTest.php index 3d66ac44..3a4a62ae 100644 --- a/Universal/Tests/Arrays/MixedKeyedUnkeyedArrayUnitTest.php +++ b/Universal/Tests/Arrays/MixedKeyedUnkeyedArrayUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class MixedKeyedUnkeyedArrayUnitTest extends AbstractSniffUnitTest +final class MixedKeyedUnkeyedArrayUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/Classes/DisallowAnonClassParenthesesUnitTest.php b/Universal/Tests/Classes/DisallowAnonClassParenthesesUnitTest.php index 2f207b96..a2d91419 100644 --- a/Universal/Tests/Classes/DisallowAnonClassParenthesesUnitTest.php +++ b/Universal/Tests/Classes/DisallowAnonClassParenthesesUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class DisallowAnonClassParenthesesUnitTest extends AbstractSniffUnitTest +final class DisallowAnonClassParenthesesUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/Classes/DisallowFinalClassUnitTest.php b/Universal/Tests/Classes/DisallowFinalClassUnitTest.php index 412b388a..3838831c 100644 --- a/Universal/Tests/Classes/DisallowFinalClassUnitTest.php +++ b/Universal/Tests/Classes/DisallowFinalClassUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class DisallowFinalClassUnitTest extends AbstractSniffUnitTest +final class DisallowFinalClassUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/Classes/RequireAnonClassParenthesesUnitTest.php b/Universal/Tests/Classes/RequireAnonClassParenthesesUnitTest.php index 3d96c09c..d3d5bb3c 100644 --- a/Universal/Tests/Classes/RequireAnonClassParenthesesUnitTest.php +++ b/Universal/Tests/Classes/RequireAnonClassParenthesesUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class RequireAnonClassParenthesesUnitTest extends AbstractSniffUnitTest +final class RequireAnonClassParenthesesUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/Classes/RequireFinalClassUnitTest.php b/Universal/Tests/Classes/RequireFinalClassUnitTest.php index 63b1fb28..2ecf0acf 100644 --- a/Universal/Tests/Classes/RequireFinalClassUnitTest.php +++ b/Universal/Tests/Classes/RequireFinalClassUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class RequireFinalClassUnitTest extends AbstractSniffUnitTest +final class RequireFinalClassUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.php b/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.php index d22c3644..470b247f 100644 --- a/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.php +++ b/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class ForeachUniqueAssignmentUnitTest extends AbstractSniffUnitTest +final class ForeachUniqueAssignmentUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/Constants/LowercaseClassResolutionKeywordUnitTest.php b/Universal/Tests/Constants/LowercaseClassResolutionKeywordUnitTest.php index ecf7adea..c8a9b2a2 100644 --- a/Universal/Tests/Constants/LowercaseClassResolutionKeywordUnitTest.php +++ b/Universal/Tests/Constants/LowercaseClassResolutionKeywordUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class LowercaseClassResolutionKeywordUnitTest extends AbstractSniffUnitTest +final class LowercaseClassResolutionKeywordUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/Constants/UppercaseMagicConstantsUnitTest.php b/Universal/Tests/Constants/UppercaseMagicConstantsUnitTest.php index 8f34af3b..83ac50a8 100644 --- a/Universal/Tests/Constants/UppercaseMagicConstantsUnitTest.php +++ b/Universal/Tests/Constants/UppercaseMagicConstantsUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class UppercaseMagicConstantsUnitTest extends AbstractSniffUnitTest +final class UppercaseMagicConstantsUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.php b/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.php index 183e02f5..3ca06caf 100644 --- a/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.php +++ b/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class DisallowAlternativeSyntaxUnitTest extends AbstractSniffUnitTest +final class DisallowAlternativeSyntaxUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/ControlStructures/DisallowLonelyIfUnitTest.php b/Universal/Tests/ControlStructures/DisallowLonelyIfUnitTest.php index 46963e07..386aef8a 100644 --- a/Universal/Tests/ControlStructures/DisallowLonelyIfUnitTest.php +++ b/Universal/Tests/ControlStructures/DisallowLonelyIfUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class DisallowLonelyIfUnitTest extends AbstractSniffUnitTest +final class DisallowLonelyIfUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/ControlStructures/IfElseDeclarationUnitTest.php b/Universal/Tests/ControlStructures/IfElseDeclarationUnitTest.php index 259ff83d..beb3afa6 100644 --- a/Universal/Tests/ControlStructures/IfElseDeclarationUnitTest.php +++ b/Universal/Tests/ControlStructures/IfElseDeclarationUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class IfElseDeclarationUnitTest extends AbstractSniffUnitTest +final class IfElseDeclarationUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/Lists/DisallowLongListSyntaxUnitTest.php b/Universal/Tests/Lists/DisallowLongListSyntaxUnitTest.php index 27d99372..38dceeb8 100644 --- a/Universal/Tests/Lists/DisallowLongListSyntaxUnitTest.php +++ b/Universal/Tests/Lists/DisallowLongListSyntaxUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class DisallowLongListSyntaxUnitTest extends AbstractSniffUnitTest +final class DisallowLongListSyntaxUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.php b/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.php index 77e2ba1f..7e7cff8f 100644 --- a/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.php +++ b/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class DisallowShortListSyntaxUnitTest extends AbstractSniffUnitTest +final class DisallowShortListSyntaxUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/Namespaces/DisallowCurlyBraceSyntaxUnitTest.php b/Universal/Tests/Namespaces/DisallowCurlyBraceSyntaxUnitTest.php index 03a664bc..b2e716eb 100644 --- a/Universal/Tests/Namespaces/DisallowCurlyBraceSyntaxUnitTest.php +++ b/Universal/Tests/Namespaces/DisallowCurlyBraceSyntaxUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class DisallowCurlyBraceSyntaxUnitTest extends AbstractSniffUnitTest +final class DisallowCurlyBraceSyntaxUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/Namespaces/DisallowDeclarationWithoutNameUnitTest.php b/Universal/Tests/Namespaces/DisallowDeclarationWithoutNameUnitTest.php index 04dafbc6..957d6ca1 100644 --- a/Universal/Tests/Namespaces/DisallowDeclarationWithoutNameUnitTest.php +++ b/Universal/Tests/Namespaces/DisallowDeclarationWithoutNameUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class DisallowDeclarationWithoutNameUnitTest extends AbstractSniffUnitTest +final class DisallowDeclarationWithoutNameUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/Namespaces/EnforceCurlyBraceSyntaxUnitTest.php b/Universal/Tests/Namespaces/EnforceCurlyBraceSyntaxUnitTest.php index 500ac107..87bd1932 100644 --- a/Universal/Tests/Namespaces/EnforceCurlyBraceSyntaxUnitTest.php +++ b/Universal/Tests/Namespaces/EnforceCurlyBraceSyntaxUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class EnforceCurlyBraceSyntaxUnitTest extends AbstractSniffUnitTest +final class EnforceCurlyBraceSyntaxUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/Namespaces/OneDeclarationPerFileUnitTest.php b/Universal/Tests/Namespaces/OneDeclarationPerFileUnitTest.php index ed66fd27..f0e1552c 100644 --- a/Universal/Tests/Namespaces/OneDeclarationPerFileUnitTest.php +++ b/Universal/Tests/Namespaces/OneDeclarationPerFileUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class OneDeclarationPerFileUnitTest extends AbstractSniffUnitTest +final class OneDeclarationPerFileUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.php b/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.php index 0504dbbd..71cf647b 100644 --- a/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.php +++ b/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class NoReservedKeywordParameterNamesUnitTest extends AbstractSniffUnitTest +final class NoReservedKeywordParameterNamesUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/OOStructures/AlphabeticExtendsImplementsUnitTest.php b/Universal/Tests/OOStructures/AlphabeticExtendsImplementsUnitTest.php index efe9d7c9..e3de6469 100644 --- a/Universal/Tests/OOStructures/AlphabeticExtendsImplementsUnitTest.php +++ b/Universal/Tests/OOStructures/AlphabeticExtendsImplementsUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class AlphabeticExtendsImplementsUnitTest extends AbstractSniffUnitTest +final class AlphabeticExtendsImplementsUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/Operators/DisallowLogicalAndOrUnitTest.php b/Universal/Tests/Operators/DisallowLogicalAndOrUnitTest.php index 54ce1d46..146d3529 100644 --- a/Universal/Tests/Operators/DisallowLogicalAndOrUnitTest.php +++ b/Universal/Tests/Operators/DisallowLogicalAndOrUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class DisallowLogicalAndOrUnitTest extends AbstractSniffUnitTest +final class DisallowLogicalAndOrUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/Operators/DisallowShortTernaryUnitTest.php b/Universal/Tests/Operators/DisallowShortTernaryUnitTest.php index 3e55161d..355abfe5 100644 --- a/Universal/Tests/Operators/DisallowShortTernaryUnitTest.php +++ b/Universal/Tests/Operators/DisallowShortTernaryUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class DisallowShortTernaryUnitTest extends AbstractSniffUnitTest +final class DisallowShortTernaryUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/Operators/DisallowStandalonePostIncrementDecrementUnitTest.php b/Universal/Tests/Operators/DisallowStandalonePostIncrementDecrementUnitTest.php index 4dd8dff9..f959e71e 100644 --- a/Universal/Tests/Operators/DisallowStandalonePostIncrementDecrementUnitTest.php +++ b/Universal/Tests/Operators/DisallowStandalonePostIncrementDecrementUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class DisallowStandalonePostIncrementDecrementUnitTest extends AbstractSniffUnitTest +final class DisallowStandalonePostIncrementDecrementUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/Operators/StrictComparisonsUnitTest.php b/Universal/Tests/Operators/StrictComparisonsUnitTest.php index f710bae9..cf335529 100644 --- a/Universal/Tests/Operators/StrictComparisonsUnitTest.php +++ b/Universal/Tests/Operators/StrictComparisonsUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class StrictComparisonsUnitTest extends AbstractSniffUnitTest +final class StrictComparisonsUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/PHP/OneStatementInShortEchoTagUnitTest.php b/Universal/Tests/PHP/OneStatementInShortEchoTagUnitTest.php index 6c04a985..fb0460d9 100644 --- a/Universal/Tests/PHP/OneStatementInShortEchoTagUnitTest.php +++ b/Universal/Tests/PHP/OneStatementInShortEchoTagUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class OneStatementInShortEchoTagUnitTest extends AbstractSniffUnitTest +final class OneStatementInShortEchoTagUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/UseStatements/DisallowUseClassUnitTest.php b/Universal/Tests/UseStatements/DisallowUseClassUnitTest.php index 7b291335..d6e52041 100644 --- a/Universal/Tests/UseStatements/DisallowUseClassUnitTest.php +++ b/Universal/Tests/UseStatements/DisallowUseClassUnitTest.php @@ -22,7 +22,7 @@ * * @since 1.0.0 */ -class DisallowUseClassUnitTest extends AbstractSniffUnitTest +final class DisallowUseClassUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/UseStatements/DisallowUseConstUnitTest.php b/Universal/Tests/UseStatements/DisallowUseConstUnitTest.php index ddc294a1..a0b7529a 100644 --- a/Universal/Tests/UseStatements/DisallowUseConstUnitTest.php +++ b/Universal/Tests/UseStatements/DisallowUseConstUnitTest.php @@ -22,7 +22,7 @@ * * @since 1.0.0 */ -class DisallowUseConstUnitTest extends AbstractSniffUnitTest +final class DisallowUseConstUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/UseStatements/DisallowUseFunctionUnitTest.php b/Universal/Tests/UseStatements/DisallowUseFunctionUnitTest.php index ed86e317..2a6218f6 100644 --- a/Universal/Tests/UseStatements/DisallowUseFunctionUnitTest.php +++ b/Universal/Tests/UseStatements/DisallowUseFunctionUnitTest.php @@ -22,7 +22,7 @@ * * @since 1.0.0 */ -class DisallowUseFunctionUnitTest extends AbstractSniffUnitTest +final class DisallowUseFunctionUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/UseStatements/LowercaseFunctionConstUnitTest.php b/Universal/Tests/UseStatements/LowercaseFunctionConstUnitTest.php index 3e481729..46318f12 100644 --- a/Universal/Tests/UseStatements/LowercaseFunctionConstUnitTest.php +++ b/Universal/Tests/UseStatements/LowercaseFunctionConstUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class LowercaseFunctionConstUnitTest extends AbstractSniffUnitTest +final class LowercaseFunctionConstUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/UseStatements/NoLeadingBackslashUnitTest.php b/Universal/Tests/UseStatements/NoLeadingBackslashUnitTest.php index 283988eb..8d58812a 100644 --- a/Universal/Tests/UseStatements/NoLeadingBackslashUnitTest.php +++ b/Universal/Tests/UseStatements/NoLeadingBackslashUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class NoLeadingBackslashUnitTest extends AbstractSniffUnitTest +final class NoLeadingBackslashUnitTest extends AbstractSniffUnitTest { /** diff --git a/Universal/Tests/WhiteSpace/DisallowInlineTabsUnitTest.php b/Universal/Tests/WhiteSpace/DisallowInlineTabsUnitTest.php index 3d59a1db..57e322e2 100644 --- a/Universal/Tests/WhiteSpace/DisallowInlineTabsUnitTest.php +++ b/Universal/Tests/WhiteSpace/DisallowInlineTabsUnitTest.php @@ -19,7 +19,7 @@ * * @since 1.0.0 */ -class DisallowInlineTabsUnitTest extends AbstractSniffUnitTest +final class DisallowInlineTabsUnitTest extends AbstractSniffUnitTest { /** From 6a3690ce44344ef130627bd67f527c1b19c6c3de Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 29 Jul 2022 13:05:58 +0200 Subject: [PATCH 016/125] Universal/PrecisionAlignment: rename a local variable ... to be more descriptive. --- .../Sniffs/WhiteSpace/PrecisionAlignmentSniff.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php b/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php index 2a4a4ab1..f65221a1 100644 --- a/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php +++ b/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php @@ -312,7 +312,7 @@ public function process(File $phpcsFile, $stackPtr) ); if ($fix === true) { - $round = (int) \round($spaces / $indent, 0); + $tabstops = (int) \round($spaces / $indent, 0); switch ($tokens[$i]['code']) { case \T_WHITESPACE: @@ -320,7 +320,7 @@ public function process(File $phpcsFile, $stackPtr) * More complex than you'd think as "length" doesn't include new lines, * but we don't want to remove new lines either. */ - $replaceLength = (((int) ($tokens[$i]['length'] / $indent) + $round) * $indent); + $replaceLength = (((int) ($tokens[$i]['length'] / $indent) + $tabstops) * $indent); $replace = \str_repeat(' ', $replaceLength); $newContent = \substr_replace($tokens[$i]['content'], $replace, 0, $tokens[$i]['length']); @@ -328,13 +328,13 @@ public function process(File $phpcsFile, $stackPtr) break; case \T_DOC_COMMENT_WHITESPACE: - $replaceLength = (((int) ($tokens[$i]['length'] / $indent) + $round) * $indent); + $replaceLength = (((int) ($tokens[$i]['length'] / $indent) + $tabstops) * $indent); $replace = \str_repeat(' ', $replaceLength); if (isset($tokens[($i + 1)]) === true && ($tokens[($i + 1)]['code'] === \T_DOC_COMMENT_STAR || $tokens[($i + 1)]['code'] === \T_DOC_COMMENT_CLOSE_TAG) - && $round === 0 + && $tabstops === 0 ) { // Maintain the extra space before the star. $replace .= ' '; @@ -354,10 +354,10 @@ public function process(File $phpcsFile, $stackPtr) case \T_PHPCS_IGNORE_FILE: case \T_END_HEREDOC: case \T_END_NOWDOC: - $replaceLength = (((int) ($length / $indent) + $round) * $indent); + $replaceLength = (((int) ($length / $indent) + $tabstops) * $indent); $replace = \str_repeat(' ', $replaceLength); - if (isset($content[0]) === true && $content[0] === '*' && $round === 0) { + if (isset($content[0]) === true && $content[0] === '*' && $tabstops === 0) { // Maintain the extra space before the star. $replace .= ' '; } From 053fa6a69c7e87a01e99221a015e4820fb1aa1dd Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 29 Jul 2022 14:43:43 +0200 Subject: [PATCH 017/125] Universal/PrecisionAlignment: remove superfluous fixed file This file is not supposed to yield errors/warnings, so the `.fixed` file is redundant. --- .../PrecisionAlignmentUnitTest.12.inc.fixed | 83 ------------------- 1 file changed, 83 deletions(-) delete mode 100644 Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.12.inc.fixed diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.12.inc.fixed b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.12.inc.fixed deleted file mode 100644 index 420d8b08..00000000 --- a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.12.inc.fixed +++ /dev/null @@ -1,83 +0,0 @@ -phpcs:set Universal.WhiteSpace.PrecisionAlignment ignoreBlankLines true - - -The line below only contains a new line char. OK. - -The line below has 4 spaces trailing whitespace! Ignore. - -The line below has 5 spaces trailing whitespace! Ignore, even though not matching tab width. - - - The line below only contains a new line char. OK. - - The line below has 4 spaces trailing whitespace! Ignore. - - The line below has 5 spaces trailing whitespace! Ignore, even though not matching tab width. - - Date: Fri, 29 Jul 2022 14:56:52 +0200 Subject: [PATCH 018/125] WhiteSpace/PrecisionAlignment: best guess tabs vs spaces when fixing Enhancement to minimize fixer conflicts/speed up fixer runs. With this change, the fixer will check whether tab replacement has been done on the indent in the original token content and if so, the fixer will use tabs for the indent replacement instead of spaces. Includes updated unit tests. Includes minor test adjustment to ensure that tabs used for inline alignment (vs indent) are disregarded when determining whether tabs or spaces should be used by the fixer. --- .../WhiteSpace/PrecisionAlignmentSniff.php | 41 ++++++++++++++++--- .../PrecisionAlignmentUnitTest.1.inc | 2 +- .../PrecisionAlignmentUnitTest.1.inc.fixed | 2 +- .../PrecisionAlignmentUnitTest.10.inc.fixed | 6 +-- .../PrecisionAlignmentUnitTest.15.inc.fixed | 10 ++--- .../PrecisionAlignmentUnitTest.17.inc.fixed | 10 ++--- .../PrecisionAlignmentUnitTest.2.css.fixed | 2 +- .../PrecisionAlignmentUnitTest.2.inc | 2 +- .../PrecisionAlignmentUnitTest.2.inc.fixed | 2 +- .../PrecisionAlignmentUnitTest.2.js.fixed | 6 +-- .../PrecisionAlignmentUnitTest.3.inc | 2 +- .../PrecisionAlignmentUnitTest.3.inc.fixed | 2 +- .../PrecisionAlignmentUnitTest.4.inc | 2 +- .../PrecisionAlignmentUnitTest.4.inc.fixed | 2 +- .../PrecisionAlignmentUnitTest.5.inc.fixed | 20 ++++----- .../PrecisionAlignmentUnitTest.6.inc.fixed | 20 ++++----- .../PrecisionAlignmentUnitTest.7.inc.fixed | 20 ++++----- .../PrecisionAlignmentUnitTest.8.inc.fixed | 16 ++++---- 18 files changed, 99 insertions(+), 68 deletions(-) diff --git a/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php b/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php index f65221a1..7707bf6d 100644 --- a/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php +++ b/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php @@ -27,9 +27,9 @@ * - Precision alignment *within* text strings (multi-line text strings, heredocs, nowdocs) * will NOT be flagged by this sniff. * - The fixer works based on "best guess" and may not always result in the desired indentation. - * - This sniff does not concern itself with tabs versus spaces. + * - This fixer will use tabs or spaces based on whether tabs where present in the original indent. * Use the PHPCS native `Generic.WhiteSpace.DisallowTabIndent` or the - * `Generic.WhiteSpace.DisallowSpaceIndent` sniffs for that. + * `Generic.WhiteSpace.DisallowSpaceIndent` sniff to clean up the results if so desired. * * @since 1.0.0 */ @@ -205,6 +205,11 @@ public function process(File $phpcsFile, $stackPtr) continue; } + $origContent = null; + if (isset($tokens[$i]['orig_content']) === true) { + $origContent = $tokens[$i]['orig_content']; + } + $spaces = 0; switch ($tokens[$i]['code']) { case \T_WHITESPACE: @@ -321,7 +326,7 @@ public function process(File $phpcsFile, $stackPtr) * but we don't want to remove new lines either. */ $replaceLength = (((int) ($tokens[$i]['length'] / $indent) + $tabstops) * $indent); - $replace = \str_repeat(' ', $replaceLength); + $replace = $this->getReplacement($replaceLength, $origContent); $newContent = \substr_replace($tokens[$i]['content'], $replace, 0, $tokens[$i]['length']); $phpcsFile->fixer->replaceToken($i, $newContent); @@ -329,7 +334,7 @@ public function process(File $phpcsFile, $stackPtr) case \T_DOC_COMMENT_WHITESPACE: $replaceLength = (((int) ($tokens[$i]['length'] / $indent) + $tabstops) * $indent); - $replace = \str_repeat(' ', $replaceLength); + $replace = $this->getReplacement($replaceLength, $origContent); if (isset($tokens[($i + 1)]) === true && ($tokens[($i + 1)]['code'] === \T_DOC_COMMENT_STAR @@ -355,7 +360,7 @@ public function process(File $phpcsFile, $stackPtr) case \T_END_HEREDOC: case \T_END_NOWDOC: $replaceLength = (((int) ($length / $indent) + $tabstops) * $indent); - $replace = \str_repeat(' ', $replaceLength); + $replace = $this->getReplacement($replaceLength, $origContent); if (isset($content[0]) === true && $content[0] === '*' && $tabstops === 0) { // Maintain the extra space before the star. @@ -378,4 +383,30 @@ public function process(File $phpcsFile, $stackPtr) // No need to look at this file again. return $phpcsFile->numTokens; } + + /** + * Get the whitespace replacement. Respect tabs vs spaces. + * + * @param int $length The target length of the replacement. + * @param string|null $origContent The original token content without tabs replaced (if available). + * + * @return string + */ + private function getReplacement($length, $origContent) + { + if ($origContent !== null) { + // Check whether tabs were part of the indent or inline alignment. + $content = \ltrim($origContent); + $whitespace = \str_replace($content, '', $origContent); + + if (\strpos($whitespace, "\t") !== false) { + // Original indent used tabs. Use tabs in replacement too. + $tabs = (int) ($length / $this->tabWidth); + $spaces = $length % $this->tabWidth; + return \str_repeat("\t", $tabs) . \str_repeat(' ', (int) $spaces); + } + } + + return \str_repeat(' ', $length); + } } diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.1.inc b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.1.inc index 6f0bfc48..8e068c02 100644 --- a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.1.inc +++ b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.1.inc @@ -20,7 +20,7 @@ /** * OK: Doc comment is indented with 4 spaces + one space for the star alignment. * - * @var string <= Warning. + * @var string <= Warning. * @access private */ diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.1.inc.fixed b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.1.inc.fixed index 5dbf5316..151c7081 100644 --- a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.1.inc.fixed +++ b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.1.inc.fixed @@ -20,7 +20,7 @@ /** * OK: Doc comment is indented with 4 spaces + one space for the star alignment. * - * @var string <= Warning. + * @var string <= Warning. * @access private */ diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.10.inc.fixed b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.10.inc.fixed index 1f9ed166..02a68088 100644 --- a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.10.inc.fixed +++ b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.10.inc.fixed @@ -1,6 +1,6 @@ diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.15.inc.fixed b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.15.inc.fixed index d7f83a96..560f7274 100644 --- a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.15.inc.fixed +++ b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.15.inc.fixed @@ -20,20 +20,20 @@ phpcs:set Universal.WhiteSpace.PrecisionAlignment ignoreAlignmentBefore[] T_FUNC /** * OK: Doc comment is indented with tabs and one space for the star alignment. * - * @var string <= Warning. + * @var string <= Warning. * @access private */ /* OK: Multi-line comment is indented with 2 tabs for the text alignment. - <= Warning, no star, but also not indented by multiples of 4 spaces. + <= Warning, no star, but also not indented by multiples of 4 spaces. OK: Another line of text [tab][space][space][space][space]. */ /* * OK: Multi-line comment is indented with tabs and one space for the star alignment. * - * <= Warning. + * <= Warning. */ function exampleFunctionD() {} // Ignored. @@ -51,7 +51,7 @@ phpcs:set Universal.WhiteSpace.PrecisionAlignment ignoreAlignmentBefore[] T_FUNC ?>

- Warning: Some inline HTML with precision alignment. + Warning: Some inline HTML with precision alignment.

+ alert('bad'); functioncall() -> chained() -> anothermethod(); $object->functioncall() - -> chained() // Warning: [tab][tab][space][space][space]. - -> anothermethod(); // Warning: [tab][tab][space][space][space]. + -> chained() // Warning: [tab][tab][space][space][space]. + -> anothermethod(); // Warning: [tab][tab][space][space][space]. ?> diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.css.fixed b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.css.fixed index 4a3e53ad..0b07a181 100644 --- a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.css.fixed +++ b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.css.fixed @@ -2,5 +2,5 @@ #login-container { margin-left: -225px; width: 450px; - height: 450px; /* Bad. */ + height: 450px; /* Bad. */ } diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.inc b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.inc index 67e5af26..5775ec86 100644 --- a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.inc +++ b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.inc @@ -20,7 +20,7 @@ phpcs:set Universal.WhiteSpace.PrecisionAlignment indent 4 /** * OK: Doc comment is indented with 4 spaces + one space for the star alignment. * - * @var string <= Warning. + * @var string <= Warning. * @access private */ diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.inc.fixed b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.inc.fixed index 1eecb3b0..59a4384b 100644 --- a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.inc.fixed +++ b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.inc.fixed @@ -20,7 +20,7 @@ phpcs:set Universal.WhiteSpace.PrecisionAlignment indent 4 /** * OK: Doc comment is indented with 4 spaces + one space for the star alignment. * - * @var string <= Warning. + * @var string <= Warning. * @access private */ diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.js.fixed b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.js.fixed index 83c87e02..b16214c9 100644 --- a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.js.fixed +++ b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.2.js.fixed @@ -2,7 +2,7 @@ var x = { abc: 1, zyz: 2, - mno: { - abc: 4 - }, + mno: { + abc: 4 + }, } diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.3.inc b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.3.inc index 257c3d24..38e9f813 100644 --- a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.3.inc +++ b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.3.inc @@ -20,7 +20,7 @@ phpcs:set Universal.WhiteSpace.PrecisionAlignment indent 3 /** * OK: Doc comment is indented with 3 spaces + one space for the star alignment. * - * @var string <= Warning. + * @var string <= Warning. * @access private */ diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.3.inc.fixed b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.3.inc.fixed index c6dfb611..597bdb21 100644 --- a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.3.inc.fixed +++ b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.3.inc.fixed @@ -20,7 +20,7 @@ phpcs:set Universal.WhiteSpace.PrecisionAlignment indent 3 /** * OK: Doc comment is indented with 3 spaces + one space for the star alignment. * - * @var string <= Warning. + * @var string <= Warning. * @access private */ diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.4.inc b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.4.inc index 016f63f4..90d1f6ed 100644 --- a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.4.inc +++ b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.4.inc @@ -78,7 +78,7 @@ phpcs:set Universal.WhiteSpace.PrecisionAlignment indent 2 * Multi-line javascript comment should not trigger this sniff. */ alert('OK'); - alert('bad'); + alert('bad'); + alert('bad'); functioncall() -> chained() -> anothermethod(); $object->functioncall() - -> chained() // Warning: [tab][tab][space][space][space]. - -> anothermethod(); // Warning: [tab][tab][space][space][space]. + -> chained() // Warning: [tab][tab][space][space][space]. + -> anothermethod(); // Warning: [tab][tab][space][space][space]. ?>

- Warning: Some inline HTML with precision alignment. + Warning: Some inline HTML with precision alignment.

+ alert('bad'); functioncall() -> chained() -> anothermethod(); $object->functioncall() - -> chained() // Warning: [tab][tab][space][space][space]. - -> anothermethod(); // Warning: [tab][tab][space][space][space]. + -> chained() // Warning: [tab][tab][space][space][space]. + -> anothermethod(); // Warning: [tab][tab][space][space][space]. ?>

- Warning: Some inline HTML with precision alignment. + Warning: Some inline HTML with precision alignment.

+ alert('bad'); functioncall() -> chained() -> anothermethod(); $object->functioncall() - -> chained() // Warning: [tab][tab][space]. - -> anothermethod(); // Warning: [tab][tab][space][space]. + -> chained() // Warning: [tab][tab][space]. + -> anothermethod(); // Warning: [tab][tab][space][space]. ?>

- Warning: Some inline HTML with precision alignment. + Warning: Some inline HTML with precision alignment.

+ alert('bad'); functioncall() -> chained() -> anothermethod(); $object->functioncall() - -> chained() // Warning: [tab][tab][space][space][space]. - -> anothermethod(); // Warning: [tab][tab][space][space][space]. + -> chained() // Warning: [tab][tab][space][space][space]. + -> anothermethod(); // Warning: [tab][tab][space][space][space]. ?>

- Warning: Some inline HTML with precision alignment. + Warning: Some inline HTML with precision alignment.

+ alert('bad'); Date: Fri, 29 Jul 2022 16:17:47 +0200 Subject: [PATCH 019/125] WhiteSpace/PrecisionAlignment: bug fix - improved handling of heredoc/nowdoc closers This commit fixes two bugs: 1. When a heredoc/nowdoc closer (PHP 7.3+ flexible syntax) is indented with tabs, PHPCS does not replace tabs with spaces in the token. This means the token `'length'` key will also be tab-based, which leads to incorrrect calculations and false positives with incorrect fixes. Fixed now by handling tab replacement in heredoc/nowdoc closer tokens in the sniff itself. 2. A heredoc/nowdoc closer using flexible syntax is not allowed to have a larger indent than any of the content of the heredoc/nowdoc. Doing so results in a parse error. With the "best guess" rounding of the indent, the sniff had a risk of introducing these kind of parse errors. Fixed now by always rounding the expected indent down to the nearest tabstop for these tokens. Includes additional unit tests to cover the changes. --- .../WhiteSpace/PrecisionAlignmentSniff.php | 45 ++++++++++-- .../PrecisionAlignmentUnitTest.11.inc | 68 +++++++++++++++++++ .../PrecisionAlignmentUnitTest.11.inc.fixed | 68 +++++++++++++++++++ .../WhiteSpace/PrecisionAlignmentUnitTest.php | 11 ++- 4 files changed, 183 insertions(+), 9 deletions(-) diff --git a/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php b/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php index 7707bf6d..f9e2914c 100644 --- a/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php +++ b/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php @@ -270,12 +270,10 @@ public function process(File $phpcsFile, $stackPtr) case \T_PHPCS_SET: case \T_PHPCS_IGNORE: case \T_PHPCS_IGNORE_FILE: - case \T_END_HEREDOC: - case \T_END_NOWDOC: /* * Indentation is included in the contents of the token for: * - inline HTML - * - PHP 7.3+ flexible heredoc/nowdoc closer identifiers; + * - PHP 7.3+ flexible heredoc/nowdoc closer identifiers (see below); * - subsequent lines of multi-line comments; * - PHPCS native annotations when part of a multi-line comment. */ @@ -303,6 +301,24 @@ public function process(File $phpcsFile, $stackPtr) --$spaces; } break; + + case \T_END_HEREDOC: + case \T_END_NOWDOC: + /* + * PHPCS does not execute tab replacement in heredoc/nowdoc closer + * tokens (last checked: PHPCS 3.7.1), so handle this ourselves. + */ + $content = $tokens[$i]['content']; + if (\strpos($tokens[$i]['content'], "\t") !== false) { + $origContent = $content; + $content = \str_replace("\t", \str_repeat(' ', $this->tabWidth), $content); + } + + $closer = \ltrim($content); + $whitespace = \str_replace($closer, '', $content); + $length = \strlen($whitespace); + $spaces = ($length % $indent); + break; } if ($spaces === 0) { @@ -317,7 +333,13 @@ public function process(File $phpcsFile, $stackPtr) ); if ($fix === true) { - $tabstops = (int) \round($spaces / $indent, 0); + if ($tokens[$i]['code'] === \T_END_HEREDOC || $tokens[$i]['code'] === \T_END_NOWDOC) { + // For heredoc/nowdoc, always round down to prevent introducing parse errors. + $tabstops = (int) \floor($spaces / $indent); + } else { + // For everything else, use "best guess". + $tabstops = (int) \round($spaces / $indent, 0); + } switch ($tokens[$i]['code']) { case \T_WHITESPACE: @@ -357,8 +379,6 @@ public function process(File $phpcsFile, $stackPtr) case \T_PHPCS_SET: case \T_PHPCS_IGNORE: case \T_PHPCS_IGNORE_FILE: - case \T_END_HEREDOC: - case \T_END_NOWDOC: $replaceLength = (((int) ($length / $indent) + $tabstops) * $indent); $replace = $this->getReplacement($replaceLength, $origContent); @@ -376,6 +396,14 @@ public function process(File $phpcsFile, $stackPtr) $phpcsFile->fixer->replaceToken($i, $newContent); break; + + case \T_END_HEREDOC: + case \T_END_NOWDOC: + $replaceLength = (((int) ($length / $indent) + $tabstops) * $indent); + $replace = $this->getReplacement($replaceLength, $origContent); + + $phpcsFile->fixer->replaceToken($i, $replace . $closer); + break; } } } @@ -397,7 +425,10 @@ private function getReplacement($length, $origContent) if ($origContent !== null) { // Check whether tabs were part of the indent or inline alignment. $content = \ltrim($origContent); - $whitespace = \str_replace($content, '', $origContent); + $whitespace = $origContent; + if ($content !== '') { + $whitespace = \str_replace($content, '', $origContent); + } if (\strpos($whitespace, "\t") !== false) { // Original indent used tabs. Use tabs in replacement too. diff --git a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.11.inc b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.11.inc index e115df93..1be28bf7 100644 --- a/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.11.inc +++ b/Universal/Tests/WhiteSpace/PrecisionAlignmentUnitTest.11.inc @@ -17,6 +17,8 @@ * The text lines are not checked and the closer MUST be at the start of the line, * as otherwise the code will result in a parse error. */ + +// Space indent. $heredoc = <<= 7.3: flexible heredoc/nowdocs. * * In this case, the indentation before the closing identifier will be checked. * As before, the text lines will not be checked. + * + * Notes: + * - These tests also safeguard that for nowdoc/heredoc closers, the indent is always rounded DOWN + * as rounding up could cause a parse error (invalid body indentation level)! + * - These tests also verify correct handling of spaces vs tabs in tokens in which PHPCS + * may not have done the replacement natively. */ + +// Space indent. $heredoc = <<= 7.3: flexible heredoc/nowdocs. * * In this case, the indentation before the closing identifier will be checked. * As before, the text lines will not be checked. + * + * Notes: + * - These tests also safeguard that for nowdoc/heredoc closers, the indent is always rounded DOWN + * as rounding up could cause a parse error (invalid body indentation level)! + * - These tests also verify correct handling of spaces vs tabs in tokens in which PHPCS + * may not have done the replacement natively. */ + +// Space indent. $heredoc = << 3, 'PrecisionAlignmentUnitTest.8.inc' => 2, 'PrecisionAlignmentUnitTest.10.inc' => 4, + 'PrecisionAlignmentUnitTest.11.inc' => 4, 'PrecisionAlignmentUnitTest.15.inc' => 4, 'PrecisionAlignmentUnitTest.17.inc' => 4, 'PrecisionAlignmentUnitTest.2.css' => 4, @@ -138,8 +139,14 @@ public function getWarningList($testFile = '') case 'PrecisionAlignmentUnitTest.11.inc': if (\PHP_VERSION_ID >= 70300) { return [ - 54 => 1, - 60 => 1, + 77 => 1, + 83 => 1, + 88 => 1, + 93 => 1, + 112 => 1, + 118 => 1, + 123 => 1, + 128 => 1, ]; } From 40140a71a764db987e1db8dbf6ef63c279a0e908 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 4 Sep 2022 14:55:29 +0100 Subject: [PATCH 020/125] Add "static analysis" Composer keyword As per https://getcomposer.org/doc/04-schema.md#keywords by including "static analysis" as a keyword in the `composer.json` file, Composer 2.4.0-RC1 and later will prompt users if the package is installed with `composer require` instead of `composer require --dev`. See https://github.com/composer/composer/pull/10960 for more info. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ad107653..76acf580 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name" : "phpcsstandards/phpcsextra", "description" : "A collection of sniffs and standards for use with PHP_CodeSniffer.", "type" : "phpcodesniffer-standard", - "keywords" : [ "phpcs", "phpcbf", "standards", "php_codesniffer", "phpcodesniffer-standard" ], + "keywords" : [ "phpcs", "phpcbf", "standards", "static analysis", "php_codesniffer", "phpcodesniffer-standard" ], "license" : "LGPL-3.0-or-later", "authors" : [ { From 72e5868146962a90f72ad1d8af6e3ed43172ecd1 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 8 Jun 2022 04:23:30 +0200 Subject: [PATCH 021/125] Composer/GH Actions: start using PHPCSDevTools 1.2.0 PHPCSDevTools 1.2.0 introduces an XSD for the XML docs which can accompany sniffs. This commit: * Updates the PHPCSDevTools to version 1.2.0. * Adds a new check against the XSD for all sniff XML Docs files. Ref: https://github.com/PHPCSStandards/PHPCSDevTools/releases/tag/1.2.0 --- .github/workflows/basics.yml | 6 +++++- composer.json | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml index d61f24aa..860eada9 100644 --- a/.github/workflows/basics.yml +++ b/.github/workflows/basics.yml @@ -61,7 +61,7 @@ jobs: # @link https://github.com/marketplace/actions/xmllint-problem-matcher - uses: korelstar/xmllint-problem-matcher@v1 - # Validate the XML file. + # Validate the Ruleset XML file. # @link http://xmlsoft.org/xmllint.html - name: Validate rulesets against schema run: xmllint --noout --schema vendor/squizlabs/php_codesniffer/phpcs.xsd ./*/ruleset.xml @@ -72,6 +72,10 @@ jobs: diff -B ./NormalizedArrays/ruleset.xml <(xmllint --format "./NormalizedArrays/ruleset.xml") diff -B ./Universal/ruleset.xml <(xmllint --format "./Universal/ruleset.xml") + # Validate the Documentation XML files. + - name: Validate documentation against schema + run: xmllint --noout --schema vendor/phpcsstandards/phpcsdevtools/DocsXsd/phpcsdocs.xsd ./*/Docs/*/*Standard.xml + # Check the code-style consistency of the PHP files. - name: Check PHP code style continue-on-error: true diff --git a/composer.json b/composer.json index 76acf580..8af28dbf 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "require-dev" : { "php-parallel-lint/php-parallel-lint": "^1.3.2", "php-parallel-lint/php-console-highlighter": "^1.0", - "phpcsstandards/phpcsdevtools": "^1.0", + "phpcsstandards/phpcsdevtools": "^1.2.0", "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0" }, "extra": { From 7cd20f1b58a01432f278aede3b03877a78bb8a3a Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 8 Jun 2022 04:49:22 +0200 Subject: [PATCH 022/125] Sniff XML docs: add schema to docs Includes adding the `` header if it didn't exist in the file. --- NormalizedArrays/Docs/Arrays/ArrayBraceSpacingStandard.xml | 6 +++++- NormalizedArrays/Docs/Arrays/CommaAfterLastStandard.xml | 6 +++++- Universal/Docs/Arrays/DisallowShortArraySyntaxStandard.xml | 6 +++++- Universal/Docs/Arrays/DuplicateArrayKeyStandard.xml | 6 +++++- Universal/Docs/Arrays/MixedArrayKeyTypesStandard.xml | 6 +++++- Universal/Docs/Arrays/MixedKeyedUnkeyedArrayStandard.xml | 6 +++++- .../Docs/Classes/DisallowAnonClassParenthesesStandard.xml | 6 +++++- Universal/Docs/Classes/DisallowFinalClassStandard.xml | 6 +++++- .../Docs/Classes/RequireAnonClassParenthesesStandard.xml | 6 +++++- Universal/Docs/Classes/RequireFinalClassStandard.xml | 6 +++++- .../Docs/CodeAnalysis/ForeachUniqueAssignmentStandard.xml | 5 ++++- Universal/Docs/CodeAnalysis/StaticInFinalClassStandard.xml | 5 ++++- .../Constants/LowercaseClassResolutionKeywordStandard.xml | 5 ++++- .../Docs/Constants/UppercaseMagicConstantsStandard.xml | 5 ++++- .../ControlStructures/DisallowAlternativeSyntaxStandard.xml | 5 ++++- .../Docs/ControlStructures/DisallowLonelyIfStandard.xml | 5 ++++- .../Docs/ControlStructures/IfElseDeclarationStandard.xml | 5 ++++- Universal/Docs/Lists/DisallowLongListSyntaxStandard.xml | 6 +++++- Universal/Docs/Lists/DisallowShortListSyntaxStandard.xml | 6 +++++- .../Docs/Namespaces/DisallowCurlyBraceSyntaxStandard.xml | 5 ++++- .../Namespaces/DisallowDeclarationWithoutNameStandard.xml | 5 ++++- .../Docs/Namespaces/EnforceCurlyBraceSyntaxStandard.xml | 5 ++++- Universal/Docs/Namespaces/OneDeclarationPerFileStandard.xml | 5 ++++- .../NoReservedKeywordParameterNamesStandard.xml | 5 ++++- .../OOStructures/AlphabeticExtendsImplementsStandard.xml | 6 +++++- Universal/Docs/Operators/DisallowLogicalAndOrStandard.xml | 6 +++++- Universal/Docs/Operators/DisallowShortTernaryStandard.xml | 6 +++++- .../DisallowStandalonePostIncrementDecrementStandard.xml | 6 +++++- Universal/Docs/Operators/StrictComparisonsStandard.xml | 6 +++++- Universal/Docs/Operators/TypeSeparatorSpacingStandard.xml | 5 ++++- Universal/Docs/PHP/OneStatementInShortEchoTagStandard.xml | 6 +++++- Universal/Docs/UseStatements/DisallowUseClassStandard.xml | 5 ++++- Universal/Docs/UseStatements/DisallowUseConstStandard.xml | 5 ++++- .../Docs/UseStatements/DisallowUseFunctionStandard.xml | 5 ++++- .../Docs/UseStatements/LowercaseFunctionConstStandard.xml | 6 +++++- Universal/Docs/UseStatements/NoLeadingBackslashStandard.xml | 6 +++++- .../Docs/WhiteSpace/AnonClassKeywordSpacingStandard.xml | 5 ++++- Universal/Docs/WhiteSpace/DisallowInlineTabsStandard.xml | 6 +++++- Universal/Docs/WhiteSpace/PrecisionAlignmentStandard.xml | 5 ++++- 39 files changed, 177 insertions(+), 39 deletions(-) diff --git a/NormalizedArrays/Docs/Arrays/ArrayBraceSpacingStandard.xml b/NormalizedArrays/Docs/Arrays/ArrayBraceSpacingStandard.xml index 28ae1bad..e8a006f0 100644 --- a/NormalizedArrays/Docs/Arrays/ArrayBraceSpacingStandard.xml +++ b/NormalizedArrays/Docs/Arrays/ArrayBraceSpacingStandard.xml @@ -1,4 +1,8 @@ - + + + + no comma after the last array item. diff --git a/Universal/Docs/Arrays/DisallowShortArraySyntaxStandard.xml b/Universal/Docs/Arrays/DisallowShortArraySyntaxStandard.xml index e37839e8..4a84b536 100644 --- a/Universal/Docs/Arrays/DisallowShortArraySyntaxStandard.xml +++ b/Universal/Docs/Arrays/DisallowShortArraySyntaxStandard.xml @@ -1,4 +1,8 @@ - + + + + + + + + + + + + + + + + - + - + - + - + - + - + - + + + + + - + - + - + - + - + + + + + + + + + + + - + + + - + - + - + + + + + - + + + - + Date: Thu, 13 Oct 2022 03:29:53 +0200 Subject: [PATCH 023/125] Composer: up the minimum PHPCS version to 3.7.1 Raise the minimum supported PHPCS version to PHPCS 3.7.1, in line with PHPCSUtils. Includes updating the GH Actions matrixes for this change. Refs: https://github.com/squizlabs/PHP_CodeSniffer/releases/tag/3.7.1 --- .github/workflows/quicktest.yml | 2 +- .github/workflows/test.yml | 4 ++-- README.md | 2 +- composer.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/quicktest.yml b/.github/workflows/quicktest.yml index 02b815c8..750ed3bc 100644 --- a/.github/workflows/quicktest.yml +++ b/.github/workflows/quicktest.yml @@ -29,7 +29,7 @@ jobs: strategy: matrix: php: ['5.4', 'latest'] - phpcs_version: ['3.7.0', 'dev-master'] + phpcs_version: ['3.7.1', 'dev-master'] name: "QTest${{ matrix.phpcs_version == 'dev-master' && ' + Lint' || '' }}: PHP ${{ matrix.php }} - PHPCS ${{ matrix.phpcs_version }}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 689dca7c..1dda147e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: # # The matrix is set up so as not to duplicate the builds which are run for code coverage. php: ['5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '8.0', '8.1'] - phpcs_version: ['3.7.0', 'dev-master'] + phpcs_version: ['3.7.1', 'dev-master'] experimental: [false] include: @@ -137,7 +137,7 @@ jobs: matrix: # 7.4 should be updated to 8.0 when higher PHPUnit versions can be supported. php: ['5.4', '7.4'] - phpcs_version: ['3.7.0', 'dev-master'] + phpcs_version: ['3.7.1', 'dev-master'] name: "Coverage${{ matrix.phpcs_version == 'dev-master' && ' + Lint' || '' }}: PHP ${{ matrix.php }} - PHPCS ${{ matrix.phpcs_version }}" diff --git a/README.md b/README.md index 551b9b63..5d3e3ea2 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Minimum Requirements ------------------------------------------- * PHP 5.4 or higher. -* [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) version **3.7.0** or higher. +* [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) version **3.7.1** or higher. * [PHPCSUtils](https://github.com/PHPCSStandards/PHPCSUtils) version **1.0.0** or higher. diff --git a/composer.json b/composer.json index 8af28dbf..4d028506 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ }, "require" : { "php" : ">=5.4", - "squizlabs/php_codesniffer" : "^3.7.0", + "squizlabs/php_codesniffer" : "^3.7.1", "dealerdirect/phpcodesniffer-composer-installer" : "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7", "phpcsstandards/phpcsutils" : "^1.0 || dev-develop" }, From ac0b76111a2c7eb6c6342f4013415b32e1e80661 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 13 Oct 2022 04:13:52 +0200 Subject: [PATCH 024/125] Changelog: improve maintainability and source readability ... by moving links to link lists under each release. --- CHANGELOG.md | 71 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e70a79bb..3b7fc3a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,18 +20,18 @@ _Nothing yet._ #### Universal -* :wrench: :books: New `Universal.Arrays.DisallowShortArraySyntax` sniff to disallow short array syntax. [#40](https://github.com/PHPCSStandards/PHPCSExtra/pull/40) +* :wrench: :books: New `Universal.Arrays.DisallowShortArraySyntax` sniff to disallow short array syntax. [#40] In contrast to the PHPCS native `Generic.Arrays.DisallowShortArraySyntax` sniff, this sniff will ignore short list syntax and not cause parse errors when the fixer is used. -* :wrench: :bar_chart: :books: New `Universal.Constants.UppercaseMagicConstants` sniff to enforce that PHP native magic constants are in uppercase. [#64](https://github.com/PHPCSStandards/PHPCSExtra/pull/64) -* :bar_chart: :books: New `Universal.Namespaces.DisallowDeclarationWithoutName` sniff to disallow namespace declarations without a namespace name. [#50](https://github.com/PHPCSStandards/PHPCSExtra/pull/50) -* :bar_chart: :books: New `Universal.Operators.DisallowLogicalAndOr` sniff to enforce the use of the boolean `&&` and `||` operators instead of the logical `and`/`or` operators. [#52](https://github.com/PHPCSStandards/PHPCSExtra/pull/52) - Note: as the [operator precedence](https://www.php.net/manual/en/language.operators.precedence.php) of the logical operators is significantly lower than the operator precedence of boolean operators, this sniff does not contain an auto-fixer. -* :bar_chart: :books: New `Universal.Operators.DisallowShortTernary` sniff to disallow the use of short ternaries `?:`. [#42](https://github.com/PHPCSStandards/PHPCSExtra/pull/42) +* :wrench: :bar_chart: :books: New `Universal.Constants.UppercaseMagicConstants` sniff to enforce that PHP native magic constants are in uppercase. [#64] +* :bar_chart: :books: New `Universal.Namespaces.DisallowDeclarationWithoutName` sniff to disallow namespace declarations without a namespace name. [#50] +* :bar_chart: :books: New `Universal.Operators.DisallowLogicalAndOr` sniff to enforce the use of the boolean `&&` and `||` operators instead of the logical `and`/`or` operators. [#52] + Note: as the [operator precedence] of the logical operators is significantly lower than the operator precedence of boolean operators, this sniff does not contain an auto-fixer. +* :bar_chart: :books: New `Universal.Operators.DisallowShortTernary` sniff to disallow the use of short ternaries `?:`. [#42] While short ternaries are useful when used correctly, the principle of them is often misunderstood and they are more often than not used incorrectly, leading to hard to debug issues and/or PHP warnings/notices. -* :wrench: :bar_chart: :books: New `Universal.Operators.DisallowStandalonePostIncrementDecrement` sniff disallow the use of post-in/decrements in stand-alone statements and discourage the use of multiple increment/decrement operators in a stand-alone statement. [#65](https://github.com/PHPCSStandards/PHPCSExtra/pull/65) -* :wrench: :bar_chart: :books: New `Universal.Operators.StrictComparisons` sniff to enforce the use of strict comparisons. [#48](https://github.com/PHPCSStandards/PHPCSExtra/pull/48) +* :wrench: :bar_chart: :books: New `Universal.Operators.DisallowStandalonePostIncrementDecrement` sniff disallow the use of post-in/decrements in stand-alone statements and discourage the use of multiple increment/decrement operators in a stand-alone statement. [#65] +* :wrench: :bar_chart: :books: New `Universal.Operators.StrictComparisons` sniff to enforce the use of strict comparisons. [#48] Warning: the auto-fixer for this sniff _may_ cause bugs in applications and should be used with care! This is considered a _risky_ fixer. -* :wrench: :bar_chart: :books: New `Universal.OOStructures.AlphabeticExtendsImplements` sniff to verify that the names used in a class "implements" statement or an interface "extends" statement are listed in alphabetic order. [#55](https://github.com/PHPCSStandards/PHPCSExtra/pull/55) +* :wrench: :bar_chart: :books: New `Universal.OOStructures.AlphabeticExtendsImplements` sniff to verify that the names used in a class "implements" statement or an interface "extends" statement are listed in alphabetic order. [#55] * This sniff contains a public `orderby` property to determine the sort order to use for the statement. If all names used are unqualified, the sort order won't make a difference. However, if one or more of the names are partially or fully qualified, the chosen sort order will determine how the sorting between unqualified, partially and fully qualified names is handled. @@ -47,38 +47,53 @@ _Nothing yet._ * When fixing, the existing spacing between the names in an `implements`/`extends` statement will not be maintained. The fixer will separate each name with a comma and one space. If alternative formatting is desired, a sniff which will check and fix the formatting should be added to the ruleset. -* :wrench: :bar_chart: :books: New `Universal.UseStatements.LowercaseFunctionConst` sniff to enforce that `function` and `const` keywords when used in an import `use` statement are always lowercase. [#58](https://github.com/PHPCSStandards/PHPCSExtra/pull/58) -* :wrench: :bar_chart: :books: New `Universal.UseStatements.NoLeadingBackslash` sniff to verify that a name being imported in an import `use` statement does not start with a leading backslash. [#46](https://github.com/PHPCSStandards/PHPCSExtra/pull/46) +* :wrench: :bar_chart: :books: New `Universal.UseStatements.LowercaseFunctionConst` sniff to enforce that `function` and `const` keywords when used in an import `use` statement are always lowercase. [#58] +* :wrench: :bar_chart: :books: New `Universal.UseStatements.NoLeadingBackslash` sniff to verify that a name being imported in an import `use` statement does not start with a leading backslash. [#46] Names in import `use` statements should always be fully qualified, so a leading backslash is not needed and it is strongly recommended not to use one. This sniff handles all types of import use statements supported by PHP, in contrast to other sniffs for the same in, for instance, the PSR12 or the Slevomat standard, which are incomplete. -* :wrench: :books: New `Universal.WhiteSpace.DisallowInlineTabs` sniff to enforce using spaces for mid-line alignment. [#43](https://github.com/PHPCSStandards/PHPCSExtra/pull/43) +* :wrench: :books: New `Universal.WhiteSpace.DisallowInlineTabs` sniff to enforce using spaces for mid-line alignment. [#43] ### Changed #### Other * The `master` branch has been renamed to `stable`. -* Composer: The version requirements for the [Composer PHPCS plugin] have been widened to allow for version 0.7.0 which supports Composer 2.0.0. [#62](https://github.com/PHPCSStandards/PHPCSExtra/pull/62) +* Composer: The version requirements for the [Composer PHPCS plugin] have been widened to allow for version 0.7.0 which supports Composer 2.0.0. [#62] * Various housekeeping. +[#40]: https://github.com/PHPCSStandards/PHPCSExtra/pull/40 +[#42]: https://github.com/PHPCSStandards/PHPCSExtra/pull/42 +[#43]: https://github.com/PHPCSStandards/PHPCSExtra/pull/43 +[#46]: https://github.com/PHPCSStandards/PHPCSExtra/pull/46 +[#48]: https://github.com/PHPCSStandards/PHPCSExtra/pull/48 +[#50]: https://github.com/PHPCSStandards/PHPCSExtra/pull/50 +[#52]: https://github.com/PHPCSStandards/PHPCSExtra/pull/52 +[#55]: https://github.com/PHPCSStandards/PHPCSExtra/pull/55 +[#58]: https://github.com/PHPCSStandards/PHPCSExtra/pull/58 +[#62]: https://github.com/PHPCSStandards/PHPCSExtra/pull/62 +[#64]: https://github.com/PHPCSStandards/PHPCSExtra/pull/64 +[#65]: https://github.com/PHPCSStandards/PHPCSExtra/pull/65 + +[operator precedence]: https://www.php.net/manual/en/language.operators.precedence.php + ## [1.0.0-alpha2] - 2020-02-18 ### Added #### Universal -* :wrench: :bar_chart: :books: New `Universal.ControlStructures.DisallowAlternativeSyntax` sniff to disallow using the alternative syntax for control structures. [#23](https://github.com/PHPCSStandards/PHPCSExtra/pull/23) +* :wrench: :bar_chart: :books: New `Universal.ControlStructures.DisallowAlternativeSyntax` sniff to disallow using the alternative syntax for control structures. [#23] - This sniff contains a `allowWithInlineHTML` property to allow alternative syntax when inline HTML is used within the control structure. In all other cases, the use of the alternative syntax will still be disallowed. - The sniff has modular error codes to allow for making exceptions based on specific control structures and/or specific control structures in combination with inline HTML. -* :bar_chart: `Universal.UseStatements.DisallowUseClass/Function/Const`: new, additional metrics about the import source will be shown in the `info` report. [#25](https://github.com/PHPCSStandards/PHPCSExtra/pull/25) +* :bar_chart: `Universal.UseStatements.DisallowUseClass/Function/Const`: new, additional metrics about the import source will be shown in the `info` report. [#25] #### Other -* Readme: installation instructions and sniff list. [#26](https://github.com/PHPCSStandards/PHPCSExtra/pull/26) +* Readme: installation instructions and sniff list. [#26] ### Changed #### Universal -* `Universal.Arrays.DuplicateArrayKey`: wording of the error message. [#18](https://github.com/PHPCSStandards/PHPCSExtra/pull/18) -* `Universal.UseStatements.DisallowUseClass/Function/Const`: the error codes have been made more modular. [#25](https://github.com/PHPCSStandards/PHPCSExtra/pull/25) +* `Universal.Arrays.DuplicateArrayKey`: wording of the error message. [#18] +* `Universal.UseStatements.DisallowUseClass/Function/Const`: the error codes have been made more modular. [#25] Each of these sniffs now has four additional error codes:
  • FoundSameNamespace, FoundSameNamespaceWithAlias for use statements importing from the same namespace;
  • @@ -87,17 +102,25 @@ _Nothing yet._ In all other circumstances, the existing error codes FoundWithAlias and FoundWithoutAlias will continue to be used. #### Other -* Improved formatting of the CLI documentation which can be viewed using `--generator=text`. [#17](https://github.com/PHPCSStandards/PHPCSExtra/pull/17) +* Improved formatting of the CLI documentation which can be viewed using `--generator=text`. [#17] * Various housekeeping. ### Fixed #### Universal -* `Universal.Arrays.DuplicateArrayKey`: improved handling of parse errors. [#34](https://github.com/PHPCSStandards/PHPCSExtra/pull/34) -* `Universal.ControlStructures.IfElseDeclaration`: the fixer will now respect tab indentation. [#19](https://github.com/PHPCSStandards/PHPCSExtra/pull/19) -* `Universal.UseStatements.DisallowUseClass/Function/Const`: the determination of whether a import is aliased in now done in a case-insensitive manner. [#25](https://github.com/PHPCSStandards/PHPCSExtra/pull/25) -* `Universal.UseStatements.DisallowUseClass/Function/Const`: an import from the global namespace would previously always be seen as non-aliased, even when it was aliased. [#25](https://github.com/PHPCSStandards/PHPCSExtra/pull/25) -* `Universal.UseStatements.DisallowUseClass/Function/Const`: improved tolerance for `use` import statements with leading backslashes. [#25](https://github.com/PHPCSStandards/PHPCSExtra/pull/25) +* `Universal.Arrays.DuplicateArrayKey`: improved handling of parse errors. [#34] +* `Universal.ControlStructures.IfElseDeclaration`: the fixer will now respect tab indentation. [#19] +* `Universal.UseStatements.DisallowUseClass/Function/Const`: the determination of whether a import is aliased in now done in a case-insensitive manner. [#25] +* `Universal.UseStatements.DisallowUseClass/Function/Const`: an import from the global namespace would previously always be seen as non-aliased, even when it was aliased. [#25] +* `Universal.UseStatements.DisallowUseClass/Function/Const`: improved tolerance for `use` import statements with leading backslashes. [#25] + +[#17]: https://github.com/PHPCSStandards/PHPCSExtra/pull/17 +[#18]: https://github.com/PHPCSStandards/PHPCSExtra/pull/18 +[#19]: https://github.com/PHPCSStandards/PHPCSExtra/pull/19 +[#23]: https://github.com/PHPCSStandards/PHPCSExtra/pull/23 +[#25]: https://github.com/PHPCSStandards/PHPCSExtra/pull/25 +[#26]: https://github.com/PHPCSStandards/PHPCSExtra/pull/26 +[#34]: https://github.com/PHPCSStandards/PHPCSExtra/pull/34 ## 1.0.0-alpha1 - 2020-01-23 From da74f92ca403e92c461b1a04cacbdfe784340a2e Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 14 Oct 2022 21:48:26 +0200 Subject: [PATCH 025/125] GH Actions: fix use of deprecated `set-output` GitHub has deprecated the use of `set-output` (and `set-state`) in favour of new environment files. This commit updates workflows to use the new methodology. Refs: * https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/ * https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#environment-files --- .github/workflows/quicktest.yml | 4 ++-- .github/workflows/test.yml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/quicktest.yml b/.github/workflows/quicktest.yml index 750ed3bc..d26b9730 100644 --- a/.github/workflows/quicktest.yml +++ b/.github/workflows/quicktest.yml @@ -43,9 +43,9 @@ jobs: id: set_ini run: | if [ "${{ matrix.phpcs_version }}" != "dev-master" ]; then - echo '::set-output name=PHP_INI::error_reporting=E_ALL & ~E_DEPRECATED, display_errors=On' + echo 'PHP_INI=error_reporting=E_ALL & ~E_DEPRECATED, display_errors=On' >> $GITHUB_OUTPUT else - echo '::set-output name=PHP_INI::error_reporting=-1, display_errors=On' + echo 'PHP_INI=error_reporting=-1, display_errors=On' >> $GITHUB_OUTPUT fi - name: Install PHP diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1dda147e..40516807 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -68,9 +68,9 @@ jobs: id: set_ini run: | if [[ "${{ matrix.phpcs_version }}" != "dev-master" && "${{ matrix.phpcs_version }}" != "4.0.x-dev" ]]; then - echo '::set-output name=PHP_INI::error_reporting=E_ALL & ~E_DEPRECATED, display_errors=On' + echo 'PHP_INI=error_reporting=E_ALL & ~E_DEPRECATED, display_errors=On' >> $GITHUB_OUTPUT else - echo '::set-output name=PHP_INI::error_reporting=-1, display_errors=On' + echo 'PHP_INI=error_reporting=-1, display_errors=On' >> $GITHUB_OUTPUT fi - name: Install PHP @@ -151,9 +151,9 @@ jobs: id: set_ini run: | if [ "${{ matrix.phpcs_version }}" != "dev-master" ]; then - echo '::set-output name=PHP_INI::error_reporting=E_ALL & ~E_DEPRECATED, display_errors=On' + echo 'PHP_INI=error_reporting=E_ALL & ~E_DEPRECATED, display_errors=On' >> $GITHUB_OUTPUT else - echo '::set-output name=PHP_INI::error_reporting=-1, display_errors=On' + echo 'PHP_INI=error_reporting=-1, display_errors=On' >> $GITHUB_OUTPUT fi - name: Install PHP From d901f9b3a9d697bfc65ef9cd34a0d329171e8d07 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 14 Oct 2022 21:48:51 +0200 Subject: [PATCH 026/125] GH Actions: update the xmllint-problem-matcher The `xmllint-problem-matcher` action runner has released a new version which updates it to use node 16. This gets rid of a warning which was shown in the action logs. Refs: * https://github.com/korelstar/xmllint-problem-matcher/releases/tag/v1.1 --- .github/workflows/basics.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml index 860eada9..bd89834b 100644 --- a/.github/workflows/basics.yml +++ b/.github/workflows/basics.yml @@ -59,7 +59,7 @@ jobs: # Show XML violations inline in the file diff. # @link https://github.com/marketplace/actions/xmllint-problem-matcher - - uses: korelstar/xmllint-problem-matcher@v1 + - uses: korelstar/xmllint-problem-matcher@v1.1 # Validate the Ruleset XML file. # @link http://xmlsoft.org/xmllint.html From 9fb1b07be55c33fcf38a3b1e05e13d1445c698f2 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 23 Oct 2022 10:44:27 +0200 Subject: [PATCH 027/125] GH Actions/basics: revert to xmllint-problem-matcher v1 As the `korelstar/xmllint-problem-matcher` repo now has a long-running `v1` branch, this update which was included in PR 132 is no longer needed (and would necessitate more frequent updates if it would remain). Ref: * korelstar/xmllint-problem-matcher 7 --- .github/workflows/basics.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml index bd89834b..860eada9 100644 --- a/.github/workflows/basics.yml +++ b/.github/workflows/basics.yml @@ -59,7 +59,7 @@ jobs: # Show XML violations inline in the file diff. # @link https://github.com/marketplace/actions/xmllint-problem-matcher - - uses: korelstar/xmllint-problem-matcher@v1.1 + - uses: korelstar/xmllint-problem-matcher@v1 # Validate the Ruleset XML file. # @link http://xmlsoft.org/xmllint.html From ed39208e7574ed2f308ed3186802bb67ddfe4393 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 23 Oct 2022 10:48:35 +0200 Subject: [PATCH 028/125] GH Actions: harden the workflow against PHPCS ruleset errors If there is a ruleset error, the `cs2pr` action doesn't receive an `xml` report and exits with a `0` error code, even though the PHPCS run failed (though not on CS errors, but on a ruleset error). This changes the GH Actions workflow to allow for that situation and still fail the build in that case. --- .github/workflows/basics.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml index 860eada9..b864d8bf 100644 --- a/.github/workflows/basics.yml +++ b/.github/workflows/basics.yml @@ -78,10 +78,11 @@ jobs: # Check the code-style consistency of the PHP files. - name: Check PHP code style - continue-on-error: true + id: phpcs run: vendor/bin/phpcs --report-full --report-checkstyle=./phpcs-report.xml - name: Show PHPCS results in PR + if: ${{ always() && steps.phpcs.outcome == 'failure' }} run: cs2pr ./phpcs-report.xml # Check that the sniffs available are feature complete. From b1620ea3c1d81017897fc65369e1c370ad1cc092 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 13 Oct 2022 07:14:50 +0200 Subject: [PATCH 029/125] Upgrade to PHPCSUtils 1.0.0-alpha4 Take advantage of new features in PHPCSUtils 1.0.0-alpha4. Ref: https://github.com/PHPCSStandards/PHPCSUtils/releases/tag/1.0.0-alpha4 --- .../Sniffs/Arrays/ArrayBraceSpacingSniff.php | 7 ++----- .../Sniffs/Arrays/CommaAfterLastSniff.php | 7 ++----- .../Sniffs/Classes/DisallowFinalClassSniff.php | 8 ++------ .../Constants/UppercaseMagicConstantsSniff.php | 4 ++-- .../DisallowAlternativeSyntaxSniff.php | 13 ++----------- .../NoReservedKeywordParameterNamesSniff.php | 3 +-- .../AlphabeticExtendsImplementsSniff.php | 6 +++--- ...allowStandalonePostIncrementDecrementSniff.php | 15 ++++++++------- .../Sniffs/WhiteSpace/DisallowInlineTabsSniff.php | 6 ++---- .../Sniffs/WhiteSpace/PrecisionAlignmentSniff.php | 6 ++---- 10 files changed, 26 insertions(+), 49 deletions(-) diff --git a/NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php b/NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php index db8e9489..d09a336a 100644 --- a/NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php +++ b/NormalizedArrays/Sniffs/Arrays/ArrayBraceSpacingSniff.php @@ -14,6 +14,7 @@ use PHP_CodeSniffer\Sniffs\Sniff; use PHP_CodeSniffer\Util\Tokens; use PHPCSUtils\Fixers\SpacesFixer; +use PHPCSUtils\Tokens\Collections; use PHPCSUtils\Utils\Arrays; /** @@ -105,11 +106,7 @@ final class ArrayBraceSpacingSniff implements Sniff */ public function register() { - return [ - \T_ARRAY, - \T_OPEN_SHORT_ARRAY, - \T_OPEN_SQUARE_BRACKET, - ]; + return Collections::arrayOpenTokensBC(); } /** diff --git a/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php b/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php index 697ec620..0560fe88 100644 --- a/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php +++ b/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php @@ -13,6 +13,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Tokens\Collections; use PHPCSUtils\Utils\Arrays; /** @@ -81,11 +82,7 @@ final class CommaAfterLastSniff implements Sniff */ public function register() { - return [ - \T_ARRAY, - \T_OPEN_SHORT_ARRAY, - \T_OPEN_SQUARE_BRACKET, - ]; + return Collections::arrayOpenTokensBC(); } /** diff --git a/Universal/Sniffs/Classes/DisallowFinalClassSniff.php b/Universal/Sniffs/Classes/DisallowFinalClassSniff.php index 326b2f7a..f1ae0ed1 100644 --- a/Universal/Sniffs/Classes/DisallowFinalClassSniff.php +++ b/Universal/Sniffs/Classes/DisallowFinalClassSniff.php @@ -12,8 +12,8 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; -use PHPCSUtils\BackCompat\BCFile; use PHPCSUtils\Utils\GetTokensAsString; +use PHPCSUtils\Utils\ObjectDeclarations; /** * Forbids classes from being declared as "final". @@ -50,11 +50,7 @@ public function register() */ public function process(File $phpcsFile, $stackPtr) { - /* - * Deliberately using the BCFile version of getClassProperties to allow - * for handling the parse error of classes declared as both final + abstract. - */ - $classProp = BCFile::getClassProperties($phpcsFile, $stackPtr); + $classProp = ObjectDeclarations::getClassProperties($phpcsFile, $stackPtr); if ($classProp['is_final'] === false) { if ($classProp['is_abstract'] === true) { $phpcsFile->recordMetric($stackPtr, 'Class declaration type', 'abstract'); diff --git a/Universal/Sniffs/Constants/UppercaseMagicConstantsSniff.php b/Universal/Sniffs/Constants/UppercaseMagicConstantsSniff.php index ad5815b1..1659f958 100644 --- a/Universal/Sniffs/Constants/UppercaseMagicConstantsSniff.php +++ b/Universal/Sniffs/Constants/UppercaseMagicConstantsSniff.php @@ -12,7 +12,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; -use PHPCSUtils\Tokens\Collections; +use PHP_CodeSniffer\Util\Tokens; /** * Verifies that PHP native `__...__` magic constants are in uppercase when used. @@ -33,7 +33,7 @@ final class UppercaseMagicConstantsSniff implements Sniff */ public function register() { - return Collections::$magicConstants; + return Tokens::$magicConstants; } /** diff --git a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php index 0a2535ff..5b7b1663 100644 --- a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php +++ b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php @@ -41,16 +41,7 @@ final class DisallowAlternativeSyntaxSniff implements Sniff */ public function register() { - return [ - \T_IF, - \T_ELSE, - \T_ELSEIF, - \T_FOR, - \T_FOREACH, - \T_SWITCH, - \T_WHILE, - \T_DECLARE, - ]; + return Collections::alternativeControlStructureSyntaxes(); } /** @@ -142,7 +133,7 @@ public function process(File $phpcsFile, $stackPtr) $phpcsFile->fixer->beginChangeset(); $phpcsFile->fixer->replaceToken($opener, '{'); - if (isset(Collections::$alternativeControlStructureSyntaxCloserTokens[$tokens[$closer]['code']]) === true) { + if (isset(Collections::alternativeControlStructureSyntaxClosers()[$tokens[$closer]['code']]) === true) { $phpcsFile->fixer->replaceToken($closer, '}'); $semicolon = $phpcsFile->findNext(Tokens::$emptyTokens, ($closer + 1), null, true); diff --git a/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php b/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php index 373be90a..555e0575 100644 --- a/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php +++ b/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php @@ -144,7 +144,7 @@ final class NoReservedKeywordParameterNamesSniff implements Sniff */ public function register() { - return Collections::functionDeclarationTokensBC(); + return Collections::functionDeclarationTokens(); } /** @@ -171,7 +171,6 @@ public function process(File $phpcsFile, $stackPtr) return; } } catch (RuntimeException $e) { - // Most likely a T_STRING which wasn't an arrow function. return; } diff --git a/Universal/Sniffs/OOStructures/AlphabeticExtendsImplementsSniff.php b/Universal/Sniffs/OOStructures/AlphabeticExtendsImplementsSniff.php index a397a6a3..a35e9a5c 100644 --- a/Universal/Sniffs/OOStructures/AlphabeticExtendsImplementsSniff.php +++ b/Universal/Sniffs/OOStructures/AlphabeticExtendsImplementsSniff.php @@ -86,7 +86,7 @@ final class AlphabeticExtendsImplementsSniff implements Sniff */ public function register() { - return (Collections::$OOCanExtend + Collections::$OOCanImplement); + return (Collections::ooCanExtend() + Collections::ooCanImplement()); } /** @@ -122,7 +122,7 @@ public function process(File $phpcsFile, $stackPtr) /* * Get the names. */ - if (isset(Collections::$OOCanImplement[$tokens[$stackPtr]['code']]) === true) { + if (isset(Collections::ooCanImplement()[$tokens[$stackPtr]['code']]) === true) { $names = ObjectDeclarations::findImplementedInterfaceNames($phpcsFile, $stackPtr); } else { $names = ObjectDeclarations::findExtendedInterfaceNames($phpcsFile, $stackPtr); @@ -165,7 +165,7 @@ public function process(File $phpcsFile, $stackPtr) * Throw the error. */ $keyword = \T_IMPLEMENTS; - if (isset(Collections::$OOCanImplement[$tokens[$stackPtr]['code']]) === false) { + if (isset(Collections::ooCanImplement()[$tokens[$stackPtr]['code']]) === false) { $keyword = \T_EXTENDS; } diff --git a/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php b/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php index 90d28753..dbea94b8 100644 --- a/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php +++ b/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php @@ -53,11 +53,11 @@ final class DisallowStandalonePostIncrementDecrementSniff implements Sniff */ public function register() { - $this->allowedTokens += Collections::$OOHierarchyKeywords; - $this->allowedTokens += Collections::$objectOperators; - $this->allowedTokens += Collections::$OONameTokens; + $this->allowedTokens += Collections::ooHierarchyKeywords(); + $this->allowedTokens += Collections::objectOperators(); + $this->allowedTokens += Collections::namespacedNameTokens(); - return Collections::$incrementDecrementOperators; + return Collections::incrementDecrementOperators(); } /** @@ -88,14 +88,15 @@ public function process(File $phpcsFile, $stackPtr) return $end; } - $counter = 0; - $lastCode = null; + $counter = 0; + $lastCode = null; + $operators = Collections::incrementDecrementOperators(); for ($i = $start; $i < $end; $i++) { if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) { continue; } - if (isset(Collections::$incrementDecrementOperators[$tokens[$i]['code']]) === true) { + if (isset($operators[$tokens[$i]['code']]) === true) { $lastCode = $tokens[$i]['code']; ++$counter; continue; diff --git a/Universal/Sniffs/WhiteSpace/DisallowInlineTabsSniff.php b/Universal/Sniffs/WhiteSpace/DisallowInlineTabsSniff.php index 56153e60..6eb2b0d2 100644 --- a/Universal/Sniffs/WhiteSpace/DisallowInlineTabsSniff.php +++ b/Universal/Sniffs/WhiteSpace/DisallowInlineTabsSniff.php @@ -15,6 +15,7 @@ use PHP_CodeSniffer\Util\Tokens; use PHPCSExtra\Universal\Helpers\DummyTokenizer; use PHPCSUtils\BackCompat\Helper; +use PHPCSUtils\Tokens\Collections; /** * Enforces using spaces for mid-line alignment. @@ -73,10 +74,7 @@ final class DisallowInlineTabsSniff implements Sniff */ public function register() { - return [ - \T_OPEN_TAG, - \T_OPEN_TAG_WITH_ECHO, - ]; + return Collections::phpOpenTags(); } /** diff --git a/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php b/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php index f9e2914c..918147b9 100644 --- a/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php +++ b/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php @@ -14,6 +14,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Util\Tokens; use PHPCSUtils\BackCompat\Helper; +use PHPCSUtils\Tokens\Collections; /** * Detects when the indentation is not a multiple of a tab-width, i.e. when precision alignment is used. @@ -140,10 +141,7 @@ public function register() // Add the ignore annotation tokens to the list of tokens to check. $this->tokensToCheck += Tokens::$phpcsCommentTokens; - return [ - \T_OPEN_TAG, - \T_OPEN_TAG_WITH_ECHO, - ]; + return Collections::phpOpenTags(); } /** From e20b0a6fb20fcd2b61e263ae0a7f6d5c9fa4c970 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 26 Jul 2021 01:01:48 +0200 Subject: [PATCH 030/125] :sparkles: New `Universal.Files.SeparateFunctionsFromOO` sniff This sniff will enforce that a file should either declare (global/namespaced) functions or declare OO structures, but not both. Nested function declarations, i.e. functions declared within a function/method will be disregarded for the purposes of this sniff. The same goes for anonymous classes, closures and arrow functions. Other notes: - This sniff has no opinion on side effects. If you want to sniff for those, use the PHPCS native `PSR1.Files.SideEffects` sniff. - 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. Includes unit tests. Includes documentation. Includes metrics. --- .../Files/SeparateFunctionsFromOOStandard.xml | 45 +++++ .../Files/SeparateFunctionsFromOOSniff.php | 180 ++++++++++++++++++ .../SeparateFunctionsFromOOUnitTest.1.inc | 40 ++++ .../SeparateFunctionsFromOOUnitTest.2.inc | 41 ++++ .../SeparateFunctionsFromOOUnitTest.3.inc | 48 +++++ .../SeparateFunctionsFromOOUnitTest.4.inc | 50 +++++ .../SeparateFunctionsFromOOUnitTest.5.inc | 35 ++++ .../SeparateFunctionsFromOOUnitTest.6.inc | 11 ++ .../SeparateFunctionsFromOOUnitTest.7.inc | 11 ++ .../Files/SeparateFunctionsFromOOUnitTest.php | 59 ++++++ 10 files changed, 520 insertions(+) create mode 100644 Universal/Docs/Files/SeparateFunctionsFromOOStandard.xml create mode 100644 Universal/Sniffs/Files/SeparateFunctionsFromOOSniff.php create mode 100644 Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.1.inc create mode 100644 Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.2.inc create mode 100644 Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.3.inc create mode 100644 Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.4.inc create mode 100644 Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.5.inc create mode 100644 Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.6.inc create mode 100644 Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.7.inc create mode 100644 Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.php diff --git a/Universal/Docs/Files/SeparateFunctionsFromOOStandard.xml b/Universal/Docs/Files/SeparateFunctionsFromOOStandard.xml new file mode 100644 index 00000000..db0a2671 --- /dev/null +++ b/Universal/Docs/Files/SeparateFunctionsFromOOStandard.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + diff --git a/Universal/Sniffs/Files/SeparateFunctionsFromOOSniff.php b/Universal/Sniffs/Files/SeparateFunctionsFromOOSniff.php new file mode 100644 index 00000000..53b2dff9 --- /dev/null +++ b/Universal/Sniffs/Files/SeparateFunctionsFromOOSniff.php @@ -0,0 +1,180 @@ + \T_START_HEREDOC, + \T_START_NOWDOC => \T_START_NOWDOC, + ]; + + /** + * Returns an array of tokens this test wants to listen for. + * + * @since 1.0.0 + * + * @return array + */ + public function register() + { + $this->search += Tokens::$ooScopeTokens; + $this->search += Collections::functionDeclarationTokens(); + + return Collections::phpOpenTags(); + } + + /** + * Processes this test, when one of its tokens is encountered. + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. + * + * @return void + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + $firstOO = null; + $firstFunction = null; + $functionCount = 0; + $OOCount = 0; + + for ($i = 0; $i < $phpcsFile->numTokens; $i++) { + // Ignore anything within square brackets. + if ($tokens[$i]['code'] !== \T_OPEN_CURLY_BRACKET + && isset($tokens[$i]['bracket_opener'], $tokens[$i]['bracket_closer']) + && $i === $tokens[$i]['bracket_opener'] + ) { + $i = $tokens[$i]['bracket_closer']; + continue; + } + + // Skip past nested arrays, function calls and arbitrary groupings. + if ($tokens[$i]['code'] === \T_OPEN_PARENTHESIS + && isset($tokens[$i]['parenthesis_closer']) + ) { + $i = $tokens[$i]['parenthesis_closer']; + continue; + } + + // Skip over potentially large docblocks. + if ($tokens[$i]['code'] === \T_DOC_COMMENT_OPEN_TAG + && isset($tokens[$i]['comment_closer']) + ) { + $i = $tokens[$i]['comment_closer']; + continue; + } + + // Ignore everything else we're not interested in. + if (isset($this->search[$tokens[$i]['code']]) === false) { + continue; + } + + // Skip over structures which won't contain anything we're interested in. + if (($tokens[$i]['code'] === \T_START_HEREDOC + || $tokens[$i]['code'] === \T_START_NOWDOC + || $tokens[$i]['code'] === \T_ANON_CLASS + || $tokens[$i]['code'] === \T_CLOSURE + || $tokens[$i]['code'] === \T_FN) + && isset($tokens[$i]['scope_condition'], $tokens[$i]['scope_closer']) + && $tokens[$i]['scope_condition'] === $i + ) { + $i = $tokens[$i]['scope_closer']; + continue; + } + + // This will be either a function declaration or an OO declaration token. + if ($tokens[$i]['code'] === \T_FUNCTION) { + if (isset($firstFunction) === false) { + $firstFunction = $i; + } + + ++$functionCount; + } else { + if (isset($firstOO) === false) { + $firstOO = $i; + } + + ++$OOCount; + } + + if (isset($tokens[$i]['scope_closer']) === true) { + $i = $tokens[$i]['scope_closer']; + } + } + + if ($functionCount > 0 && $OOCount > 0) { + $phpcsFile->recordMetric($stackPtr, 'Functions or OO declarations ?', 'Both function and OO declarations'); + + $reportToken = \max($firstFunction, $firstOO); + + $phpcsFile->addError( + 'A file should either contain function declarations or OO structure declarations, but not both.' + . ' Found %d function declaration(s) and %d OO structure declaration(s).' + . ' The first function declaration was found on line %d;' + . ' the first OO declaration was found on line %d', + $reportToken, + 'Mixed', + [ + $functionCount, + $OOCount, + $tokens[$firstFunction]['line'], + $tokens[$firstOO]['line'], + ] + ); + } elseif ($functionCount > 0) { + $phpcsFile->recordMetric($stackPtr, 'Functions or OO declarations ?', 'Only function(s)'); + } elseif ($OOCount > 0) { + $phpcsFile->recordMetric($stackPtr, 'Functions or OO declarations ?', 'Only OO structure(s)'); + } else { + $phpcsFile->recordMetric($stackPtr, 'Functions or OO declarations ?', 'Neither'); + } + + // Ignore the rest of the file. + return ($phpcsFile->numTokens + 1); + } +} diff --git a/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.1.inc b/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.1.inc new file mode 100644 index 00000000..9cab1a16 --- /dev/null +++ b/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.1.inc @@ -0,0 +1,40 @@ + => + */ + public function getErrorList($testFile = '') + { + switch ($testFile) { + case 'SeparateFunctionsFromOOUnitTest.6.inc': + return [ + 7 => 1, + ]; + + case 'SeparateFunctionsFromOOUnitTest.7.inc': + return [ + 11 => 1, + ]; + + default: + return []; + } + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return []; + } +} From a7c61fce99459aa7def78d554a775617cd18df37 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 8 Nov 2020 22:58:58 +0100 Subject: [PATCH 031/125] NormalizedArrays/ArrayBraceSpacing: safeguard upstream bugfix The `SpacesFixer` in PHPCSUtils contains a bugfix for a specific situation which affected this sniff. This adds an extra unit test for the sniff to safeguard that bugfix. Ref: PHPCSStandards/PHPCSUtils 229 --- NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc | 5 +++++ .../Tests/Arrays/ArrayBraceSpacingUnitTest.inc.fixed | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc b/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc index bd79198a..60cb8e93 100644 --- a/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc +++ b/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc @@ -316,3 +316,8 @@ $array = array($a, // phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesWhenEmpty 0 // phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesSingleLine 0 // phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesMultiLine newline + +$foo = array( + 'key' => 'value', // Comment. + // Comment. +); diff --git a/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc.fixed b/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc.fixed index 4445012b..54a2d7fd 100644 --- a/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc.fixed +++ b/NormalizedArrays/Tests/Arrays/ArrayBraceSpacingUnitTest.inc.fixed @@ -306,3 +306,8 @@ $array = array($a, // phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesWhenEmpty 0 // phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesSingleLine 0 // phpcs:set NormalizedArrays.Arrays.ArrayBraceSpacing spacesMultiLine newline + +$foo = array( + 'key' => 'value', // Comment. + // Comment. +); From ec8166e2453791b1aa664f544676ff230fdc6269 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 2 Aug 2020 07:19:52 +0200 Subject: [PATCH 032/125] DisallowStandalonePostIncrementDecrement: prevent looking for nullsafe object operator The PHPCSUtils `Collections::$objectOperators` property has been deprecated in favour of a `Collections::objectOperators()` method to allow for the PHP 8.0 `T_NULLSAFE_OBJECT_OPERATOR` token which may not be available. As the `T_NULLSAFE_OBJECT_OPERATOR` is not allowed in write-context and in/decrement is write-context, remove the token for this sniff. --- .../DisallowStandalonePostIncrementDecrementSniff.php | 6 ++++++ .../DisallowStandalonePostIncrementDecrementUnitTest.inc | 3 +++ ...sallowStandalonePostIncrementDecrementUnitTest.inc.fixed | 3 +++ 3 files changed, 12 insertions(+) diff --git a/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php b/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php index dbea94b8..40908d44 100644 --- a/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php +++ b/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php @@ -57,6 +57,12 @@ public function register() $this->allowedTokens += Collections::objectOperators(); $this->allowedTokens += Collections::namespacedNameTokens(); + /* + * Remove potential nullsafe object operator. In/decrement not allowed in write context, + * so ignore. + */ + unset($this->allowedTokens[\T_NULLSAFE_OBJECT_OPERATOR]); + return Collections::incrementDecrementOperators(); } diff --git a/Universal/Tests/Operators/DisallowStandalonePostIncrementDecrementUnitTest.inc b/Universal/Tests/Operators/DisallowStandalonePostIncrementDecrementUnitTest.inc index cdc14a74..a123dc22 100644 --- a/Universal/Tests/Operators/DisallowStandalonePostIncrementDecrementUnitTest.inc +++ b/Universal/Tests/Operators/DisallowStandalonePostIncrementDecrementUnitTest.inc @@ -70,5 +70,8 @@ $a = 10 + $i++ - 5; return $a['key'][$i++]; +// Intentional parse error. Nullsafe operator not allowed in write-context. Ignore. +$obj?->prop++; + // Intentional parse error. ++; diff --git a/Universal/Tests/Operators/DisallowStandalonePostIncrementDecrementUnitTest.inc.fixed b/Universal/Tests/Operators/DisallowStandalonePostIncrementDecrementUnitTest.inc.fixed index 50b7275b..7f333d98 100644 --- a/Universal/Tests/Operators/DisallowStandalonePostIncrementDecrementUnitTest.inc.fixed +++ b/Universal/Tests/Operators/DisallowStandalonePostIncrementDecrementUnitTest.inc.fixed @@ -70,5 +70,8 @@ $a = 10 + $i++ - 5; return $a['key'][$i++]; +// Intentional parse error. Nullsafe operator not allowed in write-context. Ignore. +$obj?->prop++; + // Intentional parse error. ++; From 542d969adfb28687a7d57dccbba0b77ee360826a Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 26 Oct 2022 07:08:59 +0200 Subject: [PATCH 033/125] :sparkles: New `Universal.CodeAnalysis.ConstructorDestructorReturn` sniff New sniff to verify that class constructor/destructors: * [error] Do not have a return type declaration. This would result in a fatal error in PHP. * [warning] Do not return a value via a return statement. Includes unit tests. Includes documentation. Inspired by: * https://wiki.php.net/rfc/make_ctor_ret_void * https://twitter.com/derickr/status/1280996420305276934 --- .../ConstructorDestructorReturnStandard.xml | 64 ++++++++ .../ConstructorDestructorReturnSniff.php | 145 ++++++++++++++++++ .../ConstructorDestructorReturnUnitTest.inc | 125 +++++++++++++++ .../ConstructorDestructorReturnUnitTest.php | 58 +++++++ 4 files changed, 392 insertions(+) create mode 100644 Universal/Docs/CodeAnalysis/ConstructorDestructorReturnStandard.xml create mode 100644 Universal/Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php create mode 100644 Universal/Tests/CodeAnalysis/ConstructorDestructorReturnUnitTest.inc create mode 100644 Universal/Tests/CodeAnalysis/ConstructorDestructorReturnUnitTest.php diff --git a/Universal/Docs/CodeAnalysis/ConstructorDestructorReturnStandard.xml b/Universal/Docs/CodeAnalysis/ConstructorDestructorReturnStandard.xml new file mode 100644 index 00000000..b44b4777 --- /dev/null +++ b/Universal/Docs/CodeAnalysis/ConstructorDestructorReturnStandard.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + : int {} +} + ]]> + + + + + + + + + + + + $this; + } + + public function __destruct() { + // Do something. + return false; + } +} + ]]> + + + diff --git a/Universal/Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php b/Universal/Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php new file mode 100644 index 00000000..a62830ed --- /dev/null +++ b/Universal/Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php @@ -0,0 +1,145 @@ +addError( + '%s can not declare a return type. Found: %s', + $properties['return_type_token'], + 'ReturnTypeFound', + $data + ); + } + + $tokens = $phpcsFile->getTokens(); + if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) { + // Abstract/interface method, live coding or parse error. + return; + } + + // Check for a value being returned. + $current = $tokens[$stackPtr]['scope_opener']; + $end = $tokens[$stackPtr]['scope_closer']; + + do { + $current = $phpcsFile->findNext(\T_RETURN, ($current + 1), $end); + if ($current === false) { + break; + } + + $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($current + 1), $end, true); + if ($next === false + || $tokens[$next]['code'] === \T_SEMICOLON + || $tokens[$next]['code'] === \T_CLOSE_TAG + ) { + // Return statement without value. + continue; + } + + $endOfStatement = BCFile::findEndOfStatement($phpcsFile, $next); + + $data = [ + $functionType, + GetTokensAsString::compact($phpcsFile, $current, $endOfStatement, true), + ]; + + $phpcsFile->addWarning( + '%s can not return a value. Found: "%s"', + $current, + 'ReturnValueFound', + $data + ); + } while ($current < $end); + } +} diff --git a/Universal/Tests/CodeAnalysis/ConstructorDestructorReturnUnitTest.inc b/Universal/Tests/CodeAnalysis/ConstructorDestructorReturnUnitTest.inc new file mode 100644 index 00000000..79bb4956 --- /dev/null +++ b/Universal/Tests/CodeAnalysis/ConstructorDestructorReturnUnitTest.inc @@ -0,0 +1,125 @@ + => + */ + public function getErrorList() + { + return [ + 85 => 1, + 89 => 1, + 101 => 1, + 116 => 1, + 118 => 1, + 122 => 1, + 124 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return [ + 86 => 1, + 90 => 1, + 95 => 1, + 103 => 1, + 107 => 1, + ]; + } +} From 8b5d5917ad827a1eeac91841849fec821d204252 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 13 Jun 2022 10:23:58 +0200 Subject: [PATCH 034/125] QA: remove some redundant/unused code --- .../NoReservedKeywordParameterNamesSniff.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php b/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php index 555e0575..81af4e0c 100644 --- a/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php +++ b/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php @@ -160,10 +160,6 @@ public function register() */ public function process(File $phpcsFile, $stackPtr) { - $tokens = $phpcsFile->getTokens(); - $content = $tokens[$stackPtr]['content']; - $contentLC = \strtolower($content); - // Get all parameters from method signature. try { $parameters = FunctionDeclarations::getParameters($phpcsFile, $stackPtr); From 28326cd8f7a4b42837ced11146104992de0d2337 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 1 Jul 2022 14:24:49 +0200 Subject: [PATCH 035/125] CS: minor fixes --- .../Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Universal/Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php b/Universal/Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php index ff06bc97..3e62bb5b 100644 --- a/Universal/Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php +++ b/Universal/Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php @@ -35,7 +35,7 @@ final class LowercaseClassResolutionKeywordSniff implements Sniff public function register() { return [ - \T_STRING + \T_STRING, ]; } From 39bc8e49170e3978dd62e51377dbd9df36fdc903 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 26 Oct 2022 08:34:14 +0200 Subject: [PATCH 036/125] Documentation: various minor fixes --- .github/workflows/quicktest.yml | 2 +- .github/workflows/test.yml | 2 +- README.md | 4 ++-- Universal/Sniffs/Operators/TypeSeparatorSpacingSniff.php | 4 ++-- Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/quicktest.yml b/.github/workflows/quicktest.yml index d26b9730..46a8639c 100644 --- a/.github/workflows/quicktest.yml +++ b/.github/workflows/quicktest.yml @@ -1,7 +1,7 @@ name: Quicktest on: - # Run on pushes, including merges, to all branches except `master`. + # Run on pushes, including merges, to all branches except for `stable` and `develop`. push: branches-ignore: - stable diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 40516807..1a362bef 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,7 +1,7 @@ name: Test on: - # Run on pushes to `master` and on all pull requests. + # Run on pushes to `stable` and `develop` and on all pull requests. push: branches: - stable diff --git a/README.md b/README.md index 5d3e3ea2..2794e830 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ Detects duplicate array keys in array declarations. #### `Universal.Arrays.MixedArrayKeyTypes` :books: -Best practice sniff: don't use a mix of integer and numeric keys for array items. +Best practice sniff: don't use a mix of integer and string keys for array items. #### `Universal.Arrays.MixedKeyedUnkeyedArray` :books: @@ -152,7 +152,7 @@ Enforce uppercase when using PHP native magic constants, like `__FILE__` et al. Disallow using the alternative syntax for control structures. * This sniff contains an `allowWithInlineHTML` property to allow alternative syntax when inline HTML is used within the control structure. In all other cases, the use of the alternative syntax will still be disallowed. - Acceped values: (bool) `true`|`false`. Defaults to `false`. + Accepted values: (bool) `true`|`false`. Defaults to `false`. * The sniff has modular error codes to allow for making exceptions based on specific control structures and/or specific control structures in combination with inline HTML. The error codes follow the following pattern: `Found[ControlStructure][WithInlineHTML]`. Examples: `FoundIf`, `FoundSwitchWithInlineHTML`. diff --git a/Universal/Sniffs/Operators/TypeSeparatorSpacingSniff.php b/Universal/Sniffs/Operators/TypeSeparatorSpacingSniff.php index b5f9d18c..cfed01dc 100644 --- a/Universal/Sniffs/Operators/TypeSeparatorSpacingSniff.php +++ b/Universal/Sniffs/Operators/TypeSeparatorSpacingSniff.php @@ -65,7 +65,7 @@ public function process(File $phpcsFile, $stackPtr) 'Expected %s before the ' . $type . ' type separator. Found: %s', $code . 'SpacesBefore', 'error', - 0, + 0, // Severity. 'Space before ' . $type . ' type separator' ); @@ -78,7 +78,7 @@ public function process(File $phpcsFile, $stackPtr) 'Expected %s after the ' . $type . ' type separator. Found: %s', $code . 'SpacesAfter', 'error', - 0, + 0, // Severity. 'Space after ' . $type . ' type separator' ); } diff --git a/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php b/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php index 918147b9..39c7464c 100644 --- a/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php +++ b/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php @@ -70,7 +70,7 @@ final class PrecisionAlignmentSniff implements Sniff * * Example usage: * ```xml - * + * * * * From c81abd2dc011157010bd95daa2954e6738706024 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 26 Oct 2022 09:51:16 +0200 Subject: [PATCH 037/125] DisallowInlineTabsUnitTest: skip on PHP 5.5 For some weird and unknown reason, the tests for this sniff have started to fail intermittently in CI on PHP 5.5 (and only on PHP 5.5). * The tests pass on PHP 5.4 and PHP 5.6-8.2. * The tests regularly pass on PHP 5.5 after restarting the build (?!). * The tests pass locally on PHP 5.5 when running with the exact same PHP version and PHPUnit versions. In other words, this is one of those mystery bugs. For now, I'm just going to skip running these tests on PHP 5.5 as the build failures are blocking PRs for unrelated features. --- .../WhiteSpace/DisallowInlineTabsUnitTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Universal/Tests/WhiteSpace/DisallowInlineTabsUnitTest.php b/Universal/Tests/WhiteSpace/DisallowInlineTabsUnitTest.php index 57e322e2..410eaedd 100644 --- a/Universal/Tests/WhiteSpace/DisallowInlineTabsUnitTest.php +++ b/Universal/Tests/WhiteSpace/DisallowInlineTabsUnitTest.php @@ -22,6 +22,24 @@ final class DisallowInlineTabsUnitTest extends AbstractSniffUnitTest { + /** + * Should this test be skipped for some reason. + * + * @return boolean + */ + protected function shouldSkipTest() + { + /* + * Skip these tests on PHP 5.5 as there is something weird going on + * which intermittently causes CI to fail on PHP 5.5 (and only on PHP 5.5). + * It is unclear why the tests would fail, it cannot be reproduced locally, + * and will some of the time pass on CI as well, so debugging this is + * next to impossible. + * But having to continuously restart builds is getting silly. + */ + return (PHP_MAJOR_VERSION === 5 && PHP_MINOR_VERSION === 5); + } + /** * Set CLI values before the file is tested. * From d0dfd66b6b7316e874715df7cd87f864f37b59d4 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 26 Oct 2022 09:04:34 +0200 Subject: [PATCH 038/125] Universal/ConstructorDestructorReturn: add auto-fixer for return type The "returns a value" warning cannot be auto-fixed as it should be looked at by a developer. However, the "return type found" error _can_ be auto-fixed, so let's do so. --- .../ConstructorDestructorReturnSniff.php | 15 ++- ...structorDestructorReturnUnitTest.inc.fixed | 124 ++++++++++++++++++ 2 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 Universal/Tests/CodeAnalysis/ConstructorDestructorReturnUnitTest.inc.fixed diff --git a/Universal/Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php b/Universal/Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php index a62830ed..d3cabac5 100644 --- a/Universal/Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php +++ b/Universal/Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php @@ -87,6 +87,7 @@ public function process(File $phpcsFile, $stackPtr) */ // Check for a return type. + $tokens = $phpcsFile->getTokens(); $properties = FunctionDeclarations::getProperties($phpcsFile, $stackPtr); if ($properties['return_type'] !== '' && $properties['return_type_token'] !== false) { $data = [ @@ -94,15 +95,25 @@ public function process(File $phpcsFile, $stackPtr) $properties['return_type'], ]; - $phpcsFile->addError( + $fix = $phpcsFile->addFixableError( '%s can not declare a return type. Found: %s', $properties['return_type_token'], 'ReturnTypeFound', $data ); + + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + + $parensCloser = $tokens[$stackPtr]['parenthesis_closer']; + for ($i = ($parensCloser + 1); $i <= $properties['return_type_end_token']; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->endChangeset(); + } } - $tokens = $phpcsFile->getTokens(); if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) { // Abstract/interface method, live coding or parse error. return; diff --git a/Universal/Tests/CodeAnalysis/ConstructorDestructorReturnUnitTest.inc.fixed b/Universal/Tests/CodeAnalysis/ConstructorDestructorReturnUnitTest.inc.fixed new file mode 100644 index 00000000..fac2b6e2 --- /dev/null +++ b/Universal/Tests/CodeAnalysis/ConstructorDestructorReturnUnitTest.inc.fixed @@ -0,0 +1,124 @@ + Date: Sun, 30 Oct 2022 01:29:25 +0200 Subject: [PATCH 039/125] :sparkles: New `Universal.Classes.ModifierKeywordOrder` sniff Sniff to standardize the modifier keyword order for class declarations, what with the `readonly` keyword being introduced in PHP 8.2. The sniff contains a `public` `$order` property which allows for configuring the preferred order. Allowed values: * `'extendability readonly'` (= default) * `'readonly extendability'` Includes fixer. Includes unit tests. Includes documentation. Includes metrics. Ref: https://wiki.php.net/rfc/readonly_classes --- .../Classes/ModifierKeywordOrderStandard.xml | 27 +++ .../Classes/ModifierKeywordOrderSniff.php | 219 ++++++++++++++++++ .../Classes/ModifierKeywordOrderUnitTest.inc | 93 ++++++++ .../ModifierKeywordOrderUnitTest.inc.fixed | 90 +++++++ .../Classes/ModifierKeywordOrderUnitTest.php | 52 +++++ 5 files changed, 481 insertions(+) create mode 100644 Universal/Docs/Classes/ModifierKeywordOrderStandard.xml create mode 100644 Universal/Sniffs/Classes/ModifierKeywordOrderSniff.php create mode 100644 Universal/Tests/Classes/ModifierKeywordOrderUnitTest.inc create mode 100644 Universal/Tests/Classes/ModifierKeywordOrderUnitTest.inc.fixed create mode 100644 Universal/Tests/Classes/ModifierKeywordOrderUnitTest.php diff --git a/Universal/Docs/Classes/ModifierKeywordOrderStandard.xml b/Universal/Docs/Classes/ModifierKeywordOrderStandard.xml new file mode 100644 index 00000000..a80ae0da --- /dev/null +++ b/Universal/Docs/Classes/ModifierKeywordOrderStandard.xml @@ -0,0 +1,27 @@ + + + + + + + + final readonly class Foo {} +abstract readonly class Bar {} + ]]> + + + readonly final class Foo {} +readonly abstract class Bar {} + ]]> + + + diff --git a/Universal/Sniffs/Classes/ModifierKeywordOrderSniff.php b/Universal/Sniffs/Classes/ModifierKeywordOrderSniff.php new file mode 100644 index 00000000..234515d8 --- /dev/null +++ b/Universal/Sniffs/Classes/ModifierKeywordOrderSniff.php @@ -0,0 +1,219 @@ +getTokens(); + $valid = Collections::classModifierKeywords() + Tokens::$emptyTokens; + $classProp = [ + 'abstract_token' => false, + 'final_token' => false, + 'readonly_token' => false, + ]; + + for ($i = ($stackPtr - 1); $i > 0; $i--) { + if (isset($valid[$tokens[$i]['code']]) === false) { + break; + } + + switch ($tokens[$i]['code']) { + case \T_ABSTRACT: + $classProp['abstract_token'] = $i; + break; + + case \T_FINAL: + $classProp['final_token'] = $i; + break; + + case \T_READONLY: + $classProp['readonly_token'] = $i; + break; + } + } + + if ($classProp['readonly_token'] === false + || ($classProp['final_token'] === false && $classProp['abstract_token'] === false) + ) { + /* + * Either no modifier keywords found at all; or only one type of modifier + * keyword (abstract/final or readonly) declared, but not both. No ordering needed. + */ + return; + } + + if ($classProp['final_token'] !== false && $classProp['abstract_token'] !== false) { + // Parse error. Ignore. + return; + } + + $readonly = $classProp['readonly_token']; + + if ($classProp['final_token'] !== false) { + $extendability = $classProp['final_token']; + } else { + $extendability = $classProp['abstract_token']; + } + + if ($readonly < $extendability) { + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, self::READONLY_EXTEND); + } else { + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, self::EXTEND_READONLY); + } + + $message = 'Class modifier keywords are not in the correct order. Expected: "%s", found: "%s"'; + + switch ($this->order) { + case self::READONLY_EXTEND: + if ($readonly < $extendability) { + // Order is correct. Nothing to do. + return; + } + + $this->handleError($phpcsFile, $extendability, $readonly); + break; + + case self::EXTEND_READONLY: + default: + if ($extendability < $readonly) { + // Order is correct. Nothing to do. + return; + } + + $this->handleError($phpcsFile, $readonly, $extendability); + break; + } + } + + /** + * Throw the error and potentially fix it. + * + * @since 1.0.0-alpha4 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $firstKeyword The position of the first keyword found. + * @param int $secondKeyword The position of the second keyword token. + * + * @return void + */ + private function handleError(File $phpcsFile, $firstKeyword, $secondKeyword) + { + $tokens = $phpcsFile->getTokens(); + + $message = 'Class modifier keywords are not in the correct order. Expected: "%s", found: "%s"'; + $data = [ + $tokens[$secondKeyword]['content'] . ' ' . $tokens[$firstKeyword]['content'], + $tokens[$firstKeyword]['content'] . ' ' . $tokens[$secondKeyword]['content'], + ]; + + $fix = $phpcsFile->addFixableError($message, $firstKeyword, 'Incorrect', $data); + + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + + $phpcsFile->fixer->replaceToken($secondKeyword, ''); + + // Prevent leaving behind trailing whitespace. + $i = ($secondKeyword + 1); + while ($tokens[$i]['code'] === \T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($i, ''); + $i++; + } + + // Use the original token content as the case used for keywords is not the concern of this sniff. + $phpcsFile->fixer->addContentBefore($firstKeyword, $tokens[$secondKeyword]['content'] . ' '); + + $phpcsFile->fixer->endChangeset(); + } + } +} diff --git a/Universal/Tests/Classes/ModifierKeywordOrderUnitTest.inc b/Universal/Tests/Classes/ModifierKeywordOrderUnitTest.inc new file mode 100644 index 00000000..a78f9711 --- /dev/null +++ b/Universal/Tests/Classes/ModifierKeywordOrderUnitTest.inc @@ -0,0 +1,93 @@ + => + */ + public function getErrorList() + { + return [ + 44 => 1, + 46 => 1, + 48 => 1, + 56 => 1, + 71 => 1, + 73 => 1, + 86 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return []; + } +} From 50383a95df888489f0fec5a2707e099988171f22 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 30 Oct 2022 12:00:35 +0100 Subject: [PATCH 040/125] :sparkles: New `Universal.Constants.ModifierKeywordOrder` sniff Sniff to standardize the modifier keyword order for OO constant declarations. The sniff contains a `public` `$order` property which allows for configuring the preferred order. Allowed values: * `'final visibility'` (= default) * `'visibility final'` Includes fixer. Includes unit tests. Includes documentation. Includes metrics. Refs: * PHP 8.1: https://wiki.php.net/rfc/final_class_const * PHP 7.1: https://wiki.php.net/rfc/class_const_visibility --- .../ModifierKeywordOrderStandard.xml | 30 +++ .../Constants/ModifierKeywordOrderSniff.php | 216 ++++++++++++++++++ .../ModifierKeywordOrderUnitTest.inc | 113 +++++++++ .../ModifierKeywordOrderUnitTest.inc.fixed | 111 +++++++++ .../ModifierKeywordOrderUnitTest.php | 52 +++++ 5 files changed, 522 insertions(+) create mode 100644 Universal/Docs/Constants/ModifierKeywordOrderStandard.xml create mode 100644 Universal/Sniffs/Constants/ModifierKeywordOrderSniff.php create mode 100644 Universal/Tests/Constants/ModifierKeywordOrderUnitTest.inc create mode 100644 Universal/Tests/Constants/ModifierKeywordOrderUnitTest.inc.fixed create mode 100644 Universal/Tests/Constants/ModifierKeywordOrderUnitTest.php diff --git a/Universal/Docs/Constants/ModifierKeywordOrderStandard.xml b/Universal/Docs/Constants/ModifierKeywordOrderStandard.xml new file mode 100644 index 00000000..f42483a0 --- /dev/null +++ b/Universal/Docs/Constants/ModifierKeywordOrderStandard.xml @@ -0,0 +1,30 @@ + + + + + + + + final public const FOO = 'foo'; +} + ]]> + + + protected final const BAR = 'foo'; +} + ]]> + + + diff --git a/Universal/Sniffs/Constants/ModifierKeywordOrderSniff.php b/Universal/Sniffs/Constants/ModifierKeywordOrderSniff.php new file mode 100644 index 00000000..6114e920 --- /dev/null +++ b/Universal/Sniffs/Constants/ModifierKeywordOrderSniff.php @@ -0,0 +1,216 @@ + \T_FINAL, + ]; + + /** + * Returns an array of tokens this test wants to listen for. + * + * @since 1.0.0-alpha4 + * + * @return array + */ + public function register() + { + // Add the visibility keywords to the constant modifier keywords. + $this->constantModifierKeywords += Tokens::$scopeModifiers; + + return [\T_CONST]; + } + + /** + * Processes this test, when one of its tokens is encountered. + * + * @since 1.0.0-alpha4 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. + * + * @return void + */ + public function process(File $phpcsFile, $stackPtr) + { + if (Scopes::isOOConstant($phpcsFile, $stackPtr) === false) { + return; + } + + /* + * Note to self: This can be switched to use the `Collections::constantModifierKeywords()` + * method as of the next version of PHPCSUtils. + */ + $tokens = $phpcsFile->getTokens(); + $valid = $this->constantModifierKeywords + Tokens::$emptyTokens; + + $finalPtr = false; + $visibilityPtr = false; + + for ($i = ($stackPtr - 1); $i > 0; $i--) { + if (isset($valid[$tokens[$i]['code']]) === false) { + break; + } + + if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) { + continue; + } + + if ($tokens[$i]['code'] === \T_FINAL) { + $finalPtr = $i; + } else { + $visibilityPtr = $i; + } + } + + if ($finalPtr === false || $visibilityPtr === false) { + /* + * Either no modifier keywords found at all; or only one type of modifier + * keyword (final or visibility) declared, but not both. No ordering needed. + */ + return; + } + + if ($visibilityPtr < $finalPtr) { + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, self::VISIBILITY_FINAL); + } else { + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, self::FINAL_VISIBILITY); + } + + $message = 'OO constant modifier keywords are not in the correct order. Expected: "%s", found: "%s"'; + + switch ($this->order) { + case self::VISIBILITY_FINAL: + if ($visibilityPtr < $finalPtr) { + // Order is correct. Nothing to do. + return; + } + + $this->handleError($phpcsFile, $finalPtr, $visibilityPtr); + break; + + case self::FINAL_VISIBILITY: + default: + if ($finalPtr < $visibilityPtr) { + // Order is correct. Nothing to do. + return; + } + + $this->handleError($phpcsFile, $visibilityPtr, $finalPtr); + break; + } + } + + /** + * Throw the error and potentially fix it. + * + * @since 1.0.0-alpha4 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $firstKeyword The position of the first keyword found. + * @param int $secondKeyword The position of the second keyword token. + * + * @return void + */ + private function handleError(File $phpcsFile, $firstKeyword, $secondKeyword) + { + $tokens = $phpcsFile->getTokens(); + + $message = 'Constant modifier keywords are not in the correct order. Expected: "%s", found: "%s"'; + $data = [ + $tokens[$secondKeyword]['content'] . ' ' . $tokens[$firstKeyword]['content'], + $tokens[$firstKeyword]['content'] . ' ' . $tokens[$secondKeyword]['content'], + ]; + + $fix = $phpcsFile->addFixableError($message, $firstKeyword, 'Incorrect', $data); + + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + + $phpcsFile->fixer->replaceToken($secondKeyword, ''); + + // Prevent leaving behind trailing whitespace. + $i = ($secondKeyword + 1); + while ($tokens[$i]['code'] === \T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($i, ''); + $i++; + } + + // Use the original token content as the case used for keywords is not the concern of this sniff. + $phpcsFile->fixer->addContentBefore($firstKeyword, $tokens[$secondKeyword]['content'] . ' '); + + $phpcsFile->fixer->endChangeset(); + } + } +} diff --git a/Universal/Tests/Constants/ModifierKeywordOrderUnitTest.inc b/Universal/Tests/Constants/ModifierKeywordOrderUnitTest.inc new file mode 100644 index 00000000..1c9544f7 --- /dev/null +++ b/Universal/Tests/Constants/ModifierKeywordOrderUnitTest.inc @@ -0,0 +1,113 @@ + => + */ + public function getErrorList() + { + return [ + 44 => 1, + 47 => 1, + 49 => 1, + 78 => 1, + 81 => 1, + 83 => 1, + 105 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return []; + } +} From 7bdb46bf8bf459913877ac85b86a8b84e8f90c57 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 27 Oct 2022 08:28:37 +0200 Subject: [PATCH 041/125] NormalizedArrays/CommaAfterLast: improve fixer for flexible heredoc/nowdoc When comma's are enforced after the last entry of a (multi-line) array, the fixer would previously always add a new line between the heredoc/nowdoc closer and the comma to prevent parse errors in PHP < 7.3. However, the "closer" token for a flexible heredoc/nowdoc will include the indentation whitespace. This allows us to distinguish between "old-style" heredoc/nowdocs and PHP 7.3+ heredoc/nowdocs. This commit updates the fixer to check for this indentation whitespace and if found, will no longer add the new line. Includes tests in a separate file as these tests can only run on PHP 7.3+ (as the token stream of a file containing flexible heredoc/nowdocs in PHP < 7.3 will be garbage after the first heredoc/nowdoc). --- .../Sniffs/Arrays/CommaAfterLastSniff.php | 6 +- ...tTest.inc => CommaAfterLastUnitTest.1.inc} | 0 ...xed => CommaAfterLastUnitTest.1.inc.fixed} | 0 .../Tests/Arrays/CommaAfterLastUnitTest.2.inc | 44 +++++++++++++ .../Arrays/CommaAfterLastUnitTest.2.inc.fixed | 44 +++++++++++++ .../Tests/Arrays/CommaAfterLastUnitTest.php | 62 ++++++++++++------- 6 files changed, 133 insertions(+), 23 deletions(-) rename NormalizedArrays/Tests/Arrays/{CommaAfterLastUnitTest.inc => CommaAfterLastUnitTest.1.inc} (100%) rename NormalizedArrays/Tests/Arrays/{CommaAfterLastUnitTest.inc.fixed => CommaAfterLastUnitTest.1.inc.fixed} (100%) create mode 100644 NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.2.inc create mode 100644 NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.2.inc.fixed diff --git a/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php b/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php index 0560fe88..59a7942c 100644 --- a/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php +++ b/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php @@ -157,8 +157,10 @@ public function process(File $phpcsFile, $stackPtr) if ($fix === true) { $extraContent = ','; - if ($tokens[$lastNonEmpty]['code'] === \T_END_HEREDOC - || $tokens[$lastNonEmpty]['code'] === \T_END_NOWDOC + if (($tokens[$lastNonEmpty]['code'] === \T_END_HEREDOC + || $tokens[$lastNonEmpty]['code'] === \T_END_NOWDOC) + // Check for indentation, if indented, it's a PHP 7.3+ heredoc/nowdoc. + && $tokens[$lastNonEmpty]['content'] === \ltrim($tokens[$lastNonEmpty]['content']) ) { // Prevent parse errors in PHP < 7.3 which doesn't support flexible heredoc/nowdoc. $extraContent = $phpcsFile->eolChar . $extraContent; diff --git a/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.inc b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.1.inc similarity index 100% rename from NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.inc rename to NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.1.inc diff --git a/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.inc.fixed b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.1.inc.fixed similarity index 100% rename from NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.inc.fixed rename to NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.1.inc.fixed diff --git a/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.2.inc b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.2.inc new file mode 100644 index 00000000..76439f5e --- /dev/null +++ b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.2.inc @@ -0,0 +1,44 @@ + << <<<"EOD" + Some content + EOD, +]; + +$array = [ + 'nowdoc' => <<<'EOD' + Some content + EOD, +]; + +// Missing comma. +$array = [ + 'heredoc' => << <<<"EOD" + Some content + EOD +]; + +$array = [ + 'nowdoc' => <<<'EOD' + Some content + EOD +]; diff --git a/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.2.inc.fixed b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.2.inc.fixed new file mode 100644 index 00000000..dd7af9a3 --- /dev/null +++ b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.2.inc.fixed @@ -0,0 +1,44 @@ + << <<<"EOD" + Some content + EOD, +]; + +$array = [ + 'nowdoc' => <<<'EOD' + Some content + EOD, +]; + +// Missing comma. +$array = [ + 'heredoc' => << <<<"EOD" + Some content + EOD, +]; + +$array = [ + 'nowdoc' => <<<'EOD' + Some content + EOD, +]; diff --git a/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.php b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.php index b0e32f19..b528729f 100644 --- a/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.php +++ b/NormalizedArrays/Tests/Arrays/CommaAfterLastUnitTest.php @@ -25,30 +25,50 @@ final class CommaAfterLastUnitTest extends AbstractSniffUnitTest /** * Returns the lines where errors should occur. * + * @param string $testFile The name of the file being tested. + * * @return array => */ - public function getErrorList() + public function getErrorList($testFile = '') { - return [ - 52 => 1, - 53 => 1, - 75 => 1, - 79 => 1, - 87 => 1, - 89 => 1, - 91 => 1, - 96 => 1, - 103 => 1, - 114 => 1, - 115 => 1, - 136 => 1, - 140 => 1, - 148 => 1, - 150 => 1, - 152 => 1, - 159 => 1, - 166 => 1, - ]; + switch ($testFile) { + case 'CommaAfterLastUnitTest.1.inc': + return [ + 52 => 1, + 53 => 1, + 75 => 1, + 79 => 1, + 87 => 1, + 89 => 1, + 91 => 1, + 96 => 1, + 103 => 1, + 114 => 1, + 115 => 1, + 136 => 1, + 140 => 1, + 148 => 1, + 150 => 1, + 152 => 1, + 159 => 1, + 166 => 1, + ]; + + case 'CommaAfterLastUnitTest.2.inc': + // This test will only work for PHP 7.3+. + if (\PHP_VERSION_ID < 70300) { + return []; + } + + return [ + 31 => 1, + 37 => 1, + 43 => 1, + ]; + + default: + return []; + } } /** From ee9225d091aa51ceaca857dba3358bfc22a35899 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 31 Oct 2022 09:38:24 +0100 Subject: [PATCH 042/125] QA: always declare metric names as class constants .. to reduce the risk of metrics not recording correctly due to typos in one of the instances recording the metrics. Note: the metrics in the "Disallow short/long list" sniffs will be addressed separately. --- .../Sniffs/Arrays/CommaAfterLastSniff.php | 11 ++++++++++- .../DisallowAnonClassParenthesesSniff.php | 17 +++++++++++++---- .../Sniffs/Classes/DisallowFinalClassSniff.php | 15 ++++++++++++--- .../RequireAnonClassParenthesesSniff.php | 13 +++++++++++-- .../Sniffs/Classes/RequireFinalClassSniff.php | 15 ++++++++++++--- .../LowercaseClassResolutionKeywordSniff.php | 15 ++++++++++++--- .../Constants/UppercaseMagicConstantsSniff.php | 15 ++++++++++++--- .../DisallowAlternativeSyntaxSniff.php | 17 +++++++++++++---- .../IfElseDeclarationSniff.php | 13 +++++++++++-- .../Files/SeparateFunctionsFromOOSniff.php | 17 +++++++++++++---- .../DisallowCurlyBraceSyntaxSniff.php | 13 +++++++++++-- .../Namespaces/EnforceCurlyBraceSyntaxSniff.php | 13 +++++++++++-- .../Operators/DisallowLogicalAndOrSniff.php | 11 ++++++++++- .../Operators/DisallowShortTernarySniff.php | 13 +++++++++++-- ...lowStandalonePostIncrementDecrementSniff.php | 13 +++++++++++-- .../Sniffs/Operators/StrictComparisonsSniff.php | 11 ++++++++++- 16 files changed, 183 insertions(+), 39 deletions(-) diff --git a/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php b/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php index 59a7942c..72b97b98 100644 --- a/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php +++ b/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php @@ -26,6 +26,15 @@ final class CommaAfterLastSniff implements Sniff { + /** + * Name of the metric. + * + * @since 1.0.0 + * + * @var string + */ + const METRIC_NAME = '%s array - comma after last item'; + /** * Whether or not to enforce a comma after the last array item in a single-line array. * @@ -140,7 +149,7 @@ public function process(File $phpcsFile, $stackPtr) $phpcsFile->recordMetric( $stackPtr, - \ucfirst($phrase) . ' array - comma after last item', + \sprintf(self::METRIC_NAME, \ucfirst($phrase)), ($isComma === true ? 'yes' : 'no') ); diff --git a/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php b/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php index afcf283a..eea517c8 100644 --- a/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php +++ b/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php @@ -23,6 +23,15 @@ final class DisallowAnonClassParenthesesSniff implements Sniff { + /** + * Name of the metric. + * + * @since 1.0.0 + * + * @var string + */ + const METRIC_NAME = 'Anon class declaration with parenthesis'; + /** * Returns an array of tokens this test wants to listen for. * @@ -57,13 +66,13 @@ public function process(File $phpcsFile, $stackPtr) || $tokens[$nextNonEmpty]['code'] !== \T_OPEN_PARENTHESIS ) { // No parentheses found. - $phpcsFile->recordMetric($stackPtr, 'Anon class declaration with parenthesis', 'no'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'no'); return; } if (isset($tokens[$nextNonEmpty]['parenthesis_closer']) === false) { // Incomplete set of parentheses. Ignore. - $phpcsFile->recordMetric($stackPtr, 'Anon class declaration with parenthesis', 'yes'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes'); return; } @@ -71,11 +80,11 @@ public function process(File $phpcsFile, $stackPtr) $hasParams = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), $closer, true); if ($hasParams !== false) { // There is something between the parentheses. Ignore. - $phpcsFile->recordMetric($stackPtr, 'Anon class declaration with parenthesis', 'yes, with parameter'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes, with parameter'); return; } - $phpcsFile->recordMetric($stackPtr, 'Anon class declaration with parenthesis', 'yes'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes'); $fix = $phpcsFile->addFixableError( 'Parenthesis not allowed when creating a new anonymous class without passing parameters', diff --git a/Universal/Sniffs/Classes/DisallowFinalClassSniff.php b/Universal/Sniffs/Classes/DisallowFinalClassSniff.php index f1ae0ed1..28e868f7 100644 --- a/Universal/Sniffs/Classes/DisallowFinalClassSniff.php +++ b/Universal/Sniffs/Classes/DisallowFinalClassSniff.php @@ -23,6 +23,15 @@ final class DisallowFinalClassSniff implements Sniff { + /** + * Name of the metric. + * + * @since 1.0.0 + * + * @var string + */ + const METRIC_NAME = 'Class declaration type'; + /** * Returns an array of tokens this test wants to listen for. * @@ -53,10 +62,10 @@ public function process(File $phpcsFile, $stackPtr) $classProp = ObjectDeclarations::getClassProperties($phpcsFile, $stackPtr); if ($classProp['is_final'] === false) { if ($classProp['is_abstract'] === true) { - $phpcsFile->recordMetric($stackPtr, 'Class declaration type', 'abstract'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'abstract'); } - $phpcsFile->recordMetric($stackPtr, 'Class declaration type', 'not abstract, not final'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'not abstract, not final'); return; } @@ -66,7 +75,7 @@ public function process(File $phpcsFile, $stackPtr) return; } - $phpcsFile->recordMetric($stackPtr, 'Class declaration type', 'final'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'final'); // No extra safeguards needed, we know the keyword will exist based on the check above. $finalKeyword = $phpcsFile->findPrevious(\T_FINAL, ($stackPtr - 1)); diff --git a/Universal/Sniffs/Classes/RequireAnonClassParenthesesSniff.php b/Universal/Sniffs/Classes/RequireAnonClassParenthesesSniff.php index 3a868a2b..ffcd45f7 100644 --- a/Universal/Sniffs/Classes/RequireAnonClassParenthesesSniff.php +++ b/Universal/Sniffs/Classes/RequireAnonClassParenthesesSniff.php @@ -22,6 +22,15 @@ final class RequireAnonClassParenthesesSniff implements Sniff { + /** + * Name of the metric. + * + * @since 1.0.0 + * + * @var string + */ + const METRIC_NAME = 'Anon class declaration with parenthesis'; + /** * Returns an array of tokens this test wants to listen for. * @@ -56,11 +65,11 @@ public function process(File $phpcsFile, $stackPtr) && $tokens[$nextNonEmpty]['code'] === \T_OPEN_PARENTHESIS ) { // Parentheses found. - $phpcsFile->recordMetric($stackPtr, 'Anon class declaration with parenthesis', 'yes'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes'); return; } - $phpcsFile->recordMetric($stackPtr, 'Anon class declaration with parenthesis', 'no'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'no'); $fix = $phpcsFile->addFixableError( 'Parenthesis required when creating a new anonymous class.', diff --git a/Universal/Sniffs/Classes/RequireFinalClassSniff.php b/Universal/Sniffs/Classes/RequireFinalClassSniff.php index 5e63e4f1..375e4c07 100644 --- a/Universal/Sniffs/Classes/RequireFinalClassSniff.php +++ b/Universal/Sniffs/Classes/RequireFinalClassSniff.php @@ -23,6 +23,15 @@ final class RequireFinalClassSniff implements Sniff { + /** + * Name of the metric. + * + * @since 1.0.0 + * + * @var string + */ + const METRIC_NAME = 'Class declaration type'; + /** * Returns an array of tokens this test wants to listen for. * @@ -52,13 +61,13 @@ public function process(File $phpcsFile, $stackPtr) { $classProp = ObjectDeclarations::getClassProperties($phpcsFile, $stackPtr); if ($classProp['is_final'] === true) { - $phpcsFile->recordMetric($stackPtr, 'Class declaration type', 'final'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'final'); return; } if ($classProp['is_abstract'] === true) { // Abstract classes can't be final. - $phpcsFile->recordMetric($stackPtr, 'Class declaration type', 'abstract'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'abstract'); return; } @@ -68,7 +77,7 @@ public function process(File $phpcsFile, $stackPtr) return; } - $phpcsFile->recordMetric($stackPtr, 'Class declaration type', 'not abstract, not final'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'not abstract, not final'); $snippet = GetTokensAsString::compact($phpcsFile, $stackPtr, $tokens[$stackPtr]['scope_opener'], true); $fix = $phpcsFile->addFixableError( diff --git a/Universal/Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php b/Universal/Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php index 3e62bb5b..f7431fdd 100644 --- a/Universal/Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php +++ b/Universal/Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php @@ -25,6 +25,15 @@ final class LowercaseClassResolutionKeywordSniff implements Sniff { + /** + * Name of the metric. + * + * @since 1.0.0 + * + * @var string + */ + const METRIC_NAME = 'Magic ::class constant case'; + /** * Returns an array of tokens this test wants to listen for. * @@ -66,7 +75,7 @@ public function process(File $phpcsFile, $stackPtr) } if ($contentLC === $content) { - $phpcsFile->recordMetric($stackPtr, 'Magic ::class constant case', 'lowercase'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'lowercase'); return; } @@ -79,10 +88,10 @@ public function process(File $phpcsFile, $stackPtr) $errorCode = ''; if (\strtoupper($content) === $content) { $errorCode = 'Uppercase'; - $phpcsFile->recordMetric($stackPtr, 'Magic ::class constant case', 'uppercase'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'uppercase'); } else { $errorCode = 'Mixedcase'; - $phpcsFile->recordMetric($stackPtr, 'Magic ::class constant case', 'mixed case'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'mixed case'); } $fix = $phpcsFile->addFixableError($error, $stackPtr, $errorCode, $data); diff --git a/Universal/Sniffs/Constants/UppercaseMagicConstantsSniff.php b/Universal/Sniffs/Constants/UppercaseMagicConstantsSniff.php index 1659f958..1b234f92 100644 --- a/Universal/Sniffs/Constants/UppercaseMagicConstantsSniff.php +++ b/Universal/Sniffs/Constants/UppercaseMagicConstantsSniff.php @@ -24,6 +24,15 @@ final class UppercaseMagicConstantsSniff implements Sniff { + /** + * Name of the metric. + * + * @since 1.0.0 + * + * @var string + */ + const METRIC_NAME = 'Magic constant case'; + /** * Returns an array of tokens this test wants to listen for. * @@ -53,7 +62,7 @@ public function process(File $phpcsFile, $stackPtr) $content = $tokens[$stackPtr]['content']; $contentUC = \strtoupper($content); if ($contentUC === $content) { - $phpcsFile->recordMetric($stackPtr, 'Magic constant case', 'uppercase'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'uppercase'); return; } @@ -66,10 +75,10 @@ public function process(File $phpcsFile, $stackPtr) if (\strtolower($content) === $content) { $errorCode = 'Lowercase'; - $phpcsFile->recordMetric($stackPtr, 'Magic constant case', 'lowercase'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'lowercase'); } else { $errorCode = 'Mixedcase'; - $phpcsFile->recordMetric($stackPtr, 'Magic constant case', 'mixed case'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'mixed case'); } $fix = $phpcsFile->addFixableError($error, $stackPtr, $errorCode, $data); diff --git a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php index 5b7b1663..c43c6d86 100644 --- a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php +++ b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php @@ -24,6 +24,15 @@ final class DisallowAlternativeSyntaxSniff implements Sniff { + /** + * Name of the metric. + * + * @since 1.0.0 + * + * @var string + */ + const METRIC_NAME = 'Control structure style'; + /** * Whether to allow the alternative syntax when it is wrapped around * inline HTML, as is often seen in views. @@ -82,7 +91,7 @@ public function process(File $phpcsFile, $stackPtr) */ if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) { // No scope opener found: inline control structure or parse error. - $phpcsFile->recordMetric($stackPtr, 'Control structure style', 'inline'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'inline'); return; } @@ -91,7 +100,7 @@ public function process(File $phpcsFile, $stackPtr) if ($tokens[$opener]['code'] !== \T_COLON) { // Curly brace syntax (not our concern). - $phpcsFile->recordMetric($stackPtr, 'Control structure style', 'curly braces'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'curly braces'); return; } @@ -102,9 +111,9 @@ public function process(File $phpcsFile, $stackPtr) ); if ($hasInlineHTML !== false) { - $phpcsFile->recordMetric($stackPtr, 'Control structure style', 'alternative syntax with inline HTML'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'alternative syntax with inline HTML'); } else { - $phpcsFile->recordMetric($stackPtr, 'Control structure style', 'alternative syntax'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'alternative syntax'); } if ($hasInlineHTML !== false && $this->allowWithInlineHTML === true) { diff --git a/Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php b/Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php index 9bdb829c..3c74e437 100644 --- a/Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php +++ b/Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php @@ -31,6 +31,15 @@ final class IfElseDeclarationSniff implements Sniff { + /** + * Name of the metric. + * + * @since 1.0.0 + * + * @var string + */ + const METRIC_NAME = 'Else(if) on a new line'; + /** * Returns an array of tokens this test wants to listen for. * @@ -90,11 +99,11 @@ public function process(File $phpcsFile, $stackPtr) } if ($tokens[$prevNonEmpty]['line'] !== $tokens[$stackPtr]['line']) { - $phpcsFile->recordMetric($stackPtr, 'Else(if) on a new line', 'yes'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes'); return; } - $phpcsFile->recordMetric($stackPtr, 'Else(if) on a new line', 'no'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'no'); $errorBase = \strtoupper($tokens[$stackPtr]['content']); $error = $errorBase . ' statement must be on a new line.'; diff --git a/Universal/Sniffs/Files/SeparateFunctionsFromOOSniff.php b/Universal/Sniffs/Files/SeparateFunctionsFromOOSniff.php index 53b2dff9..c2c1be52 100644 --- a/Universal/Sniffs/Files/SeparateFunctionsFromOOSniff.php +++ b/Universal/Sniffs/Files/SeparateFunctionsFromOOSniff.php @@ -33,6 +33,15 @@ final class SeparateFunctionsFromOOSniff implements Sniff { + /** + * Name of the metric. + * + * @since 1.0.0 + * + * @var string + */ + const METRIC_NAME = 'Functions or OO declarations ?'; + /** * Tokens this sniff searches for. * @@ -148,7 +157,7 @@ public function process(File $phpcsFile, $stackPtr) } if ($functionCount > 0 && $OOCount > 0) { - $phpcsFile->recordMetric($stackPtr, 'Functions or OO declarations ?', 'Both function and OO declarations'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'Both function and OO declarations'); $reportToken = \max($firstFunction, $firstOO); @@ -167,11 +176,11 @@ public function process(File $phpcsFile, $stackPtr) ] ); } elseif ($functionCount > 0) { - $phpcsFile->recordMetric($stackPtr, 'Functions or OO declarations ?', 'Only function(s)'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'Only function(s)'); } elseif ($OOCount > 0) { - $phpcsFile->recordMetric($stackPtr, 'Functions or OO declarations ?', 'Only OO structure(s)'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'Only OO structure(s)'); } else { - $phpcsFile->recordMetric($stackPtr, 'Functions or OO declarations ?', 'Neither'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'Neither'); } // Ignore the rest of the file. diff --git a/Universal/Sniffs/Namespaces/DisallowCurlyBraceSyntaxSniff.php b/Universal/Sniffs/Namespaces/DisallowCurlyBraceSyntaxSniff.php index f8e3bcae..00384ac6 100644 --- a/Universal/Sniffs/Namespaces/DisallowCurlyBraceSyntaxSniff.php +++ b/Universal/Sniffs/Namespaces/DisallowCurlyBraceSyntaxSniff.php @@ -22,6 +22,15 @@ final class DisallowCurlyBraceSyntaxSniff implements Sniff { + /** + * Name of the metric. + * + * @since 1.0.0 + * + * @var string + */ + const METRIC_NAME = 'Namespace declaration using curly brace syntax'; + /** * Returns an array of tokens this test wants to listen for. * @@ -57,11 +66,11 @@ public function process(File $phpcsFile, $stackPtr) if (isset($tokens[$stackPtr]['scope_condition']) === false || $tokens[$stackPtr]['scope_condition'] !== $stackPtr ) { - $phpcsFile->recordMetric($stackPtr, 'Namespace declaration using curly brace syntax', 'no'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'no'); return; } - $phpcsFile->recordMetric($stackPtr, 'Namespace declaration using curly brace syntax', 'yes'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes'); $phpcsFile->addError( 'Namespace declarations using the curly brace syntax are not allowed.', diff --git a/Universal/Sniffs/Namespaces/EnforceCurlyBraceSyntaxSniff.php b/Universal/Sniffs/Namespaces/EnforceCurlyBraceSyntaxSniff.php index 2d844653..b6676c26 100644 --- a/Universal/Sniffs/Namespaces/EnforceCurlyBraceSyntaxSniff.php +++ b/Universal/Sniffs/Namespaces/EnforceCurlyBraceSyntaxSniff.php @@ -22,6 +22,15 @@ final class EnforceCurlyBraceSyntaxSniff implements Sniff { + /** + * Name of the metric. + * + * @since 1.0.0 + * + * @var string + */ + const METRIC_NAME = 'Namespace declaration using curly brace syntax'; + /** * Returns an array of tokens this test wants to listen for. * @@ -57,11 +66,11 @@ public function process(File $phpcsFile, $stackPtr) if (isset($tokens[$stackPtr]['scope_condition']) === true && $tokens[$stackPtr]['scope_condition'] === $stackPtr ) { - $phpcsFile->recordMetric($stackPtr, 'Namespace declaration using curly brace syntax', 'yes'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes'); return; } - $phpcsFile->recordMetric($stackPtr, 'Namespace declaration using curly brace syntax', 'no'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'no'); $phpcsFile->addError( 'Namespace declarations without curly braces are not allowed.', diff --git a/Universal/Sniffs/Operators/DisallowLogicalAndOrSniff.php b/Universal/Sniffs/Operators/DisallowLogicalAndOrSniff.php index a7a2c7ab..b654c280 100644 --- a/Universal/Sniffs/Operators/DisallowLogicalAndOrSniff.php +++ b/Universal/Sniffs/Operators/DisallowLogicalAndOrSniff.php @@ -25,6 +25,15 @@ final class DisallowLogicalAndOrSniff implements Sniff { + /** + * Name of the metric. + * + * @since 1.0.0 + * + * @var string + */ + const METRIC_NAME = 'Type of and/or operator used'; + /** * The tokens this sniff records metrics for. * @@ -85,7 +94,7 @@ public function process(File $phpcsFile, $stackPtr) $tokens = $phpcsFile->getTokens(); $tokenCode = $tokens[$stackPtr]['code']; - $phpcsFile->recordMetric($stackPtr, 'Type of and/or operator used', $this->metricType[$tokenCode]); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, $this->metricType[$tokenCode]); if (isset($this->targetTokenInfo[$tokenCode]) === false) { // Already using boolean operator. diff --git a/Universal/Sniffs/Operators/DisallowShortTernarySniff.php b/Universal/Sniffs/Operators/DisallowShortTernarySniff.php index cfeeaf15..078e70bb 100644 --- a/Universal/Sniffs/Operators/DisallowShortTernarySniff.php +++ b/Universal/Sniffs/Operators/DisallowShortTernarySniff.php @@ -26,6 +26,15 @@ final class DisallowShortTernarySniff implements Sniff { + /** + * Name of the metric. + * + * @since 1.0.0 + * + * @var string + */ + const METRIC_NAME = 'Ternary usage'; + /** * Registers the tokens that this sniff wants to listen for. * @@ -52,11 +61,11 @@ public function register() public function process(File $phpcsFile, $stackPtr) { if (Operators::isShortTernary($phpcsFile, $stackPtr) === false) { - $phpcsFile->recordMetric($stackPtr, 'Ternary usage', 'long'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'long'); return; } - $phpcsFile->recordMetric($stackPtr, 'Ternary usage', 'short'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'short'); $phpcsFile->addError( 'Using short ternaries is not allowed as they are rarely used correctly', diff --git a/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php b/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php index 40908d44..f54e0268 100644 --- a/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php +++ b/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php @@ -31,6 +31,15 @@ final class DisallowStandalonePostIncrementDecrementSniff implements Sniff { + /** + * Name of the metric. + * + * @since 1.0.0 + * + * @var string + */ + const METRIC_NAME = 'In/decrement usage in stand-alone statements'; + /** * Tokens which can be expected in a stand-alone in/decrement statement. * @@ -146,7 +155,7 @@ public function process(File $phpcsFile, $stackPtr) $lastNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($end - 1), $start, true); if ($start === $stackPtr && $lastNonEmpty !== $stackPtr) { // This is already pre-in/decrement. - $phpcsFile->recordMetric($stackPtr, 'In/decrement usage in stand-alone statements', 'pre-' . $type); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'pre-' . $type); return $end; } @@ -155,7 +164,7 @@ public function process(File $phpcsFile, $stackPtr) return $end; } - $phpcsFile->recordMetric($stackPtr, 'In/decrement usage in stand-alone statements', 'post-' . $type); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'post-' . $type); $error = 'Stand-alone post-%1$s statement found. Use pre-%1$s instead: %2$s.'; $errorCode = 'Post' . \ucfirst($type) . 'Found'; diff --git a/Universal/Sniffs/Operators/StrictComparisonsSniff.php b/Universal/Sniffs/Operators/StrictComparisonsSniff.php index 2527d3ac..c1fead35 100644 --- a/Universal/Sniffs/Operators/StrictComparisonsSniff.php +++ b/Universal/Sniffs/Operators/StrictComparisonsSniff.php @@ -24,6 +24,15 @@ final class StrictComparisonsSniff implements Sniff { + /** + * Name of the metric. + * + * @since 1.0.0 + * + * @var string + */ + const METRIC_NAME = 'Type of comparison used'; + /** * The tokens this sniff records metrics for. * @@ -84,7 +93,7 @@ public function process(File $phpcsFile, $stackPtr) $tokens = $phpcsFile->getTokens(); $tokenCode = $tokens[$stackPtr]['code']; - $phpcsFile->recordMetric($stackPtr, 'Type of comparison used', $this->metricType[$tokenCode]); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, $this->metricType[$tokenCode]); if (isset($this->targetTokenInfo[$tokenCode]) === false) { // Already using strict comparison operator. From 334c8dc068ebc0d8f2bdacffcc9a318b9155be4a Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 2 Nov 2022 20:25:45 +0100 Subject: [PATCH 043/125] Universal/ConstructorDestructorReturn: improve return type fixer ... to prevent it from removing comments. Includes adjusting a pre-existing test to cover the change. --- .../Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php | 5 +++++ .../CodeAnalysis/ConstructorDestructorReturnUnitTest.inc | 2 +- .../ConstructorDestructorReturnUnitTest.inc.fixed | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Universal/Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php b/Universal/Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php index d3cabac5..afbf59a9 100644 --- a/Universal/Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php +++ b/Universal/Sniffs/CodeAnalysis/ConstructorDestructorReturnSniff.php @@ -107,6 +107,11 @@ public function process(File $phpcsFile, $stackPtr) $parensCloser = $tokens[$stackPtr]['parenthesis_closer']; for ($i = ($parensCloser + 1); $i <= $properties['return_type_end_token']; $i++) { + if (isset(Tokens::$commentTokens[$tokens[$i]['code']])) { + // Ignore comments and leave them be. + continue; + } + $phpcsFile->fixer->replaceToken($i, ''); } diff --git a/Universal/Tests/CodeAnalysis/ConstructorDestructorReturnUnitTest.inc b/Universal/Tests/CodeAnalysis/ConstructorDestructorReturnUnitTest.inc index 79bb4956..69e180f7 100644 --- a/Universal/Tests/CodeAnalysis/ConstructorDestructorReturnUnitTest.inc +++ b/Universal/Tests/CodeAnalysis/ConstructorDestructorReturnUnitTest.inc @@ -119,7 +119,7 @@ trait AbstractConstructorDestructorReturnTypes { } interface InterfaceMethodReturnTypes { - public function __construct(): void; + public function __construct(): /*comment*/ void; public function __destruct(): void; } diff --git a/Universal/Tests/CodeAnalysis/ConstructorDestructorReturnUnitTest.inc.fixed b/Universal/Tests/CodeAnalysis/ConstructorDestructorReturnUnitTest.inc.fixed index fac2b6e2..b732cf81 100644 --- a/Universal/Tests/CodeAnalysis/ConstructorDestructorReturnUnitTest.inc.fixed +++ b/Universal/Tests/CodeAnalysis/ConstructorDestructorReturnUnitTest.inc.fixed @@ -118,7 +118,7 @@ trait AbstractConstructorDestructorReturnTypes { } interface InterfaceMethodReturnTypes { - public function __construct(); + public function __construct()/*comment*/; public function __destruct(); } From bffb7c39e9d46a41e73c54d150e23211ccf4e848 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 2 Nov 2022 21:49:20 +0100 Subject: [PATCH 044/125] Universal/OneStatementInShortEchoTag: improve error code As the name of the sniff is `OneStatementInShortEchoTag`, "Found" would imply that one statement was found, while in actual fact the sniff throws an error when _multiple_ statements are found. --- Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php b/Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php index 399213ca..7a4a3be3 100644 --- a/Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php +++ b/Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php @@ -75,7 +75,7 @@ public function process(File $phpcsFile, $stackPtr) 'Only one statement is allowed when using short open echo PHP tags.' . ' Use the " Date: Mon, 31 Oct 2022 15:47:08 +0100 Subject: [PATCH 045/125] Universal/[Require/Disallow]FinalClass: rename metric What with PHP 8.2 introducing `readonly` classes, the metric name `Class declaration type` would become ambiguous as `readonly` is not taken into account for the metric (and doesn't need to be as it doesn't have a direct relationship to `abstract`/`final`). To prevent confusion about what the metric records, the metric name has been updated. --- Universal/Sniffs/Classes/DisallowFinalClassSniff.php | 2 +- Universal/Sniffs/Classes/RequireFinalClassSniff.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Universal/Sniffs/Classes/DisallowFinalClassSniff.php b/Universal/Sniffs/Classes/DisallowFinalClassSniff.php index 28e868f7..cd43afe0 100644 --- a/Universal/Sniffs/Classes/DisallowFinalClassSniff.php +++ b/Universal/Sniffs/Classes/DisallowFinalClassSniff.php @@ -30,7 +30,7 @@ final class DisallowFinalClassSniff implements Sniff * * @var string */ - const METRIC_NAME = 'Class declaration type'; + const METRIC_NAME = 'Class is abstract or final ?'; /** * Returns an array of tokens this test wants to listen for. diff --git a/Universal/Sniffs/Classes/RequireFinalClassSniff.php b/Universal/Sniffs/Classes/RequireFinalClassSniff.php index 375e4c07..a0458208 100644 --- a/Universal/Sniffs/Classes/RequireFinalClassSniff.php +++ b/Universal/Sniffs/Classes/RequireFinalClassSniff.php @@ -30,7 +30,7 @@ final class RequireFinalClassSniff implements Sniff * * @var string */ - const METRIC_NAME = 'Class declaration type'; + const METRIC_NAME = 'Class is abstract or final ?'; /** * Returns an array of tokens this test wants to listen for. From 545f531a23a30dcc74eb9586c12f39055f93b333 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 2 Nov 2022 20:14:31 +0100 Subject: [PATCH 046/125] Universal/[Require/Disallow]FinalClass: add tests with readonly classes ... to ensure the fixer continues to work correctly when the class is also `readonly`. --- Universal/Tests/Classes/DisallowFinalClassUnitTest.inc | 5 +++++ Universal/Tests/Classes/DisallowFinalClassUnitTest.inc.fixed | 5 +++++ Universal/Tests/Classes/DisallowFinalClassUnitTest.php | 2 ++ Universal/Tests/Classes/RequireFinalClassUnitTest.inc | 2 ++ Universal/Tests/Classes/RequireFinalClassUnitTest.inc.fixed | 2 ++ Universal/Tests/Classes/RequireFinalClassUnitTest.php | 1 + 6 files changed, 17 insertions(+) diff --git a/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc b/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc index 1422be78..812b9346 100644 --- a/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc +++ b/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc @@ -32,6 +32,11 @@ final final /*comment*/ class BazC implements MyInterface {} +// Test fixer when combined with PHP 8.2 "readonly" modifier. +readonly final class BazD {} + +final readonly class BazE {} + // Parse error. Remove the final keyword. final abstract class BazD {} diff --git a/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc.fixed b/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc.fixed index fd789add..abde5462 100644 --- a/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc.fixed +++ b/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc.fixed @@ -30,6 +30,11 @@ class CheckHandlingOfSuperfluousWhitespace extends Something {} /*comment*/ class BazC implements MyInterface {} +// Test fixer when combined with PHP 8.2 "readonly" modifier. +readonly class BazD {} + +readonly class BazE {} + // Parse error. Remove the final keyword. abstract class BazD {} diff --git a/Universal/Tests/Classes/DisallowFinalClassUnitTest.php b/Universal/Tests/Classes/DisallowFinalClassUnitTest.php index 3838831c..6d94272c 100644 --- a/Universal/Tests/Classes/DisallowFinalClassUnitTest.php +++ b/Universal/Tests/Classes/DisallowFinalClassUnitTest.php @@ -35,6 +35,8 @@ public function getErrorList() 31 => 1, 33 => 1, 36 => 1, + 38 => 1, + 41 => 1, ]; } diff --git a/Universal/Tests/Classes/RequireFinalClassUnitTest.inc b/Universal/Tests/Classes/RequireFinalClassUnitTest.inc index 2bf5d25b..ce1f04eb 100644 --- a/Universal/Tests/Classes/RequireFinalClassUnitTest.inc +++ b/Universal/Tests/Classes/RequireFinalClassUnitTest.inc @@ -27,5 +27,7 @@ class BazA {} class BazC implements MyInterface {} +readonly class BazD {} + // Live coding. Ignore. This must be the last test in the file. class LiveCoding diff --git a/Universal/Tests/Classes/RequireFinalClassUnitTest.inc.fixed b/Universal/Tests/Classes/RequireFinalClassUnitTest.inc.fixed index 618959d4..3459f696 100644 --- a/Universal/Tests/Classes/RequireFinalClassUnitTest.inc.fixed +++ b/Universal/Tests/Classes/RequireFinalClassUnitTest.inc.fixed @@ -27,5 +27,7 @@ final class BazA {} final class BazC implements MyInterface {} +readonly final class BazD {} + // Live coding. Ignore. This must be the last test in the file. class LiveCoding diff --git a/Universal/Tests/Classes/RequireFinalClassUnitTest.php b/Universal/Tests/Classes/RequireFinalClassUnitTest.php index 2ecf0acf..007b9e10 100644 --- a/Universal/Tests/Classes/RequireFinalClassUnitTest.php +++ b/Universal/Tests/Classes/RequireFinalClassUnitTest.php @@ -33,6 +33,7 @@ public function getErrorList() 24 => 1, 26 => 1, 28 => 1, + 30 => 1, ]; } From f14adb59beb2702f308d709c3b870c27bf67914e Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 2 Nov 2022 22:11:49 +0100 Subject: [PATCH 047/125] Universal/DisallowUseClass: "docs" update for enums This updates the phrasing referencing the different OO structures to include enums in: * The error message. * The metric names. * Documentation. --- README.md | 2 +- .../Sniffs/UseStatements/DisallowUseClassSniff.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2794e830..dba712e6 100644 --- a/README.md +++ b/README.md @@ -233,7 +233,7 @@ This is considered a **_risky_ fixer**. #### `Universal.UseStatements.DisallowUseClass` :bar_chart: :books: -Forbid using import `use` statements for classes/traits/interfaces. +Forbid using import `use` statements for classes/traits/interfaces/enums. Individual sub-types - with/without alias, global imports, imports from the same namespace - can be forbidden by including that specific error code and/or allowed including the whole sniff and excluding specific error codes. diff --git a/Universal/Sniffs/UseStatements/DisallowUseClassSniff.php b/Universal/Sniffs/UseStatements/DisallowUseClassSniff.php index fd16040f..0dc55eda 100644 --- a/Universal/Sniffs/UseStatements/DisallowUseClassSniff.php +++ b/Universal/Sniffs/UseStatements/DisallowUseClassSniff.php @@ -18,7 +18,7 @@ use PHPCSUtils\Utils\UseStatements; /** - * Disallow class/trait/interface import `use` statements. + * Disallow class/trait/interface/enum import `use` statements. * * Related sniffs: * - `Universal.UseStatements.DisallowUseFunction` @@ -36,7 +36,7 @@ final class DisallowUseClassSniff implements Sniff * * @var string */ - const METRIC_NAME_SRC = 'Use import statement source for class/interface/trait'; + const METRIC_NAME_SRC = 'Use import statement source for class/interface/trait/enum'; /** * Name of the "Use import with/without alias" metric. @@ -45,7 +45,7 @@ final class DisallowUseClassSniff implements Sniff * * @var string */ - const METRIC_NAME_ALIAS = 'Use import statement for class/interface/trait'; + const METRIC_NAME_ALIAS = 'Use import statement for class/interface/trait/enum'; /** * Keep track of which file is being scanned. @@ -121,7 +121,7 @@ public function process(File $phpcsFile, $stackPtr) } if (empty($statements['name'])) { - // No class/trait/interface import statements found. + // No class/trait/interface/enum import statements found. return; } @@ -158,7 +158,7 @@ public function process(File $phpcsFile, $stackPtr) * in case this is a non-namespaced file. */ - $error = 'Use import statements for class/interface/trait%s are not allowed.'; + $error = 'Use import statements for class/interface/trait/enum%s are not allowed.'; $error .= ' Found import statement for: "%s"'; $errorCode = 'Found'; $data = [ From 58ec58a390a66a0d55af4ebad3f2f113c88954ce Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 2 Nov 2022 22:46:13 +0100 Subject: [PATCH 048/125] Universal/AlphabeticExtendsImplements: docs/test update for enums This commit adds tests confirming that `enum ... implements ...` statements are handled correctly by the sniff. Additionally, it updates the documentation to include `enum`s and makes a minor grammar fix to one of the metric names. --- README.md | 2 +- .../OOStructures/AlphabeticExtendsImplementsSniff.php | 6 +++--- .../OOStructures/AlphabeticExtendsImplementsUnitTest.inc | 5 +++++ .../AlphabeticExtendsImplementsUnitTest.inc.fixed | 5 +++++ .../OOStructures/AlphabeticExtendsImplementsUnitTest.php | 1 + 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index dba712e6..fe99586c 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ Disallow the use of multiple namespaces within a file. #### `Universal.OOStructures.AlphabeticExtendsImplements` :wrench: :bar_chart: :books: -Enforce that the names used in a class "implements" statement or an interface "extends" statement are listed in alphabetic order. +Enforce that the names used in a class/enum "implements" statement or an interface "extends" statement are listed in alphabetic order. * This sniff contains a `orderby` property to determine the sort order to use for the statement. If all names used are unqualified, the sort order won't make a difference. diff --git a/Universal/Sniffs/OOStructures/AlphabeticExtendsImplementsSniff.php b/Universal/Sniffs/OOStructures/AlphabeticExtendsImplementsSniff.php index a35e9a5c..33c9609c 100644 --- a/Universal/Sniffs/OOStructures/AlphabeticExtendsImplementsSniff.php +++ b/Universal/Sniffs/OOStructures/AlphabeticExtendsImplementsSniff.php @@ -17,7 +17,7 @@ use PHPCSUtils\Utils\ObjectDeclarations; /** - * Verifies that the names used in a class "implements" statement or an interface "extends" statement + * Verifies that the interface names used in a class/enum "implements" statement or an interface "extends" statement, * are listed in alphabetic order. * * @since 1.0.0 @@ -32,7 +32,7 @@ final class AlphabeticExtendsImplementsSniff implements Sniff * * @var string */ - const METRIC_NAME_ALPHA = 'Interface names in implements/extends order alphabetically (%s)'; + const METRIC_NAME_ALPHA = 'Interface names in implements/extends ordered alphabetically (%s)'; /** * Name of the "interface count" metric. @@ -129,7 +129,7 @@ public function process(File $phpcsFile, $stackPtr) } if (\is_array($names) === false) { - // Class/interface doesn't extend or implement. + // Class/interface/enum doesn't extend or implement. $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME_COUNT, 0); $phpcsFile->recordMetric($stackPtr, $metricNameAlpha, 'n/a'); return; diff --git a/Universal/Tests/OOStructures/AlphabeticExtendsImplementsUnitTest.inc b/Universal/Tests/OOStructures/AlphabeticExtendsImplementsUnitTest.inc index 2d906542..8f52d572 100644 --- a/Universal/Tests/OOStructures/AlphabeticExtendsImplementsUnitTest.inc +++ b/Universal/Tests/OOStructures/AlphabeticExtendsImplementsUnitTest.inc @@ -75,6 +75,11 @@ class WithComments implements // Reset to default. // phpcs:set Universal.OOStructures.AlphabeticExtendsImplements orderby name +enum Suit implements ArrayAccess, Colorful {} // OK. + +enum Suit: string implements Colorful, ArrayAccess {} + + // Test parse error/live coding. // Intentional parse error. This has to be the last test in the file. class Unfinished extends diff --git a/Universal/Tests/OOStructures/AlphabeticExtendsImplementsUnitTest.inc.fixed b/Universal/Tests/OOStructures/AlphabeticExtendsImplementsUnitTest.inc.fixed index 6d8b3bc6..8ad25c11 100644 --- a/Universal/Tests/OOStructures/AlphabeticExtendsImplementsUnitTest.inc.fixed +++ b/Universal/Tests/OOStructures/AlphabeticExtendsImplementsUnitTest.inc.fixed @@ -72,6 +72,11 @@ class WithComments implements // Reset to default. // phpcs:set Universal.OOStructures.AlphabeticExtendsImplements orderby name +enum Suit implements ArrayAccess, Colorful {} // OK. + +enum Suit: string implements ArrayAccess, Colorful {} + + // Test parse error/live coding. // Intentional parse error. This has to be the last test in the file. class Unfinished extends diff --git a/Universal/Tests/OOStructures/AlphabeticExtendsImplementsUnitTest.php b/Universal/Tests/OOStructures/AlphabeticExtendsImplementsUnitTest.php index e3de6469..ed18a487 100644 --- a/Universal/Tests/OOStructures/AlphabeticExtendsImplementsUnitTest.php +++ b/Universal/Tests/OOStructures/AlphabeticExtendsImplementsUnitTest.php @@ -41,6 +41,7 @@ public function getErrorList() 54 => 1, 62 => 1, 68 => 1, + 80 => 1, ]; } From ed4cfc609a17baefb35b192307d77b55f4e4af0d Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 27 Oct 2022 10:11:46 +0200 Subject: [PATCH 049/125] Universal/DisallowShortListSyntax: allow for tokenizer issue in PHPCS 3.7.1 ... which was fixed in 3.7.2. The utility methods used from PHPCSUtils 1.0.0-alpha4 already take this into account, so this should be handled without problems. Includes test. Ref: * squizlabs/PHP_CodeSniffer 3632 * PHPCSStandards/PHPCSUtils 392 --- Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php | 3 ++- Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc | 5 +++++ .../Tests/Lists/DisallowShortListSyntaxUnitTest.inc.fixed | 5 +++++ Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.php | 1 + 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php b/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php index 48620ce5..43de5078 100644 --- a/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php +++ b/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php @@ -12,6 +12,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use PHPCSUtils\Tokens\Collections; use PHPCSUtils\Utils\Lists; /** @@ -31,7 +32,7 @@ final class DisallowShortListSyntaxSniff implements Sniff */ public function register() { - return [\T_OPEN_SHORT_ARRAY]; + return Collections::shortArrayListOpenTokensBC(); } /** diff --git a/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc b/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc index 14551195..bf721ada 100644 --- a/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc +++ b/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc @@ -19,5 +19,10 @@ list($a, list($b, $c, $d)) = $array; ], ] = $array; +// Test for a specific tokenizer issue which still exists in PHPCS 3.7.1 (fixed in 3.7.2). +if ( true ) + [ $a ] = [ 'hi' ]; +return $a ?? ''; + // Intentional parse error. This has to be the last test in the file. [ $a, $b diff --git a/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc.fixed b/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc.fixed index 03256735..4ac6e04d 100644 --- a/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc.fixed +++ b/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc.fixed @@ -19,5 +19,10 @@ list( ), ) = $array; +// Test for a specific tokenizer issue which still exists in PHPCS 3.7.1 (fixed in 3.7.2). +if ( true ) + list( $a ) = [ 'hi' ]; +return $a ?? ''; + // Intentional parse error. This has to be the last test in the file. [ $a, $b diff --git a/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.php b/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.php index 7e7cff8f..88421dcd 100644 --- a/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.php +++ b/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.php @@ -38,6 +38,7 @@ public function getErrorList() 12 => 2, 13 => 1, 15 => 1, + 24 => 1, ]; } From 685996f890d9e8ab690c2c21e4da4b99d469bf6e Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 29 Oct 2022 00:22:00 +0200 Subject: [PATCH 050/125] Universal/DisallowShortListSyntax: bug fix - don't skip over nested brackets ... as short arrays can contain short lists and those still need to be examined. Includes test to safeguard this. --- Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php | 5 ----- Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc | 5 +++++ .../Tests/Lists/DisallowShortListSyntaxUnitTest.inc.fixed | 5 +++++ Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.php | 1 + 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php b/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php index 43de5078..99e8898b 100644 --- a/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php +++ b/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php @@ -53,11 +53,6 @@ public function process(File $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; } diff --git a/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc b/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc index bf721ada..4ebc50d5 100644 --- a/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc +++ b/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc @@ -24,5 +24,10 @@ if ( true ) [ $a ] = [ 'hi' ]; return $a ?? ''; +// Short list in short array. +$array = [ + 'key' => [$a, $b] = $args, +]; + // Intentional parse error. This has to be the last test in the file. [ $a, $b diff --git a/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc.fixed b/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc.fixed index 4ac6e04d..a1792681 100644 --- a/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc.fixed +++ b/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.inc.fixed @@ -24,5 +24,10 @@ if ( true ) list( $a ) = [ 'hi' ]; return $a ?? ''; +// Short list in short array. +$array = [ + 'key' => list($a, $b) = $args, +]; + // Intentional parse error. This has to be the last test in the file. [ $a, $b diff --git a/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.php b/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.php index 88421dcd..09a310d7 100644 --- a/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.php +++ b/Universal/Tests/Lists/DisallowShortListSyntaxUnitTest.php @@ -39,6 +39,7 @@ public function getErrorList() 13 => 1, 15 => 1, 24 => 1, + 29 => 1, ]; } From 33f2e6e2a3f427a347254a00acef1894253ffa38 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 29 Oct 2022 00:07:07 +0200 Subject: [PATCH 051/125] Universal/DisallowShortArraySyntax: don't skip over short lists ... as for some unfanthomable reason (short) arrays can be used as keys for short lists and if so, those should still be found and fixed. Includes test to safeguard this. --- Universal/Sniffs/Arrays/DisallowShortArraySyntaxSniff.php | 3 +-- Universal/Tests/Arrays/DisallowShortArraySyntaxUnitTest.inc | 3 +++ .../Tests/Arrays/DisallowShortArraySyntaxUnitTest.inc.fixed | 3 +++ Universal/Tests/Arrays/DisallowShortArraySyntaxUnitTest.php | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Universal/Sniffs/Arrays/DisallowShortArraySyntaxSniff.php b/Universal/Sniffs/Arrays/DisallowShortArraySyntaxSniff.php index e0c076e4..bfdf98be 100644 --- a/Universal/Sniffs/Arrays/DisallowShortArraySyntaxSniff.php +++ b/Universal/Sniffs/Arrays/DisallowShortArraySyntaxSniff.php @@ -57,8 +57,7 @@ public function process(File $phpcsFile, $stackPtr) $tokens = $phpcsFile->getTokens(); if (Arrays::isShortArray($phpcsFile, $stackPtr) === false) { - // No need to examine nested subs of this short list. - return $tokens[$stackPtr]['bracket_closer']; + return; } $error = 'Short array syntax is not allowed'; diff --git a/Universal/Tests/Arrays/DisallowShortArraySyntaxUnitTest.inc b/Universal/Tests/Arrays/DisallowShortArraySyntaxUnitTest.inc index db4af2c3..4c5d362d 100644 --- a/Universal/Tests/Arrays/DisallowShortArraySyntaxUnitTest.inc +++ b/Universal/Tests/Arrays/DisallowShortArraySyntaxUnitTest.inc @@ -14,3 +14,6 @@ $foo = [ // Short list, not short array. [$a, [$b]] = $array; + +// Short array in short list. Short list should be left untouched, short array should be fixed. +['key' => $a, ['key'] => $b] = $array; diff --git a/Universal/Tests/Arrays/DisallowShortArraySyntaxUnitTest.inc.fixed b/Universal/Tests/Arrays/DisallowShortArraySyntaxUnitTest.inc.fixed index 9f94f20a..c569de91 100644 --- a/Universal/Tests/Arrays/DisallowShortArraySyntaxUnitTest.inc.fixed +++ b/Universal/Tests/Arrays/DisallowShortArraySyntaxUnitTest.inc.fixed @@ -14,3 +14,6 @@ $foo = array( // Short list, not short array. [$a, [$b]] = $array; + +// Short array in short list. Short list should be left untouched, short array should be fixed. +['key' => $a, array('key') => $b] = $array; diff --git a/Universal/Tests/Arrays/DisallowShortArraySyntaxUnitTest.php b/Universal/Tests/Arrays/DisallowShortArraySyntaxUnitTest.php index 5eaf044b..453ce81e 100644 --- a/Universal/Tests/Arrays/DisallowShortArraySyntaxUnitTest.php +++ b/Universal/Tests/Arrays/DisallowShortArraySyntaxUnitTest.php @@ -38,6 +38,7 @@ public function getErrorList() 8 => 1, 9 => 1, 11 => 1, + 19 => 1, ]; } From f1646d969d309d547d09af9f90d03794d46545c3 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 27 Oct 2022 10:03:00 +0200 Subject: [PATCH 052/125] Universal/DisallowShortArraySyntax: record metrics It's quite straight forward to record informative metrics for this sniff, so let's. --- .../Arrays/DisallowShortArraySyntaxSniff.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Universal/Sniffs/Arrays/DisallowShortArraySyntaxSniff.php b/Universal/Sniffs/Arrays/DisallowShortArraySyntaxSniff.php index bfdf98be..c4e8987c 100644 --- a/Universal/Sniffs/Arrays/DisallowShortArraySyntaxSniff.php +++ b/Universal/Sniffs/Arrays/DisallowShortArraySyntaxSniff.php @@ -12,6 +12,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use PHPCSUtils\Tokens\Collections; use PHPCSUtils\Utils\Arrays; /** @@ -29,6 +30,13 @@ final class DisallowShortArraySyntaxSniff implements Sniff { + /** + * The phrase to use for the metric recorded by this sniff. + * + * @var string + */ + const METRIC_NAME = 'Short array syntax used'; + /** * Registers the tokens that this sniff wants to listen for. * @@ -38,7 +46,7 @@ final class DisallowShortArraySyntaxSniff implements Sniff */ public function register() { - return [\T_OPEN_SHORT_ARRAY]; + return Collections::arrayOpenTokensBC(); } /** @@ -56,10 +64,18 @@ public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); + if ($tokens[$stackPtr]['code'] === \T_ARRAY) { + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'no'); + return; + } + if (Arrays::isShortArray($phpcsFile, $stackPtr) === false) { + // Square brackets, but not a short array. Probably short list or real square brackets. return; } + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes'); + $error = 'Short array syntax is not allowed'; $fix = $phpcsFile->addFixableError($error, $stackPtr, 'Found'); From 2d063433497a299f3cc0ba290c11f8fb3c965c45 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 27 Oct 2022 10:18:26 +0200 Subject: [PATCH 053/125] Universal/DisallowLongListSyntax: don't record metrics ... as recording just and only the `no` is not a useful metric and adding the logic to also record the `yes` would severely impact the performance of the sniff. --- Universal/Sniffs/Lists/DisallowLongListSyntaxSniff.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/Universal/Sniffs/Lists/DisallowLongListSyntaxSniff.php b/Universal/Sniffs/Lists/DisallowLongListSyntaxSniff.php index 595932b9..1b57510f 100644 --- a/Universal/Sniffs/Lists/DisallowLongListSyntaxSniff.php +++ b/Universal/Sniffs/Lists/DisallowLongListSyntaxSniff.php @@ -53,8 +53,6 @@ public function process(File $phpcsFile, $stackPtr) return; } - $phpcsFile->recordMetric($stackPtr, 'Short list syntax used', 'no'); - $fix = $phpcsFile->addFixableError('Long list syntax is not allowed', $stackPtr, 'Found'); if ($fix === true) { From 731f0007979a62b6ede9da3ad9a0c107ce5ccfa0 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 27 Oct 2022 10:20:12 +0200 Subject: [PATCH 054/125] Universal/DisallowShortListSyntax: improve metric recording Record both the `yes` as well as the `no`, which makes the metric feature complete. --- .../Lists/DisallowShortListSyntaxSniff.php | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php b/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php index 99e8898b..56087409 100644 --- a/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php +++ b/Universal/Sniffs/Lists/DisallowShortListSyntaxSniff.php @@ -23,6 +23,13 @@ final class DisallowShortListSyntaxSniff implements Sniff { + /** + * The phrase to use for the metric recorded by this sniff. + * + * @var string + */ + const METRIC_NAME = 'Short list syntax used'; + /** * Registers the tokens that this sniff wants to listen for. * @@ -32,7 +39,10 @@ final class DisallowShortListSyntaxSniff implements Sniff */ public function register() { - return Collections::shortArrayListOpenTokensBC(); + $targets = Collections::shortArrayListOpenTokensBC(); + $targets[\T_LIST] = \T_LIST; // Only for recording metrics. + + return $targets; } /** @@ -48,7 +58,13 @@ public function register() */ public function process(File $phpcsFile, $stackPtr) { - $tokens = $phpcsFile->getTokens(); + $tokens = $phpcsFile->getTokens(); + + if ($tokens[$stackPtr]['code'] === \T_LIST) { + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'no'); + return; + } + $openClose = Lists::getOpenClose($phpcsFile, $stackPtr); if ($openClose === false) { @@ -56,7 +72,7 @@ public function process(File $phpcsFile, $stackPtr) return; } - $phpcsFile->recordMetric($stackPtr, 'Short list syntax used', 'yes'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes'); $fix = $phpcsFile->addFixableError('Short list syntax is not allowed', $stackPtr, 'Found'); From d3b8a6e51c3a800d1ff0b133379a5525a4cd0b11 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 4 Nov 2022 19:30:29 +0100 Subject: [PATCH 055/125] GH Actions: bust the cache semi-regularly Caches used in GH Actions do not get updated, they can only be replaced by a different cache with a different cache key. Now the predefined Composer install action this repo is using already creates a pretty comprehensive cache key: > `ramsey/composer-install` will auto-generate a cache key which is composed of the following elements: > * The OS image name, like `ubuntu-latest`. > * The exact PHP version, like `8.1.11`. > * The options passed via `composer-options`. > * The dependency version setting as per `dependency-versions`. > * The working directory as per `working-directory`. > * A hash of the `composer.json` and/or `composer.lock` files. This means that aside from other factors, the cache will always be busted when changes are made to the (committed) `composer.json` or the `composer.lock` file (if the latter exists in the repo). For packages running on recent versions of PHP, it also means that the cache will automatically be busted once a month when a new PHP version comes out. ### The problem For runs on older PHP versions which don't receive updates anymore, the cache will not be busted via new PHP version releases, so effectively, the cache will only be busted when a change is made to the `composer.json`/`composer.lock` file - which may not happen that frequently on low-traffic repos. But... packages _in use_ on those older PHP versions - especially dependencies of declared dependencies - may still release new versions and those new versions will not exist in the cache and will need to be downloaded each time the action is run and over time the cache gets less and less relevant as more and more packages will need to be downloaded for each run. ### The solution To combat this issue, a new `custom-cache-suffix` option has been added to the Composer install action in version 2.2.0. This new option allows for providing some extra information to add to the cache key, which allows for busting the cache based on your own additional criteria. This commit implements the use of this `custom-cache-suffix` option for all relevant workflows in this repo. Refs: * https://github.com/ramsey/composer-install/#custom-cache-suffix * https://github.com/ramsey/composer-install/releases/tag/2.2.0 Includes removing an accidental double install. --- .github/workflows/basics.yml | 3 +++ .github/workflows/quicktest.yml | 4 ++++ .github/workflows/test.yml | 10 +++++++--- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml index b864d8bf..582cf962 100644 --- a/.github/workflows/basics.yml +++ b/.github/workflows/basics.yml @@ -51,6 +51,9 @@ jobs: # @link https://github.com/marketplace/actions/install-composer-dependencies - name: Install Composer dependencies uses: "ramsey/composer-install@v2" + with: + # Bust the cache at least once a month - output format: YYYY-MM-DD. + custom-cache-suffix: $(date -u -d "-0 month -$(($(date +%d)-1)) days" "+%F") - name: Install xmllint run: | diff --git a/.github/workflows/quicktest.yml b/.github/workflows/quicktest.yml index 46a8639c..90c699e0 100644 --- a/.github/workflows/quicktest.yml +++ b/.github/workflows/quicktest.yml @@ -63,6 +63,9 @@ jobs: - name: Install Composer dependencies - normal if: matrix.php != 'latest' uses: "ramsey/composer-install@v2" + with: + # Bust the cache at least once a month - output format: YYYY-MM-DD. + custom-cache-suffix: $(date -u -d "-0 month -$(($(date +%d)-1)) days" "+%F") # For the PHP "latest", we need to install with ignore platform reqs as not all PHPUnit 7.x dependencies allow it. - name: Install Composer dependencies - with ignore platform @@ -70,6 +73,7 @@ jobs: uses: "ramsey/composer-install@v2" with: composer-options: --ignore-platform-reqs + custom-cache-suffix: $(date -u -d "-0 month -$(($(date +%d)-1)) days" "+%F") - name: Lint against parse errors if: matrix.phpcs_version == 'dev-master' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1a362bef..39da1a50 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -101,6 +101,9 @@ jobs: - name: Install Composer dependencies - normal if: ${{ startsWith( matrix.php, '8' ) == false }} uses: "ramsey/composer-install@v2" + with: + # Bust the cache at least once a month - output format: YYYY-MM-DD. + custom-cache-suffix: $(date -u -d "-0 month -$(($(date +%d)-1)) days" "+%F") # For the PHP 8/"nightly", we need to install with ignore platform reqs as we're still using PHPUnit 7. - name: Install Composer dependencies - with ignore platform @@ -108,6 +111,7 @@ jobs: uses: "ramsey/composer-install@v2" with: composer-options: --ignore-platform-reqs + custom-cache-suffix: $(date -u -d "-0 month -$(($(date +%d)-1)) days" "+%F") - name: Lint against parse errors if: matrix.phpcs_version == 'dev-master' @@ -169,13 +173,13 @@ jobs: # Set a specific PHPCS version. composer require --no-update squizlabs/php_codesniffer:"${{ matrix.phpcs_version }}" --no-scripts --no-interaction - - name: Install Composer dependencies - normal - uses: "ramsey/composer-install@v2" - # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-composer-dependencies - name: Install Composer dependencies uses: "ramsey/composer-install@v2" + with: + # Bust the cache at least once a month - output format: YYYY-MM-DD. + custom-cache-suffix: $(date -u -d "-0 month -$(($(date +%d)-1)) days" "+%F") - name: Lint against parse errors if: matrix.phpcs_version == 'dev-master' From 011eaaea8fbb03c925680a6dcea22fdaaf209d0d Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 12 Nov 2022 14:19:48 +0100 Subject: [PATCH 056/125] Universal/DisallowAlternativeSyntax: improve test code While empty "leafs" of control structures is allowed, it is less common, so make the code used for the tests more realistic. Includes improvements to the inline documentation in the test code file to make it clearer what is being tested. --- .../DisallowAlternativeSyntaxUnitTest.inc | 31 ++++++++++++------- ...isallowAlternativeSyntaxUnitTest.inc.fixed | 31 ++++++++++++------- .../DisallowAlternativeSyntaxUnitTest.php | 8 ++--- 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.inc b/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.inc index eca779cb..16a4c8c5 100644 --- a/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.inc +++ b/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.inc @@ -4,7 +4,7 @@ * Control structures without braces. Ignore. */ if (true) - function_call($a); + // Deliberately empty. elseif (false) function_call($a); else @@ -32,14 +32,14 @@ declare(ticks=1); * Control structures using curly braces. Ignore. */ if ( true ) { - // Code. + function_call($a); } elseif (false) { - // Code. + function_call($a); } else if (false) { - // Code. + function_call($a); } else { - // Code. + // Deliberately empty. } for ($i = 1; $i <= 10; $i++) { @@ -71,11 +71,11 @@ declare(ticks=1) { * Alternative control structure syntax. */ if (true): - // Code. + function_call($a); elseif (false): - // Code. + // Deliberately empty. else: - // Code. + function_call($a); endif; for ($i = 1; $i <= 10; $i++) @@ -141,12 +141,16 @@ A is something else A is equal to 5 @@ -213,6 +220,6 @@ A is something else // Live coding. // Intentional parse error. This test has to be the last in the file. if ($a) { - // Code. + function_call($a); } else { // Code. diff --git a/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.inc.fixed b/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.inc.fixed index 05df8755..cda96ceb 100644 --- a/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.inc.fixed +++ b/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.inc.fixed @@ -4,7 +4,7 @@ * Control structures without braces. Ignore. */ if (true) - function_call($a); + // Deliberately empty. elseif (false) function_call($a); else @@ -32,14 +32,14 @@ declare(ticks=1); * Control structures using curly braces. Ignore. */ if ( true ) { - // Code. + function_call($a); } elseif (false) { - // Code. + function_call($a); } else if (false) { - // Code. + function_call($a); } else { - // Code. + // Deliberately empty. } for ($i = 1; $i <= 10; $i++) { @@ -71,11 +71,11 @@ declare(ticks=1) { * Alternative control structure syntax. */ if (true){ - // Code. + function_call($a); } elseif (false){ - // Code. + // Deliberately empty. } else{ - // Code. + function_call($a); } for ($i = 1; $i <= 10; $i++) @@ -141,12 +141,16 @@ A is something else A is equal to 5 @@ -213,6 +220,6 @@ A is something else // Live coding. // Intentional parse error. This test has to be the last in the file. if ($a) { - // Code. + function_call($a); } else { // Code. diff --git a/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.php b/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.php index 3ca06caf..ea75d232 100644 --- a/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.php +++ b/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.php @@ -46,14 +46,14 @@ public function getErrorList() 127 => 1, 131 => 1, 138 => 1, - 144 => 1, - 146 => 1, 148 => 1, - 153 => 1, + 150 => 1, + 152 => 1, 157 => 1, 161 => 1, 165 => 1, - 174 => 1, + 169 => 1, + 178 => 1, ]; } From 8288e14e5e43d2cc96566963edad87949784e84b Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 12 Nov 2022 15:03:48 +0100 Subject: [PATCH 057/125] Universal/DisallowAlternativeSyntax: add additional tests with empty control structure bodies When a control structure condition is directly followed by a PHP close tag, a semicolon is silently inserted by PHP and the control structure will turn into a control structure without body. This commit adds additional tests to safeguard that the sniff handles this correctly. --- .../DisallowAlternativeSyntaxUnitTest.inc | 9 +++- ...isallowAlternativeSyntaxUnitTest.inc.fixed | 9 +++- .../DisallowAlternativeSyntaxUnitTest.php | 42 +++++++++---------- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.inc b/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.inc index 16a4c8c5..8de9b475 100644 --- a/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.inc +++ b/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.inc @@ -19,15 +19,22 @@ foreach ($a as $k => $v) while (++$i <= 10) echo $i; +if (true) ?> + Do something unconditionally as the PHP close tag turned the if into an single statement (silently added a ;). + + $v) while (++$i <= 10) echo $i; +if (true) ?> + Do something unconditionally as the PHP close tag turned the if into an single statement (silently added a ;). + + 1, - 75 => 1, - 77 => 1, + 80 => 1, 82 => 1, - 86 => 1, - 90 => 1, - 94 => 1, - 103 => 1, - 111 => 1, - 113 => 1, - 115 => 1, - 119 => 1, - 123 => 1, - 127 => 1, - 131 => 1, + 84 => 1, + 89 => 1, + 93 => 1, + 97 => 1, + 101 => 1, + 110 => 1, + 118 => 1, + 120 => 1, + 122 => 1, + 126 => 1, + 130 => 1, + 134 => 1, 138 => 1, - 148 => 1, - 150 => 1, - 152 => 1, + 145 => 1, + 155 => 1, 157 => 1, - 161 => 1, - 165 => 1, - 169 => 1, - 178 => 1, + 159 => 1, + 164 => 1, + 168 => 1, + 172 => 1, + 176 => 1, + 185 => 1, ]; } From 8af66a6c55a4d8283b78707b03bb880e87433171 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 12 Nov 2022 15:28:37 +0100 Subject: [PATCH 058/125] Universal/DisallowAlternativeSyntax: minor docs improvements --- .../ControlStructures/DisallowAlternativeSyntaxSniff.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php index c43c6d86..93c59bad 100644 --- a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php +++ b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php @@ -37,6 +37,8 @@ final class DisallowAlternativeSyntaxSniff implements Sniff * Whether to allow the alternative syntax when it is wrapped around * inline HTML, as is often seen in views. * + * @since 1.0.0 + * * @var bool */ public $allowWithInlineHTML = false; @@ -80,9 +82,10 @@ public function process(File $phpcsFile, $stackPtr) } /* - * Ignore control structures without body. + * Ignore control structures without body (i.e. single line control structures). + * This doesn't ignore _empty_ bodies. */ - if (ControlStructures::hasBody($phpcsFile, $stackPtr) === false) { + if (ControlStructures::hasBody($phpcsFile, $stackPtr, true) === false) { return; } From 0100cbe3002986b4c57f29a7baf8a811f95d1acf Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 13 Nov 2022 19:04:39 +0100 Subject: [PATCH 059/125] Universal/DisallowAlternativeSyntax: improve metrics * Minor tweak to the metric name. * Also record metric for single line/no body control structures. --- .../ControlStructures/DisallowAlternativeSyntaxSniff.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php index 93c59bad..b62351bc 100644 --- a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php +++ b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php @@ -31,7 +31,7 @@ final class DisallowAlternativeSyntaxSniff implements Sniff * * @var string */ - const METRIC_NAME = 'Control structure style'; + const METRIC_NAME = 'Control Structure Style'; /** * Whether to allow the alternative syntax when it is wrapped around @@ -86,6 +86,7 @@ public function process(File $phpcsFile, $stackPtr) * This doesn't ignore _empty_ bodies. */ if (ControlStructures::hasBody($phpcsFile, $stackPtr, true) === false) { + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'single line (without body)'); return; } From fb697d31574ef18f307bf2e8fbe0903671e1e1ae Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 6 Nov 2022 06:03:13 +0100 Subject: [PATCH 060/125] Universal/DisallowAlternativeSyntax: improve the error message [1] When the `$allowWithInlineHTML` property is set to `true`, the error message could be regarded as confusing and lead to (unfounded) bug reports as the error message doesn't indicate that alternative control structures, in that case, are still allowed in combination with inline HTML. This commit adjusts the error message based on the `$allowWithInlineHTML` property to be more descriptive. --- .../DisallowAlternativeSyntaxSniff.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php index b62351bc..f8496384 100644 --- a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php +++ b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php @@ -124,9 +124,15 @@ public function process(File $phpcsFile, $stackPtr) return; } - $error = 'Using control structures with the alternative syntax - %1$s(): ... end%1$s; - is not allowed.'; - $code = 'Found' . \ucfirst($tokens[$stackPtr]['content']); - $data = [$tokens[$stackPtr]['content']]; + $error = 'Using control structures with the alternative syntax - %1$s(): ... end%1$s; - is not allowed'; + if ($this->allowWithInlineHTML === true) { + $error .= ' unless the control structure contains inline HTML'; + } + $error .= '.'; + + $code = 'Found' . \ucfirst($tokens[$stackPtr]['content']); + $data = [$tokens[$stackPtr]['content']]; + if ($tokens[$stackPtr]['code'] === \T_ELSEIF || $tokens[$stackPtr]['code'] === \T_ELSE) { $data = ['if']; } From a161fc2e2b9d9c77c15fe345974c362031cc905b Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 13 Nov 2022 14:34:30 +0100 Subject: [PATCH 061/125] Universal/DisallowAlternativeSyntax: improve the error message [2] Improve the readability of the error message by moving the variable part to the end of the message. --- .../ControlStructures/DisallowAlternativeSyntaxSniff.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php index f8496384..c8d0fe90 100644 --- a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php +++ b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php @@ -124,11 +124,11 @@ public function process(File $phpcsFile, $stackPtr) return; } - $error = 'Using control structures with the alternative syntax - %1$s(): ... end%1$s; - is not allowed'; + $error = 'Using control structures with the alternative syntax is not allowed'; if ($this->allowWithInlineHTML === true) { $error .= ' unless the control structure contains inline HTML'; } - $error .= '.'; + $error .= '. Found: %1$s(): ... end%1$s;'; $code = 'Found' . \ucfirst($tokens[$stackPtr]['content']); $data = [$tokens[$stackPtr]['content']]; From 954294a7e21c46548b9bc7b570f087a8c6dfc2ac Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 14 Nov 2022 01:27:33 +0100 Subject: [PATCH 062/125] Universal/DisallowAlternativeSyntax: minor tweak Group the setting of the `$code` and `$data` variables for the error message with the conditions which may adjust their value. --- .../ControlStructures/DisallowAlternativeSyntaxSniff.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php index c8d0fe90..10fa6f83 100644 --- a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php +++ b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php @@ -131,16 +131,15 @@ public function process(File $phpcsFile, $stackPtr) $error .= '. Found: %1$s(): ... end%1$s;'; $code = 'Found' . \ucfirst($tokens[$stackPtr]['content']); - $data = [$tokens[$stackPtr]['content']]; + if ($hasInlineHTML !== false) { + $code .= 'WithInlineHTML'; + } + $data = [$tokens[$stackPtr]['content']]; if ($tokens[$stackPtr]['code'] === \T_ELSEIF || $tokens[$stackPtr]['code'] === \T_ELSE) { $data = ['if']; } - if ($hasInlineHTML !== false) { - $code .= 'WithInlineHTML'; - } - $fix = $phpcsFile->addFixableError($error, $tokens[$stackPtr]['scope_opener'], $code, $data); if ($fix === false) { return; From 2c30b2c3b3bf58e6c440ad6bacea8774d9e8deae Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 13 Nov 2022 14:37:54 +0100 Subject: [PATCH 063/125] Universal/DisallowAlternativeSyntax: bug fix - ignore inline HTML in nested closed scopes ... as the HTML in that case is not necessarily echo-ed out when the control structure is executed, but rather is executed when the function in the nested closed scope is called and executed, so the inline HTML does not "belong" with the control structure. This should also potentially improve performance of the sniff when large chunks of code without inline HTML are wrapped within a control structure. Includes tests. --- .../DisallowAlternativeSyntaxSniff.php | 31 +++++++++++---- .../DisallowAlternativeSyntaxUnitTest.inc | 39 +++++++++++++++++++ ...isallowAlternativeSyntaxUnitTest.inc.fixed | 39 +++++++++++++++++++ .../DisallowAlternativeSyntaxUnitTest.php | 4 ++ 4 files changed, 105 insertions(+), 8 deletions(-) diff --git a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php index 10fa6f83..390c81ea 100644 --- a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php +++ b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php @@ -37,6 +37,9 @@ final class DisallowAlternativeSyntaxSniff implements Sniff * Whether to allow the alternative syntax when it is wrapped around * inline HTML, as is often seen in views. * + * Note: inline HTML within "closed scopes" - like function declarations -, + * within the control structure body will not be taken into account. + * * @since 1.0.0 * * @var bool @@ -108,19 +111,31 @@ public function process(File $phpcsFile, $stackPtr) return; } - $hasInlineHTML = $phpcsFile->findNext( - \T_INLINE_HTML, - $opener, - $closer - ); + $closedScopes = Collections::closedScopes(); + $find = $closedScopes; + $find[\T_INLINE_HTML] = \T_INLINE_HTML; + $inlineHTMLPtr = $opener; + $hasInlineHTML = false; + + do { + $inlineHTMLPtr = $phpcsFile->findNext($find, ($inlineHTMLPtr + 1), $closer); + if ($tokens[$inlineHTMLPtr]['code'] === \T_INLINE_HTML) { + $hasInlineHTML = true; + break; + } + + if (isset($closedScopes[$tokens[$inlineHTMLPtr]['code']], $tokens[$inlineHTMLPtr]['scope_closer'])) { + $inlineHTMLPtr = $tokens[$inlineHTMLPtr]['scope_closer']; + } + } while ($inlineHTMLPtr !== false && $inlineHTMLPtr < $closer); - if ($hasInlineHTML !== false) { + if ($hasInlineHTML === true) { $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'alternative syntax with inline HTML'); } else { $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'alternative syntax'); } - if ($hasInlineHTML !== false && $this->allowWithInlineHTML === true) { + if ($hasInlineHTML === true && $this->allowWithInlineHTML === true) { return; } @@ -131,7 +146,7 @@ public function process(File $phpcsFile, $stackPtr) $error .= '. Found: %1$s(): ... end%1$s;'; $code = 'Found' . \ucfirst($tokens[$stackPtr]['content']); - if ($hasInlineHTML !== false) { + if ($hasInlineHTML === true) { $code .= 'WithInlineHTML'; } diff --git a/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.inc b/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.inc index 8de9b475..e5352f9f 100644 --- a/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.inc +++ b/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.inc @@ -222,6 +222,45 @@ A is something else + Inline HTML in closed scopes should be disregarded. + + Inline HTML in closed scopes should be disregarded. + + Inline HTML in closed scopes should be disregarded. + + Inline HTML in closed scopes should be disregarded. + + Inline HTML in closed scopes should be disregarded. + + Inline HTML in closed scopes should be disregarded. + 1, 176 => 1, 185 => 1, + 229 => 1, + 235 => 1, + 246 => 1, + 256 => 1, ]; } From dc6c987be1cf655f1c3a145e26d30166ef971cb6 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 14 Nov 2022 01:48:34 +0100 Subject: [PATCH 064/125] Universal/DisallowAlternativeSyntax: bug fix - handle if/elseif statements in one go As things were, the sniff would examine - and potentially fix - each control structure keyword which could be used with alternative syntax individually. For potentially multi-part control structures, like `if` - `elseif` - `else`, this could lead to parse errors in the fixed code as mixing curly braces with alternative syntax within the same control structure "chain" is not allowed. This only comes into play when the `$allowWithInlineHTML` property has been set to `true` as that's the only time when an `if` would potentially be treated differently from the associated `else` (one containing inline HTML being left alone, the other not containing inline HTML being "fixed"). While `try` - `catch` and `do` - `while` are also potentially multi-part control structures, these do not allow for using the alternative control structure syntax, so for the purposes of this sniff, `if` - `elseif` - `else` chains are the only ones we need to take into account. Also take note of the fact that `else if` (with a space between) is not allowed when using alternative syntax, so we don't need to take the keyword potentially being split into account. This commit adjusts the sniff to handle these chains in one go and to prevent the potential parse error, by: * No longer sniffing for `T_ELSEIF` or `T_ELSE` tokens. Note: this does affect how the metrics are recorded. Previously a metric would be recorded for each keyword. Now a metric will be recorded only for the _first_ keyword in a chain. This doesn't make a difference for non-chained control structures, but it does make a difference for chained `if/else` sets. Also note: the metric will only be recorded for control structure keywords which support alternative syntax, not for _all_ control structures. This remains the same as before. * When examining the control structure for inline HTML, the sniff will now loop from the first matching control structure keyword till the `end*` and remembering each control structure keyword (opener/closer) encountered along the way in case of a chain. For multi-part chains, once an inline HTML token has been encountered, subsequent "leafs" will no longer be examined for inline HTML. This should yield a small performance improvement. * The error messages will now be thrown via a loop based on the remembered information about each keyword encountered in a chain. * Along the same lines, the fixer will now make all fixes for a "chain" in one go based on the remembered information about each keyword encountered in the chain. This should also help prevent potential fixer conflicts between sniffs. Includes a range of additional unit tests for this issue. Also includes additional unit tests to safeguard that the behaviour of PHPCS regarding parse errors in chained control structures will not change, as the sniff has a build-in presumption about the PHPCS behaviour with control structures using alternative syntax, so if at any point in the future, the PHPCS token stream for this would change, the sniff will need adjusting. --- .../DisallowAlternativeSyntaxSniff.php | 109 ++++++++++++------ ...> DisallowAlternativeSyntaxUnitTest.1.inc} | 98 ++++++++++++++++ ...llowAlternativeSyntaxUnitTest.1.inc.fixed} | 98 ++++++++++++++++ .../DisallowAlternativeSyntaxUnitTest.2.inc | 9 ++ .../DisallowAlternativeSyntaxUnitTest.3.inc | 10 ++ .../DisallowAlternativeSyntaxUnitTest.4.inc | 8 ++ .../DisallowAlternativeSyntaxUnitTest.php | 38 ++++-- 7 files changed, 320 insertions(+), 50 deletions(-) rename Universal/Tests/ControlStructures/{DisallowAlternativeSyntaxUnitTest.inc => DisallowAlternativeSyntaxUnitTest.1.inc} (69%) rename Universal/Tests/ControlStructures/{DisallowAlternativeSyntaxUnitTest.inc.fixed => DisallowAlternativeSyntaxUnitTest.1.inc.fixed} (68%) create mode 100644 Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.2.inc create mode 100644 Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.3.inc create mode 100644 Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.4.inc diff --git a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php index 390c81ea..86f06a6a 100644 --- a/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php +++ b/Universal/Sniffs/ControlStructures/DisallowAlternativeSyntaxSniff.php @@ -55,7 +55,12 @@ final class DisallowAlternativeSyntaxSniff implements Sniff */ public function register() { - return Collections::alternativeControlStructureSyntaxes(); + $targets = Collections::alternativeControlStructureSyntaxes(); + + // Don't look for elseif/else as they need to be dealt with in one go with the if. + unset($targets[\T_ELSEIF], $targets[\T_ELSE]); + + return $targets; } /** @@ -71,19 +76,6 @@ public function register() */ public function process(File $phpcsFile, $stackPtr) { - $tokens = $phpcsFile->getTokens(); - - /* - * Deal with `else if`. - */ - if ($tokens[$stackPtr]['code'] === \T_ELSE - && isset($tokens[$stackPtr]['scope_opener']) === false - && ControlStructures::isElseIf($phpcsFile, $stackPtr) === true - ) { - // This is an `else if` - this will be dealt with on the `if` token. - return; - } - /* * Ignore control structures without body (i.e. single line control structures). * This doesn't ignore _empty_ bodies. @@ -93,6 +85,8 @@ public function process(File $phpcsFile, $stackPtr) return; } + $tokens = $phpcsFile->getTokens(); + /* * Check if the control structure uses alternative syntax. */ @@ -111,23 +105,56 @@ public function process(File $phpcsFile, $stackPtr) return; } + /* + * As of here, we *know* the control structure must be using alternative syntax and + * must have all scope openers/closers set as, in case of parse errors, PHPCS wouldn't + * have set the scope opener, even for the first `if`. + * + * Also note that alternative syntax cannot be used with `else if`, so we don't need to take that + * into account. + */ + + /* + * Determine whether there is inline HTML. + * + * For "chained" control structures (if - elseif - else), the complete control structure + * needs to be examined in one go as these cannot be changed individually, only as a complete group. + */ $closedScopes = Collections::closedScopes(); $find = $closedScopes; $find[\T_INLINE_HTML] = \T_INLINE_HTML; - $inlineHTMLPtr = $opener; - $hasInlineHTML = false; + + $chainedIssues = []; + $hasInlineHTML = false; + $currentPtr = $stackPtr; do { - $inlineHTMLPtr = $phpcsFile->findNext($find, ($inlineHTMLPtr + 1), $closer); - if ($tokens[$inlineHTMLPtr]['code'] === \T_INLINE_HTML) { - $hasInlineHTML = true; - break; + $opener = $tokens[$currentPtr]['scope_opener']; + $closer = $tokens[$currentPtr]['scope_closer']; + $chainedIssues[$opener] = $closer; + + if ($hasInlineHTML === true) { + // No need to search the contents, we already know there is inline HTML. + $currentPtr = $closer; + continue; } - if (isset($closedScopes[$tokens[$inlineHTMLPtr]['code']], $tokens[$inlineHTMLPtr]['scope_closer'])) { - $inlineHTMLPtr = $tokens[$inlineHTMLPtr]['scope_closer']; - } - } while ($inlineHTMLPtr !== false && $inlineHTMLPtr < $closer); + $inlineHTMLPtr = $opener; + + do { + $inlineHTMLPtr = $phpcsFile->findNext($find, ($inlineHTMLPtr + 1), $closer); + if ($tokens[$inlineHTMLPtr]['code'] === \T_INLINE_HTML) { + $hasInlineHTML = true; + break; + } + + if (isset($closedScopes[$tokens[$inlineHTMLPtr]['code']], $tokens[$inlineHTMLPtr]['scope_closer'])) { + $inlineHTMLPtr = $tokens[$inlineHTMLPtr]['scope_closer']; + } + } while ($inlineHTMLPtr !== false && $inlineHTMLPtr < $closer); + + $currentPtr = $closer; + } while (isset(Collections::alternativeControlStructureSyntaxes()[$tokens[$closer]['code']]) === true); if ($hasInlineHTML === true) { $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'alternative syntax with inline HTML'); @@ -151,31 +178,37 @@ public function process(File $phpcsFile, $stackPtr) } $data = [$tokens[$stackPtr]['content']]; - if ($tokens[$stackPtr]['code'] === \T_ELSEIF || $tokens[$stackPtr]['code'] === \T_ELSE) { - $data = ['if']; + + foreach ($chainedIssues as $opener => $closer) { + $fix = $phpcsFile->addFixableError($error, $opener, $code, $data); } - $fix = $phpcsFile->addFixableError($error, $tokens[$stackPtr]['scope_opener'], $code, $data); if ($fix === false) { return; } /* - * Fix it. + * Fix all issues for this chain in one go to diminish the chance of conflicts. */ $phpcsFile->fixer->beginChangeset(); - $phpcsFile->fixer->replaceToken($opener, '{'); - - if (isset(Collections::alternativeControlStructureSyntaxClosers()[$tokens[$closer]['code']]) === true) { - $phpcsFile->fixer->replaceToken($closer, '}'); - $semicolon = $phpcsFile->findNext(Tokens::$emptyTokens, ($closer + 1), null, true); - if ($semicolon !== false && $tokens[$semicolon]['code'] === \T_SEMICOLON) { - $phpcsFile->fixer->replaceToken($semicolon, ''); + foreach ($chainedIssues as $opener => $closer) { + $phpcsFile->fixer->replaceToken($opener, '{'); + + if (isset(Collections::alternativeControlStructureSyntaxClosers()[$tokens[$closer]['code']]) === true) { + $phpcsFile->fixer->replaceToken($closer, '}'); + + $semicolon = $phpcsFile->findNext(Tokens::$emptyTokens, ($closer + 1), null, true); + if ($semicolon !== false && $tokens[$semicolon]['code'] === \T_SEMICOLON) { + $phpcsFile->fixer->replaceToken($semicolon, ''); + } + } else { + /* + * This must be an if/else using alternative syntax. + * The closer will be the next control structure keyword. + */ + $phpcsFile->fixer->addContentBefore($closer, '} '); } - } else { - // This must be an if/else using alternative syntax. The closer will be the next control structure keyword. - $phpcsFile->fixer->addContentBefore($closer, '} '); } $phpcsFile->fixer->endChangeset(); diff --git a/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.inc b/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.1.inc similarity index 69% rename from Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.inc rename to Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.1.inc index e5352f9f..ffbf13ab 100644 --- a/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.inc +++ b/Universal/Tests/ControlStructures/DisallowAlternativeSyntaxUnitTest.1.inc @@ -147,6 +147,16 @@ A is something else +Inline HTML in the if, but nowhere else. + +Inline HTML in the if, but nowhere else. + +Inline HTML in the elseif, but nowhere else. + +Inline HTML in the else, but nowhere else. + +Inline HTML in every leaf. + +Inline HTML in every leaf. + +Inline HTML in every leaf. + +Inline HTML in the if, but nowhere else. + + Inline HTML in the nested if, but nowhere else. + +
    Inline HTML
    + +Inline HTML in the if, but nowhere else. + +Inline HTML in the if, but nowhere else. + +Inline HTML in the elseif, but nowhere else. + +Inline HTML in the else, but nowhere else. + +Inline HTML in every leaf. + +Inline HTML in every leaf. + +Inline HTML in every leaf. + +Inline HTML in the if, but nowhere else. + + Inline HTML in the nested if, but nowhere else. + +
    Inline HTML
    + + Inline HTML in the nested if, but nowhere else. + => */ - public function getErrorList() + public function getErrorList($testFile = '') { + if ($testFile !== 'DisallowAlternativeSyntaxUnitTest.1.inc') { + return []; + } + return [ 80 => 1, 82 => 1, @@ -46,18 +52,26 @@ public function getErrorList() 134 => 1, 138 => 1, 145 => 1, - 155 => 1, - 157 => 1, - 159 => 1, - 164 => 1, - 168 => 1, - 172 => 1, - 176 => 1, - 185 => 1, - 229 => 1, - 235 => 1, - 246 => 1, + 151 => 1, + 154 => 1, + 156 => 1, + 165 => 1, + 167 => 1, + 169 => 1, + 174 => 1, + 178 => 1, + 182 => 1, + 186 => 1, + 195 => 1, + 239 => 1, + 245 => 1, 256 => 1, + 266 => 1, + 279 => 1, + 281 => 1, + 283 => 1, + 330 => 1, + 332 => 1, ]; } From 8c187c0fdb1420701b226b661b82a5bca4393abe Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 1 Dec 2022 19:54:00 +0100 Subject: [PATCH 065/125] Universal/DisallowAnonClassParentheses: remove redundant condition ... and annotate why the condition has been removed, as well as annotating a second likely redundant condition. Includes a minor tweak to improve the descriptiveness of the "with parameter" metric. --- .../DisallowAnonClassParenthesesSniff.php | 18 +++++++++------ ...isallowAnonClassParenthesesUnitTest.1.inc} | 0 ...wAnonClassParenthesesUnitTest.1.inc.fixed} | 0 ...DisallowAnonClassParenthesesUnitTest.2.inc | 4 ++++ ...DisallowAnonClassParenthesesUnitTest.3.inc | 4 ++++ ...DisallowAnonClassParenthesesUnitTest.4.inc | 4 ++++ .../DisallowAnonClassParenthesesUnitTest.php | 22 +++++++++++++------ 7 files changed, 38 insertions(+), 14 deletions(-) rename Universal/Tests/Classes/{DisallowAnonClassParenthesesUnitTest.inc => DisallowAnonClassParenthesesUnitTest.1.inc} (100%) rename Universal/Tests/Classes/{DisallowAnonClassParenthesesUnitTest.inc.fixed => DisallowAnonClassParenthesesUnitTest.1.inc.fixed} (100%) create mode 100644 Universal/Tests/Classes/DisallowAnonClassParenthesesUnitTest.2.inc create mode 100644 Universal/Tests/Classes/DisallowAnonClassParenthesesUnitTest.3.inc create mode 100644 Universal/Tests/Classes/DisallowAnonClassParenthesesUnitTest.4.inc diff --git a/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php b/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php index eea517c8..f6767db1 100644 --- a/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php +++ b/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php @@ -59,28 +59,32 @@ public function register() */ public function process(File $phpcsFile, $stackPtr) { - $tokens = $phpcsFile->getTokens(); - + $tokens = $phpcsFile->getTokens(); $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); - if ($nextNonEmpty === false - || $tokens[$nextNonEmpty]['code'] !== \T_OPEN_PARENTHESIS - ) { + + // Note: no need to check for `false` as PHPCS won't retokenize `class` to `T_ANON_CLASS` in that case. + if ($tokens[$nextNonEmpty]['code'] !== \T_OPEN_PARENTHESIS) { // No parentheses found. $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'no'); return; } if (isset($tokens[$nextNonEmpty]['parenthesis_closer']) === false) { - // Incomplete set of parentheses. Ignore. + /* + * Incomplete set of parentheses. Ignore. + * Shouldn't be possible as PHPCS won't retokenize `class` to `T_ANON_CLASS` in that case. + */ + // @codeCoverageIgnoreStart $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes'); return; + // @codeCoverageIgnoreEnd } $closer = $tokens[$nextNonEmpty]['parenthesis_closer']; $hasParams = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), $closer, true); if ($hasParams !== false) { // There is something between the parentheses. Ignore. - $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes, with parameter'); + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes, with parameter(s)'); return; } diff --git a/Universal/Tests/Classes/DisallowAnonClassParenthesesUnitTest.inc b/Universal/Tests/Classes/DisallowAnonClassParenthesesUnitTest.1.inc similarity index 100% rename from Universal/Tests/Classes/DisallowAnonClassParenthesesUnitTest.inc rename to Universal/Tests/Classes/DisallowAnonClassParenthesesUnitTest.1.inc diff --git a/Universal/Tests/Classes/DisallowAnonClassParenthesesUnitTest.inc.fixed b/Universal/Tests/Classes/DisallowAnonClassParenthesesUnitTest.1.inc.fixed similarity index 100% rename from Universal/Tests/Classes/DisallowAnonClassParenthesesUnitTest.inc.fixed rename to Universal/Tests/Classes/DisallowAnonClassParenthesesUnitTest.1.inc.fixed diff --git a/Universal/Tests/Classes/DisallowAnonClassParenthesesUnitTest.2.inc b/Universal/Tests/Classes/DisallowAnonClassParenthesesUnitTest.2.inc new file mode 100644 index 00000000..0fca8422 --- /dev/null +++ b/Universal/Tests/Classes/DisallowAnonClassParenthesesUnitTest.2.inc @@ -0,0 +1,4 @@ + => */ - public function getErrorList() + public function getErrorList($testFile = '') { - return [ - 22 => 1, - 23 => 1, - 24 => 1, - 27 => 1, - ]; + switch ($testFile) { + case 'DisallowAnonClassParenthesesUnitTest.1.inc': + return [ + 22 => 1, + 23 => 1, + 24 => 1, + 27 => 1, + ]; + + default: + return []; + } } /** From 3eaadf4769168499c1856899c180be5c1faf05c3 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 1 Dec 2022 19:31:21 +0100 Subject: [PATCH 066/125] Universal/RequireAnonClassParentheses: remove redundant condition --- .../Sniffs/Classes/RequireAnonClassParenthesesSniff.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Universal/Sniffs/Classes/RequireAnonClassParenthesesSniff.php b/Universal/Sniffs/Classes/RequireAnonClassParenthesesSniff.php index ffcd45f7..bee522fc 100644 --- a/Universal/Sniffs/Classes/RequireAnonClassParenthesesSniff.php +++ b/Universal/Sniffs/Classes/RequireAnonClassParenthesesSniff.php @@ -58,12 +58,11 @@ public function register() */ public function process(File $phpcsFile, $stackPtr) { - $tokens = $phpcsFile->getTokens(); - + $tokens = $phpcsFile->getTokens(); $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); - if ($nextNonEmpty !== false - && $tokens[$nextNonEmpty]['code'] === \T_OPEN_PARENTHESIS - ) { + + // Note: no need to check for `false` as PHPCS won't retokenize `class` to `T_ANON_CLASS` in that case. + if ($tokens[$nextNonEmpty]['code'] === \T_OPEN_PARENTHESIS) { // Parentheses found. $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes'); return; From d718baebfb70f3e6fadcd64eb3b2acf3c31a22f4 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 30 Nov 2022 18:40:15 +0100 Subject: [PATCH 067/125] Universal/DisallowFinalClass: minor code tweak When a class is declared as `abstract`, it would attempt to record both for the `abstract`, as well as the `not abstract, not final` metric. As a metric can only be recorded once per token, this didn't actually cause any issues, but it still made the code confusing. Fixed now. --- Universal/Sniffs/Classes/DisallowFinalClassSniff.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Universal/Sniffs/Classes/DisallowFinalClassSniff.php b/Universal/Sniffs/Classes/DisallowFinalClassSniff.php index cd43afe0..11d8d333 100644 --- a/Universal/Sniffs/Classes/DisallowFinalClassSniff.php +++ b/Universal/Sniffs/Classes/DisallowFinalClassSniff.php @@ -63,6 +63,7 @@ public function process(File $phpcsFile, $stackPtr) if ($classProp['is_final'] === false) { if ($classProp['is_abstract'] === true) { $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'abstract'); + return; } $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'not abstract, not final'); From 615b28b567f3732b5351f30807507d2d7dcd9f56 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 30 Nov 2022 18:55:47 +0100 Subject: [PATCH 068/125] Universal/DisallowFinalClass: make tests more descriptive --- Universal/Tests/Classes/DisallowFinalClassUnitTest.inc | 10 +++++----- .../Tests/Classes/DisallowFinalClassUnitTest.inc.fixed | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc b/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc index 812b9346..b3dc6fb4 100644 --- a/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc +++ b/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc @@ -22,7 +22,7 @@ class FooC { /* * Bad. */ -final class BazA {} +final class FinalClass {} final @@ -30,15 +30,15 @@ final final class CheckHandlingOfIndentation {} -final /*comment*/ class BazC implements MyInterface {} +final /*comment*/ class CheckHandlingOfComments implements MyInterface {} // Test fixer when combined with PHP 8.2 "readonly" modifier. -readonly final class BazD {} +readonly final class ReadonlyFinalClass {} -final readonly class BazE {} +final readonly class FinalReadonlyClass {} // Parse error. Remove the final keyword. -final abstract class BazD {} +final abstract class ForbiddenAbstractFinal {} // Live coding. Ignore. This must be the last test in the file. final class diff --git a/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc.fixed b/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc.fixed index abde5462..bf7db366 100644 --- a/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc.fixed +++ b/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc.fixed @@ -22,21 +22,21 @@ class FooC { /* * Bad. */ -class BazA {} +class FinalClass {} class CheckHandlingOfSuperfluousWhitespace extends Something {} class CheckHandlingOfIndentation {} -/*comment*/ class BazC implements MyInterface {} +/*comment*/ class CheckHandlingOfComments implements MyInterface {} // Test fixer when combined with PHP 8.2 "readonly" modifier. -readonly class BazD {} +readonly class ReadonlyFinalClass {} -readonly class BazE {} +readonly class FinalReadonlyClass {} // Parse error. Remove the final keyword. -abstract class BazD {} +abstract class ForbiddenAbstractFinal {} // Live coding. Ignore. This must be the last test in the file. final class From c303f5858ff7a8c8140e15fd12f411cbc958ab40 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 1 Dec 2022 19:13:56 +0100 Subject: [PATCH 069/125] Universal/DisallowFinalClass: always record the metric As things were, the sniff would bow out if the opening brace for the class could not be found (yet) and would also not record the `final` metric in that case, even though for similar code using `abstract`, the metric _would_ be recorded. This commit ensures the `final` metric will now be recorded either way. --- Universal/Sniffs/Classes/DisallowFinalClassSniff.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Universal/Sniffs/Classes/DisallowFinalClassSniff.php b/Universal/Sniffs/Classes/DisallowFinalClassSniff.php index 11d8d333..c8e62cd6 100644 --- a/Universal/Sniffs/Classes/DisallowFinalClassSniff.php +++ b/Universal/Sniffs/Classes/DisallowFinalClassSniff.php @@ -70,14 +70,14 @@ public function process(File $phpcsFile, $stackPtr) return; } + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'final'); + $tokens = $phpcsFile->getTokens(); if (isset($tokens[$stackPtr]['scope_opener']) === false) { // Live coding or parse error. return; } - $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'final'); - // No extra safeguards needed, we know the keyword will exist based on the check above. $finalKeyword = $phpcsFile->findPrevious(\T_FINAL, ($stackPtr - 1)); From 407f592ebb54a29db3b07943ca386c54b325c89f Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 30 Nov 2022 19:00:11 +0100 Subject: [PATCH 070/125] Universal/DisallowFinalClass: act on more cases As things were, the sniff would bow out if the opening brace for the class could not be found (yet). This commit changes the sniff to act in more cases, i.e. it does require for a (non-empty) token after the `class` keyword, but once that token is found, the sniff will act. Includes minor changes to how the message text is build up to prevent weird code snippets being show in the message. Includes additional test. --- .../Classes/DisallowFinalClassSniff.php | 19 ++++++++++++++----- .../Classes/DisallowFinalClassUnitTest.inc | 3 +++ .../DisallowFinalClassUnitTest.inc.fixed | 3 +++ .../Classes/DisallowFinalClassUnitTest.php | 1 + 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Universal/Sniffs/Classes/DisallowFinalClassSniff.php b/Universal/Sniffs/Classes/DisallowFinalClassSniff.php index c8e62cd6..ea9fd002 100644 --- a/Universal/Sniffs/Classes/DisallowFinalClassSniff.php +++ b/Universal/Sniffs/Classes/DisallowFinalClassSniff.php @@ -12,6 +12,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Tokens; use PHPCSUtils\Utils\GetTokensAsString; use PHPCSUtils\Utils\ObjectDeclarations; @@ -72,21 +73,29 @@ public function process(File $phpcsFile, $stackPtr) $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'final'); - $tokens = $phpcsFile->getTokens(); - if (isset($tokens[$stackPtr]['scope_opener']) === false) { + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if ($nextNonEmpty === false) { // Live coding or parse error. return; } // No extra safeguards needed, we know the keyword will exist based on the check above. $finalKeyword = $phpcsFile->findPrevious(\T_FINAL, ($stackPtr - 1)); + $snippetEnd = $nextNonEmpty; + $classCloser = ''; + + $tokens = $phpcsFile->getTokens(); + if (isset($tokens[$stackPtr]['scope_opener']) === true) { + $snippetEnd = $tokens[$stackPtr]['scope_opener']; + $classCloser = '}'; + } - $snippet = GetTokensAsString::compact($phpcsFile, $finalKeyword, $tokens[$stackPtr]['scope_opener'], true); + $snippet = GetTokensAsString::compact($phpcsFile, $finalKeyword, $snippetEnd, true); $fix = $phpcsFile->addFixableError( - 'Declaring a class as final is not allowed. Found: %s}', + 'Declaring a class as final is not allowed. Found: %s%s', $finalKeyword, 'FinalClassFound', - [$snippet] + [$snippet, $classCloser] ); if ($fix === true) { diff --git a/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc b/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc index b3dc6fb4..257d7849 100644 --- a/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc +++ b/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc @@ -40,5 +40,8 @@ final readonly class FinalReadonlyClass {} // Parse error. Remove the final keyword. final abstract class ForbiddenAbstractFinal {} +// Parse error, but not one which concerns us. Remove the final keyword. +final class UnfinishedDeclare + // Live coding. Ignore. This must be the last test in the file. final class diff --git a/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc.fixed b/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc.fixed index bf7db366..3c7ffad0 100644 --- a/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc.fixed +++ b/Universal/Tests/Classes/DisallowFinalClassUnitTest.inc.fixed @@ -38,5 +38,8 @@ readonly class FinalReadonlyClass {} // Parse error. Remove the final keyword. abstract class ForbiddenAbstractFinal {} +// Parse error, but not one which concerns us. Remove the final keyword. +class UnfinishedDeclare + // Live coding. Ignore. This must be the last test in the file. final class diff --git a/Universal/Tests/Classes/DisallowFinalClassUnitTest.php b/Universal/Tests/Classes/DisallowFinalClassUnitTest.php index 6d94272c..b3d105b6 100644 --- a/Universal/Tests/Classes/DisallowFinalClassUnitTest.php +++ b/Universal/Tests/Classes/DisallowFinalClassUnitTest.php @@ -37,6 +37,7 @@ public function getErrorList() 36 => 1, 38 => 1, 41 => 1, + 44 => 1, ]; } From f2e465b7e7e0e83503dbfec429d24f233ece25db Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 30 Nov 2022 19:06:02 +0100 Subject: [PATCH 071/125] Universal/RequireFinalClass: make tests more descriptive --- Universal/Tests/Classes/RequireFinalClassUnitTest.inc | 8 ++++---- .../Tests/Classes/RequireFinalClassUnitTest.inc.fixed | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Universal/Tests/Classes/RequireFinalClassUnitTest.inc b/Universal/Tests/Classes/RequireFinalClassUnitTest.inc index ce1f04eb..fea17339 100644 --- a/Universal/Tests/Classes/RequireFinalClassUnitTest.inc +++ b/Universal/Tests/Classes/RequireFinalClassUnitTest.inc @@ -16,18 +16,18 @@ abstract class AbstractBarC implements MyInterface {} $a = new MyClass() {} // Parse error. Not our concern. -final abstract class BazD {} +final abstract class FinalAbstractClass {} /* * Bad. */ -class BazA {} +class PlainClass {} class CheckHandlingOfIndentation extends Something {} -class BazC implements MyInterface {} +class ClassImplementing implements MyInterface {} -readonly class BazD {} +readonly class ReadonlyClass {} // Live coding. Ignore. This must be the last test in the file. class LiveCoding diff --git a/Universal/Tests/Classes/RequireFinalClassUnitTest.inc.fixed b/Universal/Tests/Classes/RequireFinalClassUnitTest.inc.fixed index 3459f696..c489d9d9 100644 --- a/Universal/Tests/Classes/RequireFinalClassUnitTest.inc.fixed +++ b/Universal/Tests/Classes/RequireFinalClassUnitTest.inc.fixed @@ -16,18 +16,18 @@ abstract class AbstractBarC implements MyInterface {} $a = new MyClass() {} // Parse error. Not our concern. -final abstract class BazD {} +final abstract class FinalAbstractClass {} /* * Bad. */ -final class BazA {} +final class PlainClass {} final class CheckHandlingOfIndentation extends Something {} -final class BazC implements MyInterface {} +final class ClassImplementing implements MyInterface {} -readonly final class BazD {} +readonly final class ReadonlyClass {} // Live coding. Ignore. This must be the last test in the file. class LiveCoding From 97d836cb83236d856409ea4e2de0cabcbfdd2806 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 1 Dec 2022 19:17:24 +0100 Subject: [PATCH 072/125] Universal/RequireFinalClass: always record the metric As things were, the sniff would bow out if the opening brace for the class could not be found (yet) and would also not record the `not abstract, not final` metric in that case, even though for similar code using `abstract` or `final`, the metric _would_ be recorded. This commit ensures the `not abstract, not final` metric will now be recorded either way. --- Universal/Sniffs/Classes/RequireFinalClassSniff.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Universal/Sniffs/Classes/RequireFinalClassSniff.php b/Universal/Sniffs/Classes/RequireFinalClassSniff.php index a0458208..5477a6b5 100644 --- a/Universal/Sniffs/Classes/RequireFinalClassSniff.php +++ b/Universal/Sniffs/Classes/RequireFinalClassSniff.php @@ -71,14 +71,14 @@ public function process(File $phpcsFile, $stackPtr) return; } + $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'not abstract, not final'); + $tokens = $phpcsFile->getTokens(); if (isset($tokens[$stackPtr]['scope_opener']) === false) { // Live coding or parse error. return; } - $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'not abstract, not final'); - $snippet = GetTokensAsString::compact($phpcsFile, $stackPtr, $tokens[$stackPtr]['scope_opener'], true); $fix = $phpcsFile->addFixableError( 'A non-abstract class should be declared as final. Found: %s}', From 7635da17a7d443c797412b38a154522ab8b9fdd8 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 30 Nov 2022 19:09:34 +0100 Subject: [PATCH 073/125] Universal/RequireFinalClass: act on more cases As things were, the sniff would bow out if the opening brace for the class could not be found (yet). This commit changes the sniff to act in more cases, i.e. it does require for a (non-empty) token after the `class` keyword, but once that token is found, the sniff will act. Includes minor changes to how the message text is build up to prevent weird code snippets being show in the message. Includes additional test. --- .../Sniffs/Classes/RequireFinalClassSniff.php | 20 ++++++++++++++----- .../Classes/RequireFinalClassUnitTest.inc | 5 ++++- .../RequireFinalClassUnitTest.inc.fixed | 5 ++++- .../Classes/RequireFinalClassUnitTest.php | 1 + 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Universal/Sniffs/Classes/RequireFinalClassSniff.php b/Universal/Sniffs/Classes/RequireFinalClassSniff.php index 5477a6b5..ed3b3ab9 100644 --- a/Universal/Sniffs/Classes/RequireFinalClassSniff.php +++ b/Universal/Sniffs/Classes/RequireFinalClassSniff.php @@ -12,6 +12,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Tokens; use PHPCSUtils\Utils\GetTokensAsString; use PHPCSUtils\Utils\ObjectDeclarations; @@ -73,18 +74,27 @@ public function process(File $phpcsFile, $stackPtr) $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'not abstract, not final'); - $tokens = $phpcsFile->getTokens(); - if (isset($tokens[$stackPtr]['scope_opener']) === false) { + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if ($nextNonEmpty === false) { // Live coding or parse error. return; } - $snippet = GetTokensAsString::compact($phpcsFile, $stackPtr, $tokens[$stackPtr]['scope_opener'], true); + $snippetEnd = $nextNonEmpty; + $classCloser = ''; + + $tokens = $phpcsFile->getTokens(); + if (isset($tokens[$stackPtr]['scope_opener']) === true) { + $snippetEnd = $tokens[$stackPtr]['scope_opener']; + $classCloser = '}'; + } + + $snippet = GetTokensAsString::compact($phpcsFile, $stackPtr, $snippetEnd, true); $fix = $phpcsFile->addFixableError( - 'A non-abstract class should be declared as final. Found: %s}', + 'A non-abstract class should be declared as final. Found: %s%s', $stackPtr, 'NonFinalClassFound', - [$snippet] + [$snippet, $classCloser] ); if ($fix === true) { diff --git a/Universal/Tests/Classes/RequireFinalClassUnitTest.inc b/Universal/Tests/Classes/RequireFinalClassUnitTest.inc index fea17339..1a8d3be7 100644 --- a/Universal/Tests/Classes/RequireFinalClassUnitTest.inc +++ b/Universal/Tests/Classes/RequireFinalClassUnitTest.inc @@ -29,5 +29,8 @@ class ClassImplementing implements MyInterface {} readonly class ReadonlyClass {} -// Live coding. Ignore. This must be the last test in the file. +// Live coding/parse error, but not one which concerns us. Add the final keyword. class LiveCoding + +// Live coding. Ignore. This must be the last test in the file. +class diff --git a/Universal/Tests/Classes/RequireFinalClassUnitTest.inc.fixed b/Universal/Tests/Classes/RequireFinalClassUnitTest.inc.fixed index c489d9d9..8878d41d 100644 --- a/Universal/Tests/Classes/RequireFinalClassUnitTest.inc.fixed +++ b/Universal/Tests/Classes/RequireFinalClassUnitTest.inc.fixed @@ -29,5 +29,8 @@ final class ClassImplementing implements MyInterface {} readonly final class ReadonlyClass {} +// Live coding/parse error, but not one which concerns us. Add the final keyword. +final class LiveCoding + // Live coding. Ignore. This must be the last test in the file. -class LiveCoding +class diff --git a/Universal/Tests/Classes/RequireFinalClassUnitTest.php b/Universal/Tests/Classes/RequireFinalClassUnitTest.php index 007b9e10..386e0786 100644 --- a/Universal/Tests/Classes/RequireFinalClassUnitTest.php +++ b/Universal/Tests/Classes/RequireFinalClassUnitTest.php @@ -34,6 +34,7 @@ public function getErrorList() 26 => 1, 28 => 1, 30 => 1, + 33 => 1, ]; } From d9f0cf13cf591493c83105f85d5d7911bdcc187a Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 30 Nov 2022 19:30:40 +0100 Subject: [PATCH 074/125] Universal/OneStatementInShortEchoTag: prevent false positive In convoluted code, which I hope to never encounter in real life, it _could_ be possible to encounter a semi-colon, which is not the end of the statement, in the statement being executed for the `echo`. This commit hardens the sniff to improve the handling of these. Includes additional tests. --- .../PHP/OneStatementInShortEchoTagSniff.php | 18 ++++++++++++++++-- .../OneStatementInShortEchoTagUnitTest.1.inc | 4 ++++ ...StatementInShortEchoTagUnitTest.1.inc.fixed | 4 ++++ .../PHP/OneStatementInShortEchoTagUnitTest.php | 7 ++++--- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php b/Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php index 7a4a3be3..169a1e65 100644 --- a/Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php +++ b/Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php @@ -56,8 +56,22 @@ public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); - $endOfStatement = $phpcsFile->findNext([\T_SEMICOLON, \T_CLOSE_TAG], ($stackPtr + 1)); - if ($endOfStatement === false + for ($endOfStatement = ($stackPtr + 1); $endOfStatement < $phpcsFile->numTokens; $endOfStatement++) { + if ($tokens[$endOfStatement]['code'] === \T_CLOSE_TAG + || $tokens[$endOfStatement]['code'] === \T_SEMICOLON + ) { + break; + } + + // Skip over anything within parenthesis. + if ($tokens[$endOfStatement]['code'] === \T_OPEN_PARENTHESIS + && isset($tokens[$endOfStatement]['parenthesis_closer']) + ) { + $endOfStatement = $tokens[$endOfStatement]['parenthesis_closer']; + } + } + + if ($endOfStatement === $phpcsFile->numTokens || $tokens[$endOfStatement]['code'] === \T_CLOSE_TAG ) { return; diff --git a/Universal/Tests/PHP/OneStatementInShortEchoTagUnitTest.1.inc b/Universal/Tests/PHP/OneStatementInShortEchoTagUnitTest.1.inc index daa88ba9..61231193 100644 --- a/Universal/Tests/PHP/OneStatementInShortEchoTagUnitTest.1.inc +++ b/Universal/Tests/PHP/OneStatementInShortEchoTagUnitTest.1.inc @@ -13,6 +13,8 @@ echo 'b'; ?>
    +
    +
    // Multiple statements in short open echo tags should be flagged.
    +
    + // Unclosed tag. This must be the last test in the file.
    +
    +
    // Multiple statements in short open echo tags should be flagged.
    +
    + // Unclosed tag. This must be the last test in the file.
    1, - 22 => 1, - 29 => 1, + 21 => 1, + 24 => 1, + 31 => 1, + 34 => 1, ]; case 'OneStatementInShortEchoTagUnitTest.2.inc': From 50b4bde6fcc05b76980720b7abfb30aa3813f811 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 1 Dec 2022 03:22:05 +0100 Subject: [PATCH 075/125] Universal/NoLeadingBackslash: minor code reorganization --- .../UseStatements/NoLeadingBackslashSniff.php | 79 ++++++++++++------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/Universal/Sniffs/UseStatements/NoLeadingBackslashSniff.php b/Universal/Sniffs/UseStatements/NoLeadingBackslashSniff.php index cca2a498..ef193ad5 100644 --- a/Universal/Sniffs/UseStatements/NoLeadingBackslashSniff.php +++ b/Universal/Sniffs/UseStatements/NoLeadingBackslashSniff.php @@ -76,43 +76,64 @@ public function process(File $phpcsFile, $stackPtr) $current = $stackPtr; do { - $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($current + 1), $endOfStatement, true); - if ($nextNonEmpty === false) { - // Reached the end of the statement. - return; + $continue = $this->processImport($phpcsFile, $current, $endOfStatement); + if ($continue === false) { + break; } - // Skip past 'function'/'const' keyword. - $contentLC = \strtolower($tokens[$nextNonEmpty]['content']); - if ($tokens[$nextNonEmpty]['code'] === \T_STRING - && ($contentLC === 'function' || $contentLC === 'const') - ) { - $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), $endOfStatement, true); - if ($nextNonEmpty === false) { - // Reached the end of the statement. - return; - } + // Move the stackPtr forward to the next part of the use statement, if any. + $current = $phpcsFile->findNext(\T_COMMA, ($current + 1), $endOfStatement); + } while ($current !== false); + } + + /** + * Examine an individual import statement. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token. + * @param int $endOfStatement End token for the current import statement. + * + * @return bool Whether or not to continue examining this import use statement. + */ + private function processImport(File $phpcsFile, $stackPtr, $endOfStatement) + { + $tokens = $phpcsFile->getTokens(); + + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), $endOfStatement, true); + if ($nextNonEmpty === false) { + // Reached the end of the statement. + return false; + } + + // Skip past 'function'/'const' keyword. + $contentLC = \strtolower($tokens[$nextNonEmpty]['content']); + if ($tokens[$nextNonEmpty]['code'] === \T_STRING + && ($contentLC === 'function' || $contentLC === 'const') + ) { + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), $endOfStatement, true); + if ($nextNonEmpty === false) { + // Reached the end of the statement. + return false; } + } - if ($tokens[$nextNonEmpty]['code'] === \T_NS_SEPARATOR) { - $phpcsFile->recordMetric($nextNonEmpty, self::METRIC_NAME, 'yes'); + if ($tokens[$nextNonEmpty]['code'] === \T_NS_SEPARATOR) { + $phpcsFile->recordMetric($nextNonEmpty, self::METRIC_NAME, 'yes'); - $error = 'An import use statement should never start with a leading backslash'; - $fix = $phpcsFile->addFixableError($error, $nextNonEmpty, 'LeadingBackslashFound'); + $error = 'An import use statement should never start with a leading backslash'; + $fix = $phpcsFile->addFixableError($error, $nextNonEmpty, 'LeadingBackslashFound'); - if ($fix === true) { - if ($tokens[$nextNonEmpty - 1]['code'] !== \T_WHITESPACE) { - $phpcsFile->fixer->replaceToken($nextNonEmpty, ' '); - } else { - $phpcsFile->fixer->replaceToken($nextNonEmpty, ''); - } + if ($fix === true) { + if ($tokens[$nextNonEmpty - 1]['code'] !== \T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($nextNonEmpty, ' '); + } else { + $phpcsFile->fixer->replaceToken($nextNonEmpty, ''); } - } else { - $phpcsFile->recordMetric($nextNonEmpty, self::METRIC_NAME, 'no'); } + } else { + $phpcsFile->recordMetric($nextNonEmpty, self::METRIC_NAME, 'no'); + } - // Move the stackPtr forward to the next part of the use statement, if any. - $current = $phpcsFile->findNext(\T_COMMA, ($current + 1), $endOfStatement); - } while ($current !== false); + return true; } } From 56cf7675a23501f0cba19082964cd4e7ccc6b5ea Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 1 Dec 2022 03:33:27 +0100 Subject: [PATCH 076/125] Universal/NoLeadingBackslash: examine imports within a group use statement Previously the sniff would only examine the start of a complete import statement, now it will also examine the partial imports within a group `use` statement. A partial import statement within a group use starting with a leading backslash is actually a parse error, which normally gets ignored by PHPCS, but this is a very specific one, which is also auto-fixable, so may as well report it. This new error for leading backslashes for partial import statements within a group use statement will be reported using a separate error code `LeadingBackslashFoundInGroup`. Includes updated and extra unit tests. --- .../UseStatements/NoLeadingBackslashSniff.php | 35 +++++++++++++++++-- .../NoLeadingBackslashUnitTest.1.inc | 12 +++++-- .../NoLeadingBackslashUnitTest.1.inc.fixed | 12 +++++-- .../NoLeadingBackslashUnitTest.5.inc | 5 +++ .../NoLeadingBackslashUnitTest.php | 5 ++- 5 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 Universal/Tests/UseStatements/NoLeadingBackslashUnitTest.5.inc diff --git a/Universal/Sniffs/UseStatements/NoLeadingBackslashSniff.php b/Universal/Sniffs/UseStatements/NoLeadingBackslashSniff.php index ef193ad5..811210b8 100644 --- a/Universal/Sniffs/UseStatements/NoLeadingBackslashSniff.php +++ b/Universal/Sniffs/UseStatements/NoLeadingBackslashSniff.php @@ -84,6 +84,28 @@ public function process(File $phpcsFile, $stackPtr) // Move the stackPtr forward to the next part of the use statement, if any. $current = $phpcsFile->findNext(\T_COMMA, ($current + 1), $endOfStatement); } while ($current !== false); + + if ($tokens[$endOfStatement]['code'] !== \T_OPEN_USE_GROUP) { + // Finished the statement. + return; + } + + $current = $endOfStatement; // Group open brace. + $endOfStatement = $phpcsFile->findNext([\T_CLOSE_USE_GROUP], ($endOfStatement + 1)); + if ($endOfStatement === false) { + // Live coding or parse error. + return; + } + + do { + $continue = $this->processImport($phpcsFile, $current, $endOfStatement, true); + if ($continue === false) { + break; + } + + // Move the stackPtr forward to the next part of the use statement, if any. + $current = $phpcsFile->findNext(\T_COMMA, ($current + 1), $endOfStatement); + } while ($current !== false); } /** @@ -92,10 +114,12 @@ public function process(File $phpcsFile, $stackPtr) * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token. * @param int $endOfStatement End token for the current import statement. + * @param bool $groupUse Whether the current statement is a partial one + * within a group use statement. * * @return bool Whether or not to continue examining this import use statement. */ - private function processImport(File $phpcsFile, $stackPtr, $endOfStatement) + private function processImport(File $phpcsFile, $stackPtr, $endOfStatement, $groupUse = false) { $tokens = $phpcsFile->getTokens(); @@ -121,7 +145,14 @@ private function processImport(File $phpcsFile, $stackPtr, $endOfStatement) $phpcsFile->recordMetric($nextNonEmpty, self::METRIC_NAME, 'yes'); $error = 'An import use statement should never start with a leading backslash'; - $fix = $phpcsFile->addFixableError($error, $nextNonEmpty, 'LeadingBackslashFound'); + $code = 'LeadingBackslashFound'; + + if ($groupUse === true) { + $error = 'Parse error: partial import use statement in a use group starting with a leading backslash'; + $code = 'LeadingBackslashFoundInGroup'; + } + + $fix = $phpcsFile->addFixableError($error, $nextNonEmpty, $code); if ($fix === true) { if ($tokens[$nextNonEmpty - 1]['code'] !== \T_WHITESPACE) { diff --git a/Universal/Tests/UseStatements/NoLeadingBackslashUnitTest.1.inc b/Universal/Tests/UseStatements/NoLeadingBackslashUnitTest.1.inc index b7fc3c8b..eb2614b8 100644 --- a/Universal/Tests/UseStatements/NoLeadingBackslashUnitTest.1.inc +++ b/Universal/Tests/UseStatements/NoLeadingBackslashUnitTest.1.inc @@ -17,10 +17,16 @@ use Vendor\Foo\ClassA as ClassABC, 1, 14 => 1, 16 => 1, - 20 => 1, + 26 => 1, + 27 => 1, + 28 => 1, + 29 => 1, ]; default: From 4dca87c9fcc7a143946ff47110d2f3373a49391e Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 1 Dec 2022 06:36:50 +0100 Subject: [PATCH 077/125] Universal/DisallowLonelyIf: add extra test .. to cover an edge case which is already handled correctly. --- .../ControlStructures/DisallowLonelyIfUnitTest.inc | 13 +++++++++++++ .../DisallowLonelyIfUnitTest.inc.fixed | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/Universal/Tests/ControlStructures/DisallowLonelyIfUnitTest.inc b/Universal/Tests/ControlStructures/DisallowLonelyIfUnitTest.inc index c50a9d2c..77c777f4 100644 --- a/Universal/Tests/ControlStructures/DisallowLonelyIfUnitTest.inc +++ b/Universal/Tests/ControlStructures/DisallowLonelyIfUnitTest.inc @@ -265,6 +265,19 @@ if ($a) { } } +/* + * Inner block mixing conditions with and without curly braces (which is perfectly valid in PHP). + * Bow out as it cannot be determined reliably whether there is something behind the last condition. + */ +if ($condition) { + doSomething(); +} else { + if ($anotherCondition) { + doSomething(); + } else if ($secondIf) + doSomething(); +} + // Live coding. // Intentional parse error. This test has to be the last in the file. if ($a) { diff --git a/Universal/Tests/ControlStructures/DisallowLonelyIfUnitTest.inc.fixed b/Universal/Tests/ControlStructures/DisallowLonelyIfUnitTest.inc.fixed index 7e36e249..f2374d68 100644 --- a/Universal/Tests/ControlStructures/DisallowLonelyIfUnitTest.inc.fixed +++ b/Universal/Tests/ControlStructures/DisallowLonelyIfUnitTest.inc.fixed @@ -240,6 +240,19 @@ if ($a) { } } +/* + * Inner block mixing conditions with and without curly braces (which is perfectly valid in PHP). + * Bow out as it cannot be determined reliably whether there is something behind the last condition. + */ +if ($condition) { + doSomething(); +} else { + if ($anotherCondition) { + doSomething(); + } else if ($secondIf) + doSomething(); +} + // Live coding. // Intentional parse error. This test has to be the last in the file. if ($a) { From 6044232f41d7fd2ece492eff946e146f9525006f Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 1 Dec 2022 18:50:29 +0100 Subject: [PATCH 078/125] Universal/DisallowLonelyIf: bow out early for a specific situation (PHP close tag) If an inner "if/else" chain using alternative syntax has a PHP close tag after the `endif` instead of a semi-colon, we **know** there will need to be a PHP open tag before the closer for the "outer" `else` can be reached, so this means there will always be additional content within the outer `else` which doesn't belong with the inner `if`, i.e. it's not a lonely `if`. This commit implements bowing out early for that situation. Includes unit test. Includes minor tweaks to the inline documentation to make it clearer that the `do-while` code is part of the "Find the end of an if - else chain." block. --- .../ControlStructures/DisallowLonelyIfSniff.php | 14 +++++++++----- .../DisallowLonelyIfUnitTest.inc | 15 +++++++++++++++ .../DisallowLonelyIfUnitTest.inc.fixed | 15 +++++++++++++++ 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/Universal/Sniffs/ControlStructures/DisallowLonelyIfSniff.php b/Universal/Sniffs/ControlStructures/DisallowLonelyIfSniff.php index e582801b..d4d6bb4f 100644 --- a/Universal/Sniffs/ControlStructures/DisallowLonelyIfSniff.php +++ b/Universal/Sniffs/ControlStructures/DisallowLonelyIfSniff.php @@ -89,16 +89,15 @@ public function process(File $phpcsFile, $stackPtr) /* * Find the end of an if - else chain. */ + $innerIfPtr = $nextNonEmpty; $innerIfToken = $tokens[$innerIfPtr]; $autoFixable = true; $innerScopeCloser = $innerIfToken['scope_closer']; - /* - * For alternative syntax fixer only. - * Remember the individual inner scope opener and closers so the fixer doesn't need - * to do the same walking over the if/else chain again. - */ + // For alternative syntax fixer only. + // Remember the individual inner scope opener and closers so the fixer doesn't need + // to do the same walking over the if/else chain again. $innerScopes = [ $innerIfToken['scope_opener'] => $innerScopeCloser, ]; @@ -116,6 +115,11 @@ public function process(File $phpcsFile, $stackPtr) true ); + if ($tokens[$nextAfter]['code'] === \T_CLOSE_TAG) { + // Not "lonely" as at the very least there must be a PHP open tag before the outer closer. + return; + } + if ($tokens[$nextAfter]['code'] === \T_SEMICOLON) { $innerScopeCloser = $nextAfter; } else { diff --git a/Universal/Tests/ControlStructures/DisallowLonelyIfUnitTest.inc b/Universal/Tests/ControlStructures/DisallowLonelyIfUnitTest.inc index 77c777f4..0289e768 100644 --- a/Universal/Tests/ControlStructures/DisallowLonelyIfUnitTest.inc +++ b/Universal/Tests/ControlStructures/DisallowLonelyIfUnitTest.inc @@ -278,6 +278,21 @@ if ($condition) { doSomething(); } +/* + * Tests involving PHP close tags to close the inner control structure. + * Ignore as these will always need an open tag to reach the outer control structure again, + * so are never the "only" thing within the `else` (so not a lonely if). + */ +if ($condition): + doSomething(); +else: + if ($anotherCondition): + doSomething(); + endif ?> + Inline HTML + + Inline HTML + Date: Thu, 1 Dec 2022 23:37:51 +0100 Subject: [PATCH 079/125] Universal/SeparateFunctionsFromOO: update tests for enums As the sniff uses predefined token collections from PHPCSUtils, it automatically takes PHP 8.1 `enum`s into account since Utils-1.0.0-alpha4. This commit adjusts pre-existing tests and adds an extra test to safeguard this. --- .../Files/SeparateFunctionsFromOOUnitTest.3.inc | 6 ++++++ .../Files/SeparateFunctionsFromOOUnitTest.4.inc | 4 ++++ .../Files/SeparateFunctionsFromOOUnitTest.8.inc | 12 ++++++++++++ .../Tests/Files/SeparateFunctionsFromOOUnitTest.php | 5 +++++ 4 files changed, 27 insertions(+) create mode 100644 Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.8.inc diff --git a/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.3.inc b/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.3.inc index b5fade2d..6a01e94c 100644 --- a/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.3.inc +++ b/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.3.inc @@ -12,12 +12,16 @@ interface fooB() {} trait fooC() {} +enum fooD: string {} + namespace First; class firstFooA() {} trait firstFooB() {} +enum firstFooD: string {} + namespace Second; interface secondFooA() { @@ -30,6 +34,8 @@ if (!class_exists('secondFooB')) { class secondFooB() {} } +enum secondFooD: string {} + // This sniff has no rules about side-effects. $globVar = new class() { public function thisIsAnAnonymousClass() {} diff --git a/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.4.inc b/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.4.inc index e4669d70..98c0afd7 100644 --- a/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.4.inc +++ b/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.4.inc @@ -11,11 +11,13 @@ namespace { interface fooB() {} trait fooC() {} + enum fooD {} } namespace First { class firstFooA() {} trait firstFooB() {} + enum firstFooD {} } namespace Second { @@ -28,6 +30,8 @@ namespace Second { if (!class_exists('secondFooB')) { class secondFooB() {} } + + enum secondFooD {} } namespace { diff --git a/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.8.inc b/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.8.inc new file mode 100644 index 00000000..61a9a7e5 --- /dev/null +++ b/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.8.inc @@ -0,0 +1,12 @@ + 1, ]; + case 'SeparateFunctionsFromOOUnitTest.8.inc': + return [ + 12 => 1, + ]; + default: return []; } From a73b66bd8756f2d7d6390c3ebbe1bf1c6db32746 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 2 Dec 2022 00:26:24 +0100 Subject: [PATCH 080/125] Universal/SeparateFunctionsFromOO: update tests for arrow functions As the sniff uses predefined token collections from PHPCSUtils, it automatically takes PHP 7.4 arrow functions into account since Utils-1.0.0-alpha4. This commit adjusts pre-existing tests to safeguard this. Includes minor test documentation updates. --- Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.1.inc | 2 ++ Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.2.inc | 2 ++ Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.3.inc | 2 ++ Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.4.inc | 2 ++ Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.5.inc | 4 +++- Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.6.inc | 2 +- Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.7.inc | 2 +- Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.8.inc | 2 +- 8 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.1.inc b/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.1.inc index 9cab1a16..47facb12 100644 --- a/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.1.inc +++ b/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.1.inc @@ -31,6 +31,8 @@ $closure = function($a, $b) { return $a + $b; }; +$arrow = fn($a, $b) => $a + $b; + define('MY_CONSTANT', 'foo'); const ANOTHER_CONSTANT = 'bar'; diff --git a/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.2.inc b/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.2.inc index de6b3fea..858a586e 100644 --- a/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.2.inc +++ b/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.2.inc @@ -31,6 +31,8 @@ namespace { return $a + $b; }; + $arrow = fn($a, $b) => $a + $b; + define('MY_CONSTANT', 'foo'); const ANOTHER_CONSTANT = 'bar'; diff --git a/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.3.inc b/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.3.inc index 6a01e94c..a5a00ded 100644 --- a/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.3.inc +++ b/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.3.inc @@ -45,6 +45,8 @@ $closure = function($a, $b) { return $a + $b; }; +$arrow = fn($a, $b) => $a + $b; + define('MY_CONSTANT', 'foo'); const ANOTHER_CONSTANT = 'bar'; diff --git a/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.4.inc b/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.4.inc index 98c0afd7..072ead0f 100644 --- a/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.4.inc +++ b/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.4.inc @@ -44,6 +44,8 @@ namespace { return $a + $b; }; + $arrow = fn($a, $b) => $a + $b; + define('MY_CONSTANT', 'foo'); const ANOTHER_CONSTANT = 'bar'; diff --git a/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.5.inc b/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.5.inc index 8598a759..8fdf87af 100644 --- a/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.5.inc +++ b/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.5.inc @@ -1,6 +1,6 @@ $a + $b; + define('MY_CONSTANT', 'foo'); const ANOTHER_CONSTANT = 'bar'; diff --git a/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.6.inc b/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.6.inc index f1e8f332..3f443d60 100644 --- a/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.6.inc +++ b/Universal/Tests/Files/SeparateFunctionsFromOOUnitTest.6.inc @@ -1,6 +1,6 @@ Date: Tue, 29 Nov 2022 18:50:55 +0100 Subject: [PATCH 081/125] Introduce new `Modernize` standard Includes updating CI and other dev related files to take the new standard into account. --- .gitattributes | 1 + .github/workflows/basics.yml | 1 + Modernize/ruleset.xml | 5 +++++ composer.json | 2 +- phpunit-bootstrap.php | 1 + phpunit.xml.dist | 2 ++ 6 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 Modernize/ruleset.xml diff --git a/.gitattributes b/.gitattributes index a54fcfe4..814f5484 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,6 +12,7 @@ /phpcs.xml.dist export-ignore /phpunit.xml.dist export-ignore /phpunit-bootstrap.php export-ignore +/Modernize/Tests/ export-ignore /NormalizedArrays/Tests/ export-ignore /Universal/Tests/ export-ignore diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml index 582cf962..119ae2f6 100644 --- a/.github/workflows/basics.yml +++ b/.github/workflows/basics.yml @@ -72,6 +72,7 @@ jobs: # Check the code-style consistency of the XML ruleset files. - name: Check XML code style run: | + diff -B ./Modernize/ruleset.xml <(xmllint --format "./Modernize/ruleset.xml") diff -B ./NormalizedArrays/ruleset.xml <(xmllint --format "./NormalizedArrays/ruleset.xml") diff -B ./Universal/ruleset.xml <(xmllint --format "./Universal/ruleset.xml") diff --git a/Modernize/ruleset.xml b/Modernize/ruleset.xml new file mode 100644 index 00000000..bbfc2ea8 --- /dev/null +++ b/Modernize/ruleset.xml @@ -0,0 +1,5 @@ + + + + A collection of sniffs to detect code modernization opportunties. + diff --git a/composer.json b/composer.json index 4d028506..e114b8db 100644 --- a/composer.json +++ b/composer.json @@ -60,7 +60,7 @@ "@remove-devcs" ], "check-complete": [ - "@php ./vendor/phpcsstandards/phpcsdevtools/bin/phpcs-check-feature-completeness ./NormalizedArrays ./Universal" + "@php ./vendor/phpcsstandards/phpcsdevtools/bin/phpcs-check-feature-completeness ./Modernize ./NormalizedArrays ./Universal" ], "test": [ "@php ./vendor/phpunit/phpunit/phpunit --filter PHPCSExtra --no-coverage ./vendor/squizlabs/php_codesniffer/tests/AllTests.php" diff --git a/phpunit-bootstrap.php b/phpunit-bootstrap.php index 3b84e312..7e14772e 100644 --- a/phpunit-bootstrap.php +++ b/phpunit-bootstrap.php @@ -67,6 +67,7 @@ * Ref: https://github.com/squizlabs/PHP_CodeSniffer/pull/1146 */ $phpcsExtraStandards = [ + 'Modernize' => true, 'NormalizedArrays' => true, 'Universal' => true, ]; diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1fb04f39..51d3c1ae 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -14,6 +14,7 @@ + ./Modernize/Tests/ ./NormalizedArrays/Tests/ ./Universal/Tests/ @@ -21,6 +22,7 @@ + ./Modernize/Sniffs/ ./NormalizedArrays/Sniffs/ ./Universal/Sniffs/ From 44735897ee889628ced0f560e4815ca401c0d79c Mon Sep 17 00:00:00 2001 From: jrfnl Date: Tue, 29 Nov 2022 19:23:46 +0100 Subject: [PATCH 082/125] :sparkles: New `Modernize.FunctionCalls.Dirname` sniff This new sniff will detect and, when possible, auto-fix two typical code modernizations which can be made related to the `dirname()` function: 1. Since PHP 5.3, calls to `dirname(__FILE__)` can be replaced by `__DIR__`. Errorcode: `Modernize.FunctionCalls.Dirname.FileConstant`. 2. Since PHP 7.0, nested function calls to `dirname()` can be changed to use the new `$levels` parameter. Errorcode: `Modernize.FunctionCalls.Dirname.Nested`. Depending on supported PHP versions of an application, the complete sniff can be enabled and/or just one of the error codes could be enabled. **Regarding the fixers:** The sniff - and more importantly, the fixer - take fully qualified function calls, named parameters, as well as trailing comma's in function calls into account. * If a comment is found in any of the code which would be removed by the fixer, the issues will still be flagged, but not be marked as fixable. * If the value of a pre-existing `$levels` parameter cannot be determined, the issues will still be flagged, but not be marked as fixable. Also take note that the fixer does not make any presumptions about the code style of the project. It is recommended to run the code style fixers together with or after this fixer, so those can adjust the code style to the style applicable in the project. Includes fixer. Includes unit tests. Includes documentation. Notes/Future scope: * The sniff is not yet compatible with PHPCS 4.x. * When PHPCSUtils releases its own abstract for sniffing function calls, this sniff should switch to it. Refs: * https://www.php.net/manual/en/function.dirname.php * PHP 5.3: https://php-legacy-docs.zend.com/manual/php5/en/migration53.global-constants * PHp 7.0: https://www.php.net/manual/en/migration70.changed-functions.php#migration70.changed-functions.core Note: the parameter was originally called `$depth`, but has since been renamed to `$levels`. --- .../Docs/FunctionCalls/DirnameStandard.xml | 40 +++ .../Sniffs/FunctionCalls/DirnameSniff.php | 339 ++++++++++++++++++ .../Tests/FunctionCalls/DirnameUnitTest.inc | 142 ++++++++ .../FunctionCalls/DirnameUnitTest.inc.fixed | 126 +++++++ .../Tests/FunctionCalls/DirnameUnitTest.php | 82 +++++ 5 files changed, 729 insertions(+) create mode 100644 Modernize/Docs/FunctionCalls/DirnameStandard.xml create mode 100644 Modernize/Sniffs/FunctionCalls/DirnameSniff.php create mode 100644 Modernize/Tests/FunctionCalls/DirnameUnitTest.inc create mode 100644 Modernize/Tests/FunctionCalls/DirnameUnitTest.inc.fixed create mode 100644 Modernize/Tests/FunctionCalls/DirnameUnitTest.php diff --git a/Modernize/Docs/FunctionCalls/DirnameStandard.xml b/Modernize/Docs/FunctionCalls/DirnameStandard.xml new file mode 100644 index 00000000..5f486646 --- /dev/null +++ b/Modernize/Docs/FunctionCalls/DirnameStandard.xml @@ -0,0 +1,40 @@ + + + + = 5.3: Usage of dirname(__FILE__) can be replaced with __DIR__. + ]]> + + + + __DIR__; + ]]> + + + dirname(__FILE__); + ]]> + + + + = 7.0: Nested calls to dirname() can be replaced by using dirname() with the $levels parameter. + ]]> + + + + dirname($file, 3); + ]]> + + + dirname(dirname(dirname($file))); + ]]> + + + diff --git a/Modernize/Sniffs/FunctionCalls/DirnameSniff.php b/Modernize/Sniffs/FunctionCalls/DirnameSniff.php new file mode 100644 index 00000000..ab944a60 --- /dev/null +++ b/Modernize/Sniffs/FunctionCalls/DirnameSniff.php @@ -0,0 +1,339 @@ +getTokens(); + + if (\strtolower($tokens[$stackPtr]['content']) !== 'dirname') { + // Not our target. + return; + } + + $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if ($nextNonEmpty === false + || $tokens[$nextNonEmpty]['code'] !== \T_OPEN_PARENTHESIS + || isset($tokens[$nextNonEmpty]['parenthesis_owner']) === true + ) { + // Not our target. + return; + } + + if (isset($tokens[$nextNonEmpty]['parenthesis_closer']) === false) { + // Live coding or parse error, ignore. + return; + } + + // Check if it is really a function call to the global function. + $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + + if (isset(Collections::objectOperators()[$tokens[$prevNonEmpty]['code']]) === true + || $tokens[$prevNonEmpty]['code'] === \T_NEW + ) { + // Method call, class instantiation or other "not our target". + return; + } + + if ($tokens[$prevNonEmpty]['code'] === \T_NS_SEPARATOR) { + $prevPrevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevNonEmpty - 1), null, true); + if ($tokens[$prevPrevToken]['code'] === \T_STRING + || $tokens[$prevPrevToken]['code'] === \T_NAMESPACE + ) { + // Namespaced function. + return; + } + } + + /* + * As of here, we can be pretty sure this is a function call to the global function. + */ + $opener = $nextNonEmpty; + $closer = $tokens[$nextNonEmpty]['parenthesis_closer']; + + $parameters = PassedParameters::getParameters($phpcsFile, $stackPtr); + $paramCount = \count($parameters); + if (empty($parameters) || $paramCount > 2) { + // No parameters or too many parameter. + return; + } + + $pathParam = PassedParameters::getParameterFromStack($parameters, 1, 'path'); + if ($pathParam === false) { + // If the path parameter doesn't exist, there's nothing to do. + return; + } + + $levelsParam = PassedParameters::getParameterFromStack($parameters, 2, 'levels'); + if ($levelsParam === false && $paramCount === 2) { + // There must be a typo in the param name or an otherwise stray parameter. Ignore. + return; + } + + /* + * PHP 5.3+: Detect use of dirname(__FILE__). + */ + if ($pathParam['clean'] === '__FILE__') { + // Determine if the issue is auto-fixable. + $hasComment = $phpcsFile->findNext(Tokens::$commentTokens, ($opener + 1), $closer); + $fixable = ($hasComment === false); + + if ($fixable === true) { + $levelsValue = $this->getLevelsValue($phpcsFile, $levelsParam); + if ($levelsParam !== false && $levelsValue === false) { + // Can't autofix if we don't know the value of the $levels parameter. + $fixable = false; + } + } + + $error = 'Use the __DIR__ constant instead of calling dirname(__FILE__) (PHP >= 5.3)'; + $code = 'FileConstant'; + + // Throw the error. + if ($fixable === false) { + $phpcsFile->addError($error, $stackPtr, $code); + return; + } + + $fix = $phpcsFile->addFixableError($error, $stackPtr, $code); + if ($fix === true) { + if ($levelsParam === false || $levelsValue === 1) { + // No $levels or $levels set to 1: we can replace the complete function call. + $phpcsFile->fixer->beginChangeset(); + + $phpcsFile->fixer->replaceToken($stackPtr, '__DIR__'); + + for ($i = ($stackPtr + 1); $i <= $closer; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + // Remove potential leading \. + if ($tokens[$prevNonEmpty]['code'] === \T_NS_SEPARATOR) { + $phpcsFile->fixer->replaceToken($prevNonEmpty, ''); + } + + $phpcsFile->fixer->endChangeset(); + } else { + // We can replace the $path parameter and will need to adjust the $levels parameter. + $filePtr = $phpcsFile->findNext(\T_FILE, $pathParam['start'], ($pathParam['end'] + 1)); + $levelsPtr = $phpcsFile->findNext(\T_LNUMBER, $levelsParam['start'], ($levelsParam['end'] + 1)); + + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($filePtr, '__DIR__'); + $phpcsFile->fixer->replaceToken($levelsPtr, ($levelsValue - 1)); + $phpcsFile->fixer->endChangeset(); + } + } + + return; + } + + /* + * PHP 7.0+: Detect use of nested calls to dirname(). + */ + if (\preg_match('`^\s*\\\\?dirname\s*\(`i', $pathParam['clean']) !== 1) { + return; + } + + /* + * Check if there is something _behind_ the nested dirname() call within the same parameter. + * + * Note: the findNext() calls are safe and will always match the dirname() function call + * as otherwise the above regex wouldn't have matched. + */ + $innerDirnamePtr = $phpcsFile->findNext(\T_STRING, $pathParam['start'], ($pathParam['end'] + 1)); + $innerOpener = $phpcsFile->findNext(\T_OPEN_PARENTHESIS, ($innerDirnamePtr + 1), ($pathParam['end'] + 1)); + if (isset($tokens[$innerOpener]['parenthesis_closer']) === false) { + // Shouldn't be possible. + return; // @codeCoverageIgnore + } + + $innerCloser = $tokens[$innerOpener]['parenthesis_closer']; + if ($innerCloser !== $pathParam['end']) { + $hasContentAfter = $phpcsFile->findNext( + Tokens::$emptyTokens, + ($innerCloser + 1), + ($pathParam['end'] + 1), + true + ); + if ($hasContentAfter !== false) { + // Matched code like: `dirname(dirname($file) . 'something')`. Ignore. + return; + } + } + + /* + * Determine if this is an auto-fixable error. + */ + + // Step 1: Are there comments ? If so, not auto-fixable as we don't want to remove comments. + $fixable = true; + for ($i = ($opener + 1); $i < $closer; $i++) { + if (isset(Tokens::$commentTokens[$tokens[$i]['code']])) { + $fixable = false; + break; + } + + if ($tokens[$i]['code'] === \T_OPEN_PARENTHESIS + && isset($tokens[$i]['parenthesis_closer']) + ) { + // Skip over everything within the nested dirname() function call. + $i = $tokens[$i]['parenthesis_closer']; + } + } + + // Step 2: Does the `$levels` parameter exist for the outer dirname() call and if so, is it usable ? + if ($fixable === true) { + $outerLevelsValue = $this->getLevelsValue($phpcsFile, $levelsParam); + if ($levelsParam !== false && $outerLevelsValue === false) { + // Can't autofix if we don't know the value of the $levels parameter. + $fixable = false; + } + } + + // Step 3: Does the `$levels` parameter exist for the inner dirname() call and if so, is it usable ? + if ($fixable === true) { + $innerParameters = PassedParameters::getParameters($phpcsFile, $innerDirnamePtr); + $innerLevelsParam = PassedParameters::getParameterFromStack($innerParameters, 2, 'levels'); + $innerLevelsValue = $this->getLevelsValue($phpcsFile, $innerLevelsParam); + if ($innerLevelsParam !== false && $innerLevelsValue === false) { + // Can't autofix if we don't know the value of the $levels parameter. + $fixable = false; + } + } + + /* + * Throw the error. + */ + $error = 'Pass the $levels parameter to the dirname() call instead of using nested dirname() calls'; + $error .= ' (PHP >= 7.0)'; + $code = 'Nested'; + + if ($fixable === false) { + $phpcsFile->addError($error, $stackPtr, $code); + return; + } + + $fix = $phpcsFile->addFixableError($error, $stackPtr, $code); + if ($fix === false) { + return; + } + + /* + * Fix the error. + */ + $phpcsFile->fixer->beginChangeset(); + + // Remove the info in the _outer_ param call. + for ($i = $opener; $i < $innerOpener; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + for ($i = ($innerCloser + 1); $i <= $closer; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + if ($innerLevelsParam !== false) { + // Inner $levels parameter already exists, just adjust the value. + $innerLevelsPtr = $phpcsFile->findNext( + \T_LNUMBER, + $innerLevelsParam['start'], + ($innerLevelsParam['end'] + 1) + ); + $phpcsFile->fixer->replaceToken($innerLevelsPtr, ($innerLevelsValue + $outerLevelsValue)); + } else { + // Inner $levels parameter does not exist yet. We need to add it. + $content = ', '; + + $prevBeforeCloser = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($innerCloser - 1), null, true); + if ($tokens[$prevBeforeCloser]['code'] === \T_COMMA) { + // Trailing comma found, no need to add the comma. + $content = ' '; + } + + $innerPathParam = PassedParameters::getParameterFromStack($innerParameters, 1, 'path'); + if (isset($innerPathParam['name_token']) === true) { + // Non-named param cannot follow named param, so add param name. + $content .= 'levels: '; + } + + $content .= ($innerLevelsValue + $outerLevelsValue); + $phpcsFile->fixer->addContentBefore($innerCloser, $content); + } + + $phpcsFile->fixer->endChangeset(); + } + + /** + * Determine the value of the $levels parameter passed to dirname(). + * + * @since 1.0.0 + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param array|false $levelsParam The information about the parameter as retrieved + * via PassedParameters::getParameterFromStack(). + * + * @return int|false Integer levels value or FALSE if the levels value couldn't be determined. + */ + private function getLevelsValue($phpcsFile, $levelsParam) + { + if ($levelsParam === false) { + return 1; + } + + $ignore = Tokens::$emptyTokens; + $ignore[] = \T_LNUMBER; + + $hasNonNumber = $phpcsFile->findNext($ignore, $levelsParam['start'], ($levelsParam['end'] + 1), true); + if ($hasNonNumber !== false) { + return false; + } + + return (int) $levelsParam['clean']; + } +} diff --git a/Modernize/Tests/FunctionCalls/DirnameUnitTest.inc b/Modernize/Tests/FunctionCalls/DirnameUnitTest.inc new file mode 100644 index 00000000..30e67bb1 --- /dev/null +++ b/Modernize/Tests/FunctionCalls/DirnameUnitTest.inc @@ -0,0 +1,142 @@ +dirname(__FILE__); +$path = $obj?->dirname(__FILE__); +$path = Package\dirname(__FILE__); +$path = \My\Package\dirname(__FILE__); +echo Foo::DIRNAME; +$obj = new dirname(__FILE__); + +echo DIRNAME . '/file.php'; + + +/* + * These should not be flagged. + */ +echo foo(__FILE__); +$path = dirname(); +$path = dirname( $file ); +$path = dirname( __DIR__ ); +$path = dirname( '.' ); +$path = dirname( get_path(__FILE__) ); +$path = dirname(__FILE__ . '/..'); + +$path = dirname( ABSPATH . '/path/to/file.php', 3 ); +$path = dirname( __DIR__, 3 ); + +$path = dirname(levels: 2); // ArgumentCountError, require param dirname missing. Ignore as not the concern of this sniff. +$path = dirname( __FILE__, 3, $extra ); // ArgumentCountError, too many args. Ignore as not the concern of this sniff. + +$path = dirname( paths: __FILE__ ); // Error: unknown named parameter. Ignore as not the concern of this sniff. +$path = dirname( __FILE__, level: 2); // Error: unknown named parameter. Ignore as not the concern of this sniff. + +$path = dirname(\dirname(__DIR__) . '/file.php'); // Nested dirname() call not stand-alone. Silly code, but should not be flagged. + + +/* + * These should be flagged for use of dirname(__FILE__). + */ +$path = dirname(__FILE__); +$path = \dirname ( __FILE__ , ) ; // Includes trailing comma. + +$path = DirName(__FILE__ /* todo: replace with __DIR__ */); // Not auto-fixable. + +// Handling of multi-line function calls. +$path = dirname( + __FILE__ +); + +// Uses __FILE__, but also has $levels parameter. +$path = dirname(__FILE__, 1); +$path = dirname(__FILE__, 2); +$path = dirname(__FILE__, 03); // Octal 3. + +$path = dirname(__FILE__, $levels); // Not auto-fixable. +$path = dirname(__FILE__, get_levels(2)); // Not auto-fixable. + +// With named parameters. +$path = dirname(path: __FILE__, levels: 1); +$path = dirname(levels: 3, path: __FILE__); + +// phpcs:ignore Modernize.FunctionCalls.Dirname.Nested -- Only apply __DIR__ fix, not $levels. +$path = dirname(dirname(dirname(dirname(__FILE__)))); + + +/* + * These should be flagged for use of nested dirname(). + */ +$path = dirname(dirname(dirname(dirname(__DIR__)))); +$path = dirname(dirname(DIRNAME(dirname(__DIR__, 2,)))); // Includes trailing comma. + +$path = dirname(\dirname($file,), 2); // Includes trailing comma in inner dirname() call. +$path = dirname(dirname(dirname($file), 2), 2); + +// Handling of multi-line function calls. +$path = dirname( + dirname( + __DIR__, + 2 + ), + 2 +); + +$path = dirname( + \dirname(__DIR__, 1), // Comment within the outer dirname() scope. + 2 +); // Not auto-fixable. + +$path = dirname( + dirname( + __DIR__, // Comment within the inner dirname() scope. + 3 + ), + 2 +); // Auto-fixable. + +$path = dirname(dirname(__DIR__, $levels), 2); // Not auto-fixable. +$path = dirname(dirname(__DIR__, 2), get_levels()); // Not auto-fixable. + +$path = dirname(dirname(dirname($file, 2), 2), FOO::LEVELS); // Partially auto-fixable. + +// With named parameters. +$path = dirname(levels: 2, path: \dirname(levels: 2, path: DIRNAME(path: dirname(path: __DIR__), levels: 2))); +$path = dirname(levels: 1, path: dirname(levels: 3, path: \DIRNAME(levels: 1, path: dirname(levels: 2, path: __DIR__,)))); // Includes trailing comma. + + +/* + * These should be flagged for both. + */ +$path = dirname(dirname(dirname(dirname(__FILE__)))); +$path = dirname(DirName(dirname(dirname(__FILE__, 2)))); + +// With named parameters. +$path = dirname(path: DirName(path: dirname(path: dirname(path: __FILE__)))); + +// With named parameters, multi-line function call, trailing comma, non-lowercase function call. +$path = dirname( + path: DirName( + levels: 2, + path: dirname( + path: dirname( + path: __FILE__ + ), + levels: 3, + ) + ) +); + + +// Parse error. +// This must be the last test in the file. +echo dirname(__FILE__ diff --git a/Modernize/Tests/FunctionCalls/DirnameUnitTest.inc.fixed b/Modernize/Tests/FunctionCalls/DirnameUnitTest.inc.fixed new file mode 100644 index 00000000..f2511799 --- /dev/null +++ b/Modernize/Tests/FunctionCalls/DirnameUnitTest.inc.fixed @@ -0,0 +1,126 @@ +dirname(__FILE__); +$path = $obj?->dirname(__FILE__); +$path = Package\dirname(__FILE__); +$path = \My\Package\dirname(__FILE__); +echo Foo::DIRNAME; +$obj = new dirname(__FILE__); + +echo DIRNAME . '/file.php'; + + +/* + * These should not be flagged. + */ +echo foo(__FILE__); +$path = dirname(); +$path = dirname( $file ); +$path = dirname( __DIR__ ); +$path = dirname( '.' ); +$path = dirname( get_path(__FILE__) ); +$path = dirname(__FILE__ . '/..'); + +$path = dirname( ABSPATH . '/path/to/file.php', 3 ); +$path = dirname( __DIR__, 3 ); + +$path = dirname(levels: 2); // ArgumentCountError, require param dirname missing. Ignore as not the concern of this sniff. +$path = dirname( __FILE__, 3, $extra ); // ArgumentCountError, too many args. Ignore as not the concern of this sniff. + +$path = dirname( paths: __FILE__ ); // Error: unknown named parameter. Ignore as not the concern of this sniff. +$path = dirname( __FILE__, level: 2); // Error: unknown named parameter. Ignore as not the concern of this sniff. + +$path = dirname(\dirname(__DIR__) . '/file.php'); // Nested dirname() call not stand-alone. Silly code, but should not be flagged. + + +/* + * These should be flagged for use of dirname(__FILE__). + */ +$path = __DIR__; +$path = __DIR__ ; // Includes trailing comma. + +$path = DirName(__FILE__ /* todo: replace with __DIR__ */); // Not auto-fixable. + +// Handling of multi-line function calls. +$path = __DIR__; + +// Uses __FILE__, but also has $levels parameter. +$path = __DIR__; +$path = dirname(__DIR__, 1); +$path = dirname(__DIR__, 2); // Octal 3. + +$path = dirname(__FILE__, $levels); // Not auto-fixable. +$path = dirname(__FILE__, get_levels(2)); // Not auto-fixable. + +// With named parameters. +$path = __DIR__; +$path = dirname(levels: 2, path: __DIR__); + +// phpcs:ignore Modernize.FunctionCalls.Dirname.Nested -- Only apply __DIR__ fix, not $levels. +$path = dirname(dirname(dirname(__DIR__))); + + +/* + * These should be flagged for use of nested dirname(). + */ +$path = dirname(__DIR__, 4); +$path = dirname(__DIR__, 5,); // Includes trailing comma. + +$path = dirname($file, 3); // Includes trailing comma in inner dirname() call. +$path = dirname($file, 5); + +// Handling of multi-line function calls. +$path = dirname( + __DIR__, + 4 + ); + +$path = dirname( + \dirname(__DIR__, 1), // Comment within the outer dirname() scope. + 2 +); // Not auto-fixable. + +$path = dirname( + __DIR__, // Comment within the inner dirname() scope. + 5 + ); // Auto-fixable. + +$path = dirname(dirname(__DIR__, $levels), 2); // Not auto-fixable. +$path = dirname(dirname(__DIR__, 2), get_levels()); // Not auto-fixable. + +$path = dirname(dirname($file, 4), FOO::LEVELS); // Partially auto-fixable. + +// With named parameters. +$path = dirname(path: __DIR__, levels: 7); +$path = dirname(levels: 7, path: __DIR__,); // Includes trailing comma. + + +/* + * These should be flagged for both. + */ +$path = dirname(__DIR__, 3); +$path = dirname(__DIR__, 4); + +// With named parameters. +$path = dirname(path: __DIR__, levels: 3); + +// With named parameters, multi-line function call, trailing comma, non-lowercase function call. +$path = dirname( + path: __DIR__ + , levels: 6); + + +// Parse error. +// This must be the last test in the file. +echo dirname(__FILE__ diff --git a/Modernize/Tests/FunctionCalls/DirnameUnitTest.php b/Modernize/Tests/FunctionCalls/DirnameUnitTest.php new file mode 100644 index 00000000..eaa73dba --- /dev/null +++ b/Modernize/Tests/FunctionCalls/DirnameUnitTest.php @@ -0,0 +1,82 @@ + + */ + public function getErrorList() + { + return [ + 50 => 1, + 51 => 1, + 53 => 1, + 56 => 1, + 61 => 1, + 62 => 1, + 63 => 1, + 65 => 1, + 66 => 1, + 69 => 1, + 70 => 1, + 73 => 1, + 79 => 3, + 80 => 3, + 82 => 1, + 83 => 2, + 86 => 1, + 94 => 1, + 99 => 1, + 107 => 1, + 108 => 1, + 110 => 2, + 113 => 3, + 114 => 3, + 120 => 4, + 121 => 4, + 124 => 4, + 127 => 1, + 128 => 1, + 130 => 1, + 131 => 1, + ]; + } + + /** + * Returns the lines where warnings should occur. + * + * The key of the array should represent the line number and the value + * should represent the number of warnings that should occur on that line. + * + * @return array + */ + public function getWarningList() + { + return []; + } +} From 63d67fe91b90e63495f825dd344f4ba83d4b8047 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 3 Dec 2022 06:13:52 +0100 Subject: [PATCH 083/125] Universal/NoReservedKeywordParameterNames: minor code simplification ... which can be made now support for PHPCS < 3.7.1 has been dropped. The sniff now only listens to tokens which are accepted by the `FunctionDeclarations::getParameters()` method, so checking for an exception should be unnecessary. Includes removing a stray `$paramNames` variable setting, which is never used. --- .../NoReservedKeywordParameterNamesSniff.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php b/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php index 81af4e0c..6df4a08a 100644 --- a/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php +++ b/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php @@ -10,7 +10,6 @@ namespace PHPCSExtra\Universal\Sniffs\NamingConventions; -use PHP_CodeSniffer\Exceptions\RuntimeException; use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; use PHPCSUtils\Tokens\Collections; @@ -161,16 +160,11 @@ public function register() public function process(File $phpcsFile, $stackPtr) { // Get all parameters from method signature. - try { - $parameters = FunctionDeclarations::getParameters($phpcsFile, $stackPtr); - if (empty($parameters)) { - return; - } - } catch (RuntimeException $e) { + $parameters = FunctionDeclarations::getParameters($phpcsFile, $stackPtr); + if (empty($parameters)) { return; } - $paramNames = []; foreach ($parameters as $param) { $name = \ltrim($param['name'], '$'); if (isset($this->reservedNames[$name]) === true) { From df7e718ce13f426c41bb91eaa14a7b1f900e941d Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 3 Dec 2022 06:22:13 +0100 Subject: [PATCH 084/125] Universal/NoReservedKeywordParameterNames: make tests more descriptive --- .../NoReservedKeywordParameterNamesUnitTest.inc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.inc b/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.inc index 48ee9d77..bf0cafc1 100644 --- a/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.inc +++ b/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.inc @@ -1,13 +1,13 @@ $callable($list); // Bad x 2. abstract class Foo { - abstract public function bar( + abstract public function abstractMethod( string &$string, Foo|false $exit, ?int $parent From d9e60cee076d6544cbb1cfe4dccb8df23553f8c3 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 3 Dec 2022 06:23:06 +0100 Subject: [PATCH 085/125] Universal/NoReservedKeywordParameterNames: add extra tests ... to safeguard bowing out for function declarations without parameters and that the sniff stays silent for unfinished function declarations. --- .../NoReservedKeywordParameterNamesUnitTest.inc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.inc b/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.inc index bf0cafc1..bd25ffbe 100644 --- a/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.inc +++ b/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.inc @@ -13,3 +13,9 @@ abstract class Foo { ?int $parent ); // Bad x 3. } + +// No parameters, nothing to do. +function noParam() {} + +// Live coding/parse error. This has to be the last test in the file. +function liveCoding($echo From 9dd2e1898fc81faeba58f2ce1deb2e0e3c13a322 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 3 Dec 2022 06:07:20 +0100 Subject: [PATCH 086/125] Universal/NoReservedKeywordParameterNames: make the check case-insensitive While parameters (variables) are case-sensitive in PHP, keywords are not. This commit improves the sniff to check for the keyword used in parameter names in a case-insensitive manner to make this sniff independent of code style rules regarding the case for parameter names. Includes additional tests. --- .../NoReservedKeywordParameterNamesSniff.php | 220 +++++++++--------- ...oReservedKeywordParameterNamesUnitTest.inc | 19 +- ...oReservedKeywordParameterNamesUnitTest.php | 6 + 3 files changed, 139 insertions(+), 106 deletions(-) diff --git a/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php b/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php index 6df4a08a..15da4aea 100644 --- a/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php +++ b/Universal/Sniffs/NamingConventions/NoReservedKeywordParameterNamesSniff.php @@ -16,7 +16,13 @@ use PHPCSUtils\Utils\FunctionDeclarations; /** - * Verifies that parameters in function declarations do not use PHP reserved keywords. + * Verifies that parameters in function declarations do not use PHP reserved keywords + * as this can lead to confusing code when using PHP 8.0+ named parameters in function calls. + * + * Note: while parameters (variables) are case-sensitive in PHP, keywords are not, + * so this sniff checks for the keywords used in parameter names in a + * case-insensitive manner to make this sniff independent of code style rules + * regarding the case for parameter names. * * @link https://www.php.net/manual/en/reserved.keywords.php * @@ -30,108 +36,108 @@ final class NoReservedKeywordParameterNamesSniff implements Sniff * * @since 1.0.0 * - * @var array(string => string) + * @var array string> Key is the lowercased keyword, value the "proper" cased keyword. */ - protected $reservedNames = [ - 'abstract' => true, - 'and' => true, - 'array' => true, - 'as' => true, - 'break' => true, - 'callable' => true, - 'case' => true, - 'catch' => true, - 'class' => true, - 'clone' => true, - 'const' => true, - 'continue' => true, - 'declare' => true, - 'default' => true, - 'die' => true, - 'do' => true, - 'echo' => true, - 'else' => true, - 'elseif' => true, - 'empty' => true, - 'enddeclare' => true, - 'endfor' => true, - 'endforeach' => true, - 'endif' => true, - 'endswitch' => true, - 'endwhile' => true, - 'enum' => true, - 'eval' => true, - 'exit' => true, - 'extends' => true, - 'final' => true, - 'finally' => true, - 'fn' => true, - 'for' => true, - 'foreach' => true, - 'function' => true, - 'global' => true, - 'goto' => true, - 'if' => true, - 'implements' => true, - 'include' => true, - 'include_once' => true, - 'instanceof' => true, - 'insteadof' => true, - 'interface' => true, - 'isset' => true, - 'list' => true, - 'match' => true, - 'namespace' => true, - 'new' => true, - 'or' => true, - 'print' => true, - 'private' => true, - 'protected' => true, - 'public' => true, - 'readonly' => true, - 'require' => true, - 'require_once' => true, - 'return' => true, - 'static' => true, - 'switch' => true, - 'throw' => true, - 'trait' => true, - 'try' => true, - 'unset' => true, - 'use' => true, - 'var' => true, - 'while' => true, - 'xor' => true, - 'yield' => true, - '__CLASS__' => true, - '__DIR__' => true, - '__FILE__' => true, - '__FUNCTION__' => true, - '__LINE__' => true, - '__METHOD__' => true, - '__NAMESPACE__' => true, - '__TRAIT__' => true, - 'int' => true, - 'float' => true, - 'bool' => true, - 'string' => true, - 'true' => true, - 'false' => true, - 'null' => true, - 'void' => true, - 'iterable' => true, - 'object' => true, - 'resource' => true, - 'mixed' => true, - 'numeric' => true, - 'never' => true, + private $reservedNames = [ + 'abstract' => 'abstract', + 'and' => 'and', + 'array' => 'array', + 'as' => 'as', + 'break' => 'break', + 'callable' => 'callable', + 'case' => 'case', + 'catch' => 'catch', + 'class' => 'class', + 'clone' => 'clone', + 'const' => 'const', + 'continue' => 'continue', + 'declare' => 'declare', + 'default' => 'default', + 'die' => 'die', + 'do' => 'do', + 'echo' => 'echo', + 'else' => 'else', + 'elseif' => 'elseif', + 'empty' => 'empty', + 'enddeclare' => 'enddeclare', + 'endfor' => 'endfor', + 'endforeach' => 'endforeach', + 'endif' => 'endif', + 'endswitch' => 'endswitch', + 'endwhile' => 'endwhile', + 'enum' => 'enum', + 'eval' => 'eval', + 'exit' => 'exit', + 'extends' => 'extends', + 'final' => 'final', + 'finally' => 'finally', + 'fn' => 'fn', + 'for' => 'for', + 'foreach' => 'foreach', + 'function' => 'function', + 'global' => 'global', + 'goto' => 'goto', + 'if' => 'if', + 'implements' => 'implements', + 'include' => 'include', + 'include_once' => 'include_once', + 'instanceof' => 'instanceof', + 'insteadof' => 'insteadof', + 'interface' => 'interface', + 'isset' => 'isset', + 'list' => 'list', + 'match' => 'match', + 'namespace' => 'namespace', + 'new' => 'new', + 'or' => 'or', + 'print' => 'print', + 'private' => 'private', + 'protected' => 'protected', + 'public' => 'public', + 'readonly' => 'readonly', + 'require' => 'require', + 'require_once' => 'require_once', + 'return' => 'return', + 'static' => 'static', + 'switch' => 'switch', + 'throw' => 'throw', + 'trait' => 'trait', + 'try' => 'try', + 'unset' => 'unset', + 'use' => 'use', + 'var' => 'var', + 'while' => 'while', + 'xor' => 'xor', + 'yield' => 'yield', + '__class__' => '__CLASS__', + '__dir__' => '__DIR__', + '__file__' => '__FILE__', + '__function__' => '__FUNCTION__', + '__line__' => '__LINE__', + '__method__' => '__METHOD__', + '__namespace__' => '__NAMESPACE__', + '__trait__' => '__TRAIT__', + 'int' => 'int', + 'float' => 'float', + 'bool' => 'bool', + 'string' => 'string', + 'true' => 'true', + 'false' => 'false', + 'null' => 'null', + 'void' => 'void', + 'iterable' => 'iterable', + 'object' => 'object', + 'resource' => 'resource', + 'mixed' => 'mixed', + 'numeric' => 'numeric', + 'never' => 'never', /* * Not reserved keywords, but equally confusing when used in the context of function calls * with named parameters. */ - 'parent' => true, - 'self' => true, + 'parent' => 'parent', + 'self' => 'self', ]; /** @@ -165,15 +171,19 @@ public function process(File $phpcsFile, $stackPtr) return; } + $message = 'It is recommended not to use reserved keyword "%s" as function parameter name. Found: %s'; + foreach ($parameters as $param) { - $name = \ltrim($param['name'], '$'); - if (isset($this->reservedNames[$name]) === true) { - $phpcsFile->addWarning( - 'It is recommended not to use reserved keywords as function parameter names. Found: %s', - $param['token'], - $name . 'Found', - [$param['name']] - ); + $name = \ltrim($param['name'], '$'); + $nameLC = \strtolower($name); + if (isset($this->reservedNames[$nameLC]) === true) { + $errorCode = $nameLC . 'Found'; + $data = [ + $this->reservedNames[$nameLC], + $param['name'], + ]; + + $phpcsFile->addWarning($message, $param['token'], $errorCode, $data); } } } diff --git a/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.inc b/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.inc index bd25ffbe..cde95c28 100644 --- a/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.inc +++ b/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.inc @@ -4,7 +4,7 @@ function noReservedKeywords( $parameter, $descriptive_name ) {} // OK. function hasReservedKeywords( $string, $echo = true ) {} // Bad x 2. $closure = function ( $foreach, $array, $require ) {}; // Bad x 3. -$fn = fn($callable, $list) => $callable($list); // Bad x 2. +$fn = fn($callable, $__FILE__) => $callable($__FILE__); // Bad x 2. abstract class Foo { abstract public function abstractMethod( @@ -14,6 +14,23 @@ abstract class Foo { ); // Bad x 3. } +/* + * Tests using less conventional param name casing. + */ +function noReservedKeywordsCasedParams( $Parameter, $DescriptiveName ) {} // OK. + +function hasReservedKeywordsCasedParams( $String, $ECHO = true ) {} // Bad x 2. +$closure = function ( $forEach, $Array, $REQUIRE ) {}; // Bad x 3. +$fn = fn($Callable, $__file__) => $Callable($__file__); // Bad x 2. + +abstract class Bar { + abstract public function abstractMethodCasedParams( + string &$STRING, + Foo|false $Exit, + ?int $PaReNt + ); // Bad x 3. +} + // No parameters, nothing to do. function noParam() {} diff --git a/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.php b/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.php index 71cf647b..e3890d19 100644 --- a/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.php +++ b/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.php @@ -46,6 +46,12 @@ public function getWarningList() 11 => 1, 12 => 1, 13 => 1, + 22 => 2, + 23 => 3, + 24 => 2, + 28 => 1, + 29 => 1, + 30 => 1, ]; } } From 0627b700557a629fbf8ebee7342ba2931107e7f2 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 3 Dec 2022 06:09:11 +0100 Subject: [PATCH 087/125] Universal/NoReservedKeywordParameterNames: add tests with PHP 8.0 constructor property promotion ... to confirm that properties set via the constructor are also checked. Properties set via the constructor can be _passed_ to the constructor using named parameters, so this check should also apply to those properties. --- .../NoReservedKeywordParameterNamesUnitTest.inc | 16 ++++++++++++++++ .../NoReservedKeywordParameterNamesUnitTest.php | 4 ++++ 2 files changed, 20 insertions(+) diff --git a/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.inc b/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.inc index cde95c28..8f06efaa 100644 --- a/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.inc +++ b/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.inc @@ -31,6 +31,22 @@ abstract class Bar { ); // Bad x 3. } +/* + * Also flag properties declared via constructor property promotion as those can also be + * passed to the class constructor as named parameters. + */ +class ConstructorPropertyPromotionNoTypes { + public function __construct( + public $const = 0.0, + protected $do = '', + private $eval = null, + ) {} +} + +class ConstructorPropertyPromotionWithTypes { + public function __construct(protected float|int $Function, public ?string &$GLOBAL = 'test', private mixed $match) {} +} + // No parameters, nothing to do. function noParam() {} diff --git a/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.php b/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.php index e3890d19..96494c0b 100644 --- a/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.php +++ b/Universal/Tests/NamingConventions/NoReservedKeywordParameterNamesUnitTest.php @@ -52,6 +52,10 @@ public function getWarningList() 28 => 1, 29 => 1, 30 => 1, + 40 => 1, + 41 => 1, + 42 => 1, + 47 => 3, ]; } } From 765e4b362a1e69dca08514b995a6fc23e367d480 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 3 Dec 2022 06:35:38 +0100 Subject: [PATCH 088/125] Universal/DisallowUse[Class|Const|Function]: minor code coverage tweak Ignore a statement which should never be reachable (defense in depth code). --- Universal/Sniffs/UseStatements/DisallowUseClassSniff.php | 2 +- Universal/Sniffs/UseStatements/DisallowUseConstSniff.php | 2 +- Universal/Sniffs/UseStatements/DisallowUseFunctionSniff.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Universal/Sniffs/UseStatements/DisallowUseClassSniff.php b/Universal/Sniffs/UseStatements/DisallowUseClassSniff.php index 0dc55eda..3ac22c3b 100644 --- a/Universal/Sniffs/UseStatements/DisallowUseClassSniff.php +++ b/Universal/Sniffs/UseStatements/DisallowUseClassSniff.php @@ -133,7 +133,7 @@ public function process(File $phpcsFile, $stackPtr) $reportPtr = $phpcsFile->findNext(\T_STRING, ($reportPtr + 1), $endOfStatement, false, $alias); if ($reportPtr === false) { // Shouldn't be possible. - continue 2; + continue 2; // @codeCoverageIgnore } $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($reportPtr + 1), $endOfStatement, true); diff --git a/Universal/Sniffs/UseStatements/DisallowUseConstSniff.php b/Universal/Sniffs/UseStatements/DisallowUseConstSniff.php index cbcaa9a0..b0a69755 100644 --- a/Universal/Sniffs/UseStatements/DisallowUseConstSniff.php +++ b/Universal/Sniffs/UseStatements/DisallowUseConstSniff.php @@ -133,7 +133,7 @@ public function process(File $phpcsFile, $stackPtr) $reportPtr = $phpcsFile->findNext(\T_STRING, ($reportPtr + 1), $endOfStatement, false, $alias); if ($reportPtr === false) { // Shouldn't be possible. - continue 2; + continue 2; // @codeCoverageIgnore } $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($reportPtr + 1), $endOfStatement, true); diff --git a/Universal/Sniffs/UseStatements/DisallowUseFunctionSniff.php b/Universal/Sniffs/UseStatements/DisallowUseFunctionSniff.php index 11cb9c2a..64aaa130 100644 --- a/Universal/Sniffs/UseStatements/DisallowUseFunctionSniff.php +++ b/Universal/Sniffs/UseStatements/DisallowUseFunctionSniff.php @@ -133,7 +133,7 @@ public function process(File $phpcsFile, $stackPtr) $reportPtr = $phpcsFile->findNext(\T_STRING, ($reportPtr + 1), $endOfStatement, false, $alias); if ($reportPtr === false) { // Shouldn't be possible. - continue 2; + continue 2; // @codeCoverageIgnore } $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($reportPtr + 1), $endOfStatement, true); From bd76b7cbfab1415526c7c877e38113844c11ea76 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 4 Dec 2022 02:56:40 +0100 Subject: [PATCH 089/125] Universal/ForeachUniqueAssignment: fix some tests These tests weren't testing the sniff well enough. --- .../Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.inc | 4 ++-- .../CodeAnalysis/ForeachUniqueAssignmentUnitTest.inc.fixed | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.inc b/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.inc index 17a07804..fdd19e67 100644 --- a/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.inc +++ b/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.inc @@ -13,8 +13,8 @@ foreach ($array as $k => &$v ) {} foreach ($array as $k['key'] => $k['value'] ) {} foreach ($array as $k[$name] => $k[$value] ) {} foreach ($data as $key => [$id, [$name, $address]]) {} -foreach ($data as list("id" => $id, "name" => $name)) {} -foreach ($array as array($a) => list($a) ) {} +foreach ($data as $key => list("id" => $id, "name" => $name)) {} +foreach ($array as $key => list($a) ) {} /* * The issue. diff --git a/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.inc.fixed b/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.inc.fixed index 0d8430cf..21387d7d 100644 --- a/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.inc.fixed +++ b/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.inc.fixed @@ -13,8 +13,8 @@ foreach ($array as $k => &$v ) {} foreach ($array as $k['key'] => $k['value'] ) {} foreach ($array as $k[$name] => $k[$value] ) {} foreach ($data as $key => [$id, [$name, $address]]) {} -foreach ($data as list("id" => $id, "name" => $name)) {} -foreach ($array as array($a) => list($a) ) {} +foreach ($data as $key => list("id" => $id, "name" => $name)) {} +foreach ($array as $key => list($a) ) {} /* * The issue. From 1559d36b723062fe6c51f5f56f14127d38c9e975 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 4 Dec 2022 03:04:56 +0100 Subject: [PATCH 090/125] Universal/ForeachUniqueAssignment: improve analysis for foreach list assignments This commit: * Prevents "accidentally correct" handling of code like `foreach ($data as [$id => $id])`, where the first `$id` is not the key for the foreach assignment, so is not the concern of this sniff. * Improves analysis for key vs value when the value is a list assignment. Previously, the key and the value would just be compared as found. Now, each individual assignment in the list will be compared against the key. Notes: nested list assignment are not (yet) taken into account. Includes improved and additional tests. --- .../ForeachUniqueAssignmentSniff.php | 95 +++++++++++++------ .../ForeachUniqueAssignmentUnitTest.inc | 12 ++- .../ForeachUniqueAssignmentUnitTest.inc.fixed | 10 +- .../ForeachUniqueAssignmentUnitTest.php | 3 +- 4 files changed, 89 insertions(+), 31 deletions(-) diff --git a/Universal/Sniffs/CodeAnalysis/ForeachUniqueAssignmentSniff.php b/Universal/Sniffs/CodeAnalysis/ForeachUniqueAssignmentSniff.php index 6275e76d..5cb5b5ee 100644 --- a/Universal/Sniffs/CodeAnalysis/ForeachUniqueAssignmentSniff.php +++ b/Universal/Sniffs/CodeAnalysis/ForeachUniqueAssignmentSniff.php @@ -13,7 +13,9 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Tokens\Collections; use PHPCSUtils\Utils\GetTokensAsString; +use PHPCSUtils\Utils\Lists; /** * Detects using the same variable for both the key as well as the value in a foreach assignment. @@ -67,46 +69,85 @@ public function process(File $phpcsFile, $stackPtr) return; } - $doubleArrowPtr = $phpcsFile->findNext(\T_DOUBLE_ARROW, ($asPtr + 1), $closer); - if ($doubleArrowPtr === false) { - // Key does not get assigned. + // Real target. + $find = [\T_DOUBLE_ARROW]; + // Prevent matching on double arrows within a list assignment. + $find += Collections::listTokens(); + + $doubleArrowPtr = $phpcsFile->findNext($find, ($asPtr + 1), $closer); + if ($doubleArrowPtr === false + || $tokens[$doubleArrowPtr]['code'] !== \T_DOUBLE_ARROW + ) { + // No key assignment. return; } - $keyAsString = GetTokensAsString::noEmpties($phpcsFile, ($asPtr + 1), ($doubleArrowPtr - 1)); - $valueAsString = GetTokensAsString::noEmpties($phpcsFile, ($doubleArrowPtr + 1), ($closer - 1)); + $isListAssignment = $phpcsFile->findNext(Tokens::$emptyTokens, ($doubleArrowPtr + 1), $closer, true); + if ($isListAssignment === false) { + // Parse error or live coding, not our concern. + } - if (\ltrim($keyAsString, '&') !== \ltrim($valueAsString, '&')) { - // Key and value not the same. - return; + $keyAsString = \ltrim(GetTokensAsString::noEmpties($phpcsFile, ($asPtr + 1), ($doubleArrowPtr - 1)), '&'); + $valueAssignments = []; + if (isset(Collections::listTokens()[$tokens[$isListAssignment]['code']]) === false) { + // Single value assignment. + $valueAssignments[] = GetTokensAsString::noEmpties($phpcsFile, ($doubleArrowPtr + 1), ($closer - 1)); + } else { + // List assignment. + $assignments = Lists::getAssignments($phpcsFile, $isListAssignment); + foreach ($assignments as $listItem) { + if ($listItem['assignment'] === '') { + // Ignore empty list assignments. + continue; + } + + // Note: this doesn't take nested lists into account (yet). + $valueAssignments[] = $listItem['assignment']; + } } - $error = 'The variables used for the key and the value in a foreach assignment should be unique.'; - $error .= 'Both the key and the value will be currently assigned to: "%s"'; + if (empty($valueAssignments)) { + // No assignments found. + return; + } - $fix = $phpcsFile->addFixableError($error, $doubleArrowPtr, 'NotUnique', [$valueAsString]); - if ($fix === true) { - $phpcsFile->fixer->beginChangeset(); + foreach ($valueAssignments as $valueAsString) { + $valueAsString = \ltrim($valueAsString, '&'); - // Remove the key. - for ($i = ($asPtr + 1); $i < ($doubleArrowPtr + 1); $i++) { - if ($tokens[$i]['code'] === \T_WHITESPACE - && isset(Tokens::$commentTokens[$tokens[($i + 1)]['code']]) - ) { - // Don't remove whitespace when followed directly by a comment. - continue; - } + if ($keyAsString !== $valueAsString) { + // Key and value not the same. + continue; + } - if (isset(Tokens::$commentTokens[$tokens[$i]['code']])) { - // Don't remove comments. - continue; + $error = 'The variables used for the key and the value in a foreach assignment should be unique.'; + $error .= 'Both the key and the value will currently be assigned to: "%s"'; + + $fix = $phpcsFile->addFixableError($error, $doubleArrowPtr, 'NotUnique', [$valueAsString]); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + + // Remove the key. + for ($i = ($asPtr + 1); $i < ($doubleArrowPtr + 1); $i++) { + if ($tokens[$i]['code'] === \T_WHITESPACE + && isset(Tokens::$commentTokens[$tokens[($i + 1)]['code']]) + ) { + // Don't remove whitespace when followed directly by a comment. + continue; + } + + if (isset(Tokens::$commentTokens[$tokens[$i]['code']])) { + // Don't remove comments. + continue; + } + + // Remove everything else. + $phpcsFile->fixer->replaceToken($i, ''); } - // Remove everything else. - $phpcsFile->fixer->replaceToken($i, ''); + $phpcsFile->fixer->endChangeset(); } - $phpcsFile->fixer->endChangeset(); + break; } } } diff --git a/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.inc b/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.inc index fdd19e67..32bd0c43 100644 --- a/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.inc +++ b/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.inc @@ -22,15 +22,23 @@ foreach ($array as $key => list($a) ) {} foreach ($array as $k => $k ) {} foreach ($array as /*comment*/ $k [ 'key' ] => $k['key'] ) {} foreach ($array as $k[$name /*comment*/] => $k[/*comment*/ $name] ) {} -foreach ($array as [$a] => [$a] ) {} -foreach ($array as list($a) => list($a) ) {} +foreach ($array as $a => [$a] ) {} +foreach ($array as $key => list($a, $key, $b) ) {} foreach ( $data as $this->prop['key'] => $this->prop['key'] ) {} +foreach ( $array as $key['key'] => list( $a, $key['key'] ) ) {} // Also detect the same variable being used in combination with a reference assignment. foreach ($a as $k => &$k) {} +// Ensure non-unique variables used in foreach list assignments are disregarded. Not the concern of this sniff. +foreach ($data as $key => list(, ,)) {} // Invalid, empty list. Bow out. +foreach ($data as [$id => $id]) {} +foreach ($data as $key => list("id" => $id, "id" => $id)) {} +foreach ($data as $key => ["id" => $id, "id" => $id]) {} + /* * Parse errors, not our concern. */ foreach ($array) {} // Missing "as". foreach ($array as $k => $k +foreach ($array as $k => ) {} diff --git a/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.inc.fixed b/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.inc.fixed index 21387d7d..0b426dc7 100644 --- a/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.inc.fixed +++ b/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.inc.fixed @@ -23,14 +23,22 @@ foreach ($array as $k ) {} foreach ($array as /*comment*/ $k['key'] ) {} foreach ($array as /*comment*/ $k[/*comment*/ $name] ) {} foreach ($array as [$a] ) {} -foreach ($array as list($a) ) {} +foreach ($array as list($a, $key, $b) ) {} foreach ( $data as $this->prop['key'] ) {} +foreach ( $array as list( $a, $key['key'] ) ) {} // Also detect the same variable being used in combination with a reference assignment. foreach ($a as &$k) {} +// Ensure non-unique variables used in foreach list assignments are disregarded. Not the concern of this sniff. +foreach ($data as $key => list(, ,)) {} // Invalid, empty list. Bow out. +foreach ($data as [$id => $id]) {} +foreach ($data as $key => list("id" => $id, "id" => $id)) {} +foreach ($data as $key => ["id" => $id, "id" => $id]) {} + /* * Parse errors, not our concern. */ foreach ($array) {} // Missing "as". foreach ($array as $k => $k +foreach ($array as $k => ) {} diff --git a/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.php b/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.php index 470b247f..675acb7a 100644 --- a/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.php +++ b/Universal/Tests/CodeAnalysis/ForeachUniqueAssignmentUnitTest.php @@ -36,7 +36,8 @@ public function getErrorList() 25 => 1, 26 => 1, 27 => 1, - 30 => 1, + 28 => 1, + 31 => 1, ]; } From c9143c0b9522e67c1a50915e95ea7ce6f3c2cce2 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 4 Dec 2022 05:23:21 +0100 Subject: [PATCH 091/125] Universal/DisallowStandalonePostIncrementDecrement: allow for statement ending with close tag A stand-alone statement doesn't necessarily have to end with a semi-colon. It can also end on a PHP close tag. This commit adjusts the sniff to take this into account. Includes adjusting some pre-existing tests to safeguard the change. --- ...owStandalonePostIncrementDecrementSniff.php | 9 ++++++++- ...tandalonePostIncrementDecrementUnitTest.inc | 18 +++++++++--------- ...onePostIncrementDecrementUnitTest.inc.fixed | 18 +++++++++--------- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php b/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php index f54e0268..e74008b7 100644 --- a/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php +++ b/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php @@ -98,7 +98,14 @@ public function process(File $phpcsFile, $stackPtr) $start = BCFile::findStartOfStatement($phpcsFile, $stackPtr); $end = BCFile::findEndOfStatement($phpcsFile, $stackPtr); - if ($tokens[$end]['code'] !== \T_SEMICOLON) { + if (isset(Collections::incrementDecrementOperators()[$tokens[$end]['code']])) { + // Statement ends on a PHP close tag, set the end pointer to the close tag. + $end = $phpcsFile->findNext(Tokens::$emptyTokens, ($end + 1), null, true); + } + + if ($tokens[$end]['code'] !== \T_SEMICOLON + && $tokens[$end]['code'] !== \T_CLOSE_TAG + ) { // Not a stand-alone statement. return $end; } diff --git a/Universal/Tests/Operators/DisallowStandalonePostIncrementDecrementUnitTest.inc b/Universal/Tests/Operators/DisallowStandalonePostIncrementDecrementUnitTest.inc index a123dc22..8767ec84 100644 --- a/Universal/Tests/Operators/DisallowStandalonePostIncrementDecrementUnitTest.inc +++ b/Universal/Tests/Operators/DisallowStandalonePostIncrementDecrementUnitTest.inc @@ -5,16 +5,16 @@ */ $i = 10; --$i; --- $i; +-- $i ?> prop; +++$obj->prop ?> prop; /* * These should throw errors. */ $i--; -$i --; +$i -- ?> prop ++; + $obj->prop ++ ?> /*comment*/ prop --; $obj->prop[$value[2]]++; @@ -55,7 +55,7 @@ $var['key' . ($i + 10) . 'key']--; * Report on, but don't auto-fix, statements with multiple in/decrementers. */ ++$i--; -++ -- $i ++ ++; +++ -- $i ++ ++ ?> prop; +++$obj->prop ?> prop; /* * These should throw errors. */ --$i; ---$i ; +--$i ?> prop ; + ++$obj->prop ?> /*comment*/ prop ; ++$obj->prop[$value[2]]; @@ -55,7 +55,7 @@ if (true) { * Report on, but don't auto-fix, statements with multiple in/decrementers. */ ++$i--; -++ -- $i ++ ++; +++ -- $i ++ ++ ?> Date: Mon, 4 May 2020 02:42:22 +0200 Subject: [PATCH 092/125] Universal/DuplicateArrayKey: add support for detecting duplicate key PHP cross-version How array keys for array items without keys are being determined, has changed in PHP 8.0. Previously, the key would be the highest previously seen numeric key + 1, providing the highest previously seen numeric key was 0 or higher. Otherwise, it would be 0. As of PHP 8.0, the key will be the highest previously seen numeric key + 1, independently of whether the previously seen numeric key was negative. The sniff will now calculate and track the keys for unkeyed array items using both the PHP < 8.0 logic as well as the PHP >= 8.0 logic. If a duplicate key would yield the same error in both PHP < 8.0 as well as PHP >= 8.0, the `Found` error code will be used. If a duplicate key would only be a duplicate on PHP < 8.0, the `FoundForPHPlt80` error code will be used and the error message will indicate the error only applies to PHP < 8.0. If a duplicate key would only be a duplicate on PHP > 8.0, the `FoundForPHPgte80` and the error message will indicate the error only applies to PHP >= 8.0. If the end-user has set the `php_version` configuration option, the sniff will respect that and only report duplicate keys for the PHP version indicated. Includes additional unit tests. P.S.: the code could possible be made smarter, but for now, this is fine as the goal of accounting for this change in PHP has been achieved. Refs: * https://3v4l.org/liBXD * https://wiki.php.net/rfc/negative_array_index * https://github.com/squizlabs/PHP_CodeSniffer/wiki/Configuration-Options#setting-the-php-version --- .../Sniffs/Arrays/DuplicateArrayKeySniff.php | 210 +++++++++++++++--- .../Arrays/DuplicateArrayKeyUnitTest.inc | 14 ++ .../Arrays/DuplicateArrayKeyUnitTest.php | 7 + 3 files changed, 200 insertions(+), 31 deletions(-) diff --git a/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php b/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php index db95f325..c084a702 100644 --- a/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php +++ b/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php @@ -13,6 +13,7 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Util\Tokens; use PHPCSUtils\AbstractSniffs\AbstractArrayDeclarationSniff; +use PHPCSUtils\BackCompat\Helper; /** * Detect duplicate array keys in array declarations. @@ -20,29 +21,61 @@ * This sniff will detect duplicate keys with high precision, though any array key * set via a variable/constant/function call is excluded from the examination. * + * The sniff will handle the change in how numeric array keys are set + * since PHP 8.0 and will flag keys which would be duplicates cross-version. + * {@link https://wiki.php.net/rfc/negative_array_index} + * * @since 1.0.0 */ final class DuplicateArrayKeySniff extends AbstractArrayDeclarationSniff { /** - * Keep track of which array keys have been seen already. + * Keep track of which array keys have been seen already on PHP < 8.0. + * + * @since 1.0.0 + * + * @var array + */ + private $keysSeenLt8 = []; + + /** + * Keep track of which array keys have been seen already on PHP >= 8.0. * * @since 1.0.0 * * @var array */ - private $keysSeen = []; + private $keysSeenGt8 = []; + + /** + * Keep track of the maximum seen integer key to know what the next value will be for + * array items without a key on PHP < 8.0. + * + * @since 1.0.0 + * + * @var int + */ + private $currentMaxIntKeyLt8; /** * Keep track of the maximum seen integer key to know what the next value will be for - * array items without a key. + * array items without a key on PHP >= 8.0. + * + * @since 1.0.0 + * + * @var int + */ + private $currentMaxIntKeyGt8; + + /** + * The current PHP version. * * @since 1.0.0 * * @var int */ - private $currentMaxIntKey = -1; + private $phpVersion; /** * Process every part of the array declaration. @@ -60,8 +93,17 @@ final class DuplicateArrayKeySniff extends AbstractArrayDeclarationSniff public function processArray(File $phpcsFile) { // Reset properties before processing this array. - $this->keysSeen = []; - $this->currentMaxIntKey = -1; + $this->keysSeenLt8 = []; + $this->keysSeenGt8 = []; + + if (isset($this->phpVersion) === false) { + $phpVersion = Helper::getConfigData('php_version'); + if ($phpVersion !== null) { + $this->phpVersion = (int) $phpVersion; + } + } + + unset($this->currentMaxIntKeyLt8, $this->currentMaxIntKeyGt8); parent::processArray($phpcsFile); } @@ -92,41 +134,131 @@ public function processKey(File $phpcsFile, $startPtr, $endPtr, $itemNr) $integerKey = \is_int($key); + $errorMsg = 'Duplicate array key found. The value will be overwritten%s.' + . ' The %s array key "%s" was first seen on line %d'; + $errorCode = 'Found'; + $errors = []; + $baseData = [ + ($integerKey === true) ? 'integer' : 'string', + $key, + ]; + /* - * Check if we've seen it before. + * Check if we've seen the key before. + * + * If no PHP version was passed, throw errors both for PHP < 8.0 and PHP >= 8.0. + * If a PHP version was set, only throw the error appropriate for the selected PHP version. + * If both errors would effectively be the same, only throw one. */ - if (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, - $this->tokens[$firstNonEmptyFirstSeen]['line'], - ]; - - $phpcsFile->addError( - 'Duplicate array key found. The value will be overwritten.' - . ' The %s array key "%s" was first seen on line %d', - $firstNonEmpty, - 'Found', - $data - ); + if (isset($this->phpVersion) === false || $this->phpVersion < 80000) { + if (isset($this->keysSeenLt8[$key]) === true) { + $errorSuffix = ''; + $errorCodeSuffix = ''; + if ($integerKey === true) { + $errorSuffix = ' when using PHP < 8.0'; + $errorCodeSuffix = 'ForPHPlt80'; + } + + $firstSeen = $this->keysSeenLt8[$key]; + $firstNonEmptyFirstSeen = $phpcsFile->findNext(Tokens::$emptyTokens, $firstSeen['ptr'], null, true); + $dataLt8 = $baseData; + $dataLt8[] = $this->tokens[$firstNonEmptyFirstSeen]['line']; + + $errors['phplt8'] = [ + 'data_subset' => $dataLt8, + 'error_suffix' => $errorSuffix, + 'code_suffix' => $errorCodeSuffix, + ]; + } + } + + if (isset($this->phpVersion) === false || $this->phpVersion >= 80000) { + if (isset($this->keysSeenGt8[$key]) === true) { + $errorSuffix = ''; + $errorCodeSuffix = ''; + if ($integerKey === true) { + $errorSuffix = ' when using PHP >= 8.0'; + $errorCodeSuffix = 'ForPHPgte80'; + } + + $firstSeen = $this->keysSeenGt8[$key]; + $firstNonEmptyFirstSeen = $phpcsFile->findNext(Tokens::$emptyTokens, $firstSeen['ptr'], null, true); + $dataGt8 = $baseData; + $dataGt8[] = $this->tokens[$firstNonEmptyFirstSeen]['line']; + + $errors['phpgt8'] = [ + 'data_subset' => $dataGt8, + 'error_suffix' => $errorSuffix, + 'code_suffix' => $errorCodeSuffix, + ]; + } + } + + if ($errors !== []) { + $firstNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $startPtr, null, true); + + if (isset($errors['phplt8'], $errors['phpgt8'])) { + if ($errors['phplt8']['data_subset'] === $errors['phpgt8']['data_subset']) { + // Only throw the error once if it would be the same for PHP < 8.0 and PHP >= 8.0. + $data = $errors['phplt8']['data_subset']; + \array_unshift($data, ''); + + $phpcsFile->addError($errorMsg, $firstNonEmpty, $errorCode, $data); + } else { + // Throw both errors. + $codeLt8 = $errorCode . $errors['phplt8']['code_suffix']; + $dataLt8 = $errors['phplt8']['data_subset']; + \array_unshift($dataLt8, $errors['phplt8']['error_suffix']); + + $phpcsFile->addError($errorMsg, $firstNonEmpty, $codeLt8, $dataLt8); + + $codeGt8 = $errorCode . $errors['phpgt8']['code_suffix']; + $dataGt8 = $errors['phpgt8']['data_subset']; + \array_unshift($dataGt8, $errors['phpgt8']['error_suffix']); + + $phpcsFile->addError($errorMsg, $firstNonEmpty, $codeGt8, $dataGt8); + } + } elseif (isset($errors['phplt8'])) { + $errorCode .= $errors['phplt8']['code_suffix']; + $data = $errors['phplt8']['data_subset']; + \array_unshift($data, $errors['phplt8']['error_suffix']); + + $phpcsFile->addError($errorMsg, $firstNonEmpty, $errorCode, $data); + } elseif (isset($errors['phpgt8'])) { + $errorCode .= $errors['phpgt8']['code_suffix']; + $data = $errors['phpgt8']['data_subset']; + \array_unshift($data, $errors['phpgt8']['error_suffix']); + + $phpcsFile->addError($errorMsg, $firstNonEmpty, $errorCode, $data); + } return; } /* - * Key not seen before. Add to array. + * Key not seen before. Add to arrays. */ - $this->keysSeen[$key] = [ + $this->keysSeenLt8[$key] = [ + 'item' => $itemNr, + 'ptr' => $startPtr, + ]; + $this->keysSeenGt8[$key] = [ 'item' => $itemNr, 'ptr' => $startPtr, ]; - if ($integerKey === true && $key > $this->currentMaxIntKey) { - $this->currentMaxIntKey = $key; + if ($integerKey === true) { + if ((isset($this->currentMaxIntKeyLt8) === false && $key > -1) + || (isset($this->currentMaxIntKeyLt8) === true && $key > $this->currentMaxIntKeyLt8) + ) { + $this->currentMaxIntKeyLt8 = $key; + } + + if (isset($this->currentMaxIntKeyGt8) === false + || $key > $this->currentMaxIntKeyGt8 + ) { + $this->currentMaxIntKeyGt8 = $key; + } } } @@ -146,8 +278,24 @@ public function processKey(File $phpcsFile, $startPtr, $endPtr, $itemNr) */ public function processNoKey(File $phpcsFile, $startPtr, $itemNr) { - ++$this->currentMaxIntKey; - $this->keysSeen[$this->currentMaxIntKey] = [ + // Track the key for PHP < 8.0. + if (isset($this->currentMaxIntKeyLt8) === false) { + $this->currentMaxIntKeyLt8 = -1; + } + + ++$this->currentMaxIntKeyLt8; + $this->keysSeenLt8[$this->currentMaxIntKeyLt8] = [ + 'item' => $itemNr, + 'ptr' => $startPtr, + ]; + + // Track the key for PHP 8.0+. + if (isset($this->currentMaxIntKeyGt8) === false) { + $this->currentMaxIntKeyGt8 = -1; + } + + ++$this->currentMaxIntKeyGt8; + $this->keysSeenGt8[$this->currentMaxIntKeyGt8] = [ 'item' => $itemNr, 'ptr' => $startPtr, ]; diff --git a/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.inc b/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.inc index 4871b645..2101f897 100644 --- a/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.inc +++ b/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.inc @@ -151,3 +151,17 @@ $testKeepingTrackOfHighestIntKey = array( '9' . '6' => 'y', // Int 96 - Error. '1.' => 'z', // String '1.' ); + +$testPHPLt8VsPHP8 = array( + -10 => 'a', + 'b', // PHP < 8: int 0; PHP 8+: -9. + 'c', // PHP < 8: int 1; PHP 8+: -8. + -4 => 'd', + 'e', // PHP < 8: int 2; PHP 8+: -3. + -9 => 'f', // Duplicate in PHP 8, not in PHP < 8. + -8 => 'g', // Duplicate in PHP 8, not in PHP < 8. + -3 => 'h', // Duplicate in PHP 8, not in PHP < 8. + 0 => 'i', // Duplicate in PHP < 8, not in PHP 8. + 1 => 'j', // Duplicate in PHP < 8, not in PHP 8. + 2 => 'k', // Duplicate in PHP < 8, not in PHP 8. +); diff --git a/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.php b/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.php index d2c1f159..7a6b291d 100644 --- a/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.php +++ b/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.php @@ -11,6 +11,7 @@ namespace PHPCSExtra\Universal\Tests\Arrays; use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; +use PHPCSUtils\BackCompat\Helper; /** * Unit test class for the DuplicateArrayKey sniff. @@ -109,6 +110,12 @@ public function getErrorList() 147 => 1, 148 => 1, 151 => 1, + 161 => 1, + 162 => 1, + 163 => 1, + 164 => 1, + 165 => 1, + 166 => 1, ]; } From 012f9fe5124a6b4299f652f3f58b8a051072fb05 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 7 Dec 2022 00:32:30 +0100 Subject: [PATCH 093/125] Universal/DuplicateArrayKey: code simplification No functional changes. --- .../Sniffs/Arrays/DuplicateArrayKeySniff.php | 133 ++++++++---------- 1 file changed, 62 insertions(+), 71 deletions(-) diff --git a/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php b/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php index c084a702..92532e5c 100644 --- a/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php +++ b/Universal/Sniffs/Arrays/DuplicateArrayKeySniff.php @@ -145,91 +145,82 @@ public function processKey(File $phpcsFile, $startPtr, $endPtr, $itemNr) /* * Check if we've seen the key before. - * - * If no PHP version was passed, throw errors both for PHP < 8.0 and PHP >= 8.0. - * If a PHP version was set, only throw the error appropriate for the selected PHP version. - * If both errors would effectively be the same, only throw one. */ - if (isset($this->phpVersion) === false || $this->phpVersion < 80000) { - if (isset($this->keysSeenLt8[$key]) === true) { - $errorSuffix = ''; - $errorCodeSuffix = ''; - if ($integerKey === true) { - $errorSuffix = ' when using PHP < 8.0'; - $errorCodeSuffix = 'ForPHPlt80'; - } - - $firstSeen = $this->keysSeenLt8[$key]; - $firstNonEmptyFirstSeen = $phpcsFile->findNext(Tokens::$emptyTokens, $firstSeen['ptr'], null, true); - $dataLt8 = $baseData; - $dataLt8[] = $this->tokens[$firstNonEmptyFirstSeen]['line']; - - $errors['phplt8'] = [ - 'data_subset' => $dataLt8, - 'error_suffix' => $errorSuffix, - 'code_suffix' => $errorCodeSuffix, - ]; + if ((isset($this->phpVersion) === false || $this->phpVersion < 80000) + && isset($this->keysSeenLt8[$key]) === true + ) { + $errors['phplt8'] = [ + 'data_subset' => $baseData, + 'error_suffix' => '', + 'code_suffix' => '', + ]; + + if ($integerKey === true) { + $errors['phplt8']['error_suffix'] = ' when using PHP < 8.0'; + $errors['phplt8']['code_suffix'] = 'ForPHPlt80'; } + + $firstSeen = $this->keysSeenLt8[$key]; + $firstNonEmptyFirstSeen = $phpcsFile->findNext(Tokens::$emptyTokens, $firstSeen['ptr'], null, true); + + $errors['phplt8']['data_subset'][] = $this->tokens[$firstNonEmptyFirstSeen]['line']; } - if (isset($this->phpVersion) === false || $this->phpVersion >= 80000) { - if (isset($this->keysSeenGt8[$key]) === true) { - $errorSuffix = ''; - $errorCodeSuffix = ''; - if ($integerKey === true) { - $errorSuffix = ' when using PHP >= 8.0'; - $errorCodeSuffix = 'ForPHPgte80'; - } - - $firstSeen = $this->keysSeenGt8[$key]; - $firstNonEmptyFirstSeen = $phpcsFile->findNext(Tokens::$emptyTokens, $firstSeen['ptr'], null, true); - $dataGt8 = $baseData; - $dataGt8[] = $this->tokens[$firstNonEmptyFirstSeen]['line']; - - $errors['phpgt8'] = [ - 'data_subset' => $dataGt8, - 'error_suffix' => $errorSuffix, - 'code_suffix' => $errorCodeSuffix, - ]; + if ((isset($this->phpVersion) === false || $this->phpVersion >= 80000) + && isset($this->keysSeenGt8[$key]) === true + ) { + $errors['phpgt8'] = [ + 'data_subset' => $baseData, + 'error_suffix' => '', + 'code_suffix' => '', + ]; + + if ($integerKey === true) { + $errors['phpgt8']['error_suffix'] = ' when using PHP >= 8.0'; + $errors['phpgt8']['code_suffix'] = 'ForPHPgte80'; } + + $firstSeen = $this->keysSeenGt8[$key]; + $firstNonEmptyFirstSeen = $phpcsFile->findNext(Tokens::$emptyTokens, $firstSeen['ptr'], null, true); + + $errors['phpgt8']['data_subset'][] = $this->tokens[$firstNonEmptyFirstSeen]['line']; } + /* + * Throw the error(s). + * + * If no PHP version was passed, throw errors both for PHP < 8.0 and PHP >= 8.0. + * If a PHP version was set, only throw the error appropriate for the selected PHP version. + * If both errors would effectively be the same, only throw one. + */ if ($errors !== []) { $firstNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $startPtr, null, true); - if (isset($errors['phplt8'], $errors['phpgt8'])) { - if ($errors['phplt8']['data_subset'] === $errors['phpgt8']['data_subset']) { - // Only throw the error once if it would be the same for PHP < 8.0 and PHP >= 8.0. - $data = $errors['phplt8']['data_subset']; - \array_unshift($data, ''); - - $phpcsFile->addError($errorMsg, $firstNonEmpty, $errorCode, $data); - } else { - // Throw both errors. - $codeLt8 = $errorCode . $errors['phplt8']['code_suffix']; - $dataLt8 = $errors['phplt8']['data_subset']; - \array_unshift($dataLt8, $errors['phplt8']['error_suffix']); - - $phpcsFile->addError($errorMsg, $firstNonEmpty, $codeLt8, $dataLt8); - - $codeGt8 = $errorCode . $errors['phpgt8']['code_suffix']; - $dataGt8 = $errors['phpgt8']['data_subset']; - \array_unshift($dataGt8, $errors['phpgt8']['error_suffix']); - - $phpcsFile->addError($errorMsg, $firstNonEmpty, $codeGt8, $dataGt8); - } - } elseif (isset($errors['phplt8'])) { - $errorCode .= $errors['phplt8']['code_suffix']; - $data = $errors['phplt8']['data_subset']; - \array_unshift($data, $errors['phplt8']['error_suffix']); + if (isset($errors['phplt8'], $errors['phpgt8']) + && $errors['phplt8']['data_subset'] === $errors['phpgt8']['data_subset'] + ) { + // Only throw the error once if it would be the same for PHP < 8.0 and PHP >= 8.0. + $data = $errors['phplt8']['data_subset']; + \array_unshift($data, ''); $phpcsFile->addError($errorMsg, $firstNonEmpty, $errorCode, $data); - } elseif (isset($errors['phpgt8'])) { - $errorCode .= $errors['phpgt8']['code_suffix']; - $data = $errors['phpgt8']['data_subset']; + return; + } + + if (isset($errors['phplt8'])) { + $code = $errorCode . $errors['phplt8']['code_suffix']; + $data = $errors['phplt8']['data_subset']; + \array_unshift($data, $errors['phplt8']['error_suffix']); + + $phpcsFile->addError($errorMsg, $firstNonEmpty, $code, $data); + } + + if (isset($errors['phpgt8'])) { + $code = $errorCode . $errors['phpgt8']['code_suffix']; + $data = $errors['phpgt8']['data_subset']; \array_unshift($data, $errors['phpgt8']['error_suffix']); - $phpcsFile->addError($errorMsg, $firstNonEmpty, $errorCode, $data); + $phpcsFile->addError($errorMsg, $firstNonEmpty, $code, $data); } return; From d830ed3a8ab3498621c0a7004c65cd0d9ebb3fe2 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 27 Oct 2022 08:22:54 +0200 Subject: [PATCH 094/125] CS/QA: various minor code tweaks --- .../Sniffs/Arrays/CommaAfterLastSniff.php | 1 + .../Classes/DisallowAnonClassParenthesesSniff.php | 6 +++--- Universal/Sniffs/Classes/DisallowFinalClassSniff.php | 4 +--- .../Classes/RequireAnonClassParenthesesSniff.php | 4 +--- Universal/Sniffs/Classes/RequireFinalClassSniff.php | 4 +--- .../LowercaseClassResolutionKeywordSniff.php | 4 +--- .../ControlStructures/DisallowLonelyIfSniff.php | 6 ++---- .../DisallowDeclarationWithoutNameSniff.php | 1 - ...DisallowStandalonePostIncrementDecrementSniff.php | 12 +++++------- .../Sniffs/PHP/OneStatementInShortEchoTagSniff.php | 4 +--- Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.php | 1 - .../Tests/WhiteSpace/DisallowInlineTabsUnitTest.php | 2 +- 12 files changed, 17 insertions(+), 32 deletions(-) diff --git a/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php b/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php index 72b97b98..9697ce8a 100644 --- a/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php +++ b/NormalizedArrays/Sniffs/Arrays/CommaAfterLastSniff.php @@ -205,6 +205,7 @@ public function process(File $phpcsFile, $stackPtr) } $phpcsFile->fixer->beginChangeset(); + for ($i = $start; $i <= $end; $i++) { $phpcsFile->fixer->replaceToken($i, ''); } diff --git a/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php b/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php index f6767db1..f420d019 100644 --- a/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php +++ b/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php @@ -41,9 +41,7 @@ final class DisallowAnonClassParenthesesSniff implements Sniff */ public function register() { - return [ - \T_ANON_CLASS, - ]; + return [\T_ANON_CLASS]; } /** @@ -98,6 +96,7 @@ public function process(File $phpcsFile, $stackPtr) if ($fix === true) { $phpcsFile->fixer->beginChangeset(); + for ($i = $nextNonEmpty; $i <= $closer; $i++) { if (isset(Tokens::$commentTokens[$tokens[$i]['code']]) === true) { continue; @@ -105,6 +104,7 @@ public function process(File $phpcsFile, $stackPtr) $phpcsFile->fixer->replaceToken($i, ''); } + $phpcsFile->fixer->endChangeset(); } } diff --git a/Universal/Sniffs/Classes/DisallowFinalClassSniff.php b/Universal/Sniffs/Classes/DisallowFinalClassSniff.php index ea9fd002..8920958a 100644 --- a/Universal/Sniffs/Classes/DisallowFinalClassSniff.php +++ b/Universal/Sniffs/Classes/DisallowFinalClassSniff.php @@ -42,9 +42,7 @@ final class DisallowFinalClassSniff implements Sniff */ public function register() { - return [ - \T_CLASS, - ]; + return [\T_CLASS]; } /** diff --git a/Universal/Sniffs/Classes/RequireAnonClassParenthesesSniff.php b/Universal/Sniffs/Classes/RequireAnonClassParenthesesSniff.php index bee522fc..c35f9f99 100644 --- a/Universal/Sniffs/Classes/RequireAnonClassParenthesesSniff.php +++ b/Universal/Sniffs/Classes/RequireAnonClassParenthesesSniff.php @@ -40,9 +40,7 @@ final class RequireAnonClassParenthesesSniff implements Sniff */ public function register() { - return [ - \T_ANON_CLASS, - ]; + return [\T_ANON_CLASS]; } /** diff --git a/Universal/Sniffs/Classes/RequireFinalClassSniff.php b/Universal/Sniffs/Classes/RequireFinalClassSniff.php index ed3b3ab9..2c46c601 100644 --- a/Universal/Sniffs/Classes/RequireFinalClassSniff.php +++ b/Universal/Sniffs/Classes/RequireFinalClassSniff.php @@ -42,9 +42,7 @@ final class RequireFinalClassSniff implements Sniff */ public function register() { - return [ - \T_CLASS, - ]; + return [\T_CLASS]; } /** diff --git a/Universal/Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php b/Universal/Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php index f7431fdd..d461dd2e 100644 --- a/Universal/Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php +++ b/Universal/Sniffs/Constants/LowercaseClassResolutionKeywordSniff.php @@ -43,9 +43,7 @@ final class LowercaseClassResolutionKeywordSniff implements Sniff */ public function register() { - return [ - \T_STRING, - ]; + return [\T_STRING]; } /** diff --git a/Universal/Sniffs/ControlStructures/DisallowLonelyIfSniff.php b/Universal/Sniffs/ControlStructures/DisallowLonelyIfSniff.php index d4d6bb4f..61825425 100644 --- a/Universal/Sniffs/ControlStructures/DisallowLonelyIfSniff.php +++ b/Universal/Sniffs/ControlStructures/DisallowLonelyIfSniff.php @@ -39,9 +39,7 @@ final class DisallowLonelyIfSniff implements Sniff */ public function register() { - return [ - \T_ELSE, - ]; + return [\T_ELSE]; } /** @@ -274,7 +272,7 @@ public function process(File $phpcsFile, $stackPtr) } // Remove the inner scope closer. - $phpcsFile->fixer->replaceToken($innerScopeCloser, ''); + $phpcsFile->fixer->replaceToken($innerScopeCloser, ''); $i = ($innerScopeCloser - 1); // Handle alternative syntax for the closer. diff --git a/Universal/Sniffs/Namespaces/DisallowDeclarationWithoutNameSniff.php b/Universal/Sniffs/Namespaces/DisallowDeclarationWithoutNameSniff.php index f19601de..b3200278 100644 --- a/Universal/Sniffs/Namespaces/DisallowDeclarationWithoutNameSniff.php +++ b/Universal/Sniffs/Namespaces/DisallowDeclarationWithoutNameSniff.php @@ -65,7 +65,6 @@ public function process(File $phpcsFile, $stackPtr) if ($name !== '') { // Named namespace. $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes'); - return; } diff --git a/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php b/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php index e74008b7..2426037a 100644 --- a/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php +++ b/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php @@ -184,15 +184,13 @@ public function process(File $phpcsFile, $stackPtr) $fix = $phpcsFile->addFixableError($error, $stackPtr, $errorCode, $data); - if ($fix === false) { - return $end; + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($stackPtr, ''); + $phpcsFile->fixer->addContentBefore($start, $tokens[$stackPtr]['content']); + $phpcsFile->fixer->endChangeset(); } - $phpcsFile->fixer->beginChangeset(); - $phpcsFile->fixer->replaceToken($stackPtr, ''); - $phpcsFile->fixer->addContentBefore($start, $tokens[$stackPtr]['content']); - $phpcsFile->fixer->endChangeset(); - return $end; } } diff --git a/Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php b/Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php index 169a1e65..a8fc725e 100644 --- a/Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php +++ b/Universal/Sniffs/PHP/OneStatementInShortEchoTagSniff.php @@ -36,9 +36,7 @@ final class OneStatementInShortEchoTagSniff implements Sniff */ public function register() { - return [ - \T_OPEN_TAG_WITH_ECHO, - ]; + return [\T_OPEN_TAG_WITH_ECHO]; } /** diff --git a/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.php b/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.php index 7a6b291d..a623a210 100644 --- a/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.php +++ b/Universal/Tests/Arrays/DuplicateArrayKeyUnitTest.php @@ -11,7 +11,6 @@ namespace PHPCSExtra\Universal\Tests\Arrays; use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; -use PHPCSUtils\BackCompat\Helper; /** * Unit test class for the DuplicateArrayKey sniff. diff --git a/Universal/Tests/WhiteSpace/DisallowInlineTabsUnitTest.php b/Universal/Tests/WhiteSpace/DisallowInlineTabsUnitTest.php index 410eaedd..3079a3ce 100644 --- a/Universal/Tests/WhiteSpace/DisallowInlineTabsUnitTest.php +++ b/Universal/Tests/WhiteSpace/DisallowInlineTabsUnitTest.php @@ -37,7 +37,7 @@ protected function shouldSkipTest() * next to impossible. * But having to continuously restart builds is getting silly. */ - return (PHP_MAJOR_VERSION === 5 && PHP_MINOR_VERSION === 5); + return (\PHP_MAJOR_VERSION === 5 && \PHP_MINOR_VERSION === 5); } /** From 4bd1a76bd57c8be7c138986e76f03bfbaccb9b8b Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 31 Oct 2022 08:59:23 +0100 Subject: [PATCH 095/125] Docs: normalize `@since` tags --- .../Classes/ModifierKeywordOrderSniff.php | 16 ++++++++-------- .../Constants/ModifierKeywordOrderSniff.php | 18 +++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Universal/Sniffs/Classes/ModifierKeywordOrderSniff.php b/Universal/Sniffs/Classes/ModifierKeywordOrderSniff.php index 234515d8..f0d1f21c 100644 --- a/Universal/Sniffs/Classes/ModifierKeywordOrderSniff.php +++ b/Universal/Sniffs/Classes/ModifierKeywordOrderSniff.php @@ -18,7 +18,7 @@ /** * Standardize the modifier keyword order for class declarations. * - * @since 1.0.0-alpha4 + * @since 1.0.0 */ final class ModifierKeywordOrderSniff implements Sniff { @@ -26,7 +26,7 @@ final class ModifierKeywordOrderSniff implements Sniff /** * Name of the metric. * - * @since 1.0.0-alpha4 + * @since 1.0.0 * * @var string */ @@ -35,7 +35,7 @@ final class ModifierKeywordOrderSniff implements Sniff /** * Order preference: abstract/final readonly. * - * @since 1.0.0-alpha4 + * @since 1.0.0 * * @var string */ @@ -44,7 +44,7 @@ final class ModifierKeywordOrderSniff implements Sniff /** * Order preference: readonly abstract/final. * - * @since 1.0.0-alpha4 + * @since 1.0.0 * * @var string */ @@ -59,7 +59,7 @@ final class ModifierKeywordOrderSniff implements Sniff * * Defaults to "extendability readonly". * - * @since 1.0.0-alpha4 + * @since 1.0.0 * * @var string */ @@ -68,7 +68,7 @@ final class ModifierKeywordOrderSniff implements Sniff /** * Returns an array of tokens this test wants to listen for. * - * @since 1.0.0-alpha4 + * @since 1.0.0 * * @return array */ @@ -80,7 +80,7 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @since 1.0.0-alpha4 + * @since 1.0.0 * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token @@ -178,7 +178,7 @@ public function process(File $phpcsFile, $stackPtr) /** * Throw the error and potentially fix it. * - * @since 1.0.0-alpha4 + * @since 1.0.0 * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $firstKeyword The position of the first keyword found. diff --git a/Universal/Sniffs/Constants/ModifierKeywordOrderSniff.php b/Universal/Sniffs/Constants/ModifierKeywordOrderSniff.php index 6114e920..8299dc0d 100644 --- a/Universal/Sniffs/Constants/ModifierKeywordOrderSniff.php +++ b/Universal/Sniffs/Constants/ModifierKeywordOrderSniff.php @@ -18,7 +18,7 @@ /** * Standardize the modifier keyword order for OO constant declarations. * - * @since 1.0.0-alpha4 + * @since 1.0.0 */ final class ModifierKeywordOrderSniff implements Sniff { @@ -26,7 +26,7 @@ final class ModifierKeywordOrderSniff implements Sniff /** * Name of the metric. * - * @since 1.0.0-alpha4 + * @since 1.0.0 * * @var string */ @@ -35,7 +35,7 @@ final class ModifierKeywordOrderSniff implements Sniff /** * Order preference: final visibility. * - * @since 1.0.0-alpha4 + * @since 1.0.0 * * @var string */ @@ -44,7 +44,7 @@ final class ModifierKeywordOrderSniff implements Sniff /** * Order preference: visibility final. * - * @since 1.0.0-alpha4 + * @since 1.0.0 * * @var string */ @@ -59,7 +59,7 @@ final class ModifierKeywordOrderSniff implements Sniff * * Defaults to "final visibility". * - * @since 1.0.0-alpha4 + * @since 1.0.0 * * @var string */ @@ -68,7 +68,7 @@ final class ModifierKeywordOrderSniff implements Sniff /** * Tokens which can be constant modifier keywords. * - * @since 1.0.0-alpha4 + * @since 1.0.0 * * @var array */ @@ -79,7 +79,7 @@ final class ModifierKeywordOrderSniff implements Sniff /** * Returns an array of tokens this test wants to listen for. * - * @since 1.0.0-alpha4 + * @since 1.0.0 * * @return array */ @@ -94,7 +94,7 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @since 1.0.0-alpha4 + * @since 1.0.0 * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $stackPtr The position of the current token @@ -175,7 +175,7 @@ public function process(File $phpcsFile, $stackPtr) /** * Throw the error and potentially fix it. * - * @since 1.0.0-alpha4 + * @since 1.0.0 * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $firstKeyword The position of the first keyword found. From c44b775be12ec5ffd4d157d89a30e9f483ac494e Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 2 Nov 2022 20:37:45 +0100 Subject: [PATCH 096/125] DisallowAnonClassParentheses: minor code readability improvement --- .../Sniffs/Classes/DisallowAnonClassParenthesesSniff.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php b/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php index f420d019..7a3c01cf 100644 --- a/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php +++ b/Universal/Sniffs/Classes/DisallowAnonClassParenthesesSniff.php @@ -78,8 +78,9 @@ public function process(File $phpcsFile, $stackPtr) // @codeCoverageIgnoreEnd } - $closer = $tokens[$nextNonEmpty]['parenthesis_closer']; - $hasParams = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextNonEmpty + 1), $closer, true); + $opener = $nextNonEmpty; + $closer = $tokens[$opener]['parenthesis_closer']; + $hasParams = $phpcsFile->findNext(Tokens::$emptyTokens, ($opener + 1), $closer, true); if ($hasParams !== false) { // There is something between the parentheses. Ignore. $phpcsFile->recordMetric($stackPtr, self::METRIC_NAME, 'yes, with parameter(s)'); @@ -97,7 +98,7 @@ public function process(File $phpcsFile, $stackPtr) if ($fix === true) { $phpcsFile->fixer->beginChangeset(); - for ($i = $nextNonEmpty; $i <= $closer; $i++) { + for ($i = $opener; $i <= $closer; $i++) { if (isset(Tokens::$commentTokens[$tokens[$i]['code']]) === true) { continue; } From 7d2f69242dc85038e9c40580cb927a061c656706 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 2 Nov 2022 22:39:59 +0100 Subject: [PATCH 097/125] Universal/PrecisionAlignment: update comment ... now upstream PR 3639 has been merged. Ref: * squizlabs/PHP_CodeSniffer 3639 --- Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php b/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php index 39c7464c..8e7bf7fa 100644 --- a/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php +++ b/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php @@ -304,7 +304,7 @@ public function process(File $phpcsFile, $stackPtr) case \T_END_NOWDOC: /* * PHPCS does not execute tab replacement in heredoc/nowdoc closer - * tokens (last checked: PHPCS 3.7.1), so handle this ourselves. + * tokens prior to PHPCS 3.7.2, so handle this ourselves. */ $content = $tokens[$i]['content']; if (\strpos($tokens[$i]['content'], "\t") !== false) { From 082a661e318f3bf0c47ace06fb77a8fd57c70f35 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 1 Dec 2022 04:33:21 +0100 Subject: [PATCH 098/125] Docs: improve a few inline comments --- Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php | 2 +- .../Operators/DisallowStandalonePostIncrementDecrementSniff.php | 2 +- Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php b/Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php index 3c74e437..01984c2c 100644 --- a/Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php +++ b/Universal/Sniffs/ControlStructures/IfElseDeclarationSniff.php @@ -94,7 +94,7 @@ public function process(File $phpcsFile, $stackPtr) */ $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); if ($prevNonEmpty === false || $tokens[$prevNonEmpty]['code'] !== \T_CLOSE_CURLY_BRACKET) { - // Parse error. Not our concern. + // Parse error or mixing braced and non-braced. Not our concern. return; } diff --git a/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php b/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php index 2426037a..905dd1ec 100644 --- a/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php +++ b/Universal/Sniffs/Operators/DisallowStandalonePostIncrementDecrementSniff.php @@ -67,7 +67,7 @@ public function register() $this->allowedTokens += Collections::namespacedNameTokens(); /* - * Remove potential nullsafe object operator. In/decrement not allowed in write context, + * Remove nullsafe object operator. In/decrement not allowed in write context, * so ignore. */ unset($this->allowedTokens[\T_NULLSAFE_OBJECT_OPERATOR]); diff --git a/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php b/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php index 8e7bf7fa..c4f970f9 100644 --- a/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php +++ b/Universal/Sniffs/WhiteSpace/PrecisionAlignmentSniff.php @@ -28,7 +28,7 @@ * - Precision alignment *within* text strings (multi-line text strings, heredocs, nowdocs) * will NOT be flagged by this sniff. * - The fixer works based on "best guess" and may not always result in the desired indentation. - * - This fixer will use tabs or spaces based on whether tabs where present in the original indent. + * - This fixer will use tabs or spaces based on whether tabs were present in the original indent. * Use the PHPCS native `Generic.WhiteSpace.DisallowTabIndent` or the * `Generic.WhiteSpace.DisallowSpaceIndent` sniff to clean up the results if so desired. * From f6595e4b0dc6524b57b16a637a6438e12aeaf8f2 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 7 Dec 2022 07:27:00 +0100 Subject: [PATCH 099/125] Universal/StaticInFinalClass: handle `static` when used in arrow function ... as long as the arrow function is within an OO construct which can be `final`. Includes tests. Includes enhancing the closure test a little as well. --- .../CodeAnalysis/StaticInFinalClassSniff.php | 23 ++++++++---- .../StaticInFinalClassUnitTest.inc | 12 ++++++- .../StaticInFinalClassUnitTest.inc.fixed | 12 ++++++- .../StaticInFinalClassUnitTest.php | 35 ++++++++++--------- 4 files changed, 57 insertions(+), 25 deletions(-) diff --git a/Universal/Sniffs/CodeAnalysis/StaticInFinalClassSniff.php b/Universal/Sniffs/CodeAnalysis/StaticInFinalClassSniff.php index b46fff5c..1e1a3857 100644 --- a/Universal/Sniffs/CodeAnalysis/StaticInFinalClassSniff.php +++ b/Universal/Sniffs/CodeAnalysis/StaticInFinalClassSniff.php @@ -48,8 +48,9 @@ final class StaticInFinalClassSniff implements Sniff public function register() { return [ - // This token is used to retrieve return types reliably. + // These tokens are used to retrieve return types reliably. \T_FUNCTION, + \T_FN, // While this is our "real" target. \T_STATIC, // But we also need this as after "instanceof", `static` is tokenized as `T_STRING in PHPCS < 4.0.0. @@ -80,7 +81,9 @@ public function process(File $phpcsFile, $stackPtr) return; } - if ($tokens[$stackPtr]['code'] === \T_FUNCTION) { + if ($tokens[$stackPtr]['code'] === \T_FUNCTION + || $tokens[$stackPtr]['code'] === \T_FN + ) { /* * Check return types for methods in final classes, anon classes and enums. * @@ -91,10 +94,18 @@ public function process(File $phpcsFile, $stackPtr) $scopeOpener = $tokens[$stackPtr]['scope_opener']; } - $ooPtr = Scopes::validDirectScope($phpcsFile, $stackPtr, $this->validOOScopes); - if ($ooPtr === false) { - // Method in a trait (not known where it is used), interface (never final) or not in an OO scope. - return $scopeOpener; + if ($tokens[$stackPtr]['code'] === \T_FUNCTION) { + $ooPtr = Scopes::validDirectScope($phpcsFile, $stackPtr, $this->validOOScopes); + if ($ooPtr === false) { + // Method in a trait (not known where it is used), interface (never final) or not in an OO scope. + return $scopeOpener; + } + } else { + $ooPtr = Conditions::getLastCondition($phpcsFile, $stackPtr, $this->validOOScopes); + if ($ooPtr === false) { + // Arrow function outside of OO. + return $scopeOpener; + } } if ($tokens[$ooPtr]['code'] === \T_CLASS) { diff --git a/Universal/Tests/CodeAnalysis/StaticInFinalClassUnitTest.inc b/Universal/Tests/CodeAnalysis/StaticInFinalClassUnitTest.inc index 2dbef3e6..9c8215e5 100644 --- a/Universal/Tests/CodeAnalysis/StaticInFinalClassUnitTest.inc +++ b/Universal/Tests/CodeAnalysis/StaticInFinalClassUnitTest.inc @@ -29,7 +29,7 @@ trait UseContextUnknown { final class ClosuresAreNotTargettedByThisSniff { public static $className = 'Bar'; public function foobar() { - $closure = function() { + $closure = static function(): static { // Returns new instance of Closure, not of ClosuresAreNotTargettedByThisSniff. // Still unnecessary, but not the target of this sniff. // Should possibly get its own sniff. @@ -63,6 +63,10 @@ function foobar() { static::$outsideClassContext; +function arrowNotInOO() { + $arrow = static fn(): static => new static(); +} + /* * OK, not used in a final class. @@ -162,6 +166,12 @@ enum FinalByDesign { } } +final class ArrowFunctionsAreOpen { + public function foobar() { + $arrow = static fn(): static => new static(); + } +} + // Live coding. This has to be the last test in the file. class LiveCoding extends Foo { diff --git a/Universal/Tests/CodeAnalysis/StaticInFinalClassUnitTest.inc.fixed b/Universal/Tests/CodeAnalysis/StaticInFinalClassUnitTest.inc.fixed index bb9e9c67..dd279c33 100644 --- a/Universal/Tests/CodeAnalysis/StaticInFinalClassUnitTest.inc.fixed +++ b/Universal/Tests/CodeAnalysis/StaticInFinalClassUnitTest.inc.fixed @@ -29,7 +29,7 @@ trait UseContextUnknown { final class ClosuresAreNotTargettedByThisSniff { public static $className = 'Bar'; public function foobar() { - $closure = function() { + $closure = static function(): static { // Returns new instance of Closure, not of ClosuresAreNotTargettedByThisSniff. // Still unnecessary, but not the target of this sniff. // Should possibly get its own sniff. @@ -63,6 +63,10 @@ function foobar() { static::$outsideClassContext; +function arrowNotInOO() { + $arrow = static fn(): static => new static(); +} + /* * OK, not used in a final class. @@ -162,6 +166,12 @@ enum FinalByDesign { } } +final class ArrowFunctionsAreOpen { + public function foobar() { + $arrow = static fn(): self => new self(); + } +} + // Live coding. This has to be the last test in the file. class LiveCoding extends Foo { diff --git a/Universal/Tests/CodeAnalysis/StaticInFinalClassUnitTest.php b/Universal/Tests/CodeAnalysis/StaticInFinalClassUnitTest.php index f6a8cf87..e4564df0 100644 --- a/Universal/Tests/CodeAnalysis/StaticInFinalClassUnitTest.php +++ b/Universal/Tests/CodeAnalysis/StaticInFinalClassUnitTest.php @@ -30,26 +30,27 @@ final class StaticInFinalClassUnitTest extends AbstractSniffUnitTest public function getErrorList() { return [ - 105 => 1, - 107 => 1, - 108 => 1, - 110 => 1, + 109 => 1, + 111 => 1, 112 => 1, - 119 => 1, - 121 => 1, - 122 => 1, - 127 => 1, - 129 => 1, - 138 => 1, - 140 => 1, - 141 => 1, - 146 => 1, - 148 => 1, - 155 => 1, - 156 => 1, - 157 => 1, + 114 => 1, + 116 => 1, + 123 => 1, + 125 => 1, + 126 => 1, + 131 => 1, + 133 => 1, + 142 => 1, + 144 => 1, + 145 => 1, + 150 => 1, + 152 => 1, 159 => 1, + 160 => 1, 161 => 1, + 163 => 1, + 165 => 1, + 171 => 2, ]; } From 45f09c978641f0ab2c2271f9ddc66b883c1f8da9 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 7 Dec 2022 07:40:45 +0100 Subject: [PATCH 100/125] Composer: add PHPCSDevCS to the dependencies PHPCSDevCS was previously not added to the `require-dev` dependencies as the minimum supported PHPCS version conflicted with the minimum supported PHPCS version of this package. Now this is no longer the case, the package can be safely added to `require-dev` and work-arounds can be removed. --- .github/workflows/basics.yml | 3 --- composer.json | 15 +++------------ 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml index 119ae2f6..4c2cf2b9 100644 --- a/.github/workflows/basics.yml +++ b/.github/workflows/basics.yml @@ -43,9 +43,6 @@ jobs: composer remove --no-update --dev phpunit/phpunit --no-scripts --no-interaction # Using PHPCS `master` as an early detection system for bugs upstream. composer require --no-update squizlabs/php_codesniffer:"dev-master" --no-interaction - # Add PHPCSDevCS - this is the CS ruleset we use. - # This is not in the composer.json as it has different minimum PHPCS reqs and would conflict. - composer require --no-update --dev phpcsstandards/phpcsdevcs:"^1.1.3" --no-interaction # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-composer-dependencies diff --git a/composer.json b/composer.json index e114b8db..8d5d0f56 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,7 @@ "require-dev" : { "php-parallel-lint/php-parallel-lint": "^1.3.2", "php-parallel-lint/php-console-highlighter": "^1.0", + "phpcsstandards/phpcsdevcs": "^1.1.5", "phpcsstandards/phpcsdevtools": "^1.2.0", "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0" }, @@ -43,21 +44,11 @@ "lint": [ "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . --show-deprecated -e php --exclude vendor --exclude .git" ], - "install-devcs": [ - "composer require --dev phpcsstandards/phpcsdevcs:\"^1.1.3\" --no-suggest --no-interaction" - ], - "remove-devcs": [ - "composer remove --dev phpcsstandards/phpcsdevcs --no-interaction" - ], "checkcs": [ - "@install-devcs", - "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs", - "@remove-devcs" + "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs" ], "fixcs": [ - "@install-devcs", - "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf", - "@remove-devcs" + "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf" ], "check-complete": [ "@php ./vendor/phpcsstandards/phpcsdevtools/bin/phpcs-check-feature-completeness ./Modernize ./NormalizedArrays ./Universal" From c1081b361885291c0e9368c7770008e6f3c8c664 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 7 Dec 2022 07:38:30 +0100 Subject: [PATCH 101/125] GH Actions/test: up the minimum required version of the coverall tooling Ref: https://github.com/php-coveralls/php-coveralls/releases --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 39da1a50..e4e20f15 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -198,7 +198,7 @@ jobs: - name: Install Coveralls if: ${{ success() }} - run: composer require php-coveralls/php-coveralls:"^2.4.2" --no-interaction + run: composer require php-coveralls/php-coveralls:"^2.5.3" --no-interaction - name: Upload coverage results to Coveralls if: ${{ success() }} From bb3597aabcc649cfca566761e03db37f2bf3d7c2 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 7 Dec 2022 07:46:01 +0100 Subject: [PATCH 102/125] GH Actions/test: remove unused steps related to PHPCS 4.x These steps/directives all relate to testing against PHPCS 4.x, but this package is not being tested against PHPCS 4.x at this time and when this will be enabled again, these work-arounds may well no longer be needed anymore anyway, so let's remove it for now and re-evaluate what is needed when testing against PHPCS 4.x is re-enabled. --- .github/workflows/test.yml | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e4e20f15..0dac504a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,10 +50,6 @@ jobs: phpcs_version: 'dev-master' experimental: true - #- php: '7.4' - # phpcs_version: '4.0.x-dev' - # experimental: true - name: "Test${{ matrix.phpcs_version == 'dev-master' && ' + Lint' || '' }}: PHP ${{ matrix.php }} - PHPCS ${{ matrix.phpcs_version }}" continue-on-error: ${{ matrix.experimental }} @@ -67,7 +63,7 @@ jobs: - name: Setup ini config id: set_ini run: | - if [[ "${{ matrix.phpcs_version }}" != "dev-master" && "${{ matrix.phpcs_version }}" != "4.0.x-dev" ]]; then + if [[ "${{ matrix.phpcs_version }}" != "dev-master" ]]; then echo 'PHP_INI=error_reporting=E_ALL & ~E_DEPRECATED, display_errors=On' >> $GITHUB_OUTPUT else echo 'PHP_INI=error_reporting=-1, display_errors=On' >> $GITHUB_OUTPUT @@ -79,23 +75,10 @@ jobs: php-version: ${{ matrix.php }} ini-values: ${{ steps.set_ini.outputs.PHP_INI }} coverage: none - env: - # Token is needed for the PHPCS 4.x run against source. - COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: 'Composer: set PHPCS version for tests' run: composer require --no-update squizlabs/php_codesniffer:"${{ matrix.phpcs_version }}" --no-interaction - - name: 'Composer: conditionally prefer source for PHPCS' - if: ${{ startsWith( matrix.phpcs_version, '4' ) }} - # --prefer-source ensures that the PHPCS native unit test framework will be available in PHPCS 4.x. - run: composer config preferred-install.squizlabs/php_codesniffer source - - - name: 'Composer: conditionally remove PHPCSDevtools' - if: ${{ startsWith( matrix.phpcs_version, '4' ) }} - # Remove devtools as it will not (yet) install on PHPCS 4.x. - run: composer remove --no-update --dev phpcsstandards/phpcsdevtools --no-interaction - # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-composer-dependencies - name: Install Composer dependencies - normal From 6d828cb2e8049e7a7ea1c294c811472ffe5004c7 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 7 Dec 2022 07:49:24 +0100 Subject: [PATCH 103/125] GH Actions: no longer allow builds to fail against PHP 8.2 * Update the PHP version on which the CS run for this package is run to PHP `latest`. * Remove the `continue-on-error` for PHP 8.2 in the `test` workflow (and remove the `experimental` key, which is now no longer used. * Update the "Tested against" badge in the README. --- .github/workflows/basics.yml | 2 +- .github/workflows/test.yml | 21 ++------------------- README.md | 2 +- 3 files changed, 4 insertions(+), 21 deletions(-) diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml index 4c2cf2b9..9029b26a 100644 --- a/.github/workflows/basics.yml +++ b/.github/workflows/basics.yml @@ -33,7 +33,7 @@ jobs: - name: Install PHP uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: 'latest' coverage: none tools: cs2pr diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0dac504a..bc82d4aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,35 +25,18 @@ jobs: runs-on: ubuntu-latest strategy: - # Keys: - # - experimental: Whether the build is "allowed to fail". matrix: # The GHA matrix works different from Travis. # You can define jobs here and then augment them with extra variables in `include`, # as well as add extra jobs in `include`. # @link https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix # - # IMPORTANT: test runs shouldn't fail because of PHPCS being incompatible with a PHP version. - # - PHPCS will run without errors on PHP 5.4 - 7.2 on any supported version. - # - PHP 7.4 needs PHPCS 3.5.0+ to run without errors. - # - PHP 8.0 needs PHPCS 3.5.7+ to run without errors. - # - PHP 8.1 needs PHPCS 3.6.1+ to run without errors. - # # The matrix is set up so as not to duplicate the builds which are run for code coverage. - php: ['5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '8.0', '8.1'] + php: ['5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '8.0', '8.1', '8.2'] phpcs_version: ['3.7.1', 'dev-master'] - experimental: [false] - - include: - # Experimental builds. These are allowed to fail. - - php: '8.2' - phpcs_version: 'dev-master' - experimental: true name: "Test${{ matrix.phpcs_version == 'dev-master' && ' + Lint' || '' }}: PHP ${{ matrix.php }} - PHPCS ${{ matrix.phpcs_version }}" - continue-on-error: ${{ matrix.experimental }} - steps: - name: Checkout code uses: actions/checkout@v3 @@ -122,7 +105,7 @@ jobs: strategy: matrix: - # 7.4 should be updated to 8.0 when higher PHPUnit versions can be supported. + # 7.4 should be updated to 8.2 when higher PHPUnit versions can be supported. php: ['5.4', '7.4'] phpcs_version: ['3.7.1', 'dev-master'] diff --git a/README.md b/README.md index fe99586c..0471fdf6 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ PHPCSExtra [![Minimum PHP Version](https://img.shields.io/packagist/php-v/phpcsstandards/phpcsextra.svg?maxAge=3600)](https://packagist.org/packages/phpcsstandards/phpcsextra) [![CS Build Status](https://github.com/PHPCSStandards/PHPCSExtra/workflows/CS/badge.svg?branch=develop)](https://github.com/PHPCSStandards/PHPCSExtra/actions?query=workflow%3ACS) [![Test Build Status](https://github.com/PHPCSStandards/PHPCSExtra/workflows/Test/badge.svg?branch=develop)](https://github.com/PHPCSStandards/PHPCSExtra/actions?query=workflow%3ATest) -[![Tested on PHP 5.4 to 8.0](https://img.shields.io/badge/tested%20on-PHP%205.4%20|%205.5%20|%205.6%20|%207.0%20|%207.1%20|%207.2%20|%207.3%20|%207.4%20|%208.0-brightgreen.svg?maxAge=2419200)](https://github.com/PHPCSStandards/PHPCSExtra/actions?query=workflow%3ATest) +[![Tested on PHP 5.4 to 8.2](https://img.shields.io/badge/tested%20on-PHP%205.4%20|%205.5%20|%205.6%20|%207.0%20|%207.1%20|%207.2%20|%207.3%20|%207.4%20|%208.0%20|%208.1%20|%208.2-brightgreen.svg?maxAge=2419200)](https://github.com/PHPCSStandards/PHPCSExtra/actions?query=workflow%3ATest) [![Coverage Status](https://coveralls.io/repos/github/PHPCSStandards/PHPCSExtra/badge.svg)](https://coveralls.io/github/PHPCSStandards/PHPCSExtra) [![License: LGPLv3](https://poser.pugx.org/phpcsstandards/phpcsextra/license)](https://github.com/PHPCSStandards/PHPCSExtra/blob/stable/LICENSE) From 2584ccfdeb5f330f9623858080f3581d94198408 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 13 Oct 2022 03:47:32 +0200 Subject: [PATCH 104/125] Changelog and readme updates for release `1.0.0-RC1` --- CHANGELOG.md | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++- README.md | 160 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 329 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b7fc3a8..8e97109d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Change Log for the PHPCSExtra standard for PHP Codesniffer +# Change Log for the PHPCSExtra standard for PHP CodeSniffer All notable changes to this project will be documented in this file. @@ -14,6 +14,173 @@ This projects adheres to [Keep a CHANGELOG](http://keepachangelog.com/) and uses _Nothing yet._ + +## [1.0.0-RC1] - 2022-12-07 + +:warning: Important: this package now requires [PHPCSUtils 1.0.0-alpha4]. Please make sure you use `--with-[all-]dependencies` when running `composer update`. :exclamation: + +### Added + +#### Modernize + +* This is a new standard with one sniff to start with. +* :wrench: :books: New `Modernize.FunctionCalls.Dirname` sniff to detect and auto-fix two typical code modernizations which can be made related to the [`dirname()`][php-manual-dirname] function. [#172] + +#### Universal + +* :wrench: :bar_chart: :books: New `Universal.Classes.DisallowAnonClassParentheses` sniff to disallow the use of parentheses when declaring an anonymous class without passing parameters. [#76], [#162] +* :wrench: :bar_chart: :books: New `Universal.Classes.RequireAnonClassParentheses` sniff to require the use of parentheses when declaring an anonymous class, whether parameters are passed or not. [#76], [#166] +* :wrench: :bar_chart: :books: New `Universal.Classes.DisallowFinalClass` sniff to disallow classes being declared `final`. [#108], [#114], [#148], [#163] +* :wrench: :bar_chart: :books: New `Universal.Classes.RequireFinalClass` sniff to require all non-`abstract` classes to be declared `final`. [#109], [#148], [#164] + Warning: the auto-fixer for this sniff _may_ have unintended side-effects for applications and should be used with care! This is considered a _risky_ fixer. +* :wrench: :bar_chart: :books: New `Universal.Classes.ModifierKeywordOrder` sniff to standardize the modifier keyword order for class declarations. [#142] + The sniff offers an `order` property to specify the preferred order. +* :wrench: :books: New `Universal.CodeAnalysis.ConstructorDestructorReturn` sniff to verify that class constructor/destructor methods 1) do not have a return type declaration and 2) do not return a value. [#137], [#140], [#146] Inspired by [@derickr]. +* :wrench: :books: New `Universal.CodeAnalysis.ForeachUniqueAssignment` sniff to detect `foreach` control structures which use the same variable for both the key as well as the value assignment as this will lead to unexpected - and most likely unintended - behaviour. [#110], [#175] + The fixer will maintain the existing behaviour of the code. Mind: this may not be the _intended_ behaviour. +* :wrench: :books: New `Universal.CodeAnalysis.StaticInFinalClass` sniff to detect using `static` instead of `self` in OO constructs which are `final`. [#116], [#180] + The sniff has modular error codes to allow for making exceptions based on the type of use for `static`. +* :wrench: :bar_chart: :books: New `Universal.Constants.LowercaseClassResolutionKeyword` sniff to enforce that the `class` keyword when used for class name resolution, i.e. `::class`, is in lowercase. [#72] +* :wrench: :bar_chart: :books: New `Universal.Constants.ModifierKeywordOrder` sniff to standardize the modifier keyword order for OO constant declarations. [#143] + The sniff offers an `order` property to specify the preferred order. +* :wrench: :books: New `Universal.ControlStructures.DisallowLonelyIf` sniff to disallow `if` statements as the only statement in an `else` block. [#85], [#168], [#169] + Inspired by the [ESLint "no lonely if"] rule. + Note: This sniff will not fix the indentation of the "inner" code. It is strongly recommended to run this sniff together with the `Generic.WhiteSpace.ScopeIndent` sniff to get the correct indentation. +* :bar_chart: :books: New `Universal.Files.SeparateFunctionsFromOO` sniff to enforce that a file should either declare (global/namespaced) functions or declare OO structures, but not both. [#95], [#170], [#171] + Nested function declarations, i.e. functions declared within a function/method will be disregarded for the purposes of this sniff. + The same goes for anonymous classes, closures and arrow functions. +* :books: New `Universal.NamingConventions.NoReservedKeywordParameterNames` sniff to verify that function parameters do not use reserved keywords as names, as this can quickly become confusing when people use them in function calls using named parameters. [#80], [#81], [#106], [#107], [#173] + The sniff has modular error codes to allow for making exceptions for specific keywords. +* :wrench: :bar_chart: :books: New `Universal.Operators.TypeSeparatorSpacing` sniff to enforce no spaces around union type and intersection type separators. [#117] +* :wrench: :books: New `Universal.PHP.OneStatementInShortEchoTag` sniff to disallow short open echo tags `= 8.0][php-rfc-negative_array_index]. [#177], [#178] + If a [`php_version` configuration option][php_version-config] has been passed to PHPCS, it will be respected by the sniff and only report duplicate keys for the configured PHP version. +* `Universal.ControlStructures.DisallowAlternativeSyntax`: the sniff will now also record a metric when single-line (no body) control structures are encountered. [#158] +* `Universal.ControlStructures.DisallowAlternativeSyntax`: the error message thrown by the sniff is now more descriptive. [#159] +* `Universal.ControlStructures.DisallowAlternativeSyntax`: metrics will no longer be recorded for `elseif` and `else` keywords, but only on the `if` keyword as the type of syntax used has to be the same for the whole "chain". [#161] +* `Universal.Lists.DisallowLongListSyntax`: the sniff will no longer record (incomplete) metrics about long vs short list usage. [#155] +* `Universal.Lists.DisallowShortListSyntax`: the sniff will now record (complete) metrics about long vs short list usage. [#155] +* `Universal.OOStructures.AlphabeticExtendsImplements`: documented support for `enum ... implements`. [#150] +* `Universal.UseStatements.DisallowUseClass`: updated error message and metric name to take PHP 8.1 `enum`s into account. [#149] +* `Universal.UseStatements.NoLeadingBackslash`: the sniff will now also flag and auto-fix leading backslashes in group use statements. [#167] + +#### Other +* Updated the sniffs for compatibility with PHPCSUtils 1.0.0-alpha4. [#134] +* Updated the sniffs to correctly handle PHP 8.0/8.1/8.2 features whenever relevant. +* Readme: Updated installation instructions for compatibility with Composer 2.2+. [#101] +* Composer: The minimum `PHP_CodeSniffer` requirement has been updated to `^3.7.1` (was ^3.3.1). [#115], [#130] +* Composer: The package will now identify itself as a static analysis tool. Thanks [@GaryJones]! [#126] +* All non-`abstract` classes in this package are now `final`. [#121] +* All XML documentation now has a schema annotation. [#128] +* Various housekeeping. + +### Fixed + +The upgrade to PHPCSUtils 1.0.0-alpha4 took care of a number of bugs, which potentially could have affected sniffs in this package. + +#### NormalizedArrays +* `NormalizedArrays.Arrays.ArrayBraceSpacing`: the sniff now allows for trailing comments after the array opener in multi-line arrays. [#118] +* `NormalizedArrays.Arrays.ArrayBraceSpacing`: trailing comments at the end of an array, but before the closer, in multi-line arrays will no longer confuse the sniff. [#135] +* `NormalizedArrays.Arrays.CommaAfterLast`: the fixer will now recognize PHP 7.3+ flexible heredoc/nowdocs and in that case, will add the comma on the same line as the heredoc/nowdoc closer. [#144] + +#### Universal +* `Universal.Arrays.DisallowShortArraySyntax`: nested short arrays in short lists will now be detected and fixed correctly. [#153] +* `Universal.ControlStructures.DisallowAlternativeSyntax`: the sniff will no longer bow out indiscriminately when the `allowWithInlineHTML` property is set to `true`. [#90], [#161] +* `Universal.ControlStructures.DisallowAlternativeSyntax`: when alternative control structure syntax is allowed in combination with inline HTML (`allowWithInlineHTML` property set to `true`), inline HTML in functions declared within the control structure body will no longer be taken into account for determining whether or not the control structure contains inline HTML. [#160] +* `Universal.Lists.DisallowShortListSyntax`: the sniff will work around a tokenizer bug in PHPCS 3.7.1, which previously could lead to false negatives. [#151]. +* `Universal.Lists.DisallowShortListSyntax`: nested short lists in short arrays will now be detected and fixed correctly. [#152] +* `Universal.Operators.DisallowStandalonePostIncrementDecrement`: the sniff will now correctly recognize stand-alone statements which end on a PHP close tag. [#176] + +[#72]: https://github.com/PHPCSStandards/PHPCSExtra/pull/72 +[#76]: https://github.com/PHPCSStandards/PHPCSExtra/pull/76 +[#80]: https://github.com/PHPCSStandards/PHPCSExtra/pull/80 +[#81]: https://github.com/PHPCSStandards/PHPCSExtra/pull/81 +[#85]: https://github.com/PHPCSStandards/PHPCSExtra/pull/85 +[#89]: https://github.com/PHPCSStandards/PHPCSExtra/pull/89 +[#90]: https://github.com/PHPCSStandards/PHPCSExtra/pull/90 +[#95]: https://github.com/PHPCSStandards/PHPCSExtra/pull/95 +[#101]: https://github.com/PHPCSStandards/PHPCSExtra/pull/101 +[#106]: https://github.com/PHPCSStandards/PHPCSExtra/pull/106 +[#107]: https://github.com/PHPCSStandards/PHPCSExtra/pull/107 +[#108]: https://github.com/PHPCSStandards/PHPCSExtra/pull/108 +[#109]: https://github.com/PHPCSStandards/PHPCSExtra/pull/109 +[#110]: https://github.com/PHPCSStandards/PHPCSExtra/pull/110 +[#114]: https://github.com/PHPCSStandards/PHPCSExtra/pull/114 +[#115]: https://github.com/PHPCSStandards/PHPCSExtra/pull/115 +[#116]: https://github.com/PHPCSStandards/PHPCSExtra/pull/116 +[#117]: https://github.com/PHPCSStandards/PHPCSExtra/pull/117 +[#118]: https://github.com/PHPCSStandards/PHPCSExtra/pull/118 +[#119]: https://github.com/PHPCSStandards/PHPCSExtra/pull/119 +[#120]: https://github.com/PHPCSStandards/PHPCSExtra/pull/120 +[#121]: https://github.com/PHPCSStandards/PHPCSExtra/pull/121 +[#122]: https://github.com/PHPCSStandards/PHPCSExtra/pull/122 +[#123]: https://github.com/PHPCSStandards/PHPCSExtra/pull/123 +[#124]: https://github.com/PHPCSStandards/PHPCSExtra/pull/124 +[#126]: https://github.com/PHPCSStandards/PHPCSExtra/pull/126 +[#128]: https://github.com/PHPCSStandards/PHPCSExtra/pull/128 +[#130]: https://github.com/PHPCSStandards/PHPCSExtra/pull/130 +[#134]: https://github.com/PHPCSStandards/PHPCSExtra/pull/134 +[#137]: https://github.com/PHPCSStandards/PHPCSExtra/pull/137 +[#140]: https://github.com/PHPCSStandards/PHPCSExtra/pull/140 +[#142]: https://github.com/PHPCSStandards/PHPCSExtra/pull/142 +[#143]: https://github.com/PHPCSStandards/PHPCSExtra/pull/143 +[#144]: https://github.com/PHPCSStandards/PHPCSExtra/pull/144 +[#146]: https://github.com/PHPCSStandards/PHPCSExtra/pull/146 +[#147]: https://github.com/PHPCSStandards/PHPCSExtra/pull/147 +[#148]: https://github.com/PHPCSStandards/PHPCSExtra/pull/148 +[#149]: https://github.com/PHPCSStandards/PHPCSExtra/pull/149 +[#150]: https://github.com/PHPCSStandards/PHPCSExtra/pull/150 +[#151]: https://github.com/PHPCSStandards/PHPCSExtra/pull/151 +[#152]: https://github.com/PHPCSStandards/PHPCSExtra/pull/152 +[#153]: https://github.com/PHPCSStandards/PHPCSExtra/pull/153 +[#154]: https://github.com/PHPCSStandards/PHPCSExtra/pull/154 +[#155]: https://github.com/PHPCSStandards/PHPCSExtra/pull/155 +[#158]: https://github.com/PHPCSStandards/PHPCSExtra/pull/158 +[#159]: https://github.com/PHPCSStandards/PHPCSExtra/pull/159 +[#160]: https://github.com/PHPCSStandards/PHPCSExtra/pull/160 +[#161]: https://github.com/PHPCSStandards/PHPCSExtra/pull/161 +[#162]: https://github.com/PHPCSStandards/PHPCSExtra/pull/162 +[#163]: https://github.com/PHPCSStandards/PHPCSExtra/pull/163 +[#164]: https://github.com/PHPCSStandards/PHPCSExtra/pull/164 +[#165]: https://github.com/PHPCSStandards/PHPCSExtra/pull/165 +[#166]: https://github.com/PHPCSStandards/PHPCSExtra/pull/166 +[#167]: https://github.com/PHPCSStandards/PHPCSExtra/pull/167 +[#168]: https://github.com/PHPCSStandards/PHPCSExtra/pull/168 +[#169]: https://github.com/PHPCSStandards/PHPCSExtra/pull/169 +[#170]: https://github.com/PHPCSStandards/PHPCSExtra/pull/170 +[#171]: https://github.com/PHPCSStandards/PHPCSExtra/pull/171 +[#172]: https://github.com/PHPCSStandards/PHPCSExtra/pull/172 +[#173]: https://github.com/PHPCSStandards/PHPCSExtra/pull/173 +[#175]: https://github.com/PHPCSStandards/PHPCSExtra/pull/175 +[#176]: https://github.com/PHPCSStandards/PHPCSExtra/pull/176 +[#177]: https://github.com/PHPCSStandards/PHPCSExtra/pull/177 +[#178]: https://github.com/PHPCSStandards/PHPCSExtra/pull/178 +[#180]: https://github.com/PHPCSStandards/PHPCSExtra/pull/180 + +[php-manual-dirname]: https://www.php.net/function.dirname +[php-rfc-negative_array_index]: https://wiki.php.net/rfc/negative_array_index +[php_version-config]: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Configuration-Options#setting-the-php-version +[ESLint "no lonely if"]: https://eslint.org/docs/rules/no-lonely-if +[PHPCSUtils 1.0.0-alpha4]: https://github.com/PHPCSStandards/PHPCSUtils/releases/tag/1.0.0-alpha4 + + ## [1.0.0-alpha3] - 2020-06-29 ### Added @@ -175,5 +342,9 @@ This initial alpha release contains the following sniffs: [Composer PHPCS plugin]: https://github.com/PHPCSStandards/composer-installer [Unreleased]: https://github.com/PHPCSStandards/PHPCSExtra/compare/stable...HEAD +[1.0.0-RC1]: https://github.com/PHPCSStandards/PHPCSExtra/compare/1.0.0-alpha3...1.0.0-rc1 [1.0.0-alpha3]: https://github.com/PHPCSStandards/PHPCSExtra/compare/1.0.0-alpha2...1.0.0-alpha3 [1.0.0-alpha2]: https://github.com/PHPCSStandards/PHPCSExtra/compare/1.0.0-alpha1...1.0.0-alpha2 + +[@derickr]: https://github.com/derickr +[@GaryJones]: https://github.com/GaryJones diff --git a/README.md b/README.md index 0471fdf6..8ec7bd40 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ PHPCSExtra + [Composer Global Installation](#composer-global-installation) * [Features](#features) * [Sniffs](#sniffs) + + [Modernize](#modernize) + [NormalizedArrays](#normalizedarrays) + [Universal](#universal) * [Contributing](#contributing) @@ -74,8 +75,9 @@ composer global require --dev phpcsstandards/phpcsextra:"^1.0" Features ------------------------------------------- -Once this project is installed, you will see two new rulesets in the list of installed standards when you run `phpcs -i`: `NormalizedArrays` and `Universal`. +Once this project is installed, you will see three new rulesets in the list of installed standards when you run `vendor/bin/phpcs -i`: `Modernize`, `NormalizedArrays` and `Universal`. +* The `Modernize` ruleset is a standard which checks code for modernization opportunaties. * The `NormalizedArrays` ruleset is a standard to check the formatting of array declarations. * The `Universal` ruleset is **NOT** a standard, but a sniff collection. It should **NOT** be included in custom rulesets as a standard as it contains contradictory rules. @@ -91,6 +93,17 @@ Sniffs * :books: = Includes CLI documentation. +### Modernize + +#### `Modernize.FunctionCalls.Dirname` :wrench: :books: + +This sniff will detect and auto-fix two typical code modernizations which can be made related to the `dirname()` function: +1. Since PHP 5.3, calls to `dirname(__FILE__)` can be replaced by `__DIR__`. + Errorcode: `Modernize.FunctionCalls.Dirname.FileConstant`. +2. Since PHP 7.0, nested function calls to `dirname()` can be changed to use the `$levels` parameter. + Errorcode: `Modernize.FunctionCalls.Dirname.Nested`. + + ### NormalizedArrays #### `NormalizedArrays.Arrays.ArrayBraceSpacing` :wrench: :bar_chart: :books: @@ -123,9 +136,10 @@ Use any of the following values to change the properties: `enforce`, `forbid` or The default for the `singleLine` property is `forbid`. The default for the `multiLine` property is `enforce`. + ### Universal -#### `Universal.Arrays.DisallowShortArraySyntax` :wrench: :books: +#### `Universal.Arrays.DisallowShortArraySyntax` :wrench: :bar_chart: :books: Disallow short array syntax. @@ -135,6 +149,13 @@ In contrast to the PHPCS native `Generic.Arrays.DisallowShortArraySyntax` sniff, Detects duplicate array keys in array declarations. +The sniff will make a distinction between keys which will be duplicate in all PHP version and (numeric) keys which will only be a duplicate key in [PHP < 8.0 or PHP >= 8.0][php-rfc-negative_array_index]. + +If a [`php_version` configuration option][php_version-config] has been passed to PHPCS using either `--config-set` or `--runtime-set`, it will be respected by the sniff and only report duplicate keys for the configured PHP version. + +[php-rfc-negative_array_index]: https://wiki.php.net/rfc/negative_array_index +[php_version-config]: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Configuration-Options#setting-the-php-version + #### `Universal.Arrays.MixedArrayKeyTypes` :books: Best practice sniff: don't use a mix of integer and string keys for array items. @@ -143,6 +164,61 @@ Best practice sniff: don't use a mix of integer and string keys for array items. Best practice sniff: don't use a mix of keyed and unkeyed array items. +#### `Universal.Classes.DisallowAnonClassParentheses` :wrench: :bar_chart: :books: + +Disallow the use of parentheses when declaring an anonymous class without passing parameters. + +#### `Universal.Classes.RequireAnonClassParentheses` :wrench: :bar_chart: :books: + +Require the use of parentheses when declaring an anonymous class, whether parameters are passed or not. + +#### `Universal.Classes.DisallowFinalClass` :wrench: :bar_chart: :books: + +Disallow classes being declared `final`. + +#### `Universal.Classes.RequireFinalClass` :wrench: :bar_chart: :books: + +Require all non-`abstract` classes to be declared `final`. + +:warning: **Warning**: the auto-fixer for this sniff _may_ have unintended side-effects for applications and should be used with care! +This is considered a **_risky_ fixer**. + +#### `Universal.Classes.ModifierKeywordOrder` :wrench: :bar_chart: :books: + +Require a consistent modifier keyword order for class declarations. + +* This sniff contains an `order` property to specify the preferred order. + Accepted values: (string) `'extendability readonly'`|`'readonly extendability'`. Defaults to `'extendability readonly'`. + +#### `Universal.CodeAnalysis.ConstructorDestructorReturn` :wrench: :books: + +* Disallows return type declarations on constructor/destructor methods - error code: `ReturnTypeFound`, auto-fixable. +* Discourages constructor/destructor methods returning a value - error code: `ReturnValueFound`. + +#### `Universal.CodeAnalysis.ForeachUniqueAssignment` :wrench: :books: + +Detects `foreach` control structures which use the same variable for both the key as well as the value assignment as this will lead to unexpected - and most likely unintended - behaviour. + +Note: The fixer will maintain the existing behaviour of the code. This may not be the _intended_ behaviour. + +#### `Universal.CodeAnalysis.StaticInFinalClass` :wrench: :books: + +Detects using `static` instead of `self` in OO constructs which are `final`. + +* The sniff has modular error codes to allow for making exceptions based on the type of use for `static`. + The available error codes are: `ReturnType`, `InstanceOf`, `NewInstance`, `ScopeResolution`. + +#### `Universal.Constants.LowercaseClassResolutionKeyword` :wrench: :bar_chart: :books: + +Enforce that the `class` keyword when used for class name resolution, i.e. `::class`, is in lowercase. + +#### `Universal.Constants.ModifierKeywordOrder` :wrench: :bar_chart: :books: + +Require a consistent modifier keyword order for OO constant declarations. + +* This sniff contains an `order` property to specify the preferred order. + Accepted values: (string) `'final visibility'`|`'visibility final'`. Defaults to `'final visibility'`. + #### `Universal.Constants.UppercaseMagicConstants` :wrench: :bar_chart: :books: Enforce uppercase when using PHP native magic constants, like `__FILE__` et al. @@ -156,14 +232,33 @@ Disallow using the alternative syntax for control structures. * The sniff has modular error codes to allow for making exceptions based on specific control structures and/or specific control structures in combination with inline HTML. The error codes follow the following pattern: `Found[ControlStructure][WithInlineHTML]`. Examples: `FoundIf`, `FoundSwitchWithInlineHTML`. +#### `Universal.ControlStructures.DisallowLonelyIf` :wrench: :books: + +Disallow `if` statements as the only statement in an `else` block. + +Note: This sniff will not fix the indentation of the "inner" code. +It is strongly recommended to run this sniff together with the `Generic.WhiteSpace.ScopeIndent` sniff to get the correct indentation. + #### `Universal.ControlStructures.IfElseDeclaration` :wrench: :bar_chart: :books: Verify that else(if) statements with braces are on a new line. -#### `Universal.Lists.DisallowLongListSyntax` :wrench: :bar_chart: :books: +#### `Universal.Files.SeparateFunctionsFromOO` :bar_chart: :books: + +Enforce for a file to either declare (global/namespaced) functions or declare OO structures, but not both. + +* Nested function declarations, i.e. functions declared within a function/method will be disregarded for the purposes of this sniff. + The same goes for anonymous classes, closures and arrow functions. +* Note: This sniff has no opinion on side effects. If you want to sniff for those, use the PHPCS native `PSR1.Files.SideEffects` sniff. +* Also note: This sniff has no opinion on multiple OO structures being declared in one file. + If you want to sniff for that, use the PHPCS native `Generic.Files.OneObjectStructurePerFile` sniff. + +#### `Universal.Lists.DisallowLongListSyntax` :wrench: :books: Disallow the use of long `list`s. +> For metrics about the use of long lists vs short lists, please use the `Universal.Lists.DisallowShortListSyntax` sniff. + #### `Universal.Lists.DisallowShortListSyntax` :wrench: :bar_chart: :books: Disallow the use of short lists. @@ -186,6 +281,13 @@ Enforce the use of the alternative namespace syntax using curly braces. Disallow the use of multiple namespaces within a file. +#### `Universal.NamingConventions.NoReservedKeywordParameterNames` :books: + +Disallow function parameters using reserved keywords as names, as this can quickly become confusing when people use them in function calls using named parameters + +* The sniff has modular error codes to allow for making exceptions for specific keywords. + The error codes follow the following pattern: `[keyword]Found`. + #### `Universal.OOStructures.AlphabeticExtendsImplements` :wrench: :bar_chart: :books: Enforce that the names used in a class/enum "implements" statement or an interface "extends" statement are listed in alphabetic order. @@ -231,6 +333,16 @@ Enforce the use of strict comparisons. :warning: **Warning**: the auto-fixer for this sniff _may_ cause bugs in applications and should be used with care! This is considered a **_risky_ fixer**. +#### `Universal.Operators.TypeSeparatorSpacing` :wrench: :bar_chart: :books: + +Enforce no spaces around the union type and intersection type operators. + +The available error codes are: `UnionTypeSpacesBefore`, `UnionTypeSpacesAfter`, `IntersectionTypeSpacesBefore`, `IntersectionTypeSpacesAfter`. + +#### `Universal.PHP.OneStatementInShortEchoTag` :wrench: :books: + +Disallow short open echo tags ` However, the sister-sniff `Generic.Whitespace.DisallowSpaceIndent` leaves mid-line tabs/spaces alone. > This sniff fills that gap. +#### `Universal.WhiteSpace.PrecisionAlignment` :wrench: :books: + +Enforce code indentation to always be a multiple of a tabstop, i.e. disallow precision alignment. + +Note: +* This sniff does not concern itself with tabs versus spaces. + It is recommended to use the sniff in combination with the PHPCS native `Generic.WhiteSpace.DisallowTabIndent` or the `Generic.WhiteSpace.DisallowSpaceIndent` sniff. +* When using this sniff with tab-based standards, please ensure that the `tab-width` is set and either don't set the `$indent` property or set it to the tab-width (or a multiple thereof). +* The fixer works based on "best guess" and may not always result in the desired indentation. Combine this sniff with the `Generic.WhiteSpace.ScopeIndent` sniff for more precise indentation fixes. + +The behaviour of the sniff is customizable via the following properties: +* `indent`: the indent used for the codebase. + Accepted values: (int|null) number of spaces. Defaults to `null`. + If this property is not set, the sniff will look to the `--tab-width` CLI value. + If that also isn't set, the default tab-width of `4` will be used. +* `ignoreAlignmentBefore`: allows for providing a list of token names for which (preceding) precision alignment should be ignored. + Accepted values: (array) token constant names. Defaults to an empty array. + Usage example: + ```xml + + + + + + + + + + + + ``` +* `ignoreBlankLines`: whether or not potential trailing whitespace on otherwise blank lines should be examined or ignored. + It is recommended to only set this to `false` if the standard including this sniff does not include the `Squiz.WhiteSpace.SuperfluousWhitespace` sniff (which is included in most standards). + Accepted values: (bool)`true`|`false`. Defaults to `true`. + Contributing ------- From 25efbd5ec0456748753deccd06fc0917a634bd68 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 24 Apr 2022 03:33:02 +0200 Subject: [PATCH 105/125] README: update badges * Update links to workflows. * Use re-usable links for links which are used repeatedly. --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8ec7bd40..7233a379 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,16 @@ PHPCSExtra