From 71144b53142e1c4c4e1b039e0215262bc7b3b7aa Mon Sep 17 00:00:00 2001 From: Dan Wallis Date: Sun, 28 Jul 2024 00:40:26 +0100 Subject: [PATCH 1/2] Rewrite PSR12.Files.DeclareStatement sniff --- .../Sniffs/Files/DeclareStatementSniff.php | 442 +++++++++++------- .../Files/DeclareStatementUnitTest.1.inc | 8 +- .../DeclareStatementUnitTest.1.inc.fixed | 18 +- .../Files/DeclareStatementUnitTest.10.inc | 3 + .../Files/DeclareStatementUnitTest.11.inc | 3 + .../Files/DeclareStatementUnitTest.12.inc | 3 + .../Files/DeclareStatementUnitTest.3.inc | 6 + .../Files/DeclareStatementUnitTest.4.inc | 64 +++ .../Files/DeclareStatementUnitTest.5.inc | 23 + .../Files/DeclareStatementUnitTest.6.inc | 3 + .../Files/DeclareStatementUnitTest.7.inc | 3 + .../Files/DeclareStatementUnitTest.8.inc | 3 + .../Files/DeclareStatementUnitTest.9.inc | 3 + .../Tests/Files/DeclareStatementUnitTest.php | 66 ++- 14 files changed, 480 insertions(+), 168 deletions(-) create mode 100644 src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.10.inc create mode 100644 src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.11.inc create mode 100644 src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.12.inc create mode 100644 src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.3.inc create mode 100644 src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.4.inc create mode 100644 src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.5.inc create mode 100644 src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.6.inc create mode 100644 src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.7.inc create mode 100644 src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.8.inc create mode 100644 src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.9.inc diff --git a/src/Standards/PSR12/Sniffs/Files/DeclareStatementSniff.php b/src/Standards/PSR12/Sniffs/Files/DeclareStatementSniff.php index d9615f9a4e..10e632795b 100644 --- a/src/Standards/PSR12/Sniffs/Files/DeclareStatementSniff.php +++ b/src/Standards/PSR12/Sniffs/Files/DeclareStatementSniff.php @@ -16,6 +16,25 @@ class DeclareStatementSniff implements Sniff { + /** + * Mapping from token code to human-readable name. Used to produce error messages. + * + * @var array + */ + private $NAMES = [ + T_DECLARE => 'declare keyword', + T_OPEN_PARENTHESIS => 'opening parenthesis', + T_STRING => 'directive', + T_EQUAL => 'equals sign', + T_LNUMBER => 'directive value', + T_CONSTANT_ENCAPSED_STRING => 'directive value', + T_CLOSE_PARENTHESIS => 'closing parenthesis', + T_SEMICOLON => 'semicolon', + T_CLOSE_TAG => 'closing PHP tag', + T_OPEN_CURLY_BRACKET => 'opening curly bracket', + T_CLOSE_CURLY_BRACKET => 'closing curly bracket', + ]; + /** * Returns an array of tokens this test wants to listen for. @@ -40,223 +59,332 @@ public function register() */ public function process(File $phpcsFile, $stackPtr) { - // Allow a byte-order mark. $tokens = $phpcsFile->getTokens(); - // There should be no space between declare keyword and opening parenthesis. - $parenthesis = ($stackPtr + 1); - if ($tokens[($stackPtr + 1)]['type'] !== 'T_OPEN_PARENTHESIS') { - $parenthesis = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); - $error = 'Expected no space between declare keyword and opening parenthesis in a declare statement'; + $openParen = $phpcsFile->findNext(T_OPEN_PARENTHESIS, $stackPtr); - if ($tokens[$parenthesis]['type'] === 'T_OPEN_PARENTHESIS') { - $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceFoundAfterDeclare'); + if ($openParen === false) { + // Live coding / parse error. + return; + } - if ($fix === true) { - $phpcsFile->fixer->replaceToken(($stackPtr + 1), ''); - } - } else { - $phpcsFile->addError($error, $parenthesis, 'SpaceFoundAfterDeclare'); - $parenthesis = $phpcsFile->findNext(T_OPEN_PARENTHESIS, ($parenthesis + 1)); - } + $this->complainIfTokensNotAdjacent( + $phpcsFile, + $stackPtr, + $openParen, + 'SpaceFoundAfterDeclare' + ); + + if (isset($tokens[$openParen]['parenthesis_closer']) === false) { + // Live coding / parse error. + return; } - // There should be no space between open parenthesis and the directive. - $string = $phpcsFile->findNext(T_WHITESPACE, ($parenthesis + 1), null, true); - if ($parenthesis !== false) { - if ($tokens[($parenthesis + 1)]['type'] !== 'T_STRING') { - $error = 'Expected no space between opening parenthesis and directive in a declare statement'; + $closeParen = $tokens[$openParen]['parenthesis_closer']; - if ($tokens[$string]['type'] === 'T_STRING') { - $fix = $phpcsFile->addFixableError($error, $parenthesis, 'SpaceFoundBeforeDirective'); + $directive = $phpcsFile->findNext(T_STRING, ($openParen + 1), $closeParen); - if ($fix === true) { - $phpcsFile->fixer->replaceToken(($parenthesis + 1), ''); - } - } else { - $phpcsFile->addError($error, $string, 'SpaceFoundBeforeDirective'); - $string = $phpcsFile->findNext(T_STRING, ($string + 1)); - } + if ($directive === false) { + // Live coding / parse error. + return; + } + + // There should be no space between open parenthesis and the directive. + $this->complainIfTokensNotAdjacent( + $phpcsFile, + $openParen, + $directive, + 'SpaceFoundBeforeDirective' + ); + + // The directive must be in lowercase. + if ($tokens[$directive]['content'] !== strtolower($tokens[$directive]['content'])) { + $error = 'The directive of a declare statement must be in lowercase'; + $fix = $phpcsFile->addFixableError($error, $directive, 'DirectiveNotLowercase'); + if ($fix === true) { + $phpcsFile->fixer->replaceToken($directive, strtolower($tokens[$directive]['content'])); } } - // There should be no space between directive and the equal sign. - $equals = $phpcsFile->findNext(T_WHITESPACE, ($string + 1), null, true); - if ($string !== false) { - // The directive must be in lowercase. - if ($tokens[$string]['content'] !== strtolower($tokens[$string]['content'])) { - $error = 'The directive of a declare statement must be in lowercase'; - $fix = $phpcsFile->addFixableError($error, $string, 'DirectiveNotLowercase'); - if ($fix === true) { - $phpcsFile->fixer->replaceToken($string, strtolower($tokens[$string]['content'])); - } + // When wishing to declare strict types in files containing markup outside PHP opening + // and closing tags, the declaration MUST be on the first line of the file and include + // an opening PHP tag, the strict types declaration and closing tag. + if ($tokens[$stackPtr]['line'] !== 1 && strtolower($tokens[$directive]['content']) === 'strict_types') { + $nonPHP = $phpcsFile->findNext(T_INLINE_HTML, 0); + if ($nonPHP !== false) { + $error = 'When declaring strict_types in a file with markup outside PHP tags, the declare statement must be on the first line'; + $phpcsFile->addError($error, $stackPtr, 'DeclareNotOnFirstLine'); } + } - if ($tokens[($string + 1)]['type'] !== 'T_EQUAL') { - $error = 'Expected no space between directive and the equals sign in a declare statement'; + $equals = $phpcsFile->findNext(T_EQUAL, ($directive + 1), $closeParen); - if ($tokens[$equals]['type'] === 'T_EQUAL') { - $fix = $phpcsFile->addFixableError($error, $equals, 'SpaceFoundAfterDirective'); + if ($equals === false) { + // Live coding / parse error. + return; + } - if ($fix === true) { - $phpcsFile->fixer->replaceToken(($string + 1), ''); - } - } else { - $phpcsFile->addError($error, $equals, 'SpaceFoundAfterDirective'); - $equals = $phpcsFile->findNext(T_EQUAL, ($equals + 1)); - } - } - }//end if + // There should be no space between directive and the equal sign. + $this->complainIfTokensNotAdjacent( + $phpcsFile, + $directive, + $equals, + 'SpaceFoundAfterDirective' + ); - // There should be no space between equal sign and directive value. - $value = $phpcsFile->findNext(T_WHITESPACE, ($equals + 1), null, true); + $value = $phpcsFile->findNext([T_LNUMBER, T_CONSTANT_ENCAPSED_STRING], ($equals + 1), $closeParen); if ($value === false) { // Live coding / parse error. return; } - if ($equals !== false) { - if ($tokens[($equals + 1)]['type'] !== 'T_LNUMBER') { - $error = 'Expected no space between equal sign and the directive value in a declare statement'; - - if ($tokens[$value]['type'] === 'T_LNUMBER') { - $fix = $phpcsFile->addFixableError($error, $value, 'SpaceFoundBeforeDirectiveValue'); + // There should be no space between equals sign and directive value. + $this->complainIfTokensNotAdjacent( + $phpcsFile, + $equals, + $value, + 'SpaceFoundBeforeDirectiveValue' + ); + + // $closeParen was defined earlier as $closeParen = $tokens[$openParen]['parenthesis_closer']; + // There should be no space between directive value and closing parenthesis. + $this->complainIfTokensNotAdjacent( + $phpcsFile, + $value, + $closeParen, + 'SpaceFoundAfterDirectiveValue' + ); + + $nextThing = $phpcsFile->findNext(Tokens::$emptyTokens, ($closeParen + 1), null, true); + + if ($nextThing === false) { + // Live coding / parse error. + return; + } - if ($fix === true) { - $phpcsFile->fixer->replaceToken(($equals + 1), ''); - } - } else { - $phpcsFile->addError($error, $value, 'SpaceFoundBeforeDirectiveValue'); - $value = $phpcsFile->findNext(T_LNUMBER, ($value + 1)); + // There should be no space between closing parenthesis and semicolon. + if ($tokens[$nextThing]['code'] === T_SEMICOLON) { + $this->complainIfTokensNotAdjacent( + $phpcsFile, + $closeParen, + $nextThing, + 'SpaceFoundBeforeSemicolon' + ); + + if (isset($nonPHP) === true && $nonPHP !== false) { + $PHPClosingTag = $phpcsFile->findNext(Tokens::$emptyTokens, ($nextThing + 1), null, true); + if ($PHPClosingTag !== false) { + $this->complainIfNotExactlyOneSpaceBetween( + $phpcsFile, + $nextThing, + $PHPClosingTag, + 'BeforeClosePHPTag' + ); } } + + return; + }//end if + + // There should be exactly one space between the close parenthesis and the closing PHP tag. + if ($tokens[$nextThing]['code'] === T_CLOSE_TAG) { + $this->complainIfNotExactlyOneSpaceBetween( + $phpcsFile, + $closeParen, + $nextThing, + 'BeforeClosePHPTag' + ); + + return; } - $parenthesis = $phpcsFile->findNext(T_WHITESPACE, ($value + 1), null, true); - if ($value !== false) { - if ($tokens[($value + 1)]['type'] !== 'T_CLOSE_PARENTHESIS') { - $error = 'Expected no space between the directive value and closing parenthesis in a declare statement'; + if ($tokens[$nextThing]['code'] === T_OPEN_CURLY_BRACKET) { + // There should be exactly one space between the closing parenthesis and the opening bracket. + $this->complainIfNotExactlyOneSpaceBetween( + $phpcsFile, + $closeParen, + $nextThing, + 'BeforeCurlyBracket' + ); + + $openBracket = $nextThing; + if (isset($tokens[$openBracket]['bracket_closer']) === false) { + // Live coding / parse error. + return; + } - if ($tokens[$parenthesis]['type'] === 'T_CLOSE_PARENTHESIS') { - $fix = $phpcsFile->addFixableError($error, $parenthesis, 'SpaceFoundAfterDirectiveValue'); + $closeBracket = $tokens[$openBracket]['bracket_closer']; - if ($fix === true) { - $phpcsFile->fixer->replaceToken(($value + 1), ''); + $openLine = $tokens[$openBracket]['line']; + $closeLine = $tokens[$closeBracket]['line']; + + $afterOpen = $phpcsFile->findNext(Tokens::$emptyTokens, ($openBracket + 1), $closeBracket, true); + $afterClose = $phpcsFile->findNext(Tokens::$emptyTokens, ($closeBracket + 1), null, true); + $beforeClose = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($closeBracket - 1), $openBracket, true); + + // The open curly bracket must be the last code on the line. + if ($afterOpen !== false && $tokens[$afterOpen]['line'] === $openLine) { + $error = 'The open curly bracket of a declare statement must be the last code on the line'; + + $fix = $phpcsFile->addFixableError($error, $afterOpen, 'CodeFoundAfterOpenCurlyBracket'); + + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + if ($tokens[($afterOpen - 1)]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken(($afterOpen - 1), ''); } - } else { - $phpcsFile->addError($error, $parenthesis, 'SpaceFoundAfterDirectiveValue'); - $parenthesis = $phpcsFile->findNext(T_CLOSE_PARENTHESIS, ($parenthesis + 1)); + + // 4 = indent size as defined by PSR12. + $indent = str_repeat(' ', (4 + $tokens[$stackPtr]['column'] - 1)); + $phpcsFile->fixer->addContentBefore($afterOpen, PHP_EOL.$indent); + $phpcsFile->fixer->endChangeset(); } } - } - // Check for semicolon. - $curlyBracket = false; - if ($tokens[($parenthesis + 1)]['type'] !== 'T_SEMICOLON') { - $token = $phpcsFile->findNext(T_WHITESPACE, ($parenthesis + 1), null, true); - - if ($tokens[$token]['type'] === 'T_OPEN_CURLY_BRACKET') { - // Block declaration. - $curlyBracket = $token; - } else if ($tokens[$token]['type'] === 'T_SEMICOLON') { - $error = 'Expected no space between the closing parenthesis and the semicolon in a declare statement'; - $fix = $phpcsFile->addFixableError($error, $parenthesis, 'SpaceFoundBeforeSemicolon'); + // The closing curly bracket must be on a new line. + if ($afterClose !== false && $tokens[$afterClose]['line'] === $closeLine) { + $error = 'The closing curly bracket of a declare statement must be the last code on the line'; + $fix = $phpcsFile->addFixableError($error, $afterClose, 'CodeFoundAfterCloseCurlyBracket'); if ($fix === true) { - $phpcsFile->fixer->replaceToken(($parenthesis + 1), ''); - } - } else if ($tokens[$token]['type'] === 'T_CLOSE_TAG') { - if ($tokens[($parenthesis)]['line'] !== $tokens[$token]['line']) { - // Close tag must be on the same line.. - $error = 'The close tag must be on the same line as the declare statement'; - $fix = $phpcsFile->addFixableError($error, $parenthesis, 'CloseTagOnNewLine'); - if ($fix === true) { - $phpcsFile->fixer->replaceToken(($parenthesis + 1), ' '); - } - } - } else { - $error = 'Expected no space between the closing parenthesis and the semicolon in a declare statement'; - $phpcsFile->addError($error, $parenthesis, 'SpaceFoundBeforeSemicolon'); - - // See if there is a semicolon or curly bracket after this token. - $token = $phpcsFile->findNext([T_WHITESPACE, T_COMMENT], ($token + 1), null, true); - if ($tokens[$token]['type'] === 'T_OPEN_CURLY_BRACKET') { - $curlyBracket = $token; + $phpcsFile->fixer->addNewlineBefore($afterClose); } - }//end if - }//end if - - if ($curlyBracket !== false) { - $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($curlyBracket - 1), null, true); - $error = 'Expected one space between closing parenthesis and opening curly bracket in a declare statement'; + } - // The opening curly bracket must on the same line with a single space between closing bracket. - if ($tokens[$prevToken]['type'] !== 'T_CLOSE_PARENTHESIS') { - $phpcsFile->addError($error, $curlyBracket, 'ExtraSpaceFoundAfterBracket'); - } else if ($phpcsFile->getTokensAsString(($prevToken + 1), ($curlyBracket - $prevToken - 1)) !== ' ') { - $fix = $phpcsFile->addFixableError($error, $curlyBracket, 'ExtraSpaceFoundAfterBracket'); + if ($beforeClose !== false && $tokens[$beforeClose]['line'] === $closeLine) { + $error = 'The closing curly bracket of a declare statement must be on a new line.'; + $fix = $phpcsFile->addFixableError($error, $closeBracket, 'CurlyBracketNotOnNewLine'); if ($fix === true) { $phpcsFile->fixer->beginChangeset(); - $phpcsFile->fixer->replaceToken(($prevToken + 1), ' '); - $nextToken = ($prevToken + 2); - while ($nextToken !== $curlyBracket) { - $phpcsFile->fixer->replaceToken($nextToken, ''); - $nextToken++; + if ($tokens[($closeBracket - 1)]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken(($closeBracket - 1), ''); } + $phpcsFile->fixer->addNewlineBefore($closeBracket); $phpcsFile->fixer->endChangeset(); } - }//end if + } - $closeCurlyBracket = $tokens[$curlyBracket]['bracket_closer']; + // Closing curly bracket must align with the declare keyword. + if ($tokens[$stackPtr]['column'] !== $tokens[$closeBracket]['column']) { + $error = 'The closing curly bracket of a declare statements must be aligned with the declare keyword'; - $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($closeCurlyBracket - 1), null, true); - $nextToken = $phpcsFile->findNext([T_WHITESPACE, T_COMMENT], ($closeCurlyBracket + 1), null, true); - $line = $tokens[$closeCurlyBracket]['line']; + $expected = ($tokens[$stackPtr]['column'] - 1); + $actual = ($tokens[$closeBracket]['column'] - 1); + $indent = trim($tokens[($closeBracket - 1)]['content'], PHP_EOL); - // The closing curly bracket must be on a new line. - if ($tokens[$prevToken]['line'] === $line || $tokens[$nextToken]['line'] === $line) { - if ($tokens[$prevToken]['line'] === $line) { - $error = 'The closing curly bracket of a declare statement must be on a new line'; - $fix = $phpcsFile->addFixableError($error, $prevToken, 'CurlyBracketNotOnNewLine'); + if ($expected > $actual) { + $fix = $phpcsFile->addFixableError($error, $closeBracket, 'CloseBracketNotAligned'); + if ($fix === true) { + $phpcsFile->fixer->addContentBefore($closeBracket, str_repeat(' ', ($expected - $actual))); + } + } else if ($tokens[($closeBracket - 1)]['code'] === T_WHITESPACE && strlen($indent) > $expected) { + $fix = $phpcsFile->addFixableError($error, $closeBracket, 'CloseBracketNotAligned'); if ($fix === true) { - $phpcsFile->fixer->addNewline($prevToken); + $phpcsFile->fixer->replaceToken(($closeBracket - 1), str_repeat(' ', $expected)); } + } else { + $phpcsFile->addError($error, $closeBracket, 'CloseBracketNotAligned'); } }//end if + }//end if - // Closing curly bracket must align with the declare keyword. - if ($tokens[$stackPtr]['column'] !== $tokens[$closeCurlyBracket]['column']) { - $error = 'The closing curly bracket of a declare statements must be aligned with the declare keyword'; + }//end process() - $fix = $phpcsFile->addFixableError($error, $closeCurlyBracket, 'CloseBracketNotAligned'); - if ($fix === true) { - $phpcsFile->fixer->replaceToken(($closeCurlyBracket - 1), str_repeat(' ', ($tokens[$stackPtr]['column'] - 1))); + + /** + * Add an error if the token is not at the expected index. Fix if possible. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $previousToken Index within $tokens[] of the first token. + * @param int $nextToken Index within $tokens[] of the second token. + * @param string $errorCode Error code to be used for this problem. + * + * @return void + */ + private function complainIfTokensNotAdjacent($phpcsFile, $previousToken, $nextToken, $errorCode) + { + if (($previousToken + 1) === $nextToken) { + return; + } + + $tokens = $phpcsFile->getTokens(); + $error = 'Expected no space between the '.$this->NAMES[$tokens[$previousToken]['code']].' and the '.$this->NAMES[$tokens[$nextToken]['code']].' in a declare statement'; + + $onlyWhitespace = true; + for ($i = ($previousToken + 1); $i < $nextToken; $i++) { + if ($tokens[$i]['code'] !== T_WHITESPACE) { + $onlyWhitespace = false; + break; + } + } + + if ($onlyWhitespace === true) { + $fix = $phpcsFile->addFixableError($error, ($previousToken + 1), $errorCode); + + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + for ($i = ($previousToken + 1); $i < $nextToken; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); } + + $phpcsFile->fixer->endChangeset(); } + } else { + $phpcsFile->addError($error, ($previousToken + 1), $errorCode); + } - // The open curly bracket must be the last code on the line. - $token = $phpcsFile->findNext(Tokens::$emptyTokens, ($curlyBracket + 1), null, true); - if ($tokens[$curlyBracket]['line'] === $tokens[$token]['line']) { - $error = 'The open curly bracket of a declare statement must be the last code on the line'; - $fix = $phpcsFile->addFixableError($error, $token, 'CodeFoundAfterCurlyBracket'); + }//end complainIfTokensNotAdjacent() - if ($fix === true) { - $phpcsFile->fixer->beginChangeset(); - $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($token - 1), null, true); - for ($i = ($prevToken + 1); $i < $token; $i++) { - $phpcsFile->fixer->replaceToken($i, ''); - } + /** + * Add an error if there is not exactly one space between tokens. Fix if possible. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $previousToken Index within $tokens[] of the first token. + * @param int $nextToken Index within $tokens[] of the second token. + * @param string $errorCode Error code suffix for this problem. + * + * @return void + */ + private function complainIfNotExactlyOneSpaceBetween($phpcsFile, $previousToken, $nextToken, $errorCode) + { + $contentBetween = $phpcsFile->getTokensAsString(($previousToken + 1), ($nextToken - $previousToken - 1), true); - $phpcsFile->fixer->addNewLineBefore($token); + if ($contentBetween === ' ') { + return; + } - $phpcsFile->fixer->endChangeset(); - } + $tokens = $phpcsFile->getTokens(); + $error = 'Expected one space between the '.$this->NAMES[$tokens[$previousToken]['code']].' and the '.$this->NAMES[$tokens[$nextToken]['code']].' in a declare statement'; + + if ($contentBetween === '') { + $fix = $phpcsFile->addFixableError($error, $nextToken, 'NoSpaceFound'.$errorCode); + if ($fix === true) { + $phpcsFile->fixer->addContentBefore($nextToken, ' '); } - }//end if - }//end process() + return; + } + + if (trim($contentBetween) !== '') { + $phpcsFile->addError($error, ($previousToken + 1), 'NonSpaceFound'.$errorCode); + return; + } + + $fix = $phpcsFile->addFixableError($error, ($nextToken - 1), 'ExtraSpaceFound'.$errorCode); + + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken(($previousToken + 1), ' '); + for ($i = ($previousToken + 2); $i < $nextToken; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->endChangeset(); + } + + }//end complainIfNotExactlyOneSpaceBetween() }//end class diff --git a/src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.1.inc b/src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.1.inc index f21daaff44..b1313267da 100644 --- a/src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.1.inc +++ b/src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.1.inc @@ -13,10 +13,10 @@ declare(/* test */ ticks /* test */ =1); declare( ticks =1 ); - declare(ticks=1) { } + declare(ticks=1) { $test = true; } declare(ticks=1) -{ } +{ $test = true; } declare(ticks=1) { @@ -48,3 +48,7 @@ declare(ticks=1) {$test = true; declare(ticks=1) { $test = true; } + +// Ensure that we handle non-number values properly. +declare(encoding='ISO-8859-1'); +declare ( encoding = 'ISO-8859-1' ) ; diff --git a/src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.1.inc.fixed b/src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.1.inc.fixed index 2338322327..c50517ceca 100644 --- a/src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.1.inc.fixed +++ b/src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.1.inc.fixed @@ -13,9 +13,11 @@ declare(/* test */ ticks /* test */ =1); declare(ticks=1); declare(ticks=1) { + $test = true; } declare(ticks=1) { + $test = true; } declare(ticks=1) { @@ -26,7 +28,8 @@ declare(ticks=1) { }//end comment declare(ticks=1) { -}$x =1; +} +$x =1; declare(ticks=1) { $test = true; @@ -36,19 +39,24 @@ declare(ticks // phpcs:ignore Standard.Category.SniffName -- fixing is undesirab =1); declare(ticks=1) { -}$x =1; // phpcs:ignore Standard.Category.SniffName -- fixing is undesirable +} +$x =1; // phpcs:ignore Standard.Category.SniffName -- fixing is undesirable declare(ticks=1) { // test } declare(ticks=1) { -$test = true; + $test = true; } declare(ticks=1) { /* test */ /* test */ -$test = true; + $test = true; } declare(ticks=1) { -$test = true; + $test = true; } + +// Ensure that we handle non-number values properly. +declare(encoding='ISO-8859-1'); +declare(encoding='ISO-8859-1'); diff --git a/src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.10.inc b/src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.10.inc new file mode 100644 index 0000000000..c6421aae47 --- /dev/null +++ b/src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.10.inc @@ -0,0 +1,3 @@ + +The line above is correct. The line below should complain that it's not the first line. + + +This line should complain. It's included because the optional semicolon is missing here. + + +This should complain that all three parts are not on the first line of the file. + + +This is fine because it's not strict_types. + + +These can be automatically fixed because there is an incorect number of spaces +between the end of the declare statement and the closing PHP tag. + + + + +This is missing its closing PHP tag. It should be the last test in this file. + 1, 11 => 3, 12 => 2, - 13 => 1, - 14 => 2, + 13 => 2, + 14 => 1, 16 => 3, + 18 => 1, 19 => 3, - 22 => 1, + 21 => 1, 24 => 1, - 26 => 3, + 26 => 2, 28 => 3, 34 => 2, + 38 => 1, 43 => 1, 46 => 1, 47 => 1, 49 => 1, + 54 => 6, + ]; + case 'DeclareStatementUnitTest.4.inc': + return [ + 3 => 1, + 4 => 1, + 5 => 1, + 6 => 1, + 7 => 1, + 8 => 1, + 9 => 1, + 10 => 1, + 11 => 1, + 12 => 1, + 13 => 1, + 14 => 1, + 15 => 1, + 16 => 1, + 17 => 1, + 18 => 1, + 19 => 1, + 20 => 1, + 21 => 1, + 23 => 1, + 25 => 1, + 27 => 1, + 29 => 1, + 31 => 1, + 33 => 1, + 35 => 1, + 37 => 1, + 39 => 1, + 41 => 1, + 43 => 1, + 45 => 1, + 46 => 1, + 47 => 1, + 48 => 1, + 49 => 1, + 50 => 1, + 52 => 1, + 54 => 1, + 56 => 1, + 58 => 1, + 60 => 1, + 62 => 1, + ]; + case 'DeclareStatementUnitTest.5.inc': + return [ + 3 => 1, + 6 => 1, + 10 => 2, + 18 => 1, + 19 => 1, + 20 => 1, + 23 => 1, ]; default: return []; From 8f89123db477fec33519ed0c828dea844ec8cf46 Mon Sep 17 00:00:00 2001 From: Dan Wallis Date: Wed, 31 Jul 2024 15:19:34 +0100 Subject: [PATCH 2/2] Support multi-directive statements --- .../Sniffs/Files/DeclareStatementSniff.php | 115 ++++++++++-------- .../Files/DeclareStatementUnitTest.1.inc | 12 ++ .../DeclareStatementUnitTest.1.inc.fixed | 12 ++ 3 files changed, 87 insertions(+), 52 deletions(-) diff --git a/src/Standards/PSR12/Sniffs/Files/DeclareStatementSniff.php b/src/Standards/PSR12/Sniffs/Files/DeclareStatementSniff.php index 10e632795b..dfd0c0c43b 100644 --- a/src/Standards/PSR12/Sniffs/Files/DeclareStatementSniff.php +++ b/src/Standards/PSR12/Sniffs/Files/DeclareStatementSniff.php @@ -82,70 +82,81 @@ public function process(File $phpcsFile, $stackPtr) $closeParen = $tokens[$openParen]['parenthesis_closer']; - $directive = $phpcsFile->findNext(T_STRING, ($openParen + 1), $closeParen); + $tokenBeforeDirective = $openParen; - if ($directive === false) { - // Live coding / parse error. - return; - } + do { + $directive = $phpcsFile->findNext(T_STRING, ($tokenBeforeDirective + 1), $closeParen); - // There should be no space between open parenthesis and the directive. - $this->complainIfTokensNotAdjacent( - $phpcsFile, - $openParen, - $directive, - 'SpaceFoundBeforeDirective' - ); + if ($directive === false) { + // Live coding / parse error. + return; + } - // The directive must be in lowercase. - if ($tokens[$directive]['content'] !== strtolower($tokens[$directive]['content'])) { - $error = 'The directive of a declare statement must be in lowercase'; - $fix = $phpcsFile->addFixableError($error, $directive, 'DirectiveNotLowercase'); - if ($fix === true) { - $phpcsFile->fixer->replaceToken($directive, strtolower($tokens[$directive]['content'])); + if ($tokens[$tokenBeforeDirective]['code'] === T_OPEN_PARENTHESIS) { + // There should be no space between open parenthesis and the directive. + $this->complainIfTokensNotAdjacent( + $phpcsFile, + $tokenBeforeDirective, + $directive, + 'SpaceFoundBeforeDirective' + ); + // There's no 'else' clause here, because PSR12 makes no mention of + // formatting of the comma in a multi-directive statement. } - } - // When wishing to declare strict types in files containing markup outside PHP opening - // and closing tags, the declaration MUST be on the first line of the file and include - // an opening PHP tag, the strict types declaration and closing tag. - if ($tokens[$stackPtr]['line'] !== 1 && strtolower($tokens[$directive]['content']) === 'strict_types') { - $nonPHP = $phpcsFile->findNext(T_INLINE_HTML, 0); - if ($nonPHP !== false) { - $error = 'When declaring strict_types in a file with markup outside PHP tags, the declare statement must be on the first line'; - $phpcsFile->addError($error, $stackPtr, 'DeclareNotOnFirstLine'); + // The directive must be in lowercase. + if ($tokens[$directive]['content'] !== strtolower($tokens[$directive]['content'])) { + $error = 'The directive of a declare statement must be in lowercase'; + $fix = $phpcsFile->addFixableError($error, $directive, 'DirectiveNotLowercase'); + if ($fix === true) { + $phpcsFile->fixer->replaceToken($directive, strtolower($tokens[$directive]['content'])); + } } - } - $equals = $phpcsFile->findNext(T_EQUAL, ($directive + 1), $closeParen); + // When wishing to declare strict types in files containing markup outside PHP opening + // and closing tags, the declaration MUST be on the first line of the file and include + // an opening PHP tag, the strict types declaration and closing tag. + if ($tokens[$stackPtr]['line'] !== 1 && strtolower($tokens[$directive]['content']) === 'strict_types') { + $nonPHP = $phpcsFile->findNext(T_INLINE_HTML, 0); + if ($nonPHP !== false) { + $error = 'When declaring strict_types in a file with markup outside PHP tags, the declare statement must be on the first line'; + $phpcsFile->addError($error, $stackPtr, 'DeclareNotOnFirstLine'); + } + } - if ($equals === false) { - // Live coding / parse error. - return; - } + $equals = $phpcsFile->findNext(T_EQUAL, ($directive + 1), $closeParen); - // There should be no space between directive and the equal sign. - $this->complainIfTokensNotAdjacent( - $phpcsFile, - $directive, - $equals, - 'SpaceFoundAfterDirective' - ); + if ($equals === false) { + // Live coding / parse error. + return; + } - $value = $phpcsFile->findNext([T_LNUMBER, T_CONSTANT_ENCAPSED_STRING], ($equals + 1), $closeParen); + // There should be no space between directive and the equal sign. + $this->complainIfTokensNotAdjacent( + $phpcsFile, + $directive, + $equals, + 'SpaceFoundAfterDirective' + ); - if ($value === false) { - // Live coding / parse error. - return; - } + $value = $phpcsFile->findNext([T_LNUMBER, T_CONSTANT_ENCAPSED_STRING], ($equals + 1), $closeParen); - // There should be no space between equals sign and directive value. - $this->complainIfTokensNotAdjacent( - $phpcsFile, - $equals, - $value, - 'SpaceFoundBeforeDirectiveValue' - ); + if ($value === false) { + // Live coding / parse error. + return; + } + + // There should be no space between equals sign and directive value. + $this->complainIfTokensNotAdjacent( + $phpcsFile, + $equals, + $value, + 'SpaceFoundBeforeDirectiveValue' + ); + + // Handle multi-directive statements. + $tokenBeforeDirective = $phpcsFile->findNext(T_COMMA, ($value + 1), $closeParen); + } while ($tokenBeforeDirective !== false); // $closeParen was defined earlier as $closeParen = $tokens[$openParen]['parenthesis_closer']; // There should be no space between directive value and closing parenthesis. diff --git a/src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.1.inc b/src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.1.inc index b1313267da..5fbfa6ec37 100644 --- a/src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.1.inc +++ b/src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.1.inc @@ -52,3 +52,15 @@ declare(ticks=1) { $test = true; // Ensure that we handle non-number values properly. declare(encoding='ISO-8859-1'); declare ( encoding = 'ISO-8859-1' ) ; + +// Multi-directive statements should be handled correctly too. +declare(strict_types=1,ticks=1); +// Note that PSR12 makes no mention of the formatting of the comma, so all of these should be valid. +declare(strict_types=1, ticks=1); +declare(strict_types=1, ticks=1); +declare(strict_types=1, +ticks=1); +declare(strict_types=1, + ticks=1); +declare(strict_types=1 , ticks=1); +declare(strict_types=1 ,ticks=1); diff --git a/src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.1.inc.fixed b/src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.1.inc.fixed index c50517ceca..ec6ec64cea 100644 --- a/src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.1.inc.fixed +++ b/src/Standards/PSR12/Tests/Files/DeclareStatementUnitTest.1.inc.fixed @@ -60,3 +60,15 @@ declare(ticks=1) { // Ensure that we handle non-number values properly. declare(encoding='ISO-8859-1'); declare(encoding='ISO-8859-1'); + +// Multi-directive statements should be handled correctly too. +declare(strict_types=1,ticks=1); +// Note that PSR12 makes no mention of the formatting of the comma, so all of these should be valid. +declare(strict_types=1, ticks=1); +declare(strict_types=1, ticks=1); +declare(strict_types=1, +ticks=1); +declare(strict_types=1, + ticks=1); +declare(strict_types=1 , ticks=1); +declare(strict_types=1 ,ticks=1);