diff --git a/Universal/Docs/DeclareStatements/BlockModeStandard.xml b/Universal/Docs/DeclareStatements/BlockModeStandard.xml new file mode 100644 index 00000000..5c9be11d --- /dev/null +++ b/Universal/Docs/DeclareStatements/BlockModeStandard.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + { + // Code. +} + +declare(strict_types=1) : + // Code. +enddeclare; + ]]> + + + diff --git a/Universal/Sniffs/DeclareStatements/BlockModeSniff.php b/Universal/Sniffs/DeclareStatements/BlockModeSniff.php new file mode 100644 index 00000000..7e79a36b --- /dev/null +++ b/Universal/Sniffs/DeclareStatements/BlockModeSniff.php @@ -0,0 +1,287 @@ + 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) { + $contentsLC = \strtolower($tokens[$i]['content']); + if (isset($this->allowedDirectives[$contentsLC])) { + $phpcsFile->recordMetric($i, self::DECLARE_TYPE_METRIC, $contentsLC); + $directiveStrings[$contentsLC] = 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 ($usesBlockMode) { + $phpcsFile->recordMetric($stackPtr, self::DECLARE_SCOPE_METRIC, 'Block mode'); + } else { + $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'])) { + $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 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 the declare statement.', + $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' // <= Duplicate error code for different message (line 175) + ); + 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; + } + } +} diff --git a/Universal/Tests/DeclareStatements/BlockModeUnitTest.1.inc b/Universal/Tests/DeclareStatements/BlockModeUnitTest.1.inc new file mode 100644 index 00000000..0a463018 --- /dev/null +++ b/Universal/Tests/DeclareStatements/BlockModeUnitTest.1.inc @@ -0,0 +1,78 @@ + => + */ + public function getErrorList($testFile = '') + { + switch ($testFile) { + 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 'BlockModeUnitTest.3.inc': + return [ + 7 => 1, + 11 => 1, + 19 => 1, + 20 => 1, + 31 => 1, + 38 => 1, + 42 => 1, + 43 => 1, + ]; + + 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 '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: + return []; + } + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() + { + return []; + } +}