From 14d6117516fdcbdc0014486d93c195242da9fbe2 Mon Sep 17 00:00:00 2001 From: quicksketch Date: Sun, 3 Jan 2021 19:18:49 -0800 Subject: [PATCH] Issue #12: Upgrade to latest version of PHPCS. Matching Drupal 8.3.12 version. --- .../CommentParser/FunctionCommentParser.php | 112 -- .../CommentParser/ParameterElement.php | 66 - .../Backdrop/CommentParser/ReturnElement.php | 129 -- .../Backdrop/Sniffs/Array/ArraySniff.php | 180 -- .../Backdrop/Sniffs/Arrays/ArraySniff.php | 248 +++ .../ClassDefinitionClosingBraceSpaceSniff.php | 75 - .../CSS/ClassDefinitionNameSpacingSniff.php | 80 +- .../ClassDefinitionOpeningBraceSpaceSniff.php | 92 - .../Sniffs/CSS/ColourDefinitionSniff.php | 44 +- .../Backdrop/Sniffs/CSS/IndentationSniff.php | 115 -- .../Classes/ClassCreateInstanceSniff.php | 116 +- .../Sniffs/Classes/ClassDeclarationSniff.php | 202 +- .../Classes/FullyQualifiedNamespaceSniff.php | 212 +++ .../Sniffs/Classes/InterfaceNameSniff.php | 25 +- .../Classes/PropertyDeclarationSniff.php | 115 ++ .../Classes/UnusedUseStatementSniff.php | 217 +++ .../Sniffs/Classes/UseGlobalClassSniff.php | 135 ++ .../Classes/UseLeadingBackslashSniff.php | 75 + .../Sniffs/Commenting/ClassCommentSniff.php | 154 ++ .../Commenting/DataTypeNamespaceSniff.php | 116 ++ .../Sniffs/Commenting/DeprecatedSniff.php | 230 +++ .../Commenting/DocCommentAlignmentSniff.php | 193 +- .../Sniffs/Commenting/DocCommentSniff.php | 531 ++++++ .../Sniffs/Commenting/DocCommentStarSniff.php | 89 + .../Sniffs/Commenting/FileCommentSniff.php | 254 ++- .../Commenting/FunctionCommentSniff.php | 1342 ++++++++----- .../Commenting/GenderNeutralCommentSniff.php | 60 + .../Sniffs/Commenting/HookCommentSniff.php | 129 ++ .../Sniffs/Commenting/InlineCommentSniff.php | 564 ++++-- .../Commenting/InlineVariableCommentSniff.php | 151 ++ .../Commenting/PostStatementCommentSniff.php | 102 + .../Sniffs/Commenting/TodoCommentSniff.php | 191 ++ .../Commenting/VariableCommentSniff.php | 209 +++ .../ControlSignatureSniff.php | 326 +++- .../ElseCatchNewlineSniff.php | 72 - .../Sniffs/ControlStructures/ElseIfSniff.php | 85 +- .../InlineControlStructureSniff.php | 40 +- .../TemplateControlStructureSniff.php | 76 - .../Sniffs/Files/EndFileNewlineSniff.php | 130 ++ .../Sniffs/Files/FileEncodingSniff.php | 95 + .../Backdrop/Sniffs/Files/LineLengthSniff.php | 86 +- .../Sniffs/Files/TxtFileLineLengthSniff.php | 64 +- .../Formatting/DisallowCloseTagSniff.php | 59 - .../Formatting/MultiLineAssignmentSniff.php | 47 +- .../MultipleStatementAlignmentSniff.php | 350 ++++ .../Sniffs/Formatting/SpaceInlineIfSniff.php | 70 +- .../Formatting/SpaceUnaryOperatorSniff.php | 108 +- .../Functions/DiscouragedFunctionsSniff.php | 63 +- .../Functions/FunctionDeclarationSniff.php | 66 - .../Sniffs/InfoFiles/AutoAddedKeysSniff.php | 89 + .../Sniffs/InfoFiles/ClassFilesSniff.php | 143 +- .../Sniffs/InfoFiles/RequiredSniff.php | 52 +- .../Sniffs/Methods/MethodDeclarationSniff.php | 40 + .../KeywordLowerCaseSniff.php | 58 - .../NamingConventions/ValidClassNameSniff.php | 36 +- .../ValidFunctionNameSniff.php | 135 +- .../NamingConventions/ValidGlobalSniff.php | 135 +- .../ValidVariableNameSniff.php | 115 +- .../Sniffs/Scope/MethodScopeSniff.php | 107 ++ .../Sniffs/Semantics/ConstantNameSniff.php | 60 +- .../Sniffs/Semantics/EmptyInstallSniff.php | 27 +- .../Sniffs/Semantics/FunctionAliasSniff.php | 456 ++--- .../Sniffs/Semantics/FunctionCall.php | 223 ++- .../Sniffs/Semantics/FunctionCallSniff.php | 252 --- .../Sniffs/Semantics/FunctionDefinition.php | 40 +- .../Sniffs/Semantics/FunctionTSniff.php | 111 +- .../Semantics/FunctionTriggerErrorSniff.php | 185 ++ .../Semantics/FunctionWatchdogSniff.php | 47 +- .../Sniffs/Semantics/InstallHooksSniff.php | 26 +- .../Sniffs/Semantics/InstallTSniff.php | 84 - .../Semantics/LStringTranslatableSniff.php | 41 +- .../Sniffs/Semantics/PregSecuritySniff.php | 70 +- .../Sniffs/Semantics/RemoteAddressSniff.php | 34 +- .../Sniffs/Semantics/TInHookMenuSniff.php | 27 +- .../Sniffs/Semantics/TInHookSchemaSniff.php | 27 +- .../Strings/ConcatenationSpacingSniff.php | 53 - .../Strings/UnnecessaryStringConcatSniff.php | 44 +- .../WhiteSpace/CloseBracketSpacingSniff.php | 55 +- .../Backdrop/Sniffs/WhiteSpace/CommaSniff.php | 85 + .../Sniffs/WhiteSpace/EmptyLinesSniff.php | 55 +- .../Sniffs/WhiteSpace/FileEndSniff.php | 103 - .../Sniffs/WhiteSpace/NamespaceSniff.php | 62 + .../WhiteSpace/ObjectOperatorIndentSniff.php | 187 +- .../WhiteSpace/ObjectOperatorSpacingSniff.php | 80 +- .../WhiteSpace/OpenBracketSpacingSniff.php | 55 +- .../WhiteSpace/OperatorSpacingSniff.php | 45 - .../WhiteSpace/ScopeClosingBraceSniff.php | 146 +- .../Sniffs/WhiteSpace/ScopeIndentSniff.php | 1660 ++++++++++++++--- coder_sniffer/Backdrop/drupalcs.info | 3 - coder_sniffer/Backdrop/ruleset.xml | 133 +- 90 files changed, 9416 insertions(+), 4260 deletions(-) delete mode 100644 coder_sniffer/Backdrop/CommentParser/FunctionCommentParser.php delete mode 100644 coder_sniffer/Backdrop/CommentParser/ParameterElement.php delete mode 100644 coder_sniffer/Backdrop/CommentParser/ReturnElement.php delete mode 100644 coder_sniffer/Backdrop/Sniffs/Array/ArraySniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Arrays/ArraySniff.php delete mode 100644 coder_sniffer/Backdrop/Sniffs/CSS/ClassDefinitionClosingBraceSpaceSniff.php delete mode 100644 coder_sniffer/Backdrop/Sniffs/CSS/ClassDefinitionOpeningBraceSpaceSniff.php delete mode 100644 coder_sniffer/Backdrop/Sniffs/CSS/IndentationSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Classes/FullyQualifiedNamespaceSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Classes/PropertyDeclarationSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Classes/UnusedUseStatementSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Classes/UseGlobalClassSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Classes/UseLeadingBackslashSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Commenting/ClassCommentSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Commenting/DataTypeNamespaceSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Commenting/DeprecatedSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Commenting/DocCommentSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Commenting/DocCommentStarSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Commenting/GenderNeutralCommentSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Commenting/HookCommentSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Commenting/InlineVariableCommentSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Commenting/PostStatementCommentSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Commenting/TodoCommentSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Commenting/VariableCommentSniff.php delete mode 100644 coder_sniffer/Backdrop/Sniffs/ControlStructures/ElseCatchNewlineSniff.php delete mode 100644 coder_sniffer/Backdrop/Sniffs/ControlStructures/TemplateControlStructureSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Files/EndFileNewlineSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Files/FileEncodingSniff.php delete mode 100644 coder_sniffer/Backdrop/Sniffs/Formatting/DisallowCloseTagSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Formatting/MultipleStatementAlignmentSniff.php delete mode 100644 coder_sniffer/Backdrop/Sniffs/Functions/FunctionDeclarationSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/InfoFiles/AutoAddedKeysSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Methods/MethodDeclarationSniff.php delete mode 100644 coder_sniffer/Backdrop/Sniffs/NamingConventions/KeywordLowerCaseSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Scope/MethodScopeSniff.php delete mode 100644 coder_sniffer/Backdrop/Sniffs/Semantics/FunctionCallSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/Semantics/FunctionTriggerErrorSniff.php delete mode 100644 coder_sniffer/Backdrop/Sniffs/Semantics/InstallTSniff.php delete mode 100644 coder_sniffer/Backdrop/Sniffs/Strings/ConcatenationSpacingSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/WhiteSpace/CommaSniff.php delete mode 100644 coder_sniffer/Backdrop/Sniffs/WhiteSpace/FileEndSniff.php create mode 100644 coder_sniffer/Backdrop/Sniffs/WhiteSpace/NamespaceSniff.php delete mode 100644 coder_sniffer/Backdrop/Sniffs/WhiteSpace/OperatorSpacingSniff.php delete mode 100644 coder_sniffer/Backdrop/drupalcs.info diff --git a/coder_sniffer/Backdrop/CommentParser/FunctionCommentParser.php b/coder_sniffer/Backdrop/CommentParser/FunctionCommentParser.php deleted file mode 100644 index 5a35032..0000000 --- a/coder_sniffer/Backdrop/CommentParser/FunctionCommentParser.php +++ /dev/null @@ -1,112 +0,0 @@ -previousElement, - $tokens, - $this->phpcsFile - ); - - $this->params[] = $param; - return $param; - - }//end parseParam() - - - /** - * Parses return elements. - * - * @param array(string) $tokens The tokens that comprise this sub element. - * - * @return Backdrop_CommentParser_ReturnElement - */ - protected function parseReturn($tokens) - { - $return = new Backdrop_CommentParser_ReturnElement( - $this->previousElement, - $tokens, - 'return', - $this->phpcsFile - ); - - $this->return = $return; - return $return; - - }//end parseReturn() - - - /** - * Returns the parameter elements that this function comment contains. - * - * Returns an empty array if no parameter elements are contained within - * this function comment. - * - * @return array(Backdrop_CommentParser_ParameterElement) - */ - public function getParams() - { - return $this->params; - - }//end getParams() - - - /** - * Returns the return element in this fucntion comment. - * - * Returns null if no return element exists in the comment. - * - * @return Backdrop_CommentParser_ReturnElement - */ - public function getReturn() - { - return $this->return; - - }//end getReturn() - - -}//end class - -?> diff --git a/coder_sniffer/Backdrop/CommentParser/ParameterElement.php b/coder_sniffer/Backdrop/CommentParser/ParameterElement.php deleted file mode 100644 index b8c31ec..0000000 --- a/coder_sniffer/Backdrop/CommentParser/ParameterElement.php +++ /dev/null @@ -1,66 +0,0 @@ -processSubElement('type', '', ' '); - } else if ($tokens[1] === '...') { - // Insert two fake tokens for the parameter type. - array_unshift($tokens, 'unknown'); - array_unshift($tokens, ' '); - parent::__construct($previousElement, $tokens, $phpcsFile); - } else { - parent::__construct($previousElement, $tokens, $phpcsFile); - } - - }//end __construct() - - -}//end class - -?> diff --git a/coder_sniffer/Backdrop/CommentParser/ReturnElement.php b/coder_sniffer/Backdrop/CommentParser/ReturnElement.php deleted file mode 100644 index c16e248..0000000 --- a/coder_sniffer/Backdrop/CommentParser/ReturnElement.php +++ /dev/null @@ -1,129 +0,0 @@ -$element = $content; - $this->$whitespace = $whitespaceBefore; - - }//end processSubElement() - - - /** - * Returns the value of the tag. - * - * @return string - */ - public function getValue() - { - return $this->value; - - }//end getValue() - - - /** - * Returns the comment associated with the value of this tag. - * - * @return string - */ - public function getComment() - { - return $this->comment; - - }//end getComment() - - - /** - * Returns the witespace before the content of this tag. - * - * @return string - */ - public function getWhitespaceBeforeValue() - { - return $this->valueWhitespace; - - }//end getWhitespaceBeforeValue() - - - /** - * Returns the witespace before the content of this tag. - * - * @return string - */ - public function getWhitespaceBeforeComment() - { - return $this->commentWhitespace; - - }//end getWhitespaceBeforeComment() - - -}//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Array/ArraySniff.php b/coder_sniffer/Backdrop/Sniffs/Array/ArraySniff.php deleted file mode 100644 index 2e45e53..0000000 --- a/coder_sniffer/Backdrop/Sniffs/Array/ArraySniff.php +++ /dev/null @@ -1,180 +0,0 @@ -getTokens(); - $lastItem = $phpcsFile->findPrevious( - PHP_CodeSniffer_Tokens::$emptyTokens, - ($tokens[$stackPtr]['parenthesis_closer'] - 1), - $stackPtr, - true - ); - - // Empty array. - if ($lastItem === $tokens[$stackPtr]['parenthesis_opener']) { - return; - } - - // Inline array. - $isInlineArray = $tokens[$tokens[$stackPtr]['parenthesis_opener']]['line'] == $tokens[$tokens[$stackPtr]['parenthesis_closer']]['line']; - - // Check if the last item in a multiline array has a "closing" comma. - if ($tokens[$lastItem]['code'] !== T_COMMA && $isInlineArray === false - && $tokens[($lastItem + 1)]['code'] !== T_CLOSE_PARENTHESIS - ) { - $phpcsFile->addWarning('A comma should follow the last multiline array item. Found: '.$tokens[$lastItem]['content'], $lastItem); - return; - } - - if ($tokens[$lastItem]['code'] === T_COMMA && $isInlineArray === true) { - $phpcsFile->addWarning('Last item of an inline array must not be followed by a comma', $lastItem); - } - - if ($isInlineArray === true) { - // Check if this array contains at least 3 elements and exceeds the 80 - // character line length. - if ($tokens[$tokens[$stackPtr]['parenthesis_closer']]['column'] > 80) { - $comma1 = $phpcsFile->findNext(T_COMMA, ($stackPtr + 1), $tokens[$stackPtr]['parenthesis_closer']); - if ($comma1 !== false) { - $comma2 = $phpcsFile->findNext(T_COMMA, ($comma1 + 1), $tokens[$stackPtr]['parenthesis_closer']); - if ($comma2 !== false) { - $error = 'If the line declaring an array spans longer than 80 characters, each element should be broken into its own line'; - $phpcsFile->addError($error, $stackPtr, 'LongLineDeclaration'); - } - } - } - - // Only continue for multi line arrays. - return; - } - - // Special case: Opening two multi line structures in one line is ugly. - if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) { - end($tokens[$stackPtr]['nested_parenthesis']); - $outerNesting = key($tokens[$stackPtr]['nested_parenthesis']); - if ($tokens[$outerNesting]['line'] === $tokens[$stackPtr]['line']) { - // We could throw a warning here that the start of the array - // definition should be on a new line by itself, but we just ignore - // it for now as this is not defined as standard. - return; - } - } - - // Find the first token on this line. - $firstLineColumn = $tokens[$stackPtr]['column']; - for ($i = $stackPtr; $i >= 0; $i--) { - // Record the first code token on the line. - if ($tokens[$i]['code'] !== T_WHITESPACE) { - $firstLineColumn = $tokens[$i]['column']; - // This could be a multi line string or comment beginning with white - // spaces. - $trimmed = ltrim($tokens[$i]['content']); - if ($trimmed !== $tokens[$i]['content']) { - $firstLineColumn = ($firstLineColumn + strpos($tokens[$i]['content'], $trimmed)); - } - } - - // It's the start of the line, so we've found our first php token. - if ($tokens[$i]['column'] === 1) { - break; - } - } - - $lineStart = $stackPtr; - // Iterate over all lines of this array. - while ($lineStart < $tokens[$stackPtr]['parenthesis_closer']) { - // Find next line start. - $newLineStart = $lineStart; - while ($tokens[$newLineStart]['line'] == $tokens[$lineStart]['line']) { - $newLineStart = $phpcsFile->findNext( - PHP_CodeSniffer_Tokens::$emptyTokens, - ($newLineStart + 1), - ($tokens[$stackPtr]['parenthesis_closer'] + 1), - true - ); - if ($newLineStart === false) { - break 2; - } - } - - if ($newLineStart === $tokens[$stackPtr]['parenthesis_closer']) { - // End of the array reached. - if ($tokens[$newLineStart]['column'] !== $firstLineColumn) { - $error = 'Array closing indentation error, expected %s spaces but found %s'; - $data = array( - $firstLineColumn - 1, - $tokens[$newLineStart]['column'] - 1, - ); - $phpcsFile->addError($error, $newLineStart, 'ArrayClosingIndentation', $data); - } - - break; - } - - // Skip lines in nested structures. - $innerNesting = end($tokens[$newLineStart]['nested_parenthesis']); - if ($innerNesting === $tokens[$stackPtr]['parenthesis_closer'] - && $tokens[$newLineStart]['column'] !== ($firstLineColumn + 2) - ) { - $error = 'Array indentation error, expected %s spaces but found %s'; - $data = array( - $firstLineColumn + 1, - $tokens[$newLineStart]['column'] - 1, - ); - $phpcsFile->addError($error, $newLineStart, 'ArrayIndentation', $data); - } - - $lineStart = $newLineStart; - }//end while - - }//end process() - - -}//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Arrays/ArraySniff.php b/coder_sniffer/Backdrop/Sniffs/Arrays/ArraySniff.php new file mode 100644 index 0000000..e7be5ea --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Arrays/ArraySniff.php @@ -0,0 +1,248 @@ + + */ + public function register() + { + return [ + T_ARRAY, + T_OPEN_SHORT_ARRAY, + ]; + + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @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(); + + // Support long and short syntax. + $parenthesisOpener = 'parenthesis_opener'; + $parenthesisCloser = 'parenthesis_closer'; + if ($tokens[$stackPtr]['code'] === T_OPEN_SHORT_ARRAY) { + $parenthesisOpener = 'bracket_opener'; + $parenthesisCloser = 'bracket_closer'; + } + + // Sanity check: this can sometimes be NULL if the array was not correctly + // parsed. + if ($tokens[$stackPtr][$parenthesisCloser] === null) { + return; + } + + $lastItem = $phpcsFile->findPrevious( + Tokens::$emptyTokens, + ($tokens[$stackPtr][$parenthesisCloser] - 1), + $stackPtr, + true + ); + + // Empty array. + if ($lastItem === $tokens[$stackPtr][$parenthesisOpener]) { + return; + } + + // Inline array. + $isInlineArray = $tokens[$tokens[$stackPtr][$parenthesisOpener]]['line'] === $tokens[$tokens[$stackPtr][$parenthesisCloser]]['line']; + + // Check if the last item in a multiline array has a "closing" comma. + if ($tokens[$lastItem]['code'] !== T_COMMA && $isInlineArray === false + && $tokens[($lastItem + 1)]['code'] !== T_CLOSE_PARENTHESIS + && $tokens[($lastItem + 1)]['code'] !== T_CLOSE_SHORT_ARRAY + && isset(Tokens::$heredocTokens[$tokens[$lastItem]['code']]) === false + ) { + $data = [$tokens[$lastItem]['content']]; + $fix = $phpcsFile->addFixableWarning('A comma should follow the last multiline array item. Found: %s', $lastItem, 'CommaLastItem', $data); + if ($fix === true) { + $phpcsFile->fixer->addContent($lastItem, ','); + } + + return; + } + + // Find the first token on this line. + $firstLineColumn = $tokens[$stackPtr]['column']; + for ($i = $stackPtr; $i >= 0; $i--) { + // If there is a PHP open tag then this must be a template file where we + // don't check indentation. + if ($tokens[$i]['code'] === T_OPEN_TAG) { + return; + } + + // Record the first code token on the line. + if ($tokens[$i]['code'] !== T_WHITESPACE) { + $firstLineColumn = $tokens[$i]['column']; + // This could be a multi line string or comment beginning with white + // spaces. + $trimmed = ltrim($tokens[$i]['content']); + if ($trimmed !== $tokens[$i]['content']) { + $firstLineColumn = ($firstLineColumn + strpos($tokens[$i]['content'], $trimmed)); + } + } + + // It's the start of the line, so we've found our first php token. + if ($tokens[$i]['column'] === 1) { + break; + } + }//end for + + $lineStart = $stackPtr; + // Iterate over all lines of this array. + while ($lineStart < $tokens[$stackPtr][$parenthesisCloser]) { + // Find next line start. + $newLineStart = $lineStart; + $currentLine = $tokens[$newLineStart]['line']; + while ($currentLine >= $tokens[$newLineStart]['line']) { + $newLineStart = $phpcsFile->findNext( + Tokens::$emptyTokens, + ($newLineStart + 1), + ($tokens[$stackPtr][$parenthesisCloser] + 1), + true + ); + + if ($newLineStart === false) { + break 2; + } + + // Long array syntax: Skip nested arrays, they are checked in a next + // run. + if ($tokens[$newLineStart]['code'] === T_ARRAY) { + $newLineStart = $tokens[$newLineStart]['parenthesis_closer']; + $currentLine = $tokens[$newLineStart]['line']; + } + + // Short array syntax: Skip nested arrays, they are checked in a next + // run. + if ($tokens[$newLineStart]['code'] === T_OPEN_SHORT_ARRAY) { + $newLineStart = $tokens[$newLineStart]['bracket_closer']; + $currentLine = $tokens[$newLineStart]['line']; + } + + // Nested structures such as closures: skip those, they are checked + // in other sniffs. If the conditions of a token are different it + // means that it is in a different nesting level. + if ($tokens[$newLineStart]['conditions'] !== $tokens[$stackPtr]['conditions']) { + // Jump to the end of the closure. + $conditionKeys = array_keys($tokens[$newLineStart]['conditions']); + $closureToken = end($conditionKeys); + if (isset($tokens[$closureToken]['scope_closer']) === true) { + $newLineStart = $tokens[$closureToken]['scope_closer']; + $currentLine = $tokens[$closureToken]['line']; + } else { + $currentLine++; + } + } + }//end while + + if ($newLineStart === $tokens[$stackPtr][$parenthesisCloser]) { + // End of the array reached. + if ($tokens[$newLineStart]['column'] !== $firstLineColumn) { + $error = 'Array closing indentation error, expected %s spaces but found %s'; + $data = [ + ($firstLineColumn - 1), + ($tokens[$newLineStart]['column'] - 1), + ]; + $fix = $phpcsFile->addFixableError($error, $newLineStart, 'ArrayClosingIndentation', $data); + if ($fix === true) { + if ($tokens[$newLineStart]['column'] === 1) { + $phpcsFile->fixer->addContentBefore($newLineStart, str_repeat(' ', ($firstLineColumn - 1))); + } else { + $phpcsFile->fixer->replaceToken(($newLineStart - 1), str_repeat(' ', ($firstLineColumn - 1))); + } + } + } + + break; + } + + $expectedColumn = ($firstLineColumn + 2); + // If the line starts with "->" then we assume an additional level of + // indentation. + if ($tokens[$newLineStart]['code'] === T_OBJECT_OPERATOR) { + $expectedColumn += 2; + } + + if ($tokens[$newLineStart]['column'] !== $expectedColumn) { + // Skip lines in nested structures such as a function call within an + // array, no defined coding standard for those. + $innerNesting = empty($tokens[$newLineStart]['nested_parenthesis']) === false + && end($tokens[$newLineStart]['nested_parenthesis']) < $tokens[$stackPtr][$parenthesisCloser]; + // Skip lines that are part of a multi-line string. + $isMultiLineString = $tokens[($newLineStart - 1)]['code'] === T_CONSTANT_ENCAPSED_STRING + && substr($tokens[($newLineStart - 1)]['content'], -1) === $phpcsFile->eolChar; + // Skip NOWDOC or HEREDOC lines. + $nowDoc = isset(Tokens::$heredocTokens[$tokens[$newLineStart]['code']]); + if ($innerNesting === false && $isMultiLineString === false && $nowDoc === false) { + $error = 'Array indentation error, expected %s spaces but found %s'; + $data = [ + ($expectedColumn - 1), + ($tokens[$newLineStart]['column'] - 1), + ]; + $fix = $phpcsFile->addFixableError($error, $newLineStart, 'ArrayIndentation', $data); + if ($fix === true) { + if ($tokens[$newLineStart]['column'] === 1) { + $phpcsFile->fixer->addContentBefore($newLineStart, str_repeat(' ', ($expectedColumn - 1))); + } else { + $phpcsFile->fixer->replaceToken(($newLineStart - 1), str_repeat(' ', ($expectedColumn - 1))); + } + } + } + }//end if + + $lineStart = $newLineStart; + }//end while + + }//end process() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/CSS/ClassDefinitionClosingBraceSpaceSniff.php b/coder_sniffer/Backdrop/Sniffs/CSS/ClassDefinitionClosingBraceSpaceSniff.php deleted file mode 100644 index 3191797..0000000 --- a/coder_sniffer/Backdrop/Sniffs/CSS/ClassDefinitionClosingBraceSpaceSniff.php +++ /dev/null @@ -1,75 +0,0 @@ -getTokens(); - - // Do not check nested style definitions as, for example, in @media style rules. - $start = $tokens[$stackPtr]['bracket_opener']; - $nested = $phpcsFile->findPrevious(T_CLOSE_CURLY_BRACKET, ($stackPtr - 1), $start); - if ($nested !== false) { - return; - } - - $prev = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr - 1), null, true); - if ($prev !== false && $tokens[$prev]['line'] !== ($tokens[$stackPtr]['line'] - 1)) { - $error = 'Expected exactly one new line before closing brace of class definition'; - $phpcsFile->addError($error, $stackPtr, 'SpacingBeforeClose'); - } - - }//end process() - - -}//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/CSS/ClassDefinitionNameSpacingSniff.php b/coder_sniffer/Backdrop/Sniffs/CSS/ClassDefinitionNameSpacingSniff.php index 7b77580..f9da5d4 100644 --- a/coder_sniffer/Backdrop/Sniffs/CSS/ClassDefinitionNameSpacingSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/CSS/ClassDefinitionNameSpacingSniff.php @@ -1,42 +1,46 @@ */ - public $supportedTokenizers = array('CSS'); + public $supportedTokenizers = ['CSS']; /** * Returns the token types that this sniff is interested in. * - * @return array(int) + * @return array */ public function register() { - return array(T_OPEN_CURLY_BRACKET); + return [T_OPEN_CURLY_BRACKET]; }//end register() @@ -44,30 +48,35 @@ public function register() /** * Processes the tokens that this sniff is interested in. * - * @param PHP_CodeSniffer_File $phpcsFile The file where the token was found. - * @param int $stackPtr The position in the stack where - * the token was found. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where the token was found. + * @param int $stackPtr The position in the stack where + * the token was found. * * @return void */ - public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); - // Find the first blank line before this openning brace, unless we get + // Do not check nested style definitions as, for example, in @media style rules. + $nested = $phpcsFile->findNext(T_OPEN_CURLY_BRACKET, ($stackPtr + 1), $tokens[$stackPtr]['bracket_closer']); + if ($nested !== false) { + return; + } + + // Find the first blank line before this opening brace, unless we get // to another style definition, comment or the start of the file. - $endTokens = array( - T_CLOSE_CURLY_BRACKET, - T_OPEN_CURLY_BRACKET, - T_COMMENT, - T_DOC_COMMENT, - T_OPEN_TAG, - ); + $endTokens = [ + T_OPEN_CURLY_BRACKET => T_OPEN_CURLY_BRACKET, + T_CLOSE_CURLY_BRACKET => T_CLOSE_CURLY_BRACKET, + T_OPEN_TAG => T_OPEN_TAG, + ]; + $endTokens += Tokens::$commentTokens; $foundContent = false; $currentLine = $tokens[$stackPtr]['line']; for ($i = ($stackPtr - 1); $i >= 0; $i--) { - if (in_array($tokens[$i]['code'], $endTokens) === true) { + if (isset($endTokens[$tokens[$i]['code']]) === true) { break; } @@ -76,20 +85,25 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) && strpos($tokens[($i + 1)]['content'], $phpcsFile->eolChar) === false ) { $error = 'Multiple selectors should each be on a single line'; - $phpcsFile->addError($error, ($i + 1), 'MultipleSelectors'); + $fix = $phpcsFile->addFixableError($error, ($i + 1), 'MultipleSelectors'); + if ($fix === true) { + $phpcsFile->fixer->addNewline($i); + } } // Selectors must be on the same line. if ($tokens[$i]['code'] === T_WHITESPACE && strpos($tokens[$i]['content'], $phpcsFile->eolChar) !== false - && in_array($tokens[($i - 1)]['code'], $endTokens) === false - && in_array($tokens[($i - 1)]['code'], array(T_WHITESPACE, T_COMMA)) == false + && isset($endTokens[$tokens[($i - 1)]['code']]) === false + && in_array($tokens[($i - 1)]['code'], [T_WHITESPACE, T_COMMA]) === false ) { $error = 'Selectors must be on a single line'; - $phpcsFile->addError($error, $i, 'SeletorSingleLine'); + $fix = $phpcsFile->addFixableError($error, $i, 'SeletorSingleLine'); + if ($fix === true) { + $phpcsFile->fixer->replaceToken($i, str_replace($phpcsFile->eolChar, ' ', $tokens[$i]['content'])); + } } - if ($tokens[$i]['line'] === $currentLine) { if ($tokens[$i]['code'] !== T_WHITESPACE) { $foundContent = true; @@ -104,11 +118,15 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) // at a gap before the style definition. $prev = $phpcsFile->findPrevious(T_WHITESPACE, $i, null, true); if ($prev !== false - && in_array($tokens[$prev]['code'], $endTokens) === false + && isset($endTokens[$tokens[$prev]['code']]) === false ) { $error = 'Blank lines are not allowed between class names'; - $phpcsFile->addError($error, ($i + 1), 'BlankLinesFound'); + $fix = $phpcsFile->addFixableError($error, ($i + 1), 'BlankLinesFound'); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($i + 1), ''); + } } + break; } @@ -120,5 +138,3 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/CSS/ClassDefinitionOpeningBraceSpaceSniff.php b/coder_sniffer/Backdrop/Sniffs/CSS/ClassDefinitionOpeningBraceSpaceSniff.php deleted file mode 100644 index 54b961a..0000000 --- a/coder_sniffer/Backdrop/Sniffs/CSS/ClassDefinitionOpeningBraceSpaceSniff.php +++ /dev/null @@ -1,92 +0,0 @@ -getTokens(); - - if ($tokens[($stackPtr - 1)]['code'] !== T_WHITESPACE) { - $error = 'Expected 1 space before opening brace of class definition; 0 found'; - $phpcsFile->addError($error, $stackPtr, 'NoneBefore'); - } else { - $content = $tokens[($stackPtr - 1)]['content']; - if ($content !== ' ') { - $length = strlen($content); - if ($length === 1) { - $length = 'tab'; - } - - $error = 'Expected 1 space before opening brace of class definition; %s found'; - $data = array($length); - $phpcsFile->addError($error, $stackPtr, 'Before', $data); - } - }//end if - - $end = $tokens[$stackPtr]['bracket_closer']; - // Do not check nested style definitions as, for example, in @media style rules. - $nested = $phpcsFile->findNext(T_OPEN_CURLY_BRACKET, ($stackPtr + 1), $end); - if ($nested !== false) { - return; - } - - $next = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); - if ($next !== false && $tokens[$next]['line'] !== ($tokens[$stackPtr]['line'] + 1)) { - $error = 'Expected exactly one new line after opening brace of class definition'; - $phpcsFile->addError($error, $stackPtr, 'After'); - } - - }//end process() - - -}//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/CSS/ColourDefinitionSniff.php b/coder_sniffer/Backdrop/Sniffs/CSS/ColourDefinitionSniff.php index a37be2d..01299a0 100644 --- a/coder_sniffer/Backdrop/Sniffs/CSS/ColourDefinitionSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/CSS/ColourDefinitionSniff.php @@ -1,16 +1,19 @@ */ - public $supportedTokenizers = array('CSS'); + public $supportedTokenizers = ['CSS']; /** * Returns the token types that this sniff is interested in. * - * @return array(int) + * @return array */ public function register() { - return array(T_COLOUR); + return [T_COLOUR]; }//end register() @@ -44,13 +47,13 @@ public function register() /** * Processes the tokens that this sniff is interested in. * - * @param PHP_CodeSniffer_File $phpcsFile The file where the token was found. - * @param int $stackPtr The position in the stack where - * the token was found. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where the token was found. + * @param int $stackPtr The position in the stack where + * the token was found. * * @return void */ - public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); $colour = $tokens[$stackPtr]['content']; @@ -58,16 +61,17 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) $expected = strtolower($colour); if ($colour !== $expected) { $error = 'CSS colours must be defined in lowercase; expected %s but found %s'; - $data = array( - $expected, - $colour, - ); - $phpcsFile->addError($error, $stackPtr, 'NotLower', $data); + $data = [ + $expected, + $colour, + ]; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'NotLower', $data); + if ($fix === true) { + $phpcsFile->fixer->replaceToken($stackPtr, $expected); + } } }//end process() }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/CSS/IndentationSniff.php b/coder_sniffer/Backdrop/Sniffs/CSS/IndentationSniff.php deleted file mode 100644 index 66ff1ff..0000000 --- a/coder_sniffer/Backdrop/Sniffs/CSS/IndentationSniff.php +++ /dev/null @@ -1,115 +0,0 @@ -getTokens(); - - $numTokens = (count($tokens) - 2); - $currentLine = 0; - $indentLevel = 0; - $nested = false; - for ($i = 1; $i < $numTokens; $i++) { - if ($tokens[$i]['code'] === T_COMMENT) { - // Dont check the indent of comments. - continue; - } - - if ($tokens[$i]['code'] === T_OPEN_CURLY_BRACKET) { - $indentLevel++; - // Check for nested style definitions as, for example, in @media style rules. - $end = $tokens[$i]['bracket_closer']; - $nested = $phpcsFile->findNext(T_OPEN_CURLY_BRACKET, ($stackPtr + 1), $end); - } else if ($tokens[$i + 1]['code'] === T_CLOSE_CURLY_BRACKET) { - $indentLevel--; - } - - if ($tokens[$i]['line'] === $currentLine) { - continue; - } - - // We started a new line, so check indent. - if ($tokens[$i]['code'] === T_WHITESPACE) { - $content = str_replace($phpcsFile->eolChar, '', $tokens[$i]['content']); - $foundIndent = strlen($content); - } else { - $foundIndent = 0; - } - - $expectedIndent = ($indentLevel * $this->indent); - if ($expectedIndent > 0 && strpos($tokens[$i]['content'], $phpcsFile->eolChar) !== false) { - if ($nested === false) { - $error = 'Blank lines are not allowed in class definitions'; - $phpcsFile->addError($error, $i, 'BlankLine'); - } - } else if ($foundIndent !== $expectedIndent) { - $error = 'Line indented incorrectly; expected %s spaces, found %s'; - $data = array( - $expectedIndent, - $foundIndent, - ); - $phpcsFile->addError($error, $i, 'Incorrect', $data); - } - - $currentLine = $tokens[$i]['line']; - }//end foreach - - }//end process() - -}//end class -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Classes/ClassCreateInstanceSniff.php b/coder_sniffer/Backdrop/Sniffs/Classes/ClassCreateInstanceSniff.php index 401f5ed..0f82c4b 100644 --- a/coder_sniffer/Backdrop/Sniffs/Classes/ClassCreateInstanceSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Classes/ClassCreateInstanceSniff.php @@ -2,38 +2,38 @@ /** * Class create instance Test. * - * PHP version 5 - * - * @category PHP - * @package PHP_CodeSniffer - * @author Peter Philipp - * @link http://pear.php.net/package/PHP_CodeSniffer + * @category PHP + * @package PHP_CodeSniffer + * @link http://pear.php.net/package/PHP_CodeSniffer */ +namespace Backdrop\Sniffs\Classes; + +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Tokens; + /** * Class create instance Test. * * Checks the declaration of the class is correct. * - * @category PHP - * @package PHP_CodeSniffer - * @author Peter Philipp - * @link http://pear.php.net/package/PHP_CodeSniffer + * @category PHP + * @package PHP_CodeSniffer + * @link http://pear.php.net/package/PHP_CodeSniffer */ -class Backdrop_Sniffs_Classes_ClassCreateInstanceSniff implements PHP_CodeSniffer_Sniff +class ClassCreateInstanceSniff implements Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { - return array( - T_NEW, - ); + return [T_NEW]; }//end register() @@ -41,31 +41,85 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token in the - * stack passed in $tokens. + * @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(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); - $nextParenthesis = $phpcsFile->findNext(array(T_OPEN_PARENTHESIS,T_SEMICOLON), $stackPtr, null, false, null, true); - if ($tokens[$nextParenthesis]['code'] != T_OPEN_PARENTHESIS || $tokens[$nextParenthesis]['line'] != $tokens[$stackPtr]['line']) { - $error = 'Calling class constructors must always include parentheses'; - $phpcsFile->addError($error, $nextParenthesis); - return; - } - - if ($tokens[$nextParenthesis-1]['code'] == T_WHITESPACE) { - $error = 'Between the class name and the opening parenthesis spaces are not welcome'; - $phpcsFile->addError($error, $nextParenthesis-1); + $commaOrColon = $phpcsFile->findNext([T_SEMICOLON, T_COLON, T_COMMA], ($stackPtr + 1)); + if ($commaOrColon === false) { + // Syntax error, nothing we can do. return; } + + // Search for an opening parenthesis in the current statement until the + // next semicolon or comma. + $nextParenthesis = $phpcsFile->findNext(T_OPEN_PARENTHESIS, ($stackPtr + 1), $commaOrColon); + if ($nextParenthesis === false) { + $error = 'Calling class constructors must always include parentheses'; + $constructor = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true, null, true); + // We can invoke the fixer if we know this is a static constructor + // function call or constructor calls with namespaces, example + // "new \DOMDocument;" or constructor with class names in variables + // "new $controller;". + if ($tokens[$constructor]['code'] === T_STRING + || $tokens[$constructor]['code'] === T_NS_SEPARATOR + || ($tokens[$constructor]['code'] === T_VARIABLE + && $tokens[($constructor + 1)]['code'] === T_SEMICOLON) + ) { + // Scan to the end of possible string\namespace parts. + $nextConstructorPart = $constructor; + while (true) { + $nextConstructorPart = $phpcsFile->findNext( + Tokens::$emptyTokens, + ($nextConstructorPart + 1), + null, + true, + null, + true + ); + if ($nextConstructorPart === false + || ($tokens[$nextConstructorPart]['code'] !== T_STRING + && $tokens[$nextConstructorPart]['code'] !== T_NS_SEPARATOR) + ) { + break; + } + + $constructor = $nextConstructorPart; + } + + $fix = $phpcsFile->addFixableError($error, $constructor, 'ParenthesisMissing'); + if ($fix === true) { + $phpcsFile->fixer->addContent($constructor, '()'); + } + + // We can invoke the fixer if we know this is a + // constructor call with class names in an array + // example "new $controller[$i];". + } else if ($tokens[$constructor]['code'] === T_VARIABLE + && $tokens[($constructor + 1)]['code'] === T_OPEN_SQUARE_BRACKET + ) { + // Scan to the end of possible multilevel arrays. + $nextConstructorPart = $constructor; + do { + $nextConstructorPart = $tokens[($nextConstructorPart + 1)]['bracket_closer']; + } while ($tokens[($nextConstructorPart + 1)]['code'] === T_OPEN_SQUARE_BRACKET); + + $fix = $phpcsFile->addFixableError($error, $nextConstructorPart, 'ParenthesisMissing'); + if ($fix === true) { + $phpcsFile->fixer->addContent($nextConstructorPart, '()'); + } + } else { + $phpcsFile->addError($error, $stackPtr, 'ParenthesisMissing'); + }//end if + }//end if + }//end process() }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Classes/ClassDeclarationSniff.php b/coder_sniffer/Backdrop/Sniffs/Classes/ClassDeclarationSniff.php index b31e04f..5b73fc6 100644 --- a/coder_sniffer/Backdrop/Sniffs/Classes/ClassDeclarationSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Classes/ClassDeclarationSniff.php @@ -2,46 +2,42 @@ /** * Class Declaration Test. * - * PHP version 5 - * - * @category PHP - * @package PHP_CodeSniffer - * @author Greg Sherwood - * @author Marc McIntyre - * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600) - * @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence - * @link http://pear.php.net/package/PHP_CodeSniffer + * @category PHP + * @package PHP_CodeSniffer + * @link http://pear.php.net/package/PHP_CodeSniffer */ +namespace Backdrop\Sniffs\Classes; + +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Standards\PSR2\Sniffs\Classes\ClassDeclarationSniff as PSR2ClassDeclarationSniff; +use PHP_CodeSniffer\Util\Tokens; + /** * Class Declaration Test. * * Checks the declaration of the class is correct. * - * @category PHP - * @package PHP_CodeSniffer - * @author Greg Sherwood - * @author Marc McIntyre - * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600) - * @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence - * @version Release: 1.2.0RC3 - * @link http://pear.php.net/package/PHP_CodeSniffer + * @category PHP + * @package PHP_CodeSniffer + * @link http://pear.php.net/package/PHP_CodeSniffer */ -class Backdrop_Sniffs_Classes_ClassDeclarationSniff implements PHP_CodeSniffer_Sniff +class ClassDeclarationSniff extends PSR2ClassDeclarationSniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { - return array( - T_CLASS, - T_INTERFACE, - ); + return [ + T_CLASS, + T_INTERFACE, + T_TRAIT, + ]; }//end register() @@ -49,67 +45,141 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token in the - * stack passed in $tokens. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param integer $stackPtr The position of the current token in the + * stack passed in $tokens. * * @return void */ - public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { - $tokens = $phpcsFile->getTokens(); + $tokens = $phpcsFile->getTokens(); + $errorData = [strtolower($tokens[$stackPtr]['content'])]; if (isset($tokens[$stackPtr]['scope_opener']) === false) { - $error = 'Possible parse error: '; - $error .= $tokens[$stackPtr]['content']; - $error .= ' missing opening or closing brace'; - $phpcsFile->addWarning($error, $stackPtr); + $error = 'Possible parse error: %s missing opening or closing brace'; + $phpcsFile->addWarning($error, $stackPtr, 'MissingBrace', $errorData); return; } - $curlyBrace = $tokens[$stackPtr]['scope_opener']; - $lastContent = $phpcsFile->findPrevious(T_WHITESPACE, ($curlyBrace - 1), $stackPtr, true); - $classLine = $tokens[$lastContent]['line']; - $braceLine = $tokens[$curlyBrace]['line']; - if ($braceLine != $classLine) { - $error = 'Opening brace of a '; - $error .= $tokens[$stackPtr]['content']; - $error .= ' must be on the same line as the definition'; - $phpcsFile->addError($error, $curlyBrace); - return; - } /* else if ($braceLine > ($classLine + 1)) { - $difference = ($braceLine - $classLine - 1); - $difference .= ($difference === 1) ? ' line' : ' lines'; - $error = 'Opening brace of a '; - $error .= $tokens[$stackPtr]['content']; - $error .= ' must be on the line following the '; - $error .= $tokens[$stackPtr]['content']; - $error .= ' declaration; found '.$difference; - $phpcsFile->addError($error, $curlyBrace); + $openingBrace = $tokens[$stackPtr]['scope_opener']; + + $next = $phpcsFile->findNext(T_WHITESPACE, ($openingBrace + 1), null, true); + if ($tokens[$next]['line'] === $tokens[$openingBrace]['line'] && $tokens[$next]['code'] !== T_CLOSE_CURLY_BRACKET) { + $error = 'Opening brace must be the last content on the line'; + $fix = $phpcsFile->addFixableError($error, $openingBrace, 'ContentAfterBrace'); + if ($fix === true) { + $phpcsFile->fixer->addNewline($openingBrace); + } + } + + $previous = $phpcsFile->findPrevious(T_WHITESPACE, ($openingBrace - 1), null, true); + $decalrationLine = $tokens[$previous]['line']; + $braceLine = $tokens[$openingBrace]['line']; + + $lineDifference = ($braceLine - $decalrationLine); + + if ($lineDifference > 0) { + $error = 'Opening brace should be on the same line as the declaration'; + $fix = $phpcsFile->addFixableError($error, $openingBrace, 'BraceOnNewLine'); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + for ($i = ($previous + 1); $i < $openingBrace; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->addContent($previous, ' '); + $phpcsFile->fixer->endChangeset(); + } + return; - } */ + } - if ($tokens[($curlyBrace + 1)]['content'] !== $phpcsFile->eolChar && $tokens[($curlyBrace + 1)]['code'] !== T_CLOSE_CURLY_BRACKET) { - $type = strtolower($tokens[$stackPtr]['content']); - $error = "Opening $type brace must be on a line by itself"; - $phpcsFile->addError($error, $curlyBrace); + $openingBrace = $tokens[$stackPtr]['scope_opener']; + if ($tokens[($openingBrace - 1)]['code'] !== T_WHITESPACE) { + $length = 0; + } else if ($tokens[($openingBrace - 1)]['content'] === "\t") { + $length = '\t'; + } else { + $length = strlen($tokens[($openingBrace - 1)]['content']); } - if ($tokens[($curlyBrace - 1)]['code'] != T_WHITESPACE) { - $prevContent = $tokens[($curlyBrace - 1)]['content']; - if ($prevContent !== $phpcsFile->eolChar) { - $blankSpace = substr($prevContent, strpos($prevContent, $phpcsFile->eolChar)); - $spaces = strlen($blankSpace); - if ($spaces !== 0) { - $error = "Expected 1 space before opening brace; $spaces found"; - $phpcsFile->addError($error, $curlyBrace); + if ($length !== 1) { + $error = 'Expected 1 space before opening brace; found %s'; + $data = [$length]; + $fix = $phpcsFile->addFixableError($error, $openingBrace, 'SpaceBeforeBrace', $data); + if ($fix === true) { + if ($length === 0) { + $phpcsFile->fixer->replaceToken(($openingBrace), ' {'); + } else { + $phpcsFile->fixer->replaceToken(($openingBrace - 1), ' '); } } } + // Now call the open spacing method from PSR2. + $this->processOpen($phpcsFile, $stackPtr); + + $this->processClose($phpcsFile, $stackPtr); + }//end process() -}//end class + /** + * Processes the closing section of a class declaration. + * + * @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 processClose(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); -?> + // Just in case. + if (isset($tokens[$stackPtr]['scope_closer']) === false) { + return; + } + + // Check that the closing brace comes right after the code body. + $closeBrace = $tokens[$stackPtr]['scope_closer']; + $prevContent = $phpcsFile->findPrevious(T_WHITESPACE, ($closeBrace - 1), null, true); + if ($prevContent !== $tokens[$stackPtr]['scope_opener'] + && $tokens[$prevContent]['line'] !== ($tokens[$closeBrace]['line'] - 2) + // If the class only contains a comment no extra line is needed. + && isset(Tokens::$commentTokens[$tokens[$prevContent]['code']]) === false + ) { + $error = 'The closing brace for the %s must have an empty line before it'; + $data = [$tokens[$stackPtr]['content']]; + $fix = $phpcsFile->addFixableError($error, $closeBrace, 'CloseBraceAfterBody', $data); + + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + for ($i = ($prevContent + 1); $i < $closeBrace; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->replaceToken($closeBrace, $phpcsFile->eolChar.$phpcsFile->eolChar.$tokens[$closeBrace]['content']); + + $phpcsFile->fixer->endChangeset(); + } + }//end if + + // Check the closing brace is on it's own line, but allow + // for comments like "//end class". + $nextContent = $phpcsFile->findNext(T_COMMENT, ($closeBrace + 1), null, true); + if ($tokens[$nextContent]['content'] !== $phpcsFile->eolChar + && $tokens[$nextContent]['line'] === $tokens[$closeBrace]['line'] + ) { + $type = strtolower($tokens[$stackPtr]['content']); + $error = 'Closing %s brace must be on a line by itself'; + $data = [$tokens[$stackPtr]['content']]; + $phpcsFile->addError($error, $closeBrace, 'CloseBraceSameLine', $data); + } + + }//end processClose() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Classes/FullyQualifiedNamespaceSniff.php b/coder_sniffer/Backdrop/Sniffs/Classes/FullyQualifiedNamespaceSniff.php new file mode 100644 index 0000000..cd6c4f6 --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Classes/FullyQualifiedNamespaceSniff.php @@ -0,0 +1,212 @@ + + */ + public function register() + { + return [T_NS_SEPARATOR]; + + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the + * token was found. + * @param int $stackPtr The position in the PHP_CodeSniffer + * file's token stack where the token + * was found. + * + * @return void|int Optionally returns a stack pointer. The sniff will not be + * called again on the current file until the returned stack + * pointer is reached. Return $phpcsFile->numTokens + 1 to skip + * the rest of the file. + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + // Skip this sniff in *api.php files because they want to have fully + // qualified names for documentation purposes. + if (substr($phpcsFile->getFilename(), -8) === '.api.php') { + return ($phpcsFile->numTokens + 1); + } + + // We are only interested in a backslash embedded between strings, which + // means this is a class reference with more than once namespace part. + if ($tokens[($stackPtr - 1)]['code'] !== T_STRING || $tokens[($stackPtr + 1)]['code'] !== T_STRING) { + return; + } + + // Check if this is a use statement and ignore those. + $before = $phpcsFile->findPrevious([T_STRING, T_NS_SEPARATOR, T_WHITESPACE, T_COMMA, T_AS], $stackPtr, null, true); + if ($tokens[$before]['code'] === T_USE || $tokens[$before]['code'] === T_NAMESPACE) { + return $phpcsFile->findNext([T_STRING, T_NS_SEPARATOR, T_WHITESPACE, T_COMMA, T_AS], ($stackPtr + 1), null, true); + } else { + $before = $phpcsFile->findPrevious([T_STRING, T_NS_SEPARATOR, T_WHITESPACE], $stackPtr, null, true); + } + + // If this is a namespaced function call then ignore this because use + // statements for functions are not possible in PHP 5.5 and lower. + $after = $phpcsFile->findNext([T_STRING, T_NS_SEPARATOR, T_WHITESPACE], $stackPtr, null, true); + if ($tokens[$after]['code'] === T_OPEN_PARENTHESIS && $tokens[$before]['code'] !== T_NEW) { + return ($after + 1); + } + + $fullName = $phpcsFile->getTokensAsString(($before + 1), ($after - 1 - $before)); + $fullName = trim($fullName, "\ \n"); + $parts = explode('\\', $fullName); + $className = end($parts); + + // Check if there is a use statement already for this class and + // namespace. + $conflict = false; + $alreadyUsed = false; + $aliasName = false; + $useStatement = $phpcsFile->findNext(T_USE, 0); + while ($useStatement !== false && empty($tokens[$useStatement]['conditions']) === true) { + $endPtr = $phpcsFile->findEndOfStatement($useStatement); + $useEnd = ($phpcsFile->findNext([T_STRING, T_NS_SEPARATOR, T_WHITESPACE], ($useStatement + 1), null, true) - 1); + $useFullName = trim($phpcsFile->getTokensAsString(($useStatement + 1), ($useEnd - $useStatement))); + + // Check if use statement contains an alias. + $asPtr = $phpcsFile->findNext(T_AS, ($useEnd + 1), $endPtr); + if ($asPtr !== false) { + $aliasName = trim($phpcsFile->getTokensAsString(($asPtr + 1), ($endPtr - 1 - $asPtr))); + } + + if (strcasecmp($useFullName, $fullName) === 0) { + $alreadyUsed = true; + break; + } + + $parts = explode('\\', $useFullName); + $useClassName = end($parts); + + // Check if the resulting classname would conflict with another + // use statement. + if ($aliasName === $className || $useClassName === $className) { + $conflict = true; + break; + } + + $aliasName = false; + // Check if we're currently in a multi-use statement. + if ($tokens[$endPtr]['code'] === T_COMMA) { + $useStatement = $endPtr; + continue; + } + + $useStatement = $phpcsFile->findNext(T_USE, ($endPtr + 1)); + }//end while + + if ($conflict === false) { + $classStatement = $phpcsFile->findNext(T_CLASS, 0); + while ($classStatement !== false) { + $afterClassStatement = $phpcsFile->findNext(T_WHITESPACE, ($classStatement + 1), null, true); + // Check for 'class ClassName' declarations. + if ($tokens[$afterClassStatement]['code'] === T_STRING) { + $declaredName = $tokens[$afterClassStatement]['content']; + if ($declaredName === $className) { + $conflict = true; + break; + } + } + + $classStatement = $phpcsFile->findNext(T_CLASS, ($classStatement + 1)); + } + } + + $error = 'Namespaced classes/interfaces/traits should be referenced with use statements'; + if ($conflict === true) { + $fix = false; + $phpcsFile->addError($error, $stackPtr, 'UseStatementMissing'); + } else { + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'UseStatementMissing'); + } + + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + + // Replace the fully qualified name with the local name. + for ($i = ($before + 1); $i < $after; $i++) { + if ($tokens[$i]['code'] !== T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($i, ''); + } + } + + // Use alias name if available. + if ($aliasName !== false) { + $phpcsFile->fixer->addContentBefore(($after - 1), $aliasName); + } else { + $phpcsFile->fixer->addContentBefore(($after - 1), $className); + } + + // Insert use statement at the beginning of the file if it is not there + // already. Also check if another sniff (for example + // UnusedUseStatementSniff) has already deleted the use statement, then + // we need to add it back. + if ($alreadyUsed === false + || $phpcsFile->fixer->getTokenContent($useStatement) !== $tokens[$useStatement]['content'] + ) { + if ($aliasName !== false) { + $use = "use $fullName as $aliasName;"; + } else { + $use = "use $fullName;"; + } + + // Check if there is a group of use statements and add it there. + $useStatement = $phpcsFile->findNext(T_USE, 0); + if ($useStatement !== false && empty($tokens[$useStatement]['conditions']) === true) { + $phpcsFile->fixer->addContentBefore($useStatement, "$use\n"); + } else { + // Check if there is an @file comment. + $beginning = 0; + $fileComment = $phpcsFile->findNext(T_WHITESPACE, ($beginning + 1), null, true); + if ($tokens[$fileComment]['code'] === T_DOC_COMMENT_OPEN_TAG) { + $beginning = $tokens[$fileComment]['comment_closer']; + $phpcsFile->fixer->addContent($beginning, "\n\n$use\n"); + } else { + $phpcsFile->fixer->addContent($beginning, "$use\n"); + } + } + }//end if + + $phpcsFile->fixer->endChangeset(); + }//end if + + // Continue after this class reference so that errors for this are not + // flagged multiple times. + return $phpcsFile->findNext([T_STRING, T_NS_SEPARATOR], ($stackPtr + 1), null, true); + + }//end process() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Classes/InterfaceNameSniff.php b/coder_sniffer/Backdrop/Sniffs/Classes/InterfaceNameSniff.php index 89c08bc..665c5fd 100644 --- a/coder_sniffer/Backdrop/Sniffs/Classes/InterfaceNameSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Classes/InterfaceNameSniff.php @@ -1,14 +1,17 @@ */ public function register() { - return array(T_INTERFACE); + return [T_INTERFACE]; }//end register() @@ -35,13 +38,13 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token in - * the stack passed in $tokens. + * @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(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); $namePtr = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); @@ -55,5 +58,3 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Classes/PropertyDeclarationSniff.php b/coder_sniffer/Backdrop/Sniffs/Classes/PropertyDeclarationSniff.php new file mode 100644 index 0000000..e67d147 --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Classes/PropertyDeclarationSniff.php @@ -0,0 +1,115 @@ +getTokens(); + + if ($tokens[$stackPtr]['content'][1] === '_') { + $error = 'Property name "%s" should not be prefixed with an underscore to indicate visibility'; + $data = [$tokens[$stackPtr]['content']]; + $phpcsFile->addWarning($error, $stackPtr, 'Underscore', $data); + } + + // Detect multiple properties defined at the same time. Throw an error + // for this, but also only process the first property in the list so we don't + // repeat errors. + $find = Tokens::$scopeModifiers; + $find = array_merge($find, [T_VARIABLE, T_VAR, T_SEMICOLON]); + $prev = $phpcsFile->findPrevious($find, ($stackPtr - 1)); + if ($tokens[$prev]['code'] === T_VARIABLE) { + return; + } + + if ($tokens[$prev]['code'] === T_VAR) { + $error = 'The var keyword must not be used to declare a property'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'VarUsed'); + if ($fix === true) { + $phpcsFile->fixer->replaceToken($prev, 'public'); + } + } + + $next = $phpcsFile->findNext([T_VARIABLE, T_SEMICOLON], ($stackPtr + 1)); + if ($tokens[$next]['code'] === T_VARIABLE) { + $error = 'There must not be more than one property declared per statement'; + $phpcsFile->addError($error, $stackPtr, 'Multiple'); + } + + $modifier = $phpcsFile->findPrevious(Tokens::$scopeModifiers, $stackPtr); + if (($modifier === false) || ($tokens[$modifier]['line'] !== $tokens[$stackPtr]['line'])) { + $error = 'Visibility must be declared on property "%s"'; + $data = [$tokens[$stackPtr]['content']]; + $phpcsFile->addError($error, $stackPtr, 'ScopeMissing', $data); + } + + }//end processMemberVar() + + + /** + * Processes normal variables. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position where the token was found. + * + * @return void + */ + protected function processVariable(File $phpcsFile, $stackPtr) + { + /* + We don't care about normal variables. + */ + + }//end processVariable() + + + /** + * Processes variables in double quoted strings. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position where the token was found. + * + * @return void + */ + protected function processVariableInString(File $phpcsFile, $stackPtr) + { + /* + We don't care about normal variables. + */ + + }//end processVariableInString() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Classes/UnusedUseStatementSniff.php b/coder_sniffer/Backdrop/Sniffs/Classes/UnusedUseStatementSniff.php new file mode 100644 index 0000000..f91154c --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Classes/UnusedUseStatementSniff.php @@ -0,0 +1,217 @@ + + */ + public function register() + { + return [T_USE]; + + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @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(); + + // Only check use statements in the global scope. + if (empty($tokens[$stackPtr]['conditions']) === false) { + return; + } + + // Seek to the end of the statement and get the string before the semi colon. + $semiColon = $phpcsFile->findEndOfStatement($stackPtr); + if ($tokens[$semiColon]['code'] !== T_SEMICOLON) { + return; + } + + $classPtr = $phpcsFile->findPrevious( + Tokens::$emptyTokens, + ($semiColon - 1), + null, + true + ); + + if ($tokens[$classPtr]['code'] !== T_STRING) { + return; + } + + // Search where the class name is used. PHP treats class names case + // insensitive, that's why we cannot search for the exact class name string + // and need to iterate over all T_STRING tokens in the file. + $classUsed = $phpcsFile->findNext(T_STRING, ($classPtr + 1)); + $lowerClassName = strtolower($tokens[$classPtr]['content']); + + // Check if the referenced class is in the same namespace as the current + // file. If it is then the use statement is not necessary. + $namespacePtr = $phpcsFile->findPrevious([T_NAMESPACE], $stackPtr); + // Check if the use statement does aliasing with the "as" keyword. Aliasing + // is allowed even in the same namespace. + $aliasUsed = $phpcsFile->findPrevious(T_AS, ($classPtr - 1), $stackPtr); + + if ($namespacePtr !== false && $aliasUsed === false) { + $nsEnd = $phpcsFile->findNext( + [ + T_NS_SEPARATOR, + T_STRING, + T_WHITESPACE, + ], + ($namespacePtr + 1), + null, + true + ); + $namespace = trim($phpcsFile->getTokensAsString(($namespacePtr + 1), ($nsEnd - $namespacePtr - 1))); + + $useNamespacePtr = $phpcsFile->findNext([T_STRING], ($stackPtr + 1)); + $useNamespaceEnd = $phpcsFile->findNext( + [ + T_NS_SEPARATOR, + T_STRING, + ], + ($useNamespacePtr + 1), + null, + true + ); + $useNamespace = rtrim($phpcsFile->getTokensAsString($useNamespacePtr, ($useNamespaceEnd - $useNamespacePtr - 1)), '\\'); + + if (strcasecmp($namespace, $useNamespace) === 0) { + $classUsed = false; + } + }//end if + + while ($classUsed !== false) { + if (strtolower($tokens[$classUsed]['content']) === $lowerClassName) { + // If the name is used in a PHP 7 function return type declaration + // stop. + if ($tokens[$classUsed]['code'] === T_RETURN_TYPE) { + return; + } + + $beforeUsage = $phpcsFile->findPrevious( + Tokens::$emptyTokens, + ($classUsed - 1), + null, + true + ); + // If a backslash is used before the class name then this is some other + // use statement. + if (in_array( + $tokens[$beforeUsage]['code'], + [ + T_USE, + T_NS_SEPARATOR, + // If an object operator is used then this is a method call + // with the same name as the class name. Which means this is + // not referring to the class. + T_OBJECT_OPERATOR, + // Function definition, not class invocation. + T_FUNCTION, + // Static method call, not class invocation. + T_DOUBLE_COLON, + ] + ) === false + ) { + return; + } + + // Trait use statement within a class. + if ($tokens[$beforeUsage]['code'] === T_USE && empty($tokens[$beforeUsage]['conditions']) === false) { + return; + } + }//end if + + $classUsed = $phpcsFile->findNext([T_STRING, T_RETURN_TYPE], ($classUsed + 1)); + }//end while + + $warning = 'Unused use statement'; + $fix = $phpcsFile->addFixableWarning($warning, $stackPtr, 'UnusedUse'); + if ($fix === true) { + // Remove the whole use statement line. + $phpcsFile->fixer->beginChangeset(); + for ($i = $stackPtr; $i <= $semiColon; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + // Also remove whitespace after the semicolon (new lines). + while (isset($tokens[$i]) === true && $tokens[$i]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($i, ''); + if (strpos($tokens[$i]['content'], $phpcsFile->eolChar) !== false) { + break; + } + + $i++; + } + + // Replace @var data types in doc comments with the fully qualified class + // name. + $useNamespacePtr = $phpcsFile->findNext([T_STRING], ($stackPtr + 1)); + $useNamespaceEnd = $phpcsFile->findNext( + [ + T_NS_SEPARATOR, + T_STRING, + ], + ($useNamespacePtr + 1), + null, + true + ); + $fullNamespace = $phpcsFile->getTokensAsString($useNamespacePtr, ($useNamespaceEnd - $useNamespacePtr)); + + $tag = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($stackPtr + 1)); + + while ($tag !== false) { + if (($tokens[$tag]['content'] === '@var' || $tokens[$tag]['content'] === '@return') + && isset($tokens[($tag + 1)]) === true + && $tokens[($tag + 1)]['code'] === T_DOC_COMMENT_WHITESPACE + && isset($tokens[($tag + 2)]) === true + && $tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING + && strpos($tokens[($tag + 2)]['content'], $tokens[$classPtr]['content']) === 0 + ) { + $replacement = '\\'.$fullNamespace.substr($tokens[($tag + 2)]['content'], strlen($tokens[$classPtr]['content'])); + $phpcsFile->fixer->replaceToken(($tag + 2), $replacement); + } + + $tag = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($tag + 1)); + } + + $phpcsFile->fixer->endChangeset(); + }//end if + + }//end process() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Classes/UseGlobalClassSniff.php b/coder_sniffer/Backdrop/Sniffs/Classes/UseGlobalClassSniff.php new file mode 100644 index 0000000..24ccc12 --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Classes/UseGlobalClassSniff.php @@ -0,0 +1,135 @@ + + */ + public function register() + { + return [T_USE]; + + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where the + * token was found. + * @param int $stackPtr The position in the PHP_CodeSniffer + * file's token stack where the token + * was found. + * + * @return void|int Optionally returns a stack pointer. The sniff will not be + * called again on the current file until the returned stack + * pointer is reached. Return $phpcsFile->numTokens + 1 to skip + * the rest of the file. + */ + public function process(File $phpcsFile, $stackPtr) + { + + $tokens = $phpcsFile->getTokens(); + + // Find the first declaration, marking the end of the use statements. + $bodyStart = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT, T_FUNCTION], 0); + + // Ensure we are in the global scope, to exclude trait use statements. + if (empty($tokens[$stackPtr]['conditions']) === false) { + return; + } + + // End of the full statement. + $stmtEnd = $phpcsFile->findNext(T_SEMICOLON, $stackPtr); + + $lineStart = $stackPtr; + // Iterate through a potential multiline use statement. + while (false !== $lineEnd = $phpcsFile->findNext([T_SEMICOLON, T_COMMA], ($lineStart + 1), ($stmtEnd + 1))) { + // We are only interested in imports that contain no backslash, + // which means this is a class without a namespace. + // Also skip function imports. + if ($phpcsFile->findNext(T_NS_SEPARATOR, $lineStart, $lineEnd) !== false + || $phpcsFile->findNext(T_STRING, $lineStart, $lineEnd, false, 'function') !== false + ) { + $lineStart = $lineEnd; + continue; + } + + // The first string token is the class name. + $class = $phpcsFile->findNext(T_STRING, $lineStart, $lineEnd); + $className = $tokens[$class]['content']; + // If there is more than one string token, the last one is the alias. + $alias = $phpcsFile->findPrevious(T_STRING, $lineEnd, $stackPtr); + $aliasName = $tokens[$alias]['content']; + + $error = 'Non-namespaced classes/interfaces/traits should not be referenced with use statements'; + $fix = $phpcsFile->addFixableError($error, $class, 'RedundantUseStatement'); + + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + + // Remove the entire line by default. + $start = $lineStart; + $end = $lineEnd; + $next = $phpcsFile->findNext(T_WHITESPACE, ($end + 1), null, true); + + if ($tokens[$lineStart]['code'] === T_COMMA) { + // If there are lines before this one, + // then leave the ending delimiter in place. + $end = ($lineEnd - 1); + } else if ($tokens[$lineEnd]['code'] === T_COMMA) { + // If there are lines after, but not before, + // then leave the use keyword. + $start = $class; + } else if ($tokens[$next]['code'] === T_USE) { + // If the whole statement is removed, and there is one after it, + // then also remove the linebreaks. + $end = ($next - 1); + } + + for ($i = $start; $i <= $end; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + // Find all usages of the class, and add a leading backslash. + // Only start looking after the end of the use statement block. + $i = $bodyStart; + while (false !== $i = $phpcsFile->findNext(T_STRING, ($i + 1), null, false, $aliasName)) { + if ($tokens[($i - 1)]['code'] !== T_NS_SEPARATOR) { + $phpcsFile->fixer->replaceToken($i, '\\'.$className); + } + } + + $phpcsFile->fixer->endChangeset(); + }//end if + + $lineStart = $lineEnd; + }//end while + + }//end process() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Classes/UseLeadingBackslashSniff.php b/coder_sniffer/Backdrop/Sniffs/Classes/UseLeadingBackslashSniff.php new file mode 100644 index 0000000..4362470 --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Classes/UseLeadingBackslashSniff.php @@ -0,0 +1,75 @@ + + */ + public function register() + { + return [T_USE]; + + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @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(); + + // Only check use statements in the global scope. + if (empty($tokens[$stackPtr]['conditions']) === false) { + return; + } + + $startPtr = $phpcsFile->findNext( + Tokens::$emptyTokens, + ($stackPtr + 1), + null, + true + ); + + if ($startPtr !== false && $tokens[$startPtr]['code'] === T_NS_SEPARATOR) { + $error = 'When importing a class with "use", do not include a leading \\'; + $fix = $phpcsFile->addFixableError($error, $startPtr, 'SeparatorStart'); + if ($fix === true) { + $phpcsFile->fixer->replaceToken($startPtr, ''); + } + } + + }//end process() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Commenting/ClassCommentSniff.php b/coder_sniffer/Backdrop/Sniffs/Commenting/ClassCommentSniff.php new file mode 100644 index 0000000..ca23a69 --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Commenting/ClassCommentSniff.php @@ -0,0 +1,154 @@ + + * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + * @version Release: @package_version@ + * @link http://pear.php.net/package/PHP_CodeSniffer + */ +class ClassCommentSniff implements Sniff +{ + + + /** + * Returns an array of tokens this test wants to listen for. + * + * @return array + */ + public function register() + { + return [ + T_CLASS, + T_INTERFACE, + T_TRAIT, + ]; + + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @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(); + $find = Tokens::$methodPrefixes; + $find[] = T_WHITESPACE; + $name = $tokens[$stackPtr]['content']; + + $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1), null, true); + if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG + && $tokens[$commentEnd]['code'] !== T_COMMENT + ) { + $fix = $phpcsFile->addFixableError('Missing %s doc comment', $stackPtr, 'Missing', [$name]); + if ($fix === true) { + $phpcsFile->fixer->addContent($commentEnd, "\n/**\n *\n */"); + } + + return; + } + + // Try and determine if this is a file comment instead of a class comment. + if ($tokens[$commentEnd]['code'] === T_DOC_COMMENT_CLOSE_TAG) { + $start = ($tokens[$commentEnd]['comment_opener'] - 1); + } else { + $start = ($commentEnd - 1); + } + + $fileTag = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($start + 1), $commentEnd, false, '@file'); + if ($fileTag !== false) { + // This is a file comment. + $fix = $phpcsFile->addFixableError('Missing %s doc comment', $stackPtr, 'Missing', [$name]); + if ($fix === true) { + $phpcsFile->fixer->addContent($commentEnd, "\n/**\n *\n */"); + } + + return; + } + + if ($tokens[$commentEnd]['code'] === T_COMMENT) { + $fix = $phpcsFile->addFixableError('You must use "/**" style comments for a %s comment', $stackPtr, 'WrongStyle', [$name]); + if ($fix === true) { + // Convert the comment into a doc comment. + $phpcsFile->fixer->beginChangeset(); + $comment = ''; + for ($i = $commentEnd; $tokens[$i]['code'] === T_COMMENT; $i--) { + $comment = ' *'.ltrim($tokens[$i]['content'], '/* ').$comment; + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->replaceToken($commentEnd, "/**\n".rtrim($comment, "*/\n")."\n */"); + $phpcsFile->fixer->endChangeset(); + } + + return; + } + + if ($tokens[$commentEnd]['line'] !== ($tokens[$stackPtr]['line'] - 1)) { + $error = 'There must be exactly one newline after the %s comment'; + $fix = $phpcsFile->addFixableError($error, $commentEnd, 'SpacingAfter', [$name]); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + for ($i = ($commentEnd + 1); $tokens[$i]['code'] === T_WHITESPACE && $i < $stackPtr; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->addContent($commentEnd, "\n"); + $phpcsFile->fixer->endChangeset(); + } + } + + $comment = []; + for ($i = $start; $i < $commentEnd; $i++) { + if ($tokens[$i]['code'] === T_DOC_COMMENT_TAG) { + break; + } + + if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { + $comment[] = $tokens[$i]['content']; + } + } + + $words = explode(' ', implode(' ', $comment)); + if (count($words) <= 2) { + $className = $phpcsFile->getDeclarationName($stackPtr); + + foreach ($words as $word) { + // Check if the comment contains the class name. + if (strpos($word, $className) !== false) { + $error = 'The class short comment should describe what the class does and not simply repeat the class name'; + $phpcsFile->addWarning($error, $commentEnd, 'Short'); + break; + } + } + } + + }//end process() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Commenting/DataTypeNamespaceSniff.php b/coder_sniffer/Backdrop/Sniffs/Commenting/DataTypeNamespaceSniff.php new file mode 100644 index 0000000..38c3edc --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Commenting/DataTypeNamespaceSniff.php @@ -0,0 +1,116 @@ + + */ + public function register() + { + return [T_USE]; + + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @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(); + + // Only check use statements in the global scope. + if (empty($tokens[$stackPtr]['conditions']) === false) { + return; + } + + // Seek to the end of the statement and get the string before the semi colon. + $semiColon = $phpcsFile->findEndOfStatement($stackPtr); + if ($tokens[$semiColon]['code'] !== T_SEMICOLON) { + return; + } + + $classPtr = $phpcsFile->findPrevious( + Tokens::$emptyTokens, + ($semiColon - 1), + null, + true + ); + + if ($tokens[$classPtr]['code'] !== T_STRING) { + return; + } + + // Replace @var data types in doc comments with the fully qualified class + // name. + $useNamespacePtr = $phpcsFile->findNext([T_STRING], ($stackPtr + 1)); + $useNamespaceEnd = $phpcsFile->findNext( + [ + T_NS_SEPARATOR, + T_STRING, + ], + ($useNamespacePtr + 1), + null, + true + ); + $fullNamespace = $phpcsFile->getTokensAsString($useNamespacePtr, ($useNamespaceEnd - $useNamespacePtr)); + + $tag = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($stackPtr + 1)); + + while ($tag !== false) { + if (($tokens[$tag]['content'] === '@var' + || $tokens[$tag]['content'] === '@return' + || $tokens[$tag]['content'] === '@param' + || $tokens[$tag]['content'] === '@throws') + && isset($tokens[($tag + 1)]) === true + && $tokens[($tag + 1)]['code'] === T_DOC_COMMENT_WHITESPACE + && isset($tokens[($tag + 2)]) === true + && $tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING + && strpos($tokens[($tag + 2)]['content'], $tokens[$classPtr]['content']) === 0 + ) { + $error = 'Data types in %s tags need to be fully namespaced'; + $data = [$tokens[$tag]['content']]; + $fix = $phpcsFile->addFixableError($error, ($tag + 2), 'DataTypeNamespace', $data); + if ($fix === true) { + $replacement = '\\'.$fullNamespace.substr($tokens[($tag + 2)]['content'], strlen($tokens[$classPtr]['content'])); + $phpcsFile->fixer->replaceToken(($tag + 2), $replacement); + } + } + + $tag = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($tag + 1)); + }//end while + + }//end process() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Commenting/DeprecatedSniff.php b/coder_sniffer/Backdrop/Sniffs/Commenting/DeprecatedSniff.php new file mode 100644 index 0000000..aff3585 --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Commenting/DeprecatedSniff.php @@ -0,0 +1,230 @@ + + */ + public function register() + { + if (defined('PHP_CODESNIFFER_IN_TESTS') === true) { + $this->debug = false; + } + + return [T_DOC_COMMENT_TAG]; + + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @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) + { + $debug = Config::getConfigData('deprecated_debug'); + if ($debug !== null) { + $this->debug = (bool) $debug; + } + + $tokens = $phpcsFile->getTokens(); + + // Only process @deprecated tags. + if (strcasecmp($tokens[$stackPtr]['content'], '@deprecated') !== 0) { + return; + } + + // Get the end point of the comment block which has the deprecated tag. + $commentEnd = $phpcsFile->findNext(T_DOC_COMMENT_CLOSE_TAG, ($stackPtr + 1)); + + // Get the full @deprecated text which may cover multiple lines. + $textItems = []; + $lastLine = $tokens[($stackPtr + 1)]['line']; + for ($i = ($stackPtr + 1); $i < $commentEnd; $i++) { + if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { + if ($tokens[$i]['line'] <= ($lastLine + 1)) { + $textItems[$i] = $tokens[$i]['content']; + $lastLine = $tokens[$i]['line']; + } else { + break; + } + } + + // Found another tag, so we have all the deprecation text. + if ($tokens[$i]['code'] === T_DOC_COMMENT_TAG) { + break; + } + } + + // The standard format for the deprecation text is: + // @deprecated in %in-version% and is removed from %removal-version%. %extra-info%. + $standardFormat = "@deprecated in %%deprecation-version%% and is removed from %%removal-version%%. %%extra-info%%."; + + // Use (?U) 'ungreedy' before the removal-version so that only the text + // up to the first dot+space is matched, as there may be more than one + // sentence in the extra-info part. + $fullText = trim(implode(' ', $textItems)); + $matches = []; + preg_match('/^in (.+) and is removed from (?U)(.+)(?:\. | |\.$|$)(.*)$/', $fullText, $matches); + // There should be 4 items in $matches: 0 is full text, 1 = in-version, + // 2 = removal-version, 3 = extra-info (can be blank at this stage). + if (count($matches) !== 4) { + // The full text does not match the standard. Try to find fixes by + // testing with a relaxed set of criteria, based on common + // formatting variations. This is designed for Core fixes only. + $error = "The text '@deprecated %s' does not match the standard format: ".$standardFormat; + // All of the standard text should be on the first comment line, so + // try to match with common formatting errors to allow an automatic + // fix. If not possible then report a normal error. + $matchesFix = []; + $fix = null; + if (count($textItems) > 0) { + // Get just the first line of the text. + $key = array_keys($textItems)[0]; + $text1 = $textItems[$key]; + // Matching on (backdrop|) here says that we are only attempting to provide + // automatic fixes for Backdrop core, and if the project is missing we are + // assuming it is Backdrop core. Deprecations for contrib projects are much + // less frequent and faults can be corrected manually. + preg_match('/^(.*)(as of|in) (backdrop|)( |:|)+([\d\.\-xdev\?]+)(,| |. |)(.*)(removed|removal)([ |from|before|in|the]*) (backdrop|)( |:|)([\d\-\.xdev]+)( |,|$)+(?:release|)(?:[\.,])*(.*)$/i', $text1, $matchesFix); + + if (count($matchesFix) >= 12) { + // It is a Backdrop core deprecation and is fixable. + if (empty($matchesFix[1]) === false && $this->debug === true) { + // For info, to check it is acceptable to remove the text in [1]. + echo('DEBUG: File: '.$phpcsFile->path.', line '.$tokens[($stackPtr)]['line'].PHP_EOL); + echo('DEBUG: "@deprecated '.$text1.'"'.PHP_EOL); + echo('DEBUG: Fix will remove: "'.$matchesFix[1].'"'.PHP_EOL); + } + + $ver1 = str_Replace(['-dev', 'x'], ['', '0'], trim($matchesFix[5], '.')); + $ver2 = str_Replace(['-dev', 'x'], ['', '0'], trim($matchesFix[12], '.')); + // If the version is short, add enough '.0' to correct it. + while (substr_count($ver1, '.') < 2) { + $ver1 .= '.0'; + } + + while (substr_count($ver2, '.') < 2) { + $ver2 .= '.0'; + } + + $correctedText = trim('in backdrop:'.$ver1.' and is removed from backdrop:'.$ver2.'. '.trim($matchesFix[14])); + // If $correctedText is longer than 65 this will make the whole line + // exceed 80 so give a warning if running with debug. + if (strlen($correctedText) > 65 && $this->debug === true) { + echo('WARNING: File '.$phpcsFile->path.', line '.$tokens[($stackPtr)]['line'].PHP_EOL); + echo('WARNING: Original = * @deprecated '.$text1.PHP_EOL); + echo('WARNING: Corrected = * @deprecated '.$correctedText.PHP_EOL); + echo('WARNING: New line length '.(strlen($correctedText) + 15).' exceeds standard 80 character limit'.PHP_EOL); + } + + $fix = $phpcsFile->addFixableError($error, $key, 'IncorrectTextLayout', [$fullText]); + if ($fix === true) { + $phpcsFile->fixer->replaceToken($key, $correctedText); + } + }//end if + }//end if + + if ($fix === null) { + // There was no automatic fix, so give a normal error. + $phpcsFile->addError($error, $stackPtr, 'IncorrectTextLayout', [$fullText]); + } + } else { + // The text follows the basic layout. Now check that the versions + // match backdrop:n.n.n or project:n.x-n.n or project:n.x-n.n-version[n] + // or project:n.n.n or project:n.n.n-version[n]. + // The text must be all lower case and numbers can be one or two digits. + foreach (['deprecation-version' => $matches[1], 'removal-version' => $matches[2]] as $name => $version) { + if (preg_match('/^[a-z\d_]+:(\d{1,2}\.\d{1,2}\.\d{1,2}|\d{1,2}\.x\-\d{1,2}\.\d{1,2})(-[a-z]{1,5}\d{1,2})?$/', $version) === 0) { + $error = "The %s '%s' does not match the lower-case machine-name standard: backdrop:n.n.n or project:n.x-n.n or project:n.x-n.n-version[n] or project:n.n.n or project:n.n.n-version[n]"; + $phpcsFile->addWarning($error, $stackPtr, 'DeprecatedVersionFormat', [$name, $version]); + } + } + + // The 'IncorrectTextLayout' above is designed to pass if all is ok + // except for missing extra info. This is a common fault so provide + // a separate check and message for this. + if ($matches[3] === '') { + $error = 'The @deprecated tag must have %extra-info%. The standard format is: '.str_replace('%%', '%', $standardFormat); + $phpcsFile->addError($error, $stackPtr, 'MissingExtraInfo', []); + } + }//end if + + // The next tag in this comment block after @deprecated must be @see. + $seeTag = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($stackPtr + 1), $commentEnd, false, '@see'); + if ($seeTag === false) { + $error = 'Each @deprecated tag must have a @see tag immediately following it'; + $phpcsFile->addError($error, $stackPtr, 'DeprecatedMissingSeeTag'); + return; + } + + // Check the format of the @see url. + $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, ($seeTag + 1), $commentEnd); + // If the @see tag exists but has no content then $string will be empty + // and $tokens[$string]['content'] will return 'addFixableError($error, $string, 'DeprecatedPeriodAfterSeeUrl', [$crLink]); + if ($fix === true) { + // Remove all of the the trailing punctuation. + $content = substr($crLink, 0, -(strlen($matches[4]))); + $phpcsFile->fixer->replaceToken($string, $content); + }//end if + } else if (empty($matches) === true) { + $error = "The @see url '%s' does not match the standard: http(s)://www.backdrop.org/node/n or http(s)://www.backdrop.org/project/aaa/issues/n"; + $phpcsFile->addWarning($error, $seeTag, 'DeprecatedWrongSeeUrlFormat', [$crLink]); + } + + }//end process() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Commenting/DocCommentAlignmentSniff.php b/coder_sniffer/Backdrop/Sniffs/Commenting/DocCommentAlignmentSniff.php index ba998b7..48143da 100644 --- a/coder_sniffer/Backdrop/Sniffs/Commenting/DocCommentAlignmentSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Commenting/DocCommentAlignmentSniff.php @@ -1,37 +1,40 @@ */ public function register() { - return array(T_DOC_COMMENT); + return [T_DOC_COMMENT_OPEN_TAG]; }//end register() @@ -39,111 +42,123 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. + * @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(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); // We are only interested in function/class/interface doc block comments. - $nextToken = $phpcsFile->findNext(PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr + 1), null, true); - $ignore = array( - T_CLASS, - T_INTERFACE, - T_FUNCTION, - T_PUBLIC, - T_PRIVATE, - T_PROTECTED, - T_STATIC, - T_ABSTRACT, - ); - - if (in_array($tokens[$nextToken]['code'], $ignore) === false) { + $ignore = Tokens::$emptyTokens; + if ($phpcsFile->tokenizerType === 'JS') { + $ignore[] = T_EQUAL; + $ignore[] = T_STRING; + $ignore[] = T_OBJECT_OPERATOR; + } + + $nextToken = $phpcsFile->findNext($ignore, ($stackPtr + 1), null, true); + $ignore = [ + T_CLASS => true, + T_INTERFACE => true, + T_FUNCTION => true, + T_PUBLIC => true, + T_PRIVATE => true, + T_PROTECTED => true, + T_STATIC => true, + T_ABSTRACT => true, + T_PROPERTY => true, + T_OBJECT => true, + T_PROTOTYPE => true, + T_VAR => true, + ]; + + if (isset($ignore[$tokens[$nextToken]['code']]) === false) { // Could be a file comment. - $prevToken = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr - 1), null, true); + $prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); if ($tokens[$prevToken]['code'] !== T_OPEN_TAG) { return; } } - // We only want to get the first comment in a block. If there is - // a comment on the line before this one, return. - $docComment = $phpcsFile->findPrevious(T_DOC_COMMENT, ($stackPtr - 1)); - if ($docComment !== false) { - if ($tokens[$docComment]['line'] === ($tokens[$stackPtr]['line'] - 1)) { - return; + // There must be one space after each star (unless it is an empty comment line) + // and all the stars must be aligned correctly. + $requiredColumn = ($tokens[$stackPtr]['column'] + 1); + $endComment = $tokens[$stackPtr]['comment_closer']; + for ($i = ($stackPtr + 1); $i <= $endComment; $i++) { + if ($tokens[$i]['code'] !== T_DOC_COMMENT_STAR + && $tokens[$i]['code'] !== T_DOC_COMMENT_CLOSE_TAG + ) { + continue; } - } - $comments = array($stackPtr); - $currentComment = $stackPtr; - $lastComment = $stackPtr; - while (($currentComment = $phpcsFile->findNext(T_DOC_COMMENT, ($currentComment + 1))) !== false) { - if ($tokens[$lastComment]['line'] === ($tokens[$currentComment]['line'] - 1)) { - $comments[] = $currentComment; - $lastComment = $currentComment; - } else { - break; + if ($tokens[$i]['code'] === T_DOC_COMMENT_CLOSE_TAG) { + // Can't process the close tag if it is not the first thing on the line. + $prev = $phpcsFile->findPrevious(T_DOC_COMMENT_WHITESPACE, ($i - 1), $stackPtr, true); + if ($tokens[$prev]['line'] === $tokens[$i]['line']) { + continue; + } } - } - // The $comments array now contains pointers to each token in the - // comment block. - $requiredColumn = strpos($tokens[$stackPtr]['content'], '*'); - $requiredColumn += $tokens[$stackPtr]['column']; - - foreach ($comments as $commentPointer) { - // Check the spacing after each asterisk. - $content = $tokens[$commentPointer]['content']; - $firstChar = substr($content, 0, 1); - $lastChar = substr($content, -1); - if ($firstChar !== '/' && $lastChar !== '/') { - $matches = array(); - preg_match('|^(\s+)?\*(\s+)?@|', $content, $matches); - if (empty($matches) === false) { - if (isset($matches[2]) === false) { - $error = 'Expected 1 space between asterisk and tag; 0 found'; - $phpcsFile->addError($error, $commentPointer, 'NoSpaceBeforeTag'); + if ($tokens[$i]['column'] !== $requiredColumn) { + $error = 'Expected %s space(s) before asterisk; %s found'; + $data = [ + ($requiredColumn - 1), + ($tokens[$i]['column'] - 1), + ]; + $fix = $phpcsFile->addFixableError($error, $i, 'SpaceBeforeStar', $data); + if ($fix === true) { + $padding = str_repeat(' ', ($requiredColumn - 1)); + if ($tokens[$i]['column'] === 1) { + $phpcsFile->fixer->addContentBefore($i, $padding); } else { - $length = strlen($matches[2]); - if ($length !== 1) { - $error = 'Expected 1 space between asterisk and tag; %s found'; - $data = array($length); - $phpcsFile->addError($error, $commentPointer, 'SpaceBeforeTag', $data); - } + $phpcsFile->fixer->replaceToken(($i - 1), $padding); } } - }//end foreach + } - // Check the alignment of each asterisk. - $currentColumn = strpos($content, '*'); - $currentColumn += $tokens[$commentPointer]['column']; + if ($tokens[$i]['code'] !== T_DOC_COMMENT_STAR) { + continue; + } - if ($currentColumn === $requiredColumn) { - // Star is aligned correctly. + if ($tokens[($i + 2)]['line'] !== $tokens[$i]['line']) { + // Line is empty. continue; } - $error = 'Expected %s space(s) before asterisk; %s found'; - $data = array( - ($requiredColumn - 1), - ($currentColumn - 1), - ); - $phpcsFile->addError($error, $commentPointer, 'SpaceBeforeAsterisk', $data); - }//end foreach - - if (trim($tokens[($lastComment - 1)]['content']) === '*') { - $error = 'Additional blank line found at the end of doc comment'; - $phpcsFile->addError($error, ($lastComment - 1), 'BlankLine'); - } + if ($tokens[($i + 1)]['code'] !== T_DOC_COMMENT_WHITESPACE) { + $error = 'Expected 1 space after asterisk; 0 found'; + $fix = $phpcsFile->addFixableError($error, $i, 'NoSpaceAfterStar'); + if ($fix === true) { + $phpcsFile->fixer->addContent($i, ' '); + } + } else if ($tokens[($i + 2)]['code'] === T_DOC_COMMENT_TAG + && $tokens[($i + 1)]['content'] !== ' ' + // Special @code/@endcode/@see tags can have more than 1 space. + && in_array( + $tokens[($i + 2)]['content'], + [ + '@param', + '@return', + '@throws', + '@ingroup', + '@var', + ] + ) === true + ) { + $error = 'Expected 1 space after asterisk; %s found'; + $data = [strlen($tokens[($i + 1)]['content'])]; + $fix = $phpcsFile->addFixableError($error, $i, 'SpaceAfterStar', $data); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($i + 1), ' '); + } + }//end if + }//end for }//end process() }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Commenting/DocCommentSniff.php b/coder_sniffer/Backdrop/Sniffs/Commenting/DocCommentSniff.php new file mode 100644 index 0000000..4fed753 --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Commenting/DocCommentSniff.php @@ -0,0 +1,531 @@ + + */ + public function register() + { + return [T_DOC_COMMENT_OPEN_TAG]; + + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @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(); + $commentEnd = $phpcsFile->findNext(T_DOC_COMMENT_CLOSE_TAG, ($stackPtr + 1)); + $commentStart = $tokens[$commentEnd]['comment_opener']; + + $empty = [ + T_DOC_COMMENT_WHITESPACE, + T_DOC_COMMENT_STAR, + ]; + + $short = $phpcsFile->findNext($empty, ($stackPtr + 1), $commentEnd, true); + if ($short === false) { + // No content at all. + $error = 'Doc comment is empty'; + $phpcsFile->addError($error, $stackPtr, 'Empty'); + return; + } + + // Ignore doc blocks in functions, this is handled by InlineCommentSniff. + if (empty($tokens[$stackPtr]['conditions']) === false && in_array(T_FUNCTION, $tokens[$stackPtr]['conditions']) === true) { + return; + } + + // The first line of the comment should just be the /** code. + // In JSDoc there are cases with @lends that are on the same line as code. + if ($tokens[$short]['line'] === $tokens[$stackPtr]['line'] && $phpcsFile->tokenizerType !== 'JS') { + $error = 'The open comment tag must be the only content on the line'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'ContentAfterOpen'); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->addNewline($stackPtr); + $phpcsFile->fixer->addContentBefore($short, '* '); + $phpcsFile->fixer->endChangeset(); + } + } + + // The last line of the comment should just be the */ code. + $prev = $phpcsFile->findPrevious($empty, ($commentEnd - 1), $stackPtr, true); + if ($tokens[$commentEnd]['content'] !== '*/') { + $error = 'Wrong function doc comment end; expected "*/", found "%s"'; + $phpcsFile->addError($error, $commentEnd, 'WrongEnd', [$tokens[$commentEnd]['content']]); + } + + // Check for additional blank lines at the end of the comment. + if ($tokens[$prev]['line'] < ($tokens[$commentEnd]['line'] - 1)) { + $error = 'Additional blank lines found at end of doc comment'; + $fix = $phpcsFile->addFixableError($error, $commentEnd, 'SpacingAfter'); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + for ($i = ($prev + 1); $i < $commentEnd; $i++) { + if ($tokens[($i + 1)]['line'] === $tokens[$commentEnd]['line']) { + break; + } + + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->endChangeset(); + } + } + + // The short description of @file comments is one line below. + if ($tokens[$short]['code'] === T_DOC_COMMENT_TAG && $tokens[$short]['content'] === '@file') { + $next = $phpcsFile->findNext($empty, ($short + 1), $commentEnd, true); + if ($next !== false) { + $fileShort = $short; + $short = $next; + } + } + + // Do not check defgroup sections, they have no short description. Also don't + // check PHPUnit tests doc blocks because they might not have a description. + if (in_array($tokens[$short]['content'], ['@defgroup', '@addtogroup', '@}', '@coversDefaultClass']) === true) { + return; + } + + // Check for a comment description. + if ($tokens[$short]['code'] !== T_DOC_COMMENT_STRING) { + // JSDoc has many cases of @type declaration that don't have a + // description. + if ($phpcsFile->tokenizerType === 'JS') { + return; + } + + // PHPUnit test methods are allowed to skip the short description and + // only provide an @covers annotation. + if ($tokens[$short]['content'] === '@covers') { + return; + } + + $error = 'Missing short description in doc comment'; + $phpcsFile->addError($error, $stackPtr, 'MissingShort'); + return; + } + + if (isset($fileShort) === true) { + $start = $fileShort; + } else { + $start = $stackPtr; + } + + // No extra newline before short description. + if ($tokens[$short]['line'] !== ($tokens[$start]['line'] + 1)) { + $error = 'Doc comment short description must be on the first line'; + $fix = $phpcsFile->addFixableError($error, $short, 'SpacingBeforeShort'); + if ($fix === true) { + // Move file comment short description to the next line. + if (isset($fileShort) === true && $tokens[$short]['line'] === $tokens[$start]['line']) { + $phpcsFile->fixer->addContentBefore($fileShort, "\n *"); + } else { + $phpcsFile->fixer->beginChangeset(); + for ($i = $start; $i < $short; $i++) { + if ($tokens[$i]['line'] === $tokens[$start]['line']) { + continue; + } else if ($tokens[$i]['line'] === $tokens[$short]['line']) { + break; + } + + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->endChangeset(); + } + } + }//end if + + if ($tokens[($short - 1)]['content'] !== ' ' + && strpos($tokens[($short - 1)]['content'], $phpcsFile->eolChar) === false + ) { + $error = 'Function comment short description must start with exactly one space'; + $fix = $phpcsFile->addFixableError($error, $short, 'ShortStartSpace'); + if ($fix === true) { + if ($tokens[($short - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) { + $phpcsFile->fixer->replaceToken(($short - 1), ' '); + } else { + $phpcsFile->fixer->addContent(($short - 1), ' '); + } + } + } + + // Account for the fact that a short description might cover + // multiple lines. + $shortContent = $tokens[$short]['content']; + $shortEnd = $short; + for ($i = ($short + 1); $i < $commentEnd; $i++) { + if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { + if ($tokens[$i]['line'] === ($tokens[$shortEnd]['line'] + 1)) { + $shortContent .= $tokens[$i]['content']; + $shortEnd = $i; + } else { + break; + } + } + + if ($tokens[$i]['code'] === T_DOC_COMMENT_TAG) { + break; + } + } + + // Remove any trailing white spaces which are detected by other sniffs. + $shortContent = trim($shortContent); + + if (preg_match('|\p{Lu}|u', $shortContent[0]) === 0 + // Allow both variants of inheritdoc comments. + && $shortContent !== '{@inheritdoc}' + && $shortContent !== '{@inheritDoc}' + // Ignore Features module export files that just use the file name as + // comment. + && $shortContent !== basename($phpcsFile->getFilename()) + ) { + $error = 'Doc comment short description must start with a capital letter'; + // If we cannot capitalize the first character then we don't have a + // fixable error. + if ($tokens[$short]['content'] === ucfirst($tokens[$short]['content'])) { + $phpcsFile->addError($error, $short, 'ShortNotCapital'); + } else { + $fix = $phpcsFile->addFixableError($error, $short, 'ShortNotCapital'); + if ($fix === true) { + $phpcsFile->fixer->replaceToken($short, ucfirst($tokens[$short]['content'])); + } + } + } + + $lastChar = substr($shortContent, -1); + // Allow these characters as valid line-ends not requiring to be fixed. + if (in_array($lastChar, ['.', '!', '?', ')']) === false + // Allow both variants of inheritdoc comments. + && $shortContent !== '{@inheritdoc}' + && $shortContent !== '{@inheritDoc}' + // Ignore Features module export files that just use the file name as + // comment. + && $shortContent !== basename($phpcsFile->getFilename()) + ) { + $error = 'Doc comment short description must end with a full stop'; + // If the last character is alphanumeric and the content is all on one line then fix it. + if (preg_match('/[a-zA-Z0-9]/', $lastChar) === 1 + && $tokens[$short]['line'] === $tokens[$shortEnd]['line'] + ) { + $fix = $phpcsFile->addFixableError($error, $shortEnd, 'ShortFullStop'); + if ($fix === true) { + $phpcsFile->fixer->addContent($shortEnd, '.'); + } + } else { + // The correct fix is not obvious, so report an error and leave for manual correction. + $phpcsFile->addError($error, $shortEnd, 'ShortFullStop'); + } + } + + if ($tokens[$short]['line'] !== $tokens[$shortEnd]['line']) { + $error = 'Doc comment short description must be on a single line, further text should be a separate paragraph'; + $phpcsFile->addError($error, $shortEnd, 'ShortSingleLine'); + } + + $long = $phpcsFile->findNext($empty, ($shortEnd + 1), ($commentEnd - 1), true); + if ($long === false) { + return; + } + + if ($tokens[$long]['code'] === T_DOC_COMMENT_STRING) { + if ($tokens[$long]['line'] !== ($tokens[$shortEnd]['line'] + 2)) { + $error = 'There must be exactly one blank line between descriptions in a doc comment'; + $fix = $phpcsFile->addFixableError($error, $long, 'SpacingBetween'); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + for ($i = ($shortEnd + 1); $i < $long; $i++) { + if ($tokens[$i]['line'] === $tokens[$shortEnd]['line']) { + continue; + } else if ($tokens[$i]['line'] === ($tokens[$long]['line'] - 1)) { + break; + } + + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->endChangeset(); + } + } + + if (preg_match('|\p{Lu}|u', $tokens[$long]['content'][0]) === 0 + && $tokens[$long]['content'] !== ucfirst($tokens[$long]['content']) + ) { + $error = 'Doc comment long description must start with a capital letter'; + $fix = $phpcsFile->addFixableError($error, $long, 'LongNotCapital'); + if ($fix === true) { + $phpcsFile->fixer->replaceToken($long, ucfirst($tokens[$long]['content'])); + } + } + + // Account for the fact that a description might cover multiple lines. + $longContent = $tokens[$long]['content']; + $longEnd = $long; + for ($i = ($long + 1); $i < $commentEnd; $i++) { + if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { + if ($tokens[$i]['line'] <= ($tokens[$longEnd]['line'] + 1)) { + $longContent .= $tokens[$i]['content']; + $longEnd = $i; + } else { + break; + } + } + + if ($tokens[$i]['code'] === T_DOC_COMMENT_TAG) { + if ($tokens[$i]['line'] <= ($tokens[$longEnd]['line'] + 1) + // Allow link tags within the long comment itself. + && ($tokens[$i]['content'] === '@link' || $tokens[$i]['content'] === '@endlink') + ) { + $longContent .= $tokens[$i]['content']; + $longEnd = $i; + } else { + break; + } + } + }//end for + + // Remove any trailing white spaces which are detected by other sniffs. + $longContent = trim($longContent); + + if (preg_match('/[a-zA-Z]$/', $longContent) === 1) { + $error = 'Doc comment long description must end with a full stop'; + $fix = $phpcsFile->addFixableError($error, $longEnd, 'LongFullStop'); + if ($fix === true) { + $phpcsFile->fixer->addContent($longEnd, '.'); + } + } + }//end if + + if (empty($tokens[$commentStart]['comment_tags']) === true) { + // No tags in the comment. + return; + } + + $firstTag = $tokens[$commentStart]['comment_tags'][0]; + $prev = $phpcsFile->findPrevious($empty, ($firstTag - 1), $stackPtr, true); + // This does not apply to @file, @code, @link and @endlink tags. + if ($tokens[$firstTag]['line'] !== ($tokens[$prev]['line'] + 2) + && isset($fileShort) === false + && in_array($tokens[$firstTag]['content'], ['@code', '@link', '@endlink']) === false + ) { + $error = 'There must be exactly one blank line before the tags in a doc comment'; + $fix = $phpcsFile->addFixableError($error, $firstTag, 'SpacingBeforeTags'); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + for ($i = ($prev + 1); $i < $firstTag; $i++) { + if ($tokens[$i]['line'] === $tokens[$firstTag]['line']) { + break; + } + + $phpcsFile->fixer->replaceToken($i, ''); + } + + $indent = str_repeat(' ', $tokens[$stackPtr]['column']); + $phpcsFile->fixer->addContent($prev, $phpcsFile->eolChar.$indent.'*'.$phpcsFile->eolChar); + $phpcsFile->fixer->endChangeset(); + } + } + + // Break out the tags into groups and check alignment within each. + // A tag group is one where there are no blank lines between tags. + // The param tag group is special as it requires all @param tags to be inside. + $tagGroups = []; + $groupid = 0; + $paramGroupid = null; + $currentTag = null; + $previousTag = null; + $isNewGroup = null; + $checkTags = [ + '@param', + '@return', + '@throws', + ]; + foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { + if ($pos > 0) { + // If this tag is not in the same column as the initial tag then + // it must be an inline comment tag and should be ignored here. + if ($tokens[$tag]['column'] !== $tokens[$firstTag]['column']) { + continue; + } + + $prev = $phpcsFile->findPrevious( + T_DOC_COMMENT_STRING, + ($tag - 1), + $tokens[$commentStart]['comment_tags'][($pos - 1)] + ); + + if ($prev === false) { + $prev = $tokens[$commentStart]['comment_tags'][($pos - 1)]; + } + + $isNewGroup = $tokens[$prev]['line'] !== ($tokens[$tag]['line'] - 1); + if ($isNewGroup === true) { + $groupid++; + } + }//end if + + $currentTag = $tokens[$tag]['content']; + if ($currentTag === '@param') { + if (($paramGroupid === null + && empty($tagGroups[$groupid]) === false) + || ($paramGroupid !== null + && $paramGroupid !== $groupid) + ) { + $error = 'Parameter tags must be grouped together in a doc comment'; + $phpcsFile->addError($error, $tag, 'ParamGroup'); + } + + if ($paramGroupid === null) { + $paramGroupid = $groupid; + } + + // All of the $checkTags sections should be separated by a blank + // line both before and after the sections. + } else if ($isNewGroup === false + && (in_array($currentTag, $checkTags) === true || in_array($previousTag, $checkTags) === true) + && $previousTag !== $currentTag + ) { + $error = 'Separate the %s and %s sections by a blank line.'; + $fix = $phpcsFile->addFixableError($error, $tag, 'TagGroupSpacing', [$previousTag, $currentTag]); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($tag - 1), "\n".str_repeat(' ', ($tokens[$tag]['column'] - 3)).'* '); + } + }//end if + + $previousTag = $currentTag; + $tagGroups[$groupid][] = $tag; + }//end foreach + + foreach ($tagGroups as $group) { + $maxLength = 0; + $paddings = []; + $pos = 0; + foreach ($group as $pos => $tag) { + $tagLength = strlen($tokens[$tag]['content']); + if ($tagLength > $maxLength) { + $maxLength = $tagLength; + } + + // Check for a value. No value means no padding needed. + $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd); + if ($string !== false && $tokens[$string]['line'] === $tokens[$tag]['line']) { + $paddings[$tag] = strlen($tokens[($tag + 1)]['content']); + } + } + + // Check that there was single blank line after the tag block + // but account for a multi-line tag comments. + $lastTag = $group[$pos]; + $next = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($lastTag + 3), $commentEnd); + if ($next !== false && $tokens[$next]['column'] === $tokens[$firstTag]['column']) { + $prev = $phpcsFile->findPrevious([T_DOC_COMMENT_TAG, T_DOC_COMMENT_STRING], ($next - 1), $commentStart); + if ($tokens[$next]['line'] !== ($tokens[$prev]['line'] + 2)) { + $error = 'There must be a single blank line after a tag group'; + $fix = $phpcsFile->addFixableError($error, $lastTag, 'SpacingAfterTagGroup'); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + for ($i = ($prev + 1); $i < $next; $i++) { + if ($tokens[$i]['line'] === $tokens[$next]['line']) { + break; + } + + $phpcsFile->fixer->replaceToken($i, ''); + } + + $indent = str_repeat(' ', $tokens[$stackPtr]['column']); + $phpcsFile->fixer->addContent($prev, $phpcsFile->eolChar.$indent.'*'.$phpcsFile->eolChar); + $phpcsFile->fixer->endChangeset(); + } + } + }//end if + + // Now check paddings. + foreach ($paddings as $tag => $padding) { + if ($padding !== 1) { + $error = 'Tag value indented incorrectly; expected 1 space but found %s'; + $data = [$padding]; + + $fix = $phpcsFile->addFixableError($error, ($tag + 1), 'TagValueIndent', $data); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($tag + 1), ' '); + } + } + } + }//end foreach + + // If there is a param group, it needs to be first; with the exception of + // @code, @todo and link tags. + if ($paramGroupid !== null && $paramGroupid !== 0 + && in_array($tokens[$tokens[$commentStart]['comment_tags'][0]]['content'], ['@code', '@todo', '@link', '@endlink', '@codingStandardsIgnoreStart']) === false + // In JSDoc we can have many other valid tags like @function or + // @constructor before the param tags. + && $phpcsFile->tokenizerType !== 'JS' + ) { + $error = 'Parameter tags must be defined first in a doc comment'; + $phpcsFile->addError($error, $tagGroups[$paramGroupid][0], 'ParamNotFirst'); + } + + $foundTags = []; + $lastPos = 0; + foreach ($tokens[$stackPtr]['comment_tags'] as $pos => $tag) { + $tagName = $tokens[$tag]['content']; + // Skip code tags, they can be anywhere. + if (in_array($tagName, $checkTags) === false) { + continue; + } + + if (isset($foundTags[$tagName]) === true) { + $lastTag = $tokens[$stackPtr]['comment_tags'][$lastPos]; + if ($tokens[$lastTag]['content'] !== $tagName) { + $error = 'Tags must be grouped together in a doc comment'; + $phpcsFile->addError($error, $tag, 'TagsNotGrouped'); + } + } + + $foundTags[$tagName] = true; + $lastPos = $pos; + } + + }//end process() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Commenting/DocCommentStarSniff.php b/coder_sniffer/Backdrop/Sniffs/Commenting/DocCommentStarSniff.php new file mode 100644 index 0000000..40be6e3 --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Commenting/DocCommentStarSniff.php @@ -0,0 +1,89 @@ + + */ + public function register() + { + return [T_DOC_COMMENT_OPEN_TAG]; + + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @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(); + + $lastLineChecked = $tokens[$stackPtr]['line']; + for ($i = ($stackPtr + 1); $i < ($tokens[$stackPtr]['comment_closer'] - 1); $i++) { + // We are only interested in the beginning of the line. + if ($tokens[$i]['line'] === $lastLineChecked) { + continue; + } + + // The first token on the line must be a whitespace followed by a star. + if ($tokens[$i]['code'] === T_DOC_COMMENT_WHITESPACE) { + if ($tokens[($i + 1)]['code'] !== T_DOC_COMMENT_STAR) { + $error = 'Doc comment star missing'; + $fix = $phpcsFile->addFixableError($error, $i, 'StarMissing'); + if ($fix === true) { + if (strpos($tokens[$i]['content'], $phpcsFile->eolChar) !== false) { + $phpcsFile->fixer->replaceToken($i, str_repeat(' ', $tokens[$stackPtr]['column'])."* \n"); + } else { + $phpcsFile->fixer->replaceToken($i, str_repeat(' ', $tokens[$stackPtr]['column']).'* '); + } + + // Ordering of lines might have changed - stop here. The + // fixer will restart the sniff if there are remaining fixes. + return; + } + } + } else if ($tokens[$i]['code'] !== T_DOC_COMMENT_STAR) { + $error = 'Doc comment star missing'; + $fix = $phpcsFile->addFixableError($error, $i, 'StarMissing'); + if ($fix === true) { + $phpcsFile->fixer->addContentBefore($i, str_repeat(' ', $tokens[$stackPtr]['column']).'* '); + } + }//end if + + $lastLineChecked = $tokens[$i]['line']; + }//end for + + }//end process() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Commenting/FileCommentSniff.php b/coder_sniffer/Backdrop/Sniffs/Commenting/FileCommentSniff.php index ce7a624..9bd5de2 100644 --- a/coder_sniffer/Backdrop/Sniffs/Commenting/FileCommentSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Commenting/FileCommentSniff.php @@ -2,16 +2,15 @@ /** * Parses and verifies the doc comments for files. * - * PHP version 5 - * * @category PHP * @package PHP_CodeSniffer * @link http://pear.php.net/package/PHP_CodeSniffer */ -if (class_exists('PHP_CodeSniffer_CommentParser_ClassCommentParser', true) === false) { - throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_CommentParser_ClassCommentParser not found'); -} +namespace Backdrop\Sniffs\Commenting; + +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; /** * Parses and verifies the doc comments for files. @@ -27,18 +26,18 @@ * @link http://pear.php.net/package/PHP_CodeSniffer */ -class Backdrop_Sniffs_Commenting_FileCommentSniff implements PHP_CodeSniffer_Sniff +class FileCommentSniff implements Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { - return array(T_OPEN_TAG); + return [T_OPEN_TAG]; }//end register() @@ -46,89 +45,204 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. + * @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 + * @return int */ - public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { - // We are only interested if this is the first open tag. - if ($stackPtr !== 0) { - if ($phpcsFile->findPrevious(T_OPEN_TAG, ($stackPtr - 1)) !== false) { - return; + $tokens = $phpcsFile->getTokens(); + $commentStart = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); + + // Files containing exactly one class, interface or trait are allowed to + // ommit a file doc block. If a namespace is used then the file comment must + // be omitted. + $oopKeyword = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT], $stackPtr); + if ($oopKeyword !== false) { + $namespace = $phpcsFile->findNext(T_NAMESPACE, $stackPtr); + // Check if the file contains multiple classes/interfaces/traits - then a + // file doc block is allowed. + $secondOopKeyword = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT], ($oopKeyword + 1)); + // Namespaced classes, interfaces and traits should not have an @file doc + // block. + if (($tokens[$commentStart]['code'] === T_DOC_COMMENT_OPEN_TAG + || $tokens[$commentStart]['code'] === T_COMMENT) + && $secondOopKeyword === false + && $namespace !== false + ) { + if ($tokens[$commentStart]['code'] === T_COMMENT) { + $phpcsFile->addError('Namespaced classes, interfaces and traits should not begin with a file doc comment', $commentStart, 'NamespaceNoFileDoc'); + } else { + $fix = $phpcsFile->addFixableError('Namespaced classes, interfaces and traits should not begin with a file doc comment', $commentStart, 'NamespaceNoFileDoc'); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + + for ($i = $commentStart; $i <= ($tokens[$commentStart]['comment_closer'] + 1); $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + // If, after removing the comment, there are two new lines + // remove them. + if ($tokens[($commentStart - 1)]['content'] === "\n" && $tokens[$i]['content'] === "\n") { + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->endChangeset(); + } + } + }//end if + + if ($namespace !== false) { + return ($phpcsFile->numTokens + 1); } - } - $tokens = $phpcsFile->getTokens(); + // Search for global functions before and after the class. + $function = $phpcsFile->findPrevious(T_FUNCTION, ($oopKeyword - 1)); + if ($function === false) { + $function = $phpcsFile->findNext(T_FUNCTION, ($tokens[$oopKeyword]['scope_closer'] + 1)); + } - // Find the next non whitespace token. - $commentStart - = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); + $fileTag = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($commentStart + 1), null, false, '@file'); - $errorToken = ($stackPtr + 1); - if (isset($tokens[$errorToken]) === false) { - $errorToken--; - } + // No other classes, no other global functions and no explicit @file tag + // anywhere means it is ok to skip the file comment. + if ($secondOopKeyword === false && $function === false && $fileTag === false) { + return ($phpcsFile->numTokens + 1); + } + }//end if - if ($tokens[$commentStart]['code'] === T_CLOSE_TAG) { - // We are only interested if this is the first open tag. - return; - } else if ($tokens[$commentStart]['code'] === T_COMMENT) { - $error = 'You must use "/**" style comments for a file comment'; - $phpcsFile->addError($error, $errorToken, 'WrongStyle'); - return; - } else if ($commentStart === false - || $tokens[$commentStart]['code'] !== T_DOC_COMMENT - ) { - $phpcsFile->addError('Missing file doc comment', $errorToken, 'Missing'); - return; - } else { - // Extract the header comment docblock. - $commentEnd = $phpcsFile->findNext( - T_DOC_COMMENT, - ($commentStart + 1), - null, - true - ); - - $commentEnd--; - - if ($tokens[$commentStart]['content'] !== ('/**'.$phpcsFile->eolChar)) { - $phpcsFile->addError('The first line in the file doc comment must only be "/**"', $commentStart); - return; + if ($tokens[$commentStart]['code'] === T_COMMENT) { + $fix = $phpcsFile->addFixableError('You must use "/**" style comments for a file comment', $commentStart, 'WrongStyle'); + if ($fix === true) { + $content = $tokens[$commentStart]['content']; + + // If the comment starts with something like "/**" then we just + // insert a space after the stars. + if (strpos($content, '/**') === 0) { + $phpcsFile->fixer->replaceToken($commentStart, str_replace('/**', '/** ', $content)); + } else if (strpos($content, '/*') === 0) { + // Just turn the /* ... */ style comment into a /** ... */ style + // comment. + $phpcsFile->fixer->replaceToken($commentStart, str_replace('/*', '/**', $content)); + } else { + $content = trim(ltrim($tokens[$commentStart]['content'], '/# ')); + $phpcsFile->fixer->replaceToken($commentStart, "/**\n * @file\n * $content\n */\n"); + } } - $fileLine = $phpcsFile->findNext(T_DOC_COMMENT, ($commentStart + 1)); - if ($tokens[$fileLine]['content'] !== (' * @file'.$phpcsFile->eolChar)) { - // If the comment is not followed by whitespace, it is probably not a file doc comment. - if (($tokens[$commentEnd]['line'] + 1) === $tokens[$phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), null, true)]['line']) { - $phpcsFile->addError('Missing file doc comment', $errorToken, 'Missing'); - return; + return ($phpcsFile->numTokens + 1); + } else if ($commentStart === false || $tokens[$commentStart]['code'] !== T_DOC_COMMENT_OPEN_TAG) { + $fix = $phpcsFile->addFixableError('Missing file doc comment', 0, 'Missing'); + if ($fix === true) { + // Only PHP has a real opening tag, additional newline at the + // beginning here. + if ($phpcsFile->tokenizerType === 'PHP') { + // In templates add the file doc block to the very beginning of + // the file. + if ($tokens[0]['code'] === T_INLINE_HTML) { + $phpcsFile->fixer->addContentBefore(0, "\n"); + } else { + $phpcsFile->fixer->addContent($stackPtr, "\n/**\n * @file\n */\n"); + } } else { - $phpcsFile->addError('The second line in the file doc comment must be " * @file"', $fileLine); + $phpcsFile->fixer->addContent($stackPtr, "/**\n * @file\n */\n"); + } + } + + return ($phpcsFile->numTokens + 1); + }//end if + + $commentEnd = $tokens[$commentStart]['comment_closer']; + $fileTag = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($commentStart + 1), $commentEnd, false, '@file'); + $next = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), null, true); + + // If there is no @file tag and the next line is a function or class + // definition then the file docblock is mising. + if ($tokens[$next]['line'] === ($tokens[$commentEnd]['line'] + 1) + && $tokens[$next]['code'] === T_FUNCTION + ) { + if ($fileTag === false) { + $fix = $phpcsFile->addFixableError('Missing file doc comment', $stackPtr, 'Missing'); + if ($fix === true) { + // Only PHP has a real opening tag, additional newline at the + // beginning here. + if ($phpcsFile->tokenizerType === 'PHP') { + $phpcsFile->fixer->addContent($stackPtr, "\n/**\n * @file\n */\n"); + } else { + $phpcsFile->fixer->addContent($stackPtr, "/**\n * @file\n */\n"); + } } + + return ($phpcsFile->numTokens + 1); } + }//end if - $descriptionLine = $phpcsFile->findNext(T_DOC_COMMENT, ($fileLine + 1), ($commentEnd + 1)); - if ($descriptionLine !== false && preg_match('/^ \* [^\s]+/', $tokens[$descriptionLine]['content']) === 0) { - $error = 'The third line in the file doc comment must contain a description and must not be indented'; - $phpcsFile->addError($error, $descriptionLine, 'DescriptionMissing'); + if ($fileTag === false || $tokens[$fileTag]['line'] !== ($tokens[$commentStart]['line'] + 1)) { + $secondLine = $phpcsFile->findNext([T_DOC_COMMENT_STAR, T_DOC_COMMENT_CLOSE_TAG], ($commentStart + 1), $commentEnd); + $fix = $phpcsFile->addFixableError('The second line in the file doc comment must be "@file"', $secondLine, 'FileTag'); + if ($fix === true) { + if ($fileTag === false) { + $phpcsFile->fixer->addContent($commentStart, "\n * @file"); + } else { + // Delete the @file tag at its current position and insert one + // after the beginning of the comment. + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->addContent($commentStart, "\n * @file"); + $phpcsFile->fixer->replaceToken($fileTag, ''); + $phpcsFile->fixer->endChangeset(); + } } - if ($tokens[$commentEnd]['content'] !== (' */')) { - $phpcsFile->addError('The last line in the file doc comment must be " */"', $commentEnd); + return ($phpcsFile->numTokens + 1); + } + + // Exactly one blank line after the file comment. + if ($tokens[$next]['line'] !== ($tokens[$commentEnd]['line'] + 2) + && $next !== false && $tokens[$next]['code'] !== T_CLOSE_TAG + ) { + $error = 'There must be exactly one blank line after the file comment'; + $fix = $phpcsFile->addFixableError($error, $commentEnd, 'SpacingAfterComment'); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + $uselessLine = ($commentEnd + 1); + while ($uselessLine < $next) { + $phpcsFile->fixer->replaceToken($uselessLine, ''); + $uselessLine++; + } + + $phpcsFile->fixer->addContent($commentEnd, "\n\n"); + $phpcsFile->fixer->endChangeset(); } - if (($tokens[$commentEnd]['line'] + 1) === $tokens[$phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), null, true)]['line']) { - $phpcsFile->addError('File doc comments must be followed by a blank line.', ($commentEnd + 1), 'SpacingAfter'); + return ($phpcsFile->numTokens + 1); + } + + // Template file: no blank line after the file comment. + if ($tokens[$next]['line'] !== ($tokens[$commentEnd]['line'] + 1) + && $tokens[$next]['line'] > $tokens[$commentEnd]['line'] + && $tokens[$next]['code'] === T_CLOSE_TAG + ) { + $error = 'There must be no blank line after the file comment in a template'; + $fix = $phpcsFile->addFixableError($error, $commentEnd, 'TeamplateSpacingAfterComment'); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + $uselessLine = ($commentEnd + 1); + while ($uselessLine < $next) { + $phpcsFile->fixer->replaceToken($uselessLine, ''); + $uselessLine++; + } + + $phpcsFile->fixer->addContent($commentEnd, "\n"); + $phpcsFile->fixer->endChangeset(); } - }//end if + } + + // Ignore the rest of the file. + return ($phpcsFile->numTokens + 1); }//end process() }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Commenting/FunctionCommentSniff.php b/coder_sniffer/Backdrop/Sniffs/Commenting/FunctionCommentSniff.php index 4b2c9d3..67f887f 100644 --- a/coder_sniffer/Backdrop/Sniffs/Commenting/FunctionCommentSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Commenting/FunctionCommentSniff.php @@ -2,84 +2,77 @@ /** * Parses and verifies the doc comments for functions. * - * PHP version 5 - * * @category PHP * @package PHP_CodeSniffer * @link http://pear.php.net/package/PHP_CodeSniffer */ +namespace Backdrop\Sniffs\Commenting; + +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Tokens; + /** * Parses and verifies the doc comments for functions. Largely copied from - * Squiz_Sniffs_Commenting_FunctionCommentSniff. + * PHP_CodeSniffer\Standards\Squiz\Sniffs\Commenting\FunctionCommentSniff. * * @category PHP * @package PHP_CodeSniffer * @link http://pear.php.net/package/PHP_CodeSniffer */ -class Backdrop_Sniffs_Commenting_FunctionCommentSniff implements PHP_CodeSniffer_Sniff +class FunctionCommentSniff implements Sniff { /** - * The name of the method that we are currently processing. - * - * @var string - */ - private $_methodName = ''; - - /** - * The position in the stack where the fucntion token was found. - * - * @var int - */ - private $_functionToken = null; - - /** - * The position in the stack where the class token was found. - * - * @var int - */ - private $_classToken = null; - - /** - * The function comment parser for the current method. - * - * @var Backdrop_CommentParser_FunctionCommentParser - */ - protected $commentParser = null; - - /** - * The current PHP_CodeSniffer_File object we are processing. + * A map of invalid data types to valid ones for param and return documentation. * - * @var PHP_CodeSniffer_File + * @var array */ - protected $currentFile = null; + public static $invalidTypes = [ + 'Array' => 'array', + 'array()' => 'array', + '[]' => 'array', + 'boolean' => 'bool', + 'Boolean' => 'bool', + 'integer' => 'int', + 'str' => 'string', + 'stdClass' => 'object', + '\stdClass' => 'object', + 'number' => 'int', + 'String' => 'string', + 'type' => 'mixed', + 'NULL' => 'null', + 'FALSE' => 'false', + 'TRUE' => 'true', + 'Bool' => 'bool', + 'Int' => 'int', + 'Integer' => 'int', + 'TRUEFALSE' => 'bool', + ]; /** - * A map of invalid data types to valid ones for param and return documentation. + * An array of variable types for param/var we will check. * - * @var array + * @var array */ - protected $invalidTypes = array( - 'Array' => 'array', - 'boolean' => 'bool', - 'Boolean' => 'bool', - 'integer' => 'int', - 'str' => 'string', - 'stdClass' => 'object', - 'number' => 'int', - 'String' => 'string', - ); + public $allowedTypes = [ + 'array', + 'mixed', + 'object', + 'resource', + 'callable', + ]; /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { - return array(T_FUNCTION); + return [T_FUNCTION]; }//end register() @@ -87,472 +80,840 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. + * @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(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { - $this->currentFile = $phpcsFile; - $tokens = $phpcsFile->getTokens(); + $find = Tokens::$methodPrefixes; + $find[] = T_WHITESPACE; + + $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1), null, true); + $beforeCommentEnd = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($commentEnd - 1), null, true); + if (($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG + && $tokens[$commentEnd]['code'] !== T_COMMENT) + || ($beforeCommentEnd !== false + // If there is something more on the line than just the comment then the + // comment does not belong to the function. + && $tokens[$beforeCommentEnd]['line'] === $tokens[$commentEnd]['line']) + ) { + $fix = $phpcsFile->addFixableError('Missing function doc comment', $stackPtr, 'Missing'); + if ($fix === true) { + $before = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), ($stackPtr + 1), true); + $phpcsFile->fixer->addContentBefore($before, "/**\n *\n */\n"); + } - $find = array( - T_COMMENT, - T_DOC_COMMENT, - T_CLASS, - T_FUNCTION, - T_OPEN_TAG, - ); - - $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1)); - - if ($commentEnd === false) { return; } - // If the token that we found was a class or a function, then this - // function has no doc comment. - $code = $tokens[$commentEnd]['code']; - - if ($code === T_COMMENT) { - // The function might actually be missing a comment, and this last comment - // found is just commenting a bit of code on a line. So if it is not the - // only thing on the line, assume we found nothing. - $prevContent = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, $commentEnd); - if ($tokens[$commentEnd]['line'] === $tokens[$commentEnd]['line']) { - $error = 'Missing function doc comment'; - $phpcsFile->addError($error, $stackPtr, 'Missing'); - } else { - $error = 'You must use "/**" style comments for a function comment'; - $phpcsFile->addError($error, $stackPtr, 'WrongStyle'); + if ($tokens[$commentEnd]['code'] === T_COMMENT) { + $fix = $phpcsFile->addFixableError('You must use "/**" style comments for a function comment', $stackPtr, 'WrongStyle'); + if ($fix === true) { + // Convert the comment into a doc comment. + $phpcsFile->fixer->beginChangeset(); + $comment = ''; + for ($i = $commentEnd; $tokens[$i]['code'] === T_COMMENT; $i--) { + $comment = ' *'.ltrim($tokens[$i]['content'], '/* ').$comment; + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->replaceToken($commentEnd, "/**\n".rtrim($comment, "*/\n")."\n */\n"); + $phpcsFile->fixer->endChangeset(); } - return; - } else if ($code !== T_DOC_COMMENT) { - $error = 'Missing function doc comment'; - $phpcsFile->addError($error, $stackPtr, 'Missing'); - return; - } else if (trim($tokens[$commentEnd]['content']) !== '*/') { - $error = 'Wrong function doc comment end; expected "*/", found "%s"'; - $phpcsFile->addError($error, $commentEnd, 'WrongEnd', array(trim($tokens[$commentEnd]['content']))); - return; - } - // If there is any code between the function keyword and the doc block - // then the doc block is not for us. - $ignore = PHP_CodeSniffer_Tokens::$scopeModifiers; - $ignore[] = T_STATIC; - $ignore[] = T_WHITESPACE; - $ignore[] = T_ABSTRACT; - $ignore[] = T_FINAL; - $prevToken = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true); - if ($prevToken !== $commentEnd) { - $phpcsFile->addError('Missing function doc comment', $stackPtr, 'Missing'); return; } - $this->_functionToken = $stackPtr; + $commentStart = $tokens[$commentEnd]['comment_opener']; + foreach ($tokens[$commentStart]['comment_tags'] as $tag) { + // This is a file comment, not a function comment. + if ($tokens[$tag]['content'] === '@file') { + $fix = $phpcsFile->addFixableError('Missing function doc comment', $stackPtr, 'Missing'); + if ($fix === true) { + $before = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), ($stackPtr + 1), true); + $phpcsFile->fixer->addContentBefore($before, "/**\n *\n */\n"); + } - $this->_classToken = null; - foreach ($tokens[$stackPtr]['conditions'] as $condPtr => $condition) { - if ($condition === T_CLASS || $condition === T_INTERFACE) { - $this->_classToken = $condPtr; - break; + return; } - } - // If the first T_OPEN_TAG is right before the comment and does not immediately precede the function - // probably a file comment. - $commentStart = ($phpcsFile->findPrevious(T_DOC_COMMENT, ($commentEnd - 1), null, true) + 1); - $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($commentStart - 1), null, true); - if ($tokens[$prevToken]['code'] === T_OPEN_TAG) { - // Is this the first open tag - if (($stackPtr === 0 || $phpcsFile->findPrevious(T_OPEN_TAG, ($prevToken - 1)) === false) - && (($tokens[$commentEnd]['line'] + 1) !== $tokens[$stackPtr]['line'])) { - $phpcsFile->addError('Missing function doc comment', $stackPtr, 'Missing'); - return; + if ($tokens[$tag]['content'] === '@see') { + // Make sure the tag isn't empty. + $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd); + if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) { + $error = 'Content missing for @see tag in function comment'; + $phpcsFile->addError($error, $tag, 'EmptySees'); + } + } + }//end foreach + + if ($tokens[$commentEnd]['line'] !== ($tokens[$stackPtr]['line'] - 1)) { + $error = 'There must be no blank lines after the function comment'; + $fix = $phpcsFile->addFixableError($error, $commentEnd, 'SpacingAfter'); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($commentEnd + 1), ''); } } - $commentString = $phpcsFile->getTokensAsString($commentStart, ($commentEnd - $commentStart + 1)); - $this->_methodName = $phpcsFile->getDeclarationName($stackPtr); + $this->processReturn($phpcsFile, $stackPtr, $commentStart); + $this->processThrows($phpcsFile, $stackPtr, $commentStart); + $this->processParams($phpcsFile, $stackPtr, $commentStart); + $this->processSees($phpcsFile, $stackPtr, $commentStart); - try { - $this->commentParser = new Backdrop_CommentParser_FunctionCommentParser($commentString, $phpcsFile); - $this->commentParser->parse(); - } catch (PHP_CodeSniffer_CommentParser_ParserException $e) { - $line = ($e->getLineWithinComment() + $commentStart); - $phpcsFile->addError($e->getMessage(), $line, 'FailedParse'); - return; - } + }//end process() - $comment = $this->commentParser->getComment(); - if (is_null($comment) === true) { - $error = 'Function doc comment is empty'; - $phpcsFile->addError($error, $commentStart, 'Empty'); - return; - } - // The first line of the comment should just be the /** code. - $eolPos = strpos($commentString, $phpcsFile->eolChar); - $firstLine = substr($commentString, 0, $eolPos); - if ($firstLine !== '/**') { - $error = 'The open comment tag must be the only content on the line'; - $phpcsFile->addError($error, $commentStart, 'ContentAfterOpen'); - } + /** + * Process the return comment of this function comment. + * + * @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. + * @param int $commentStart The position in the stack where the comment started. + * + * @return void + */ + protected function processReturn(File $phpcsFile, $stackPtr, $commentStart) + { + $tokens = $phpcsFile->getTokens(); - // Check that the comment is imidiately before the function definition. - if (($tokens[$commentEnd]['line'] + 1) !== $tokens[$stackPtr]['line']) { - $error = 'Function doc comment must end on the line before the function definition'; - $phpcsFile->addError($error, $commentEnd, 'EmptyLinesAfterDoc'); + // Skip constructor and destructor. + $className = ''; + foreach ($tokens[$stackPtr]['conditions'] as $condPtr => $condition) { + if ($condition === T_CLASS || $condition === T_INTERFACE) { + $className = $phpcsFile->getDeclarationName($condPtr); + $className = strtolower(ltrim($className, '_')); + } } - $this->processParams($commentStart); - $this->processReturn($commentStart, $commentEnd); - $this->processThrows($commentStart); - $this->processSees($commentStart); + $methodName = $phpcsFile->getDeclarationName($stackPtr); + $isSpecialMethod = ($methodName === '__construct' || $methodName === '__destruct'); + $methodName = strtolower(ltrim($methodName, '_')); - // Check if hook implementation doc is formated correctly. - if (preg_match('/^[\s]*Implement[^\n]+?hook_[^\n]+/i', $comment->getShortComment(), $matches)) { - if (!strstr($matches[0], 'Implements ') || strstr($matches[0], 'Implements of') - || !preg_match('/ (drush_)?hook_[a-zA-Z0-9_]+\(\)( for [a-z0-9_-]+(\(\)|\.tpl\.php|\.html.twig))?\.$/', $matches[0]) - ) { - $phpcsFile->addWarning('Format should be "* Implements hook_foo().", "* Implements hook_foo_BAR_ID_bar() for xyz_bar().",, "* Implements hook_foo_BAR_ID_bar() for xyz-bar.html.twig.", or "* Implements hook_foo_BAR_ID_bar() for xyz-bar.tpl.php.".', $commentStart + 1); - } else { - // Check that a hook implementation does not duplicate @param and - // @return documentation. - $params = $this->commentParser->getParams(); - if (empty($params) === false) { - $param = array_shift($params); - $errorPos = ($param->getLine() + $commentStart); - $warn = 'Hook implementations should not duplicate @param documentation'; - $phpcsFile->addWarning($warn, $errorPos, 'HookParamDoc'); + $return = null; + $end = $stackPtr; + foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { + if ($tokens[$tag]['content'] === '@return') { + if ($return !== null) { + $error = 'Only 1 @return tag is allowed in a function comment'; + $phpcsFile->addError($error, $tag, 'DuplicateReturn'); + return; } - $return = $this->commentParser->getReturn(); - if ($return !== null) { - $errorPos = ($commentStart + $this->commentParser->getReturn()->getLine()); - $warn = 'Hook implementations should not duplicate @return documentation'; - $phpcsFile->addWarning($warn, $errorPos, 'HookReturnDoc'); + $return = $tag; + // Any strings until the next tag belong to this comment. + if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) { + $skipTags = [ + '@code', + '@endcode', + ]; + $skipPos = ($pos + 1); + while (isset($tokens[$commentStart]['comment_tags'][$skipPos]) === true + && in_array($tokens[$commentStart]['comment_tags'][$skipPos], $skipTags) === true + ) { + $skipPos++; + } + + $end = $tokens[$commentStart]['comment_tags'][$skipPos]; + } else { + $end = $tokens[$commentStart]['comment_closer']; } }//end if - }//end if + }//end foreach - // Check for a comment description. - $short = $comment->getShortComment(); - if (trim($short) === '') { - $error = 'Missing short description in function doc comment'; - $phpcsFile->addError($error, $commentStart, 'MissingShort'); - return; - } - - // No extra newline before short description. - $newlineCount = 0; - $newlineSpan = strspn($short, $phpcsFile->eolChar); - if ($short !== '' && $newlineSpan > 0) { - $line = ($newlineSpan > 1) ? 'newlines' : 'newline'; - $error = "Extra $line found before function comment short description"; - $phpcsFile->addError($error, ($commentStart + 1)); - return; - } + $type = null; + if ($isSpecialMethod === false) { + if ($return !== null) { + $type = trim($tokens[($return + 2)]['content']); + if (empty($type) === true || $tokens[($return + 2)]['code'] !== T_DOC_COMMENT_STRING) { + $error = 'Return type missing for @return tag in function comment'; + $phpcsFile->addError($error, $return, 'MissingReturnType'); + } else if (strpos($type, ' ') === false) { + // Check return type (can be multiple, separated by '|'). + $typeNames = explode('|', $type); + $suggestedNames = []; + $hasNull = false; + + foreach ($typeNames as $i => $typeName) { + if (strtolower($typeName) === 'null') { + $hasNull = true; + } - $newlineCount = (substr_count($short, $phpcsFile->eolChar) + 1); + $suggestedName = static::suggestType($typeName); + if (in_array($suggestedName, $suggestedNames) === false) { + $suggestedNames[] = $suggestedName; + } + } - // Exactly one blank line between short and long description. - $long = $comment->getLongComment(); - if (empty($long) === false) { - $between = $comment->getWhiteSpaceBetween(); - $newlineBetween = substr_count($between, $phpcsFile->eolChar); - if ($newlineBetween !== 2) { - $error = 'There must be exactly one blank line between descriptions in function comment'; - $phpcsFile->addError($error, ($commentStart + $newlineCount + 1), 'SpacingAfterShort'); - } + $suggestedType = implode('|', $suggestedNames); + if ($type !== $suggestedType) { + $error = 'Expected "%s" but found "%s" for function return type'; + $data = [ + $suggestedType, + $type, + ]; + $fix = $phpcsFile->addFixableError($error, $return, 'InvalidReturn', $data); + if ($fix === true) { + $content = $suggestedType; + $phpcsFile->fixer->replaceToken(($return + 2), $content); + } + }//end if - $newlineCount += $newlineBetween; - } + if ($type === 'void') { + $error = 'If there is no return value for a function, there must not be a @return tag.'; + $phpcsFile->addError($error, $return, 'VoidReturn'); + } else if ($type !== 'mixed') { + // If return type is not void, there needs to be a return statement + // somewhere in the function that returns something. + if (isset($tokens[$stackPtr]['scope_closer']) === true) { + $endToken = $tokens[$stackPtr]['scope_closer']; + $foundReturnToken = false; + $searchStart = $stackPtr; + $foundNonVoidReturn = false; + do { + $returnToken = $phpcsFile->findNext([T_RETURN, T_YIELD, T_YIELD_FROM], $searchStart, $endToken); + if ($returnToken === false && $foundReturnToken === false) { + $error = '@return doc comment specified, but function has no return statement'; + $phpcsFile->addError($error, $return, 'InvalidNoReturn'); + } else { + // Check for return token as the last loop after the last return + // in the function will enter this else condition + // but without the returnToken. + if ($returnToken !== false) { + $foundReturnToken = true; + $semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true); + if ($tokens[$semicolon]['code'] === T_SEMICOLON) { + // Void return is allowed if the @return type has null in it. + if ($hasNull === false) { + $error = 'Function return type is not void, but function is returning void here'; + $phpcsFile->addError($error, $returnToken, 'InvalidReturnNotVoid'); + } + } else { + $foundNonVoidReturn = true; + }//end if + + $searchStart = ($returnToken + 1); + }//end if + }//end if + } while ($returnToken !== false); + + if ($foundNonVoidReturn === false && $foundReturnToken === true) { + $error = 'Function return type is not void, but function does not have a non-void return statement'; + $phpcsFile->addError($error, $return, 'InvalidReturnNotVoid'); + } + }//end if + }//end if + }//end if - // Short description must be single line and end with a full stop. - $testShort = trim($short); - $lastChar = $testShort[(strlen($testShort) - 1)]; - if (substr_count($testShort, $phpcsFile->eolChar) !== 0) { - $error = 'Function comment short description must be on a single line, further text should be a separate paragraph'; - $phpcsFile->addError($error, ($commentStart + 1), 'ShortSingleLine'); - } + $comment = ''; + for ($i = ($return + 3); $i < $end; $i++) { + if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { + $indent = 0; + if ($tokens[($i - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) { + $indent = strlen($tokens[($i - 1)]['content']); + } - if (strpos($short, $testShort) !== 1) { - $error = 'Function comment short description must start with exactly one space'; - $phpcsFile->addError($error, ($commentStart + 1), 'ShortStartSpace'); - } + $comment .= ' '.$tokens[$i]['content']; + $commentLines[] = [ + 'comment' => $tokens[$i]['content'], + 'token' => $i, + 'indent' => $indent, + ]; + if ($indent < 3) { + $error = 'Return comment indentation must be 3 spaces, found %s spaces'; + $fix = $phpcsFile->addFixableError($error, $i, 'ReturnCommentIndentation', [$indent]); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($i - 1), ' '); + } + } + } + }//end for + + // The first line of the comment must be indented no more than 3 + // spaces, the following lines can be more so we only check the first + // line. + if (empty($commentLines[0]['indent']) === false && $commentLines[0]['indent'] > 3) { + $error = 'Return comment indentation must be 3 spaces, found %s spaces'; + $fix = $phpcsFile->addFixableError($error, ($commentLines[0]['token'] - 1), 'ReturnCommentIndentation', [$commentLines[0]['indent']]); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($commentLines[0]['token'] - 1), ' '); + } + } - if ($testShort !== '{@inheritdoc}') { - if (preg_match('|[A-Z]|', $testShort[0]) === 0) { - $error = 'Function comment short description must start with a capital letter'; - $phpcsFile->addError($error, ($commentStart + 1), 'ShortNotCapital'); - } + if ($comment === '' && $type !== '$this' && $type !== 'static') { + if (strpos($type, ' ') !== false) { + $error = 'Description for the @return value must be on the next line'; + } else { + $error = 'Description for the @return value is missing'; + } - if ($lastChar !== '.') { - $error = 'Function comment short description must end with a full stop'; - $phpcsFile->addError($error, ($commentStart + 1), 'ShortFullStop'); - } - } + $phpcsFile->addError($error, $return, 'MissingReturnComment'); + } else if (strpos($type, ' ') !== false) { + if (preg_match('/^([^\s]+)[\s]+(\$[^\s]+)[\s]*$/', $type, $matches) === 1) { + $error = 'Return type must not contain variable name "%s"'; + $data = [$matches[2]]; + $fix = $phpcsFile->addFixableError($error, ($return + 2), 'ReturnVarName', $data); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($return + 2), $matches[1]); + } + } else { + $error = 'Return type "%s" must not contain spaces'; + $data = [$type]; + $phpcsFile->addError($error, $return, 'ReturnTypeSpaces', $data); + } + }//end if + }//end if + }//end if - }//end process() + }//end processReturn() /** * Process any throw tags that this function comment has. * - * @param int $commentStart The position in the stack where the - * comment started. + * @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. + * @param int $commentStart The position in the stack where the comment started. * * @return void */ - protected function processThrows($commentStart) + protected function processThrows(File $phpcsFile, $stackPtr, $commentStart) { - if (count($this->commentParser->getThrows()) === 0) { - return; - } + $tokens = $phpcsFile->getTokens(); - foreach ($this->commentParser->getThrows() as $throw) { + foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { + if ($tokens[$tag]['content'] !== '@throws') { + continue; + } - $exception = $throw->getValue(); - $errorPos = ($commentStart + $throw->getLine()); + if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) { + $error = 'Exception type missing for @throws tag in function comment'; + $phpcsFile->addError($error, $tag, 'InvalidThrows'); + } else { + // Any strings until the next tag belong to this comment. + if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) { + $end = $tokens[$commentStart]['comment_tags'][($pos + 1)]; + } else { + $end = $tokens[$commentStart]['comment_closer']; + } - if ($exception === '') { - $error = '@throws tag must contain the exception class name'; - $this->currentFile->addError($error, $errorPos, 'EmptyThrows'); - } - } + $comment = ''; + $throwStart = null; + for ($i = ($tag + 3); $i < $end; $i++) { + if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { + if ($throwStart === null) { + $throwStart = $i; + } + + $indent = 0; + if ($tokens[($i - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) { + $indent = strlen($tokens[($i - 1)]['content']); + } + + $comment .= ' '.$tokens[$i]['content']; + if ($indent < 3) { + $error = 'Throws comment indentation must be 3 spaces, found %s spaces'; + $phpcsFile->addError($error, $i, 'TrhowsCommentIndentation', [$indent]); + } + } + } + + $comment = trim($comment); + + if ($comment === '') { + if (str_word_count($tokens[($tag + 2)]['content'], 0, '\\_') > 1) { + $error = '@throws comment must be on the next line'; + $phpcsFile->addError($error, $tag, 'ThrowsComment'); + } + + return; + } + + // Starts with a capital letter and ends with a fullstop. + $firstChar = $comment[0]; + if (strtoupper($firstChar) !== $firstChar) { + $error = '@throws tag comment must start with a capital letter'; + $phpcsFile->addError($error, $throwStart, 'ThrowsNotCapital'); + } + + $lastChar = substr($comment, -1); + if (in_array($lastChar, ['.', '!', '?']) === false) { + $error = '@throws tag comment must end with a full stop'; + $phpcsFile->addError($error, $throwStart, 'ThrowsNoFullStop'); + } + }//end if + }//end foreach }//end processThrows() /** - * Process the return comment of this function comment. + * Process the function parameter comments. * - * @param int $commentStart The position in the stack where the comment started. - * @param int $commentEnd The position in the stack where the comment ended. + * @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. + * @param int $commentStart The position in the stack where the comment started. * * @return void */ - protected function processReturn($commentStart, $commentEnd) + protected function processParams(File $phpcsFile, $stackPtr, $commentStart) { - // Skip constructor and destructor. - $className = ''; - if ($this->_classToken !== null) { - $className = $this->currentFile->getDeclarationName($this->_classToken); - $className = strtolower(ltrim($className, '_')); - } + $tokens = $phpcsFile->getTokens(); - $methodName = strtolower(ltrim($this->_methodName, '_')); - $isSpecialMethod = ($this->_methodName === '__construct' || $this->_methodName === '__destruct'); + $params = []; + $maxType = 0; + $maxVar = 0; + foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { + if ($tokens[$tag]['content'] !== '@param') { + continue; + } - if ($isSpecialMethod === false && $methodName !== $className) { - $return = $this->commentParser->getReturn(); - if ($return !== null) { - $errorPos = ($commentStart + $this->commentParser->getReturn()->getLine()); - if (trim($return->getRawContent()) === '') { - $error = '@return tag is empty in function comment'; - $this->currentFile->addError($error, $errorPos, 'EmptyReturn'); - return; + $type = ''; + $typeSpace = 0; + $var = ''; + $varSpace = 0; + $comment = ''; + $commentLines = []; + if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) { + $matches = []; + preg_match('/([^$&]*)(?:((?:\$|&)[^\s]+)(?:(\s+)(.*))?)?/', $tokens[($tag + 2)]['content'], $matches); + + $typeLen = strlen($matches[1]); + $type = trim($matches[1]); + $typeSpace = ($typeLen - strlen($type)); + $typeLen = strlen($type); + if ($typeLen > $maxType) { + $maxType = $typeLen; } - $comment = $return->getComment(); - $commentWhitespace = $return->getWhitespaceBeforeComment(); - if (substr_count($return->getWhitespaceBeforeValue(), $this->currentFile->eolChar) > 0) { - $error = 'Data type of return value is missing'; - $this->currentFile->addError($error, $errorPos, 'MissingReturnType'); - // Treat the value as part of the comment. - $comment = $return->getValue().' '.$comment; - $commentWhitespace = $return->getWhitespaceBeforeValue(); - } else if ($return->getWhitespaceBeforeValue() !== ' ') { - $error = 'Expected 1 space before return type'; - $this->currentFile->addError($error, $errorPos, 'SpacingBeforeReturnType'); + // If there is more than one word then it is a comment that should be + // on the next line. + if (isset($matches[4]) === true && ($typeLen > 0 + || preg_match('/[^\s]+[\s]+[^\s]+/', $matches[4]) === 1) + ) { + $comment = $matches[4]; + $error = 'Parameter comment must be on the next line'; + $fix = $phpcsFile->addFixableError($error, ($tag + 2), 'ParamCommentNewLine'); + if ($fix === true) { + $parts = $matches; + unset($parts[0]); + $parts[3] = "\n * "; + $phpcsFile->fixer->replaceToken(($tag + 2), implode('', $parts)); + } } - if (strpos($return->getValue(), '$') !== false && $return->getValue() !== '$this') { - $error = '@return data type must not contain "$"'; - $this->currentFile->addError($error, $errorPos, '$InReturnType'); + if (isset($matches[2]) === true) { + $var = $matches[2]; + } else { + $var = ''; } - if (in_array($return->getValue(), array('unknown_type', '', 'type')) === true) { - $error = 'Expected a valid @return data type, but found %s'; - $data = array($return->getValue()); - $this->currentFile->addError($error, $errorPos, 'InvalidReturnType', $data); + if (substr($var, -1) === '.') { + $error = 'Doc comment parameter name "%s" must not end with a dot'; + $fix = $phpcsFile->addFixableError($error, ($tag + 2), 'ParamNameDot', [$var]); + if ($fix === true) { + $content = $type.' '.substr($var, 0, -1); + $phpcsFile->fixer->replaceToken(($tag + 2), $content); + } + + // Continue with the next parameter to avoid confusing + // overlapping errors further down. + continue; } - if (strtolower($return->getValue()) === 'void') { - $error = 'If there is no return value for a function, there must not be a @return tag.'; - $this->currentFile->addError($error, $errorPos, 'VoidReturn'); + $varLen = strlen($var); + if ($varLen > $maxVar) { + $maxVar = $varLen; } - if (isset($this->invalidTypes[$return->getValue()]) === true) { - $error = 'Invalid @return data type, expected %s but found %s'; - $data = array($this->invalidTypes[$return->getValue()], $return->getValue()); - $this->currentFile->addError($error, $errorPos, 'InvalidReturnTypeName', $data); + // Any strings until the next tag belong to this comment. + if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) { + // Ignore code tags and include them within this comment. + $skipTags = [ + '@code', + '@endcode', + '@link', + ]; + $skipPos = $pos; + while (isset($tokens[$commentStart]['comment_tags'][($skipPos + 1)]) === true) { + $skipPos++; + if (in_array($tokens[$tokens[$commentStart]['comment_tags'][$skipPos]]['content'], $skipTags) === false + // Stop when we reached the next tag on the outer @param level. + && $tokens[$tokens[$commentStart]['comment_tags'][$skipPos]]['column'] === $tokens[$tag]['column'] + ) { + break; + } + } + + if ($tokens[$tokens[$commentStart]['comment_tags'][$skipPos]]['column'] === ($tokens[$tag]['column'] + 2)) { + $end = $tokens[$commentStart]['comment_closer']; + } else { + $end = $tokens[$commentStart]['comment_tags'][$skipPos]; + } + } else { + $end = $tokens[$commentStart]['comment_closer']; + }//end if + + for ($i = ($tag + 3); $i < $end; $i++) { + if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { + $indent = 0; + if ($tokens[($i - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) { + $indent = strlen($tokens[($i - 1)]['content']); + // There can be @code or @link tags within an @param comment. + if ($tokens[($i - 2)]['code'] === T_DOC_COMMENT_TAG) { + $indent = 0; + if ($tokens[($i - 3)]['code'] === T_DOC_COMMENT_WHITESPACE) { + $indent = strlen($tokens[($i - 3)]['content']); + } + } + } + + $comment .= ' '.$tokens[$i]['content']; + $commentLines[] = [ + 'comment' => $tokens[$i]['content'], + 'token' => $i, + 'indent' => $indent, + ]; + if ($indent < 3) { + $error = 'Parameter comment indentation must be 3 spaces, found %s spaces'; + $fix = $phpcsFile->addFixableError($error, $i, 'ParamCommentIndentation', [$indent]); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($i - 1), ' '); + } + } + }//end if + }//end for + + // The first line of the comment must be indented no more than 3 + // spaces, the following lines can be more so we only check the first + // line. + if (empty($commentLines[0]['indent']) === false && $commentLines[0]['indent'] > 3) { + $error = 'Parameter comment indentation must be 3 spaces, found %s spaces'; + $fix = $phpcsFile->addFixableError($error, ($commentLines[0]['token'] - 1), 'ParamCommentIndentation', [$commentLines[0]['indent']]); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($commentLines[0]['token'] - 1), ' '); + } } - if (trim($comment) === '') { - $error = 'Missing comment for @return statement'; - $this->currentFile->addError($error, $errorPos, 'MissingReturnComment'); - } else if (substr_count($commentWhitespace, $this->currentFile->eolChar) !== 1) { - $error = 'Return comment must be on the next line'; - $this->currentFile->addError($error, $errorPos, 'ReturnCommentNewLine'); - } else if (substr_count($commentWhitespace, ' ') !== 3) { - $error = 'Return comment indentation must be 2 additional spaces'; - $this->currentFile->addError($error, $errorPos + 1, 'ParamCommentIndentation'); + if ($comment === '') { + $error = 'Missing parameter comment'; + $phpcsFile->addError($error, $tag, 'MissingParamComment'); + $commentLines[] = ['comment' => '']; + }//end if + + $variableArguments = false; + // Allow the "..." @param doc for a variable number of parameters. + // This could happen with type defined as @param array ... or + // without type defined as @param ... + if ($tokens[($tag + 2)]['content'] === '...' + || (substr($tokens[($tag + 2)]['content'], -3) === '...' + && count(explode(' ', $tokens[($tag + 2)]['content'])) === 2) + ) { + $variableArguments = true; } - } - } - }//end processReturn() + if ($typeLen === 0) { + $error = 'Missing parameter type'; + // If there is just one word as comment at the end of the line + // then this is probably the data type. Move it before the + // variable name. + if (isset($matches[4]) === true && preg_match('/[^\s]+[\s]+[^\s]+/', $matches[4]) === 0) { + $fix = $phpcsFile->addFixableError($error, $tag, 'MissingParamType'); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($tag + 2), $matches[4].' '.$var); + } + } else { + $phpcsFile->addError($error, $tag, 'MissingParamType'); + } + } + if (empty($matches[2]) === true && $variableArguments === false) { + $error = 'Missing parameter name'; + $phpcsFile->addError($error, $tag, 'MissingParamName'); + } + } else { + $error = 'Missing parameter type'; + $phpcsFile->addError($error, $tag, 'MissingParamType'); + }//end if - /** - * Process the function parameter comments. - * - * @param int $commentStart The position in the stack where - * the comment started. - * - * @return void - */ - protected function processParams($commentStart) - { - $realParams = $this->currentFile->getMethodParameters($this->_functionToken); + $params[] = [ + 'tag' => $tag, + 'type' => $type, + 'var' => $var, + 'comment' => $comment, + 'commentLines' => $commentLines, + 'type_space' => $typeSpace, + 'var_space' => $varSpace, + ]; + }//end foreach + + $realParams = $phpcsFile->getMethodParameters($stackPtr); + $foundParams = []; + + $checkPos = 0; + foreach ($params as $pos => $param) { + if ($param['var'] === '') { + continue; + } - $params = $this->commentParser->getParams(); - $foundParams = array(); + $foundParams[] = $param['var']; - if (empty($params) === false) { - // There must be an empty line before the parameter block. - if (substr_count($params[0]->getWhitespaceBefore(), $this->currentFile->eolChar) < 2) { - $error = 'There must be an empty line before the parameter block'; - $errorPos = ($params[0]->getLine() + $commentStart); - $this->currentFile->addError($error, $errorPos, 'SpacingBeforeParams'); + // If the type is empty, the whole line is empty. + if ($param['type'] === '') { + continue; } - $lastParm = (count($params) - 1); - if (substr_count($params[$lastParm]->getWhitespaceAfter(), $this->currentFile->eolChar) !== 2) { - $error = 'Last parameter comment requires a blank newline after it'; - $errorPos = ($params[$lastParm]->getLine() + $commentStart); - $this->currentFile->addError($error, $errorPos, 'SpacingAfterParams'); + // Make sure the param name is correct. + $matched = false; + // Parameter documentation can be omitted for some parameters, so we have + // to search the rest for a match. + $realName = ''; + while (isset($realParams[($checkPos)]) === true) { + $realName = $realParams[$checkPos]['name']; + + if ($realName === $param['var'] || ($realParams[$checkPos]['pass_by_reference'] === true + && ('&'.$realName) === $param['var']) + ) { + $matched = true; + break; + } + + $checkPos++; } - $checkPos = 0; - foreach ($params as $param) { - $paramComment = trim($param->getComment()); - $errorPos = ($param->getLine() + $commentStart); + // Check the param type value. This could also be multiple parameter + // types separated by '|'. + $typeNames = explode('|', $param['type']); + $suggestedNames = []; + foreach ($typeNames as $i => $typeName) { + $suggestedNames[] = static::suggestType($typeName); + } - // Make sure that there is only one space before the var type. - if ($param->getWhitespaceBeforeType() !== ' ') { - $error = 'Expected 1 space before variable type'; - $this->currentFile->addError($error, $errorPos, 'SpacingBeforeParamType'); + $suggestedType = implode('|', $suggestedNames); + if (preg_match('/\s/', $param['type']) === 1) { + $error = 'Parameter type "%s" must not contain spaces'; + $data = [$param['type']]; + $phpcsFile->addError($error, $param['tag'], 'ParamTypeSpaces', $data); + } else if ($param['type'] !== $suggestedType) { + $error = 'Expected "%s" but found "%s" for parameter type'; + $data = [ + $suggestedType, + $param['type'], + ]; + $fix = $phpcsFile->addFixableError($error, $param['tag'], 'IncorrectParamVarName', $data); + if ($fix === true) { + $content = $suggestedType; + $content .= str_repeat(' ', $param['type_space']); + $content .= $param['var']; + $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content); } + } - // Make sure they are in the correct order, - // and have the correct name. - $pos = $param->getPosition(); + $suggestedName = ''; + $typeName = ''; + if (count($typeNames) === 1) { + $typeName = $param['type']; + $suggestedName = static::suggestType($typeName); + } - $paramName = '[ UNKNOWN ]'; - if ($param->getVarName() !== '') { - $paramName = $param->getVarName(); + // This runs only if there is only one type name and the type name + // is not one of the disallowed type names. + if (count($typeNames) === 1 && $typeName === $suggestedName) { + // Check type hint for array and custom type. + $suggestedTypeHint = ''; + if (strpos($suggestedName, 'array') !== false) { + $suggestedTypeHint = 'array'; + } else if (strpos($suggestedName, 'callable') !== false) { + $suggestedTypeHint = 'callable'; + } else if (substr($suggestedName, -2) === '[]') { + $suggestedTypeHint = 'array'; + } else if ($suggestedName === 'object') { + $suggestedTypeHint = ''; + } else if (in_array($typeName, $this->allowedTypes) === false) { + $suggestedTypeHint = $suggestedName; } - // Make sure the names of the parameter comment matches the - // actual parameter. - $matched = false; - // Parameter documentation can be ommitted for some parameters, so - // we have to search the rest for a match. - while (isset($realParams[($checkPos)]) === true) { - $realName = $realParams[($checkPos)]['name']; - $expectedParamName = $realName; - $isReference = $realParams[($checkPos)]['pass_by_reference']; - - // Append ampersand to name if passing by reference. - if ($isReference === true && substr($paramName, 0, 1) === '&') { - $expectedParamName = '&'.$realName; + if ($suggestedTypeHint !== '' && isset($realParams[$checkPos]) === true) { + $typeHint = $realParams[$checkPos]['type_hint']; + // Primitive type hints are allowed to be omitted. + if ($typeHint === '' && in_array($suggestedTypeHint, ['string', 'int', 'float', 'bool']) === false) { + $error = 'Type hint "%s" missing for %s'; + $data = [ + $suggestedTypeHint, + $param['var'], + ]; + $phpcsFile->addError($error, $stackPtr, 'TypeHintMissing', $data); + } else if ($typeHint !== $suggestedTypeHint && $typeHint !== '') { + // The type hint could be fully namespaced, so we check + // for the part after the last "\". + $nameParts = explode('\\', $suggestedTypeHint); + $lastPart = end($nameParts); + if ($lastPart !== $typeHint && $this->isAliasedType($typeHint, $suggestedTypeHint, $phpcsFile) === false) { + $error = 'Expected type hint "%s"; found "%s" for %s'; + $data = [ + $lastPart, + $typeHint, + $param['var'], + ]; + $phpcsFile->addError($error, $stackPtr, 'IncorrectTypeHint', $data); + } + }//end if + } else if ($suggestedTypeHint === '' + && isset($realParams[$checkPos]) === true + ) { + $typeHint = $realParams[$checkPos]['type_hint']; + if ($typeHint !== '' + && $typeHint !== 'stdClass' + && $typeHint !== '\stdClass' + // As of PHP 7.2, object is a valid type hint. + && $typeHint !== 'object' + ) { + $error = 'Unknown type hint "%s" found for %s'; + $data = [ + $typeHint, + $param['var'], + ]; + $phpcsFile->addError($error, $stackPtr, 'InvalidTypeHint', $data); } + }//end if + }//end if - if ($expectedParamName === $paramName) { - $matched = true; - break; + // Check number of spaces after the type. + $spaces = 1; + if ($param['type_space'] !== $spaces) { + $error = 'Expected %s spaces after parameter type; %s found'; + $data = [ + $spaces, + $param['type_space'], + ]; + + $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamType', $data); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + + $content = $param['type']; + $content .= str_repeat(' ', $spaces); + $content .= $param['var']; + $content .= str_repeat(' ', $param['var_space']); + // At this point there is no description expected in the + // @param line so no need to append comment. + $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content); + + // Fix up the indent of additional comment lines. + foreach ($param['commentLines'] as $lineNum => $line) { + if ($lineNum === 0 + || $param['commentLines'][$lineNum]['indent'] === 0 + ) { + continue; + } + + $newIndent = ($param['commentLines'][$lineNum]['indent'] + $spaces - $param['type_space']); + $phpcsFile->fixer->replaceToken( + ($param['commentLines'][$lineNum]['token'] - 1), + str_repeat(' ', $newIndent) + ); } - $checkPos++; - } + $phpcsFile->fixer->endChangeset(); + }//end if + }//end if - if ($matched === false && $paramName !== '...') { - if ($checkPos >= $pos) { - $code = 'ParamNameNoMatch'; - $data = array( - $paramName, - $realParams[($pos - 1)]['name'], - $pos, - ); - $error = 'Doc comment for var %s does not match '; - if (strtolower($paramName) === strtolower($realParams[($pos - 1)]['name'])) { - $error .= 'case of '; - $code = 'ParamNameNoCaseMatch'; - } + if ($matched === false) { + if ($checkPos >= $pos) { + $code = 'ParamNameNoMatch'; + $data = [ + $param['var'], + $realName, + ]; + + $error = 'Doc comment for parameter %s does not match '; + if (strtolower($param['var']) === strtolower($realName)) { + $error .= 'case of '; + $code = 'ParamNameNoCaseMatch'; + } - $error .= 'actual variable name %s at position %s'; - $this->currentFile->addError($error, $errorPos, $code, $data); - // Reset the parameter position to check for following - // parameters. - $checkPos = ($pos - 1); - } else { - // We must have an extra parameter comment. - $error = 'Superfluous doc comment at position '.$pos; - $this->currentFile->addError($error, $errorPos, 'ExtraParamComment'); - }//end if + $error .= 'actual variable name %s'; + + $phpcsFile->addError($error, $param['tag'], $code, $data); + // Reset the parameter position to check for following + // parameters. + $checkPos = ($pos - 1); + } else if (substr($param['var'], -4) !== ',...') { + // We must have an extra parameter comment. + $error = 'Superfluous parameter comment'; + $phpcsFile->addError($error, $param['tag'], 'ExtraParamComment'); }//end if + }//end if - $checkPos++; + $checkPos++; - if ($param->getVarName() === '') { - $error = 'Missing parameter name at position '.$pos; - $this->currentFile->addError($error, $errorPos, 'MissingParamName'); - } + if ($param['comment'] === '') { + continue; + } - if ($param->getType() === '') { - $error = 'Missing parameter type at position '.$pos; - $this->currentFile->addError($error, $errorPos, 'MissingParamType'); - } + // Param comments must start with a capital letter and end with the full stop. + if (isset($param['commentLines'][0]['comment']) === true) { + $firstChar = $param['commentLines'][0]['comment']; + } else { + $firstChar = $param['comment']; + } - if (in_array($param->getType(), array('unknown_type', '', 'type')) === true) { - $error = 'Expected a valid @param data type, but found %s'; - $data = array($param->getType()); - $this->currentFile->addError($error, $errorPos, 'InvalidParamType', $data); + if (preg_match('|\p{Lu}|u', $firstChar) === 0) { + $error = 'Parameter comment must start with a capital letter'; + if (isset($param['commentLines'][0]['token']) === true) { + $commentToken = $param['commentLines'][0]['token']; + } else { + $commentToken = $param['tag']; } - if (isset($this->invalidTypes[$param->getType()]) === true) { - $error = 'Invalid @param data type, expected %s but found %s'; - $data = array($this->invalidTypes[$param->getType()], $param->getType()); - $this->currentFile->addError($error, $errorPos, 'InvalidParamTypeName', $data); - } + $phpcsFile->addError($error, $commentToken, 'ParamCommentNotCapital'); + } - if ($paramComment === '') { - $error = 'Missing comment for param "%s" at position %s'; - $data = array( - $paramName, - $pos, - ); - $this->currentFile->addError($error, $errorPos, 'MissingParamComment', $data); - } else if (substr_count($param->getWhitespaceBeforeComment(), $this->currentFile->eolChar) !== 1) { - $error = 'Parameter comment must be on the next line at position '.$pos; - $this->currentFile->addError($error, $errorPos, 'ParamCommentNewLine'); - } else if (substr_count($param->getWhitespaceBeforeComment(), ' ') !== 3) { - $error = 'Parameter comment indentation must be 2 additional spaces at position '.$pos; - $this->currentFile->addError($error, ($errorPos + 1), 'ParamCommentIndentation'); + $lastChar = substr($param['comment'], -1); + if (in_array($lastChar, ['.', '!', '?', ')']) === false) { + $error = 'Parameter comment must end with a full stop'; + if (empty($param['commentLines']) === true) { + $commentToken = ($param['tag'] + 2); + } else { + $lastLine = end($param['commentLines']); + $commentToken = $lastLine['token']; } - }//end foreach - }//end if - $realNames = array(); - foreach ($realParams as $realParam) { - $realNames[] = $realParam['name']; + // Don't show an error if the end of the comment is in a code + // example. + if ($this->isInCodeExample($phpcsFile, $commentToken, $param['tag']) === false) { + $fix = $phpcsFile->addFixableError($error, $commentToken, 'ParamCommentFullStop'); + if ($fix === true) { + // Add a full stop as the last character of the comment. + $phpcsFile->fixer->addContent($commentToken, '.'); + } + } + } + }//end foreach + + // Missing parameters only apply to methods and not function because on + // functions it is allowed to leave out param comments for form constructors + // for example. + // It is also allowed to ommit pram tags completely, in which case we don't + // throw errors. Only throw errors if param comments exists but are + // incomplete on class methods. + if ($tokens[$stackPtr]['level'] > 0 && empty($foundParams) === false) { + foreach ($realParams as $realParam) { + $realParamKeyName = $realParam['name']; + if (in_array($realParamKeyName, $foundParams) === false + && ($realParam['pass_by_reference'] === true + && in_array("&$realParamKeyName", $foundParams) === true) === false + ) { + $error = 'Parameter %s is not described in comment'; + $phpcsFile->addError($error, $commentStart, 'ParamMissingDefinition', [$realParam['name']]); + } + } } }//end processParams() @@ -561,34 +922,167 @@ protected function processParams($commentStart) /** * Process the function "see" comments. * + * @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. + * @param int $commentStart The position in the stack where the comment started. + * * @return void */ - protected function processSees($commentStart) + protected function processSees(File $phpcsFile, $stackPtr, $commentStart) { - $sees = $this->commentParser->getSees(); - foreach ($sees as $see) { - $errorPos = $see->getLine() + $commentStart; - if ($see->getWhitespaceBeforeContent() !== ' ') { - $error = 'Expected 1 space before see reference'; - $this->currentFile->addError($error, $errorPos, 'SpacingBeforeSee'); + $tokens = $phpcsFile->getTokens(); + foreach ($tokens[$commentStart]['comment_tags'] as $tag) { + if ($tokens[$tag]['content'] !== '@see') { + continue; + } + + if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) { + $comment = $tokens[($tag + 2)]['content']; + if (strpos($comment, ' ') !== false) { + $error = 'The @see reference should not contain any additional text'; + $phpcsFile->addError($error, $tag, 'SeeAdditionalText'); + continue; + } + + if (preg_match('/[\.!\?]$/', $comment) === 1) { + $error = 'Trailing punctuation for @see references is not allowed.'; + $fix = $phpcsFile->addFixableError($error, $tag, 'SeePunctuation'); + if ($fix === true) { + // Replace the last character from the comment which is + // already tested to be a punctuation. + $content = substr($comment, 0, -1); + $phpcsFile->fixer->replaceToken(($tag + 2), $content); + }//end if + } } + }//end foreach - $comment = trim($see->getContent()); - if (strpos($comment, ' ') !== false) { - $error = 'The @see reference should not contain any additional text'; - $this->currentFile->addError($error, $errorPos, 'SeeAdditionalText'); + }//end processSees() + + + /** + * Returns a valid variable type for param/var tag. + * + * @param string $type The variable type to process. + * + * @return string + */ + public static function suggestType($type) + { + if (isset(static::$invalidTypes[$type]) === true) { + return static::$invalidTypes[$type]; + } + + if ($type === '$this') { + return $type; + } + + $type = preg_replace('/[^a-zA-Z0-9_\\\[\]]/', '', $type); + + return $type; + + }//end suggestType() + + + /** + * Checks if a used type hint is an alias defined by a "use" statement. + * + * @param string $typeHint The type hint used. + * @param string $suggestedTypeHint The fully qualified type to + * check against. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being checked. + * + * @return boolean + */ + protected function isAliasedType($typeHint, $suggestedTypeHint, File $phpcsFile) + { + $tokens = $phpcsFile->getTokens(); + + // Iterate over all "use" statements in the file. + $usePtr = 0; + while ($usePtr !== false) { + $usePtr = $phpcsFile->findNext(T_USE, ($usePtr + 1)); + if ($usePtr === false) { + return false; + } + + // Only check use statements in the global scope. + if (empty($tokens[$usePtr]['conditions']) === false) { continue; } - if (preg_match('/[\.!\?]$/', $comment) === 1) { - $error = 'Trailing punctuation for @see references is not allowed.'; - $this->currentFile->addError($error, $errorPos, 'SeePunctuation'); + + // Now comes the original class name, possibly with namespace + // backslashes. + $originalClass = $phpcsFile->findNext(Tokens::$emptyTokens, ($usePtr + 1), null, true); + if ($originalClass === false || ($tokens[$originalClass]['code'] !== T_STRING + && $tokens[$originalClass]['code'] !== T_NS_SEPARATOR) + ) { + continue; + } + + $originalClassName = ''; + while (in_array($tokens[$originalClass]['code'], [T_STRING, T_NS_SEPARATOR]) === true) { + $originalClassName .= $tokens[$originalClass]['content']; + $originalClass++; + } + + if (ltrim($originalClassName, '\\') !== ltrim($suggestedTypeHint, '\\')) { + continue; + } + + // Now comes the "as" keyword signaling an alias name for the class. + $asPtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($originalClass + 1), null, true); + if ($asPtr === false || $tokens[$asPtr]['code'] !== T_AS) { + continue; + } + + // Now comes the name the class is aliased to. + $aliasPtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($asPtr + 1), null, true); + if ($aliasPtr === false || $tokens[$aliasPtr]['code'] !== T_STRING + || $tokens[$aliasPtr]['content'] !== $typeHint + ) { + continue; } + // We found a use statement that aliases the used type hint! + return true; + }//end while + + }//end isAliasedType() + + + /** + * Determines if a comment line is part of an @code/@endcode example. + * + * @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. + * @param int $commentStart The position of the start of the comment + * in the stack passed in $tokens. + * + * @return boolean Returns true if the comment line is within a @code block, + * false otherwise. + */ + protected function isInCodeExample(File $phpcsFile, $stackPtr, $commentStart) + { + $tokens = $phpcsFile->getTokens(); + if (strpos($tokens[$stackPtr]['content'], '@code') !== false) { + return true; } - }//end processSees() + $prevTag = $phpcsFile->findPrevious([T_DOC_COMMENT_TAG], ($stackPtr - 1), $commentStart); + if ($prevTag === false) { + return false; + } + + if ($tokens[$prevTag]['content'] === '@code') { + return true; + } + return false; -}//end class + }//end isInCodeExample() -?> + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Commenting/GenderNeutralCommentSniff.php b/coder_sniffer/Backdrop/Sniffs/Commenting/GenderNeutralCommentSniff.php new file mode 100644 index 0000000..8cb16f5 --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Commenting/GenderNeutralCommentSniff.php @@ -0,0 +1,60 @@ + + */ + public function register() + { + return [ + T_COMMENT, + T_DOC_COMMENT_STRING, + ]; + + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @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 ((bool) preg_match('/(^|\W)(he|her|hers|him|his|she)($|\W)/i', $tokens[$stackPtr]['content']) === true) { + $phpcsFile->addError('Unnecessarily gendered language in a comment', $stackPtr, 'GenderNeutral'); + } + + }//end process() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Commenting/HookCommentSniff.php b/coder_sniffer/Backdrop/Sniffs/Commenting/HookCommentSniff.php new file mode 100644 index 0000000..886c56f --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Commenting/HookCommentSniff.php @@ -0,0 +1,129 @@ + + */ + public function register() + { + return [T_FUNCTION]; + + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @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(); + + // We are only interested in the most outer scope, ignore methods in classes for example. + if (empty($tokens[$stackPtr]['conditions']) === false) { + return; + } + + $commentEnd = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); + if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG) { + return; + } + + $commentStart = $tokens[$commentEnd]['comment_opener']; + + $empty = [ + T_DOC_COMMENT_WHITESPACE, + T_DOC_COMMENT_STAR, + ]; + + $short = $phpcsFile->findNext($empty, ($commentStart + 1), $commentEnd, true); + if ($short === false) { + // No content at all. + return; + } + + // Account for the fact that a short description might cover + // multiple lines. + $shortContent = $tokens[$short]['content']; + $shortEnd = $short; + for ($i = ($short + 1); $i < $commentEnd; $i++) { + if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { + if ($tokens[$i]['line'] === ($tokens[$shortEnd]['line'] + 1)) { + $shortContent .= $tokens[$i]['content']; + $shortEnd = $i; + } else { + break; + } + } + } + + // Check if a hook implementation doc block is formatted correctly. + if (preg_match('/^[\s]*Implement[^\n]+?hook_[^\n]+/i', $shortContent, $matches) === 1) { + if (strstr($matches[0], 'Implements ') === false || strstr($matches[0], 'Implements of') !== false + || preg_match('/ (drush_)?hook_[a-zA-Z0-9_]+\(\)( for .+)?\.$/', $matches[0]) !== 1 + ) { + $phpcsFile->addWarning('Format should be "* Implements hook_foo().", "* Implements hook_foo_BAR_ID_bar() for xyz_bar().",, "* Implements hook_foo_BAR_ID_bar() for xyz-bar.html.twig.", "* Implements hook_foo_BAR_ID_bar() for xyz-bar.tpl.php.", or "* Implements hook_foo_BAR_ID_bar() for block templates."', $short, 'HookCommentFormat'); + } else { + // Check that a hook implementation does not duplicate @param and + // @return documentation. + foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { + if ($tokens[$tag]['content'] === '@param') { + $warn = 'Hook implementations should not duplicate @param documentation'; + $phpcsFile->addWarning($warn, $tag, 'HookParamDoc'); + } + + if ($tokens[$tag]['content'] === '@return') { + $warn = 'Hook implementations should not duplicate @return documentation'; + $phpcsFile->addWarning($warn, $tag, 'HookReturnDoc'); + } + } + }//end if + + return; + }//end if + + // Check if the doc block just repeats the function name with + // "Implements example_hook_name()". + $functionName = $phpcsFile->getDeclarationName($stackPtr); + if ($functionName !== null && preg_match("/^[\s]*Implements $functionName\(\)\.$/i", $shortContent) === 1) { + $error = 'Hook implementations must be documented with "Implements hook_example()."'; + $fix = $phpcsFile->addFixableError($error, $short, 'HookRepeat'); + if ($fix === true) { + $newComment = preg_replace('/Implements [^_]+/', 'Implements hook', $shortContent); + $phpcsFile->fixer->replaceToken($short, $newComment); + } + } + + }//end process() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Commenting/InlineCommentSniff.php b/coder_sniffer/Backdrop/Sniffs/Commenting/InlineCommentSniff.php index 3665416..6bcc1fb 100644 --- a/coder_sniffer/Backdrop/Sniffs/Commenting/InlineCommentSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Commenting/InlineCommentSniff.php @@ -1,49 +1,45 @@ - * @author Marc McIntyre - * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600) - * @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence - * @link http://pear.php.net/package/PHP_CodeSniffer + * @category PHP + * @package PHP_CodeSniffer + * @link http://pear.php.net/package/PHP_CodeSniffer */ +namespace Backdrop\Sniffs\Commenting; + +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Tokens; + /** - * PHP_CodeSniffer_Sniffs_Backdrop_Commenting_InlineCommentSniff. + * \Backdrop\Sniffs\Commenting\InlineCommentSniff. * * Checks that no perl-style comments are used. Checks that inline comments ("//") * have a space after //, start capitalized and end with proper punctuation. - * Largely copied from Squiz_Sniffs_Commenting_InlineCommentSniff. + * Largely copied from + * \PHP_CodeSniffer\Standards\Squiz\Sniffs\Commenting\InlineCommentSniff. * - * @category PHP - * @package PHP_CodeSniffer - * @author Greg Sherwood - * @author Marc McIntyre - * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600) - * @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence - * @version Release: 1.2.0RC3 - * @link http://pear.php.net/package/PHP_CodeSniffer + * @category PHP + * @package PHP_CodeSniffer + * @link http://pear.php.net/package/PHP_CodeSniffer */ -class Backdrop_Sniffs_Commenting_InlineCommentSniff implements PHP_CodeSniffer_Sniff +class InlineCommentSniff implements Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { - return array( - T_COMMENT, - T_DOC_COMMENT, - ); + return [ + T_COMMENT, + T_DOC_COMMENT_OPEN_TAG, + ]; }//end register() @@ -51,77 +47,126 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. + * @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 + * @return int|void */ - public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); // If this is a function/class/interface doc block comment, skip it. // We are only interested in inline doc block comments, which are // not allowed. - if ($tokens[$stackPtr]['code'] === T_DOC_COMMENT) { + if ($tokens[$stackPtr]['code'] === T_DOC_COMMENT_OPEN_TAG) { $nextToken = $phpcsFile->findNext( - PHP_CodeSniffer_Tokens::$emptyTokens, + Tokens::$emptyTokens, ($stackPtr + 1), null, true ); - $ignore = array( - T_CLASS, - T_INTERFACE, - T_FUNCTION, - T_PUBLIC, - T_PRIVATE, - T_PROTECTED, - T_FINAL, - T_STATIC, - T_ABSTRACT, - T_CONST, - T_OBJECT, - T_PROPERTY, - ); + $ignore = [ + T_CLASS, + T_INTERFACE, + T_TRAIT, + T_FUNCTION, + T_CLOSURE, + T_PUBLIC, + T_PRIVATE, + T_PROTECTED, + T_FINAL, + T_STATIC, + T_ABSTRACT, + T_CONST, + T_PROPERTY, + T_INCLUDE, + T_INCLUDE_ONCE, + T_REQUIRE, + T_REQUIRE_ONCE, + T_VAR, + ]; // Also ignore all doc blocks defined in the outer scope (no scope // conditions are set). - if (in_array($tokens[$nextToken]['code'], $ignore) === true + if (in_array($tokens[$nextToken]['code'], $ignore, true) === true || empty($tokens[$stackPtr]['conditions']) === true ) { return; - } else { - $prevToken = $phpcsFile->findPrevious( - PHP_CodeSniffer_Tokens::$emptyTokens, - ($stackPtr - 1), - null, - true - ); - - if ($tokens[$prevToken]['code'] === T_OPEN_TAG) { + } + + if ($phpcsFile->tokenizerType === 'JS') { + // We allow block comments if a function or object + // is being assigned to a variable. + $ignore = Tokens::$emptyTokens; + $ignore[] = T_EQUAL; + $ignore[] = T_STRING; + $ignore[] = T_OBJECT_OPERATOR; + $nextToken = $phpcsFile->findNext($ignore, ($nextToken + 1), null, true); + if ($tokens[$nextToken]['code'] === T_FUNCTION + || $tokens[$nextToken]['code'] === T_CLOSURE + || $tokens[$nextToken]['code'] === T_OBJECT + || $tokens[$nextToken]['code'] === T_PROTOTYPE + ) { return; } + } - // Only error once per comment. - if (substr($tokens[$stackPtr]['content'], 0, 3) === '/**') { - $error = 'Inline doc block comments are not allowed; use "// Comment" instead'; + $prevToken = $phpcsFile->findPrevious( + Tokens::$emptyTokens, + ($stackPtr - 1), + null, + true + ); + + if ($tokens[$prevToken]['code'] === T_OPEN_TAG) { + return; + } + + // Inline doc blocks are allowed in JSDoc. + if ($tokens[$stackPtr]['content'] === '/**' && $phpcsFile->tokenizerType !== 'JS') { + // The only exception to inline doc blocks is the /** @var */ + // declaration. Allow that in any form. + $varTag = $phpcsFile->findNext([T_DOC_COMMENT_TAG], ($stackPtr + 1), $tokens[$stackPtr]['comment_closer'], false, '@var'); + if ($varTag === false) { + $error = 'Inline doc block comments are not allowed; use "/* Comment */" or "// Comment" instead'; $phpcsFile->addError($error, $stackPtr, 'DocBlock'); } - }//end if + } }//end if - if ($tokens[$stackPtr]['content']{0} === '#') { + if ($tokens[$stackPtr]['content'][0] === '#') { $error = 'Perl-style comments are not allowed; use "// Comment" instead'; - $phpcsFile->addError($error, $stackPtr, 'WrongStyle'); + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'WrongStyle'); + if ($fix === true) { + $comment = ltrim($tokens[$stackPtr]['content'], "# \t"); + $phpcsFile->fixer->replaceToken($stackPtr, "// $comment"); + } } - $comment = rtrim($tokens[$stackPtr]['content']); + // We don't want end of block comments. Check if the last token before the + // comment is a closing curly brace. + $previousContent = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); + if ($tokens[$previousContent]['line'] === $tokens[$stackPtr]['line']) { + if ($tokens[$previousContent]['code'] === T_CLOSE_CURLY_BRACKET) { + return; + } + + // Special case for JS files. + if ($tokens[$previousContent]['code'] === T_COMMA + || $tokens[$previousContent]['code'] === T_SEMICOLON + ) { + $lastContent = $phpcsFile->findPrevious(T_WHITESPACE, ($previousContent - 1), null, true); + if ($tokens[$lastContent]['code'] === T_CLOSE_CURLY_BRACKET) { + return; + } + } + } // Only want inline comments. - if (substr($comment, 0, 2) !== '//') { + if (substr($tokens[$stackPtr]['content'], 0, 2) !== '//') { return; } @@ -130,86 +175,275 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) return; } - // Verify the indentation of this comment line. - $this->processIndentation($phpcsFile, $stackPtr); + $commentTokens = [$stackPtr]; - // The below section determines if a comment block is correctly capitalised, - // and ends in a full-stop. It will find the last comment in a block, and - // work its way up. - $nextComment = $phpcsFile->findNext(array(T_COMMENT), ($stackPtr + 1), null, false); + $nextComment = $stackPtr; + $lastComment = $stackPtr; + while (($nextComment = $phpcsFile->findNext(T_COMMENT, ($nextComment + 1), null, false)) !== false) { + if ($tokens[$nextComment]['line'] !== ($tokens[$lastComment]['line'] + 1)) { + break; + } - if (($nextComment !== false) && (($tokens[$nextComment]['line']) === ($tokens[$stackPtr]['line'] + 1))) { - return; - } + // Only want inline comments. + if (substr($tokens[$nextComment]['content'], 0, 2) !== '//') { + break; + } - $topComment = $stackPtr; - $lastComment = $stackPtr; - while (($topComment = $phpcsFile->findPrevious(array(T_COMMENT), ($lastComment - 1), null, false)) !== false) { - if ($tokens[$topComment]['line'] !== ($tokens[$lastComment]['line'] - 1)) { + // There is a comment on the very next line. If there is + // no code between the comments, they are part of the same + // comment block. + $prevNonWhitespace = $phpcsFile->findPrevious(T_WHITESPACE, ($nextComment - 1), $lastComment, true); + if ($prevNonWhitespace !== $lastComment) { break; } - $lastComment = $topComment; - } + // A comment starting with "@" means a new comment section. + if (preg_match('|^//[\s]*@|', $tokens[$nextComment]['content']) === 1) { + break; + } + + $commentTokens[] = $nextComment; + $lastComment = $nextComment; + }//end while - $topComment = $lastComment; - $commentText = ''; + $commentText = ''; + $lastCommentToken = $stackPtr; + foreach ($commentTokens as $lastCommentToken) { + $comment = rtrim($tokens[$lastCommentToken]['content']); - for ($i = $topComment; $i <= $stackPtr; $i++) { - if ($tokens[$i]['code'] === T_COMMENT) { - $commentText .= trim(substr($tokens[$i]['content'], 2)); + if (trim(substr($comment, 2)) === '') { + continue; } - } + + $spaceCount = 0; + $tabFound = false; + + $commentLength = strlen($comment); + for ($i = 2; $i < $commentLength; $i++) { + if ($comment[$i] === "\t") { + $tabFound = true; + break; + } + + if ($comment[$i] !== ' ') { + break; + } + + $spaceCount++; + } + + $fix = false; + if ($tabFound === true) { + $error = 'Tab found before comment text; expected "// %s" but found "%s"'; + $data = [ + ltrim(substr($comment, 2)), + $comment, + ]; + $fix = $phpcsFile->addFixableError($error, $lastCommentToken, 'TabBefore', $data); + } else if ($spaceCount === 0) { + $error = 'No space found before comment text; expected "// %s" but found "%s"'; + $data = [ + substr($comment, 2), + $comment, + ]; + $fix = $phpcsFile->addFixableError($error, $lastCommentToken, 'NoSpaceBefore', $data); + }//end if + + if ($fix === true) { + $newComment = '// '.ltrim($tokens[$lastCommentToken]['content'], "/\t "); + $phpcsFile->fixer->replaceToken($lastCommentToken, $newComment); + } + + if ($spaceCount > 1) { + // Check if there is a comment on the previous line that justifies the + // indentation. + $prevComment = $phpcsFile->findPrevious([T_COMMENT], ($lastCommentToken - 1), null, false); + if (($prevComment !== false) && (($tokens[$prevComment]['line']) === ($tokens[$lastCommentToken]['line'] - 1))) { + $prevCommentText = rtrim($tokens[$prevComment]['content']); + $prevSpaceCount = 0; + for ($i = 2; $i < strlen($prevCommentText); $i++) { + if ($prevCommentText[$i] !== ' ') { + break; + } + + $prevSpaceCount++; + } + + if ($spaceCount > $prevSpaceCount && $prevSpaceCount > 0) { + // A previous comment could be a list item or @todo. + $indentationStarters = [ + '-', + '@todo', + ]; + $words = preg_split('/\s+/', $prevCommentText); + $numberedList = (bool) preg_match('/^[0-9]+\./', $words[1]); + if (in_array($words[1], $indentationStarters) === true) { + if ($spaceCount !== ($prevSpaceCount + 2)) { + $error = 'Comment indentation error after %s element, expected %s spaces'; + $fix = $phpcsFile->addFixableError($error, $lastCommentToken, 'SpacingBefore', [$words[1], ($prevSpaceCount + 2)]); + if ($fix === true) { + $newComment = '//'.str_repeat(' ', ($prevSpaceCount + 2)).ltrim($tokens[$lastCommentToken]['content'], "/\t "); + $phpcsFile->fixer->replaceToken($lastCommentToken, $newComment); + } + } + } else if ($numberedList === true) { + $expectedSpaceCount = ($prevSpaceCount + strlen($words[1]) + 1); + if ($spaceCount !== $expectedSpaceCount) { + $error = 'Comment indentation error, expected %s spaces'; + $fix = $phpcsFile->addFixableError($error, $lastCommentToken, 'SpacingBefore', [$expectedSpaceCount]); + if ($fix === true) { + $newComment = '//'.str_repeat(' ', $expectedSpaceCount).ltrim($tokens[$lastCommentToken]['content'], "/\t "); + $phpcsFile->fixer->replaceToken($lastCommentToken, $newComment); + } + } + } else { + $error = 'Comment indentation error, expected only %s spaces'; + $phpcsFile->addError($error, $lastCommentToken, 'SpacingBefore', [$prevSpaceCount]); + }//end if + }//end if + } else { + $error = '%s spaces found before inline comment; expected "// %s" but found "%s"'; + $data = [ + $spaceCount, + substr($comment, (2 + $spaceCount)), + $comment, + ]; + $fix = $phpcsFile->addFixableError($error, $lastCommentToken, 'SpacingBefore', $data); + if ($fix === true) { + $phpcsFile->fixer->replaceToken($lastCommentToken, '// '.substr($comment, (2 + $spaceCount)).$phpcsFile->eolChar); + } + }//end if + }//end if + + $commentText .= trim(substr($tokens[$lastCommentToken]['content'], 2)); + }//end foreach if ($commentText === '') { $error = 'Blank comments are not allowed'; - $phpcsFile->addError($error, $stackPtr, 'Empty'); - return; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'Empty'); + if ($fix === true) { + $phpcsFile->fixer->replaceToken($stackPtr, ''); + } + + return ($lastCommentToken + 1); } $words = preg_split('/\s+/', $commentText); - if ($commentText[0] !== strtoupper($commentText[0])) { + if (preg_match('/^\p{Ll}/u', $commentText) === 1) { // Allow special lower cased words that contain non-alpha characters // (function references, machine names with underscores etc.). - $matches = array(); + $matches = []; preg_match('/[a-z]+/', $words[0], $matches); - if ($matches[0] === $words[0]) { + if (isset($matches[0]) === true && $matches[0] === $words[0]) { $error = 'Inline comments must start with a capital letter'; - $phpcsFile->addError($error, $topComment, 'NotCapital'); + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'NotCapital'); + if ($fix === true) { + $newComment = preg_replace("/$words[0]/", ucfirst($words[0]), $tokens[$stackPtr]['content'], 1); + $phpcsFile->fixer->replaceToken($stackPtr, $newComment); + } } } - $commentCloser = $commentText[(strlen($commentText) - 1)]; - $acceptedClosers = array( - 'full-stops' => '.', - 'exclamation marks' => '!', - 'or question marks' => '?', - ); - - if (in_array($commentCloser, $acceptedClosers) === false) { - // Allow @tag style comments without punctuation - $firstWord = $words[0]; - if (strpos($firstWord, '@') !== 0) { - // Allow special last words like URLs or function references - // without punctuation. - $lastWord = $words[count($words) - 1]; - $matches = array(); - preg_match('/((\()?[$a-zA-Z]+\)|([$a-zA-Z]+))/', $lastWord, - $matches); - if (isset($matches[0]) === true && $matches[0] === $lastWord) { - $error = 'Inline comments must end in %s'; - $ender = ''; - foreach ($acceptedClosers as $closerName => $symbol) { - $ender .= $closerName.', '; + // Only check the end of comment character if the start of the comment + // is a letter, indicating that the comment is just standard text. + if (preg_match('/^\p{L}/u', $commentText) === 1) { + $commentCloser = $commentText[(strlen($commentText) - 1)]; + $acceptedClosers = [ + 'full-stops' => '.', + 'exclamation marks' => '!', + 'question marks' => '?', + 'colons' => ':', + 'or closing parentheses' => ')', + ]; + + // Allow special last words like URLs or function references + // without punctuation. + $lastWord = $words[(count($words) - 1)]; + $matches = []; + preg_match('/https?:\/\/.+/', $lastWord, $matches); + $isUrl = isset($matches[0]) === true; + preg_match('/[$a-zA-Z_]+\([$a-zA-Z_]*\)/', $lastWord, $matches); + $isFunction = isset($matches[0]) === true; + + // Also allow closing tags like @endlink or @endcode. + $isEndTag = $lastWord[0] === '@'; + + if (in_array($commentCloser, $acceptedClosers, true) === false + && $isUrl === false && $isFunction === false && $isEndTag === false + ) { + $error = 'Inline comments must end in %s'; + $ender = ''; + foreach ($acceptedClosers as $closerName => $symbol) { + $ender .= ' '.$closerName.','; + } + + $ender = trim($ender, ' ,'); + $data = [$ender]; + $fix = $phpcsFile->addFixableError($error, $lastCommentToken, 'InvalidEndChar', $data); + if ($fix === true) { + $newContent = preg_replace('/(\s+)$/', '.$1', $tokens[$lastCommentToken]['content']); + $phpcsFile->fixer->replaceToken($lastCommentToken, $newContent); + } + } + }//end if + + // Finally, the line below the last comment cannot be empty if this inline + // comment is on a line by itself. + if ($tokens[$previousContent]['line'] < $tokens[$stackPtr]['line']) { + $next = $phpcsFile->findNext(T_WHITESPACE, ($lastCommentToken + 1), null, true); + if ($next === false) { + // Ignore if the comment is the last non-whitespace token in a file. + return ($lastCommentToken + 1); + } + + if ($tokens[$next]['code'] === T_DOC_COMMENT_OPEN_TAG) { + // If this inline comment is followed by a docblock, + // ignore spacing as docblock/function etc spacing rules + // are likely to conflict with our rules. + return ($lastCommentToken + 1); + } + + $errorCode = 'SpacingAfter'; + + if (isset($tokens[$stackPtr]['conditions']) === true) { + $conditions = $tokens[$stackPtr]['conditions']; + $type = end($conditions); + $conditionPtr = key($conditions); + + if (($type === T_FUNCTION || $type === T_CLOSURE) + && $tokens[$conditionPtr]['scope_closer'] === $next + ) { + $errorCode = 'SpacingAfterAtFunctionEnd'; + } + } + + for ($i = ($lastCommentToken + 1); $i < $phpcsFile->numTokens; $i++) { + if ($tokens[$i]['line'] === ($tokens[$lastCommentToken]['line'] + 1)) { + if ($tokens[$i]['code'] !== T_WHITESPACE) { + return ($lastCommentToken + 1); + } + } else if ($tokens[$i]['line'] > ($tokens[$lastCommentToken]['line'] + 1)) { + break; + } + } + + $error = 'There must be no blank line following an inline comment'; + $fix = $phpcsFile->addFixableWarning($error, $lastCommentToken, $errorCode); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + for ($i = ($lastCommentToken + 1); $i < $next; $i++) { + if ($tokens[$i]['line'] === $tokens[$next]['line']) { + break; } - $ender = rtrim($ender, ', '); - $data = array($ender); - $phpcsFile->addError($error, $stackPtr, 'InvalidEndChar', - $data); + $phpcsFile->fixer->replaceToken($i, ''); } + + $phpcsFile->fixer->endChangeset(); } - } + }//end if + + return ($lastCommentToken + 1); }//end process() @@ -217,19 +451,23 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) /** * Determines if a comment line is part of an @code/@endcode example. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. + * @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 boolean Returns true if the comment line is within a @code block, * false otherwise. */ - protected function isInCodeExample(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + protected function isInCodeExample(File $phpcsFile, $stackPtr) { - $tokens = $phpcsFile->getTokens(); + $tokens = $phpcsFile->getTokens(); + if ($tokens[$stackPtr]['content'] === '// @code'.$phpcsFile->eolChar) { + return true; + } + $prevComment = $stackPtr; $lastComment = $stackPtr; - while (($prevComment = $phpcsFile->findPrevious(array(T_COMMENT), ($lastComment - 1), null, false)) !== false) { + while (($prevComment = $phpcsFile->findPrevious([T_COMMENT], ($lastComment - 1), null, false)) !== false) { if ($tokens[$prevComment]['line'] !== ($tokens[$lastComment]['line'] - 1)) { return false; } @@ -250,80 +488,4 @@ protected function isInCodeExample(PHP_CodeSniffer_File $phpcsFile, $stackPtr) }//end isInCodeExample() - /** - * Checks the indentation level of the comment contents. - * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. - * - * @return void - */ - protected function processIndentation(PHP_CodeSniffer_File $phpcsFile, $stackPtr) - { - $tokens = $phpcsFile->getTokens(); - $comment = rtrim($tokens[$stackPtr]['content']); - $spaceCount = 0; - for ($i = 2; $i < strlen($comment); $i++) { - if ($comment[$i] !== ' ') { - break; - } - - $spaceCount++; - } - - if ($spaceCount === 0 && strlen($comment) > 2) { - $error = 'No space before comment text; expected "// %s" but found "%s"'; - $data = array( - substr($comment, 2), - $comment, - ); - $phpcsFile->addError($error, $stackPtr, 'NoSpaceBefore', $data); - } - - if ($spaceCount > 1) { - // Check if there is a comment on the previous line that justifies the - // indentation. - $prevComment = $phpcsFile->findPrevious(array(T_COMMENT), ($stackPtr - 1), null, false); - if (($prevComment !== false) && (($tokens[$prevComment]['line']) === ($tokens[$stackPtr]['line'] - 1))) { - $prevCommentText = rtrim($tokens[$prevComment]['content']); - $prevSpaceCount = 0; - for ($i = 2; $i < strlen($prevCommentText); $i++) { - if ($prevCommentText[$i] !== ' ') { - break; - } - - $prevSpaceCount++; - } - - if ($spaceCount > $prevSpaceCount && $prevSpaceCount > 0) { - // A previous comment could be a list item or @todo. - $indentationStarters = array('-', '@todo'); - $words = preg_split('/\s+/', $prevCommentText); - if (in_array($words[1], $indentationStarters) === true) { - if ($spaceCount !== ($prevSpaceCount + 2)) { - $error = 'Comment indentation error after %s element, expected %s spaces'; - $phpcsFile->addError($error, $stackPtr, 'SpacingBefore', array($words[1], $prevSpaceCount + 2)); - } - } else { - $error = 'Comment indentation error, expected only %s spaces'; - $phpcsFile->addError($error, $stackPtr, 'SpacingBefore', array($prevSpaceCount)); - } - } - } else { - $error = '%s spaces found before inline comment; expected "// %s" but found "%s"'; - $data = array( - $spaceCount, - substr($comment, (2 + $spaceCount)), - $comment, - ); - $phpcsFile->addError($error, $stackPtr, 'SpacingBefore', $data); - }//end if - }//end if - - }//end processIndentation() - - }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Commenting/InlineVariableCommentSniff.php b/coder_sniffer/Backdrop/Sniffs/Commenting/InlineVariableCommentSniff.php new file mode 100644 index 0000000..14a02ce --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Commenting/InlineVariableCommentSniff.php @@ -0,0 +1,151 @@ + + */ + public function register() + { + return [ + T_COMMENT, + T_DOC_COMMENT_TAG, + ]; + + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @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(); + $ignore = [ + T_CLASS, + T_INTERFACE, + T_TRAIT, + T_FUNCTION, + T_CLOSURE, + T_PUBLIC, + T_PRIVATE, + T_PROTECTED, + T_FINAL, + T_STATIC, + T_ABSTRACT, + T_CONST, + T_PROPERTY, + T_INCLUDE, + T_INCLUDE_ONCE, + T_REQUIRE, + T_REQUIRE_ONCE, + T_VAR, + ]; + + // If this is a function/class/interface doc block comment, skip it. + $nextToken = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if (in_array($tokens[$nextToken]['code'], $ignore, true) === true) { + return; + } + + if ($tokens[$stackPtr]['code'] === T_COMMENT) { + if (strpos($tokens[$stackPtr]['content'], '@var') !== false) { + $warning = 'Inline @var declarations should use the /** */ delimiters'; + + if (strpos($tokens[$stackPtr]['content'], '#') === 0 || strpos($tokens[$stackPtr]['content'], '//') === 0) { + // If this comment contains '*/' then the developer is mixing + // inline comment styles. This could be commented out code, + // so leave this line alone completely. + if (strpos($tokens[$stackPtr]['content'], '*/') !== false) { + return; + } + + if ($phpcsFile->addFixableWarning($warning, $stackPtr, 'VarInline') === true) { + // Hashtag and slash based comments contain a trailing + // new line. + $varContent = rtrim($tokens[$stackPtr]['content']); + + // Remove all leading hashtags and slashes. + $varContent = ltrim($varContent, '/# '); + + $phpcsFile->fixer->replaceToken($stackPtr, ('/** '.$varContent." */\n")); + } + } else { + if ($phpcsFile->addFixableWarning($warning, $stackPtr, 'VarInline') === true) { + $phpcsFile->fixer->replaceToken($stackPtr, substr_replace($tokens[$stackPtr]['content'], '/**', 0, 2)); + } + }//end if + }//end if + + return; + }//end if + + // Skip if it's not a variable declaration. + if ($tokens[$stackPtr]['content'] !== '@var') { + return; + } + + // Get the content of the @var tag to determine the order. + $varContent = ''; + $varContentPtr = $phpcsFile->findNext(T_DOC_COMMENT_STRING, ($stackPtr + 1)); + if ($varContentPtr !== false) { + $varContent = $tokens[$varContentPtr]['content']; + } + + if (strpos($varContent, '$') === 0) { + $warning = 'The variable name should be defined after the type'; + + $parts = explode(' ', $varContent, 3); + if (isset($parts[1]) === true) { + if ($phpcsFile->addFixableWarning($warning, $varContentPtr, 'VarInlineOrder') === true) { + // Switch type and variable name. + $replace = [ + $parts[1], + $parts[0], + ]; + if (isset($parts[2]) === true) { + $replace[] = $parts[2]; + } + + $phpcsFile->fixer->replaceToken($varContentPtr, implode(' ', $replace)); + } + } else { + $phpcsFile->addWarning($warning, $varContentPtr, 'VarInlineOrder'); + } + }//end if + + }//end process() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Commenting/PostStatementCommentSniff.php b/coder_sniffer/Backdrop/Sniffs/Commenting/PostStatementCommentSniff.php new file mode 100644 index 0000000..7b81d11 --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Commenting/PostStatementCommentSniff.php @@ -0,0 +1,102 @@ + + */ + public function register() + { + return [T_COMMENT]; + + }//end register() + + + /** + * Processes this sniff, when one of its tokens is encountered. + * + * @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 (substr($tokens[$stackPtr]['content'], 0, 2) !== '//') { + return; + } + + $commentLine = $tokens[$stackPtr]['line']; + $lastContent = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); + + if ($tokens[$lastContent]['line'] !== $commentLine) { + return; + } + + if ($tokens[$lastContent]['code'] === T_CLOSE_CURLY_BRACKET) { + return; + } + + // Special case for JS files. + if ($tokens[$lastContent]['code'] === T_COMMA + || $tokens[$lastContent]['code'] === T_SEMICOLON + ) { + $lastContent = $phpcsFile->findPrevious(T_WHITESPACE, ($lastContent - 1), null, true); + if ($tokens[$lastContent]['code'] === T_CLOSE_CURLY_BRACKET) { + return; + } + } + + $error = 'Comments may not appear after statements'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'Found'); + if ($fix === true) { + if ($tokens[$lastContent]['code'] === T_OPEN_TAG) { + $phpcsFile->fixer->addNewlineBefore($stackPtr); + return; + } + + $lineStart = $stackPtr; + while ($tokens[$lineStart]['line'] === $tokens[$stackPtr]['line'] + && $tokens[$lineStart]['code'] !== T_OPEN_TAG + ) { + $lineStart--; + } + + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->addContent($lineStart, $tokens[$stackPtr]['content']); + $phpcsFile->fixer->replaceToken($stackPtr, $phpcsFile->eolChar); + $phpcsFile->fixer->endChangeset(); + } + + }//end process() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Commenting/TodoCommentSniff.php b/coder_sniffer/Backdrop/Sniffs/Commenting/TodoCommentSniff.php new file mode 100644 index 0000000..a087ec6 --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Commenting/TodoCommentSniff.php @@ -0,0 +1,191 @@ + + */ + public function register() + { + if (defined('PHP_CODESNIFFER_IN_TESTS') === true) { + $this->debug = false; + } + + return [ + T_COMMENT, + T_DOC_COMMENT_TAG, + T_DOC_COMMENT_STRING, + ]; + + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @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) + { + $debug = Config::getConfigData('todo_debug'); + if ($debug !== null) { + $this->debug = (bool) $debug; + } + + $tokens = $phpcsFile->getTokens(); + if ($this->debug === true) { + echo "\n------\n\$tokens[$stackPtr] = ".print_r($tokens[$stackPtr], true).PHP_EOL; + echo 'code = '.$tokens[$stackPtr]['code'].', type = '.$tokens[$stackPtr]['type']."\n"; + } + + // Standard comments and multi-line comments where the "@" is missing so + // it does not register as a T_DOC_COMMENT_TAG. + if ($tokens[$stackPtr]['code'] === T_COMMENT || $tokens[$stackPtr]['code'] === T_DOC_COMMENT_STRING) { + $comment = $tokens[$stackPtr]['content']; + if ($this->debug === true) { + echo "Getting \$comment from \$tokens[$stackPtr]['content']\n"; + } + + $this->checkTodoFormat($phpcsFile, $stackPtr, $comment, $tokens); + } else if ($tokens[$stackPtr]['code'] === T_DOC_COMMENT_TAG) { + // Document comment tag (i.e. comments that begin with "@"). + // Determine if this is related at all and build the full comment line + // from the various segments that the line is parsed into. + $expression = '/^@to/i'; + $comment = $tokens[$stackPtr]['content']; + if ((bool) preg_match($expression, $comment) === true) { + if ($this->debug === true) { + echo "Attempting to build comment\n"; + } + + $index = ($stackPtr + 1); + while ($tokens[$index]['line'] === $tokens[$stackPtr]['line']) { + $comment .= $tokens[$index]['content']; + $index++; + } + + if ($this->debug === true) { + echo "Result comment = $comment\n"; + } + + $this->checkTodoFormat($phpcsFile, $stackPtr, $comment, $tokens); + }//end if + }//end if + + }//end process() + + + /** + * Checks a comment string for the correct syntax. + * + * @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. + * @param string $comment The comment text. + * @param array $tokens The token data. + * + * @return void + */ + private function checkTodoFormat(File $phpcsFile, int $stackPtr, string $comment, array $tokens) + { + if ($this->debug === true) { + echo "Checking \$comment = '$comment'\n"; + } + + $expression = '/(?x) # Set free-space mode to allow this commenting. + ^(\/|\s)* # At the start optionally match any forward slashes and spaces + (?i) # set case-insensitive mode. + (?=( # Start a positive non-consuming look-ahead to find all possible todos + @+to(-|\s|)+do # if one or more @ allow spaces and - between the to and do. + \h*(-|:)* # Also match the trailing "-" or ":" so they can be replaced. + | # or + to(-)*do # If no @ then only accept todo or to-do or to--do, etc, no spaces. + (\s-|:)* # Also match the trailing "-" or ":" so they can be replaced. + )) + (?-i) # Reset to case-sensitive + (?! # Start another non-consuming look-ahead, this time negative + @todo\s # It has to match lower-case @todo followed by one space + (?!-|:)\S # and then any non-space except "-" or ":". + )/m'; + + if ((bool) preg_match($expression, $comment, $matches) === true) { + if ($this->debug === true) { + echo "Failed regex - give message\n"; + } + + $commentTrimmed = trim($comment, " /\r\n"); + if ($commentTrimmed === '@todo') { + // We can't fix a comment that doesn't have any text. + $phpcsFile->addWarning("'%s' should match the format '@todo Fix problem X here.'", $stackPtr, 'TodoFormat', [$commentTrimmed]); + $fix = false; + } else { + // Comments with description text are fixable. + $fix = $phpcsFile->addFixableWarning("'%s' should match the format '@todo Fix problem X here.'", $stackPtr, 'TodoFormat', [$commentTrimmed]); + } + + if ($fix === true) { + if ($tokens[$stackPtr]['code'] === T_DOC_COMMENT_TAG) { + // Rewrite the comment past the token content to an empty + // string as part of it may be part of the match, but not in + // the token content. Then replace the token content with + // the fixed comment from the matched content. + $phpcsFile->fixer->beginChangeset(); + $index = ($stackPtr + 1); + while ($tokens[$index]['line'] === $tokens[$stackPtr]['line']) { + $phpcsFile->fixer->replaceToken($index, ''); + $index++; + } + + $fixedTodo = str_replace($matches[2], '@todo ', $comment); + $phpcsFile->fixer->replaceToken($stackPtr, $fixedTodo); + $phpcsFile->fixer->endChangeset(); + } else { + // The full comment line text is available here, so the + // replacement is fairly straightforward. + $fixedTodo = str_replace($matches[2], '@todo ', $tokens[$stackPtr]['content']); + $phpcsFile->fixer->replaceToken($stackPtr, $fixedTodo); + }//end if + }//end if + }//end if + + }//end checkTodoFormat() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Commenting/VariableCommentSniff.php b/coder_sniffer/Backdrop/Sniffs/Commenting/VariableCommentSniff.php new file mode 100644 index 0000000..65d60df --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Commenting/VariableCommentSniff.php @@ -0,0 +1,209 @@ +getTokens(); + $ignore = [ + T_PUBLIC, + T_PRIVATE, + T_PROTECTED, + T_VAR, + T_STATIC, + T_WHITESPACE, + T_STRING, + T_NS_SEPARATOR, + T_NULLABLE, + ]; + + $commentEnd = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true); + if ($commentEnd === false + || ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG + && $tokens[$commentEnd]['code'] !== T_COMMENT) + ) { + $phpcsFile->addError('Missing member variable doc comment', $stackPtr, 'Missing'); + return; + } + + if ($tokens[$commentEnd]['code'] === T_COMMENT) { + $fix = $phpcsFile->addFixableError('You must use "/**" style comments for a member variable comment', $stackPtr, 'WrongStyle'); + if ($fix === true) { + // Convert the comment into a doc comment. + $phpcsFile->fixer->beginChangeset(); + $comment = ''; + for ($i = $commentEnd; $tokens[$i]['code'] === T_COMMENT; $i--) { + $comment = ' *'.ltrim($tokens[$i]['content'], '/* ').$comment; + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->replaceToken($commentEnd, "/**\n".rtrim($comment, "*/\n")."\n */\n"); + $phpcsFile->fixer->endChangeset(); + } + + return; + } else if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG) { + return; + } else { + // Make sure the comment we have found belongs to us. + $commentFor = $phpcsFile->findNext([T_VARIABLE, T_CLASS, T_INTERFACE], ($commentEnd + 1)); + if ($commentFor !== $stackPtr) { + return; + } + }//end if + + $commentStart = $tokens[$commentEnd]['comment_opener']; + + // Ignore variable comments that use inheritdoc, allow both variants. + $commentContent = $phpcsFile->getTokensAsString($commentStart, ($commentEnd - $commentStart)); + if (strpos($commentContent, '{@inheritdoc}') !== false + || strpos($commentContent, '{@inheritDoc}') !== false + ) { + return; + } + + $foundVar = null; + foreach ($tokens[$commentStart]['comment_tags'] as $tag) { + if ($tokens[$tag]['content'] === '@var') { + if ($foundVar !== null) { + $error = 'Only one @var tag is allowed in a member variable comment'; + $phpcsFile->addError($error, $tag, 'DuplicateVar'); + } else { + $foundVar = $tag; + } + } else if ($tokens[$tag]['content'] === '@see') { + // Make sure the tag isn't empty. + $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd); + if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) { + $error = 'Content missing for @see tag in member variable comment'; + $phpcsFile->addError($error, $tag, 'EmptySees'); + } + }//end if + }//end foreach + + // The @var tag is the only one we require. + if ($foundVar === null) { + $error = 'Missing @var tag in member variable comment'; + $phpcsFile->addError($error, $commentEnd, 'MissingVar'); + return; + } + + $firstTag = $tokens[$commentStart]['comment_tags'][0]; + if ($foundVar !== null && $tokens[$firstTag]['content'] !== '@var') { + $error = 'The @var tag must be the first tag in a member variable comment'; + $phpcsFile->addError($error, $foundVar, 'VarOrder'); + } + + // Make sure the tag isn't empty and has the correct padding. + $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $foundVar, $commentEnd); + if ($string === false || $tokens[$string]['line'] !== $tokens[$foundVar]['line']) { + $error = 'Content missing for @var tag in member variable comment'; + $phpcsFile->addError($error, $foundVar, 'EmptyVar'); + return; + } + + $varType = $tokens[($foundVar + 2)]['content']; + + // There may be multiple types separated by pipes. + $suggestedTypes = []; + foreach (explode('|', $varType) as $type) { + $suggestedTypes[] = FunctionCommentSniff::suggestType($type); + } + + $suggestedType = implode('|', $suggestedTypes); + + // Detect and auto-fix the common mistake that the variable name is + // appended to the type declaration. + $matches = []; + if (preg_match('/^([^\s]+)(\s+\$.+)$/', $varType, $matches) === 1) { + $error = 'Do not append variable name "%s" to the type declaration in a member variable comment'; + $data = [ + trim($matches[2]), + ]; + $fix = $phpcsFile->addFixableError($error, ($foundVar + 2), 'InlineVariableName', $data); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($foundVar + 2), $matches[1]); + } + } else if ($varType !== $suggestedType) { + $error = 'Expected "%s" but found "%s" for @var tag in member variable comment'; + $data = [ + $suggestedType, + $varType, + ]; + $fix = $phpcsFile->addFixableError($error, ($foundVar + 2), 'IncorrectVarType', $data); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($foundVar + 2), $suggestedType); + } + }//end if + + }//end processMemberVar() + + + /** + * Called to process a normal variable. + * + * Not required for this sniff. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where this token was found. + * @param int $stackPtr The position where the double quoted + * string was found. + * + * @return void + */ + protected function processVariable(File $phpcsFile, $stackPtr) + { + + }//end processVariable() + + + /** + * Called to process variables found in double quoted strings. + * + * Not required for this sniff. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The PHP_CodeSniffer file where this token was found. + * @param int $stackPtr The position where the double quoted + * string was found. + * + * @return void + */ + protected function processVariableInString(File $phpcsFile, $stackPtr) + { + + }//end processVariableInString() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/ControlStructures/ControlSignatureSniff.php b/coder_sniffer/Backdrop/Sniffs/ControlStructures/ControlSignatureSniff.php index 6dabc36..b94de3d 100644 --- a/coder_sniffer/Backdrop/Sniffs/ControlStructures/ControlSignatureSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/ControlStructures/ControlSignatureSniff.php @@ -2,73 +2,305 @@ /** * Verifies that control statements conform to their coding standards. * - * PHP version 5 - * - * @category PHP - * @package PHP_CodeSniffer - * @author Greg Sherwood - * @author Marc McIntyre - * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600) - * @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence - * @link http://pear.php.net/package/PHP_CodeSniffer + * @category PHP + * @package PHP_CodeSniffer + * @link http://pear.php.net/package/PHP_CodeSniffer */ -if (class_exists('PHP_CodeSniffer_Standards_AbstractPatternSniff', true) === false) { - throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_Standards_AbstractPatternSniff not found'); -} +namespace Backdrop\Sniffs\ControlStructures; + +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Tokens; /** * Verifies that control statements conform to their coding standards. * - * @category PHP - * @package PHP_CodeSniffer - * @author Greg Sherwood - * @author Marc McIntyre - * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600) - * @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence - * @version Release: 1.2.0RC3 - * @link http://pear.php.net/package/PHP_CodeSniffer + * Largely copied from + * \PHP_CodeSniffer\Standards\Squiz\Sniffs\ControlStructures\ControlSignatureSniff + * and adapted for Backdrop's else on new lines. + * + * @category PHP + * @package PHP_CodeSniffer + * @link http://pear.php.net/package/PHP_CodeSniffer */ -class Backdrop_Sniffs_ControlStructures_ControlSignatureSniff extends PHP_CodeSniffer_Standards_AbstractPatternSniff +class ControlSignatureSniff implements Sniff { /** - * Constructs a PEAR_Sniffs_ControlStructures_ControlSignatureSniff. + * Returns an array of tokens this test wants to listen for. + * + * @return int[] */ - public function __construct() + public function register() { - parent::__construct(true); + return [ + T_TRY, + T_CATCH, + T_DO, + T_WHILE, + T_FOR, + T_IF, + T_FOREACH, + T_ELSE, + T_ELSEIF, + T_SWITCH, + ]; - }//end __construct() + }//end register() /** - * Returns the patterns that this test wishes to verify. + * Processes this test, when one of its tokens is encountered. * - * @return array(string) + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the + * stack passed in $tokens. + * + * @return void */ - protected function getPatterns() + public function process(File $phpcsFile, $stackPtr) { - return array( - 'do {EOL...} while (...);EOL', - 'while (...) {EOL', - 'switch (...) {EOL', - 'for (...) {EOL', - 'if (...) {EOL', - 'foreach (...) {EOL', - // The EOL preceding the else/elseif keywords is not detected - // correctly, so we have - // Backdrop_Sniffs_ControlStructures_ElseNewlineSniff to - // cover that. - 'elseif (...) {EOL', - 'else {EOL', - 'do {EOL', - ); - - }//end getPatterns() + $tokens = $phpcsFile->getTokens(); + if (isset($tokens[($stackPtr + 1)]) === false) { + return; + } -}//end class + // Single space after the keyword. + $found = 1; + if ($tokens[($stackPtr + 1)]['code'] !== T_WHITESPACE) { + $found = 0; + } else if ($tokens[($stackPtr + 1)]['content'] !== ' ') { + if (strpos($tokens[($stackPtr + 1)]['content'], $phpcsFile->eolChar) !== false) { + $found = 'newline'; + } else { + $found = strlen($tokens[($stackPtr + 1)]['content']); + } + } + + if ($found !== 1) { + $error = 'Expected 1 space after %s keyword; %s found'; + $data = [ + strtoupper($tokens[$stackPtr]['content']), + $found, + ]; + + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceAfterKeyword', $data); + if ($fix === true) { + if ($found === 0) { + $phpcsFile->fixer->addContent($stackPtr, ' '); + } else { + $phpcsFile->fixer->replaceToken(($stackPtr + 1), ' '); + } + } + } + + // Single space after closing parenthesis. + if (isset($tokens[$stackPtr]['parenthesis_closer']) === true + && isset($tokens[$stackPtr]['scope_opener']) === true + ) { + $closer = $tokens[$stackPtr]['parenthesis_closer']; + $opener = $tokens[$stackPtr]['scope_opener']; + $content = $phpcsFile->getTokensAsString(($closer + 1), ($opener - $closer - 1)); + + if ($content !== ' ') { + $error = 'Expected 1 space after closing parenthesis; found %s'; + if (trim($content) === '') { + $found = strlen($content); + } else { + $found = '"'.str_replace($phpcsFile->eolChar, '\n', $content).'"'; + } + + $fix = $phpcsFile->addFixableError($error, $closer, 'SpaceAfterCloseParenthesis', [$found]); + if ($fix === true) { + if ($closer === ($opener - 1)) { + $phpcsFile->fixer->addContent($closer, ' '); + } else { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->addContent($closer, ' '.$tokens[$opener]['content']); + $phpcsFile->fixer->replaceToken($opener, ''); + + if ($tokens[$opener]['line'] !== $tokens[$closer]['line']) { + $next = $phpcsFile->findNext(T_WHITESPACE, ($opener + 1), null, true); + if ($tokens[$next]['line'] !== $tokens[$opener]['line']) { + for ($i = ($opener + 1); $i < $next; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + } + } + + $phpcsFile->fixer->endChangeset(); + } + } + }//end if + }//end if + + // Single newline after opening brace. + if (isset($tokens[$stackPtr]['scope_opener']) === true) { + $opener = $tokens[$stackPtr]['scope_opener']; + for ($next = ($opener + 1); $next < $phpcsFile->numTokens; $next++) { + $code = $tokens[$next]['code']; + + if ($code === T_WHITESPACE + || ($code === T_INLINE_HTML + && trim($tokens[$next]['content']) === '') + ) { + continue; + } + + // Skip all empty tokens on the same line as the opener. + if ($tokens[$next]['line'] === $tokens[$opener]['line'] + && (isset(Tokens::$emptyTokens[$code]) === true + || $code === T_CLOSE_TAG) + ) { + continue; + } -?> + // We found the first bit of a code, or a comment on the + // following line. + break; + }//end for + + if ($tokens[$next]['line'] === $tokens[$opener]['line']) { + $error = 'Newline required after opening brace'; + $fix = $phpcsFile->addFixableError($error, $opener, 'NewlineAfterOpenBrace'); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + for ($i = ($opener + 1); $i < $next; $i++) { + if (trim($tokens[$i]['content']) !== '') { + break; + } + + // Remove whitespace. + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->addContent($opener, $phpcsFile->eolChar); + $phpcsFile->fixer->endChangeset(); + } + }//end if + } else if ($tokens[$stackPtr]['code'] === T_WHILE) { + // Zero spaces after parenthesis closer. + $closer = $tokens[$stackPtr]['parenthesis_closer']; + $found = 0; + if ($tokens[($closer + 1)]['code'] === T_WHITESPACE) { + if (strpos($tokens[($closer + 1)]['content'], $phpcsFile->eolChar) !== false) { + $found = 'newline'; + } else { + $found = strlen($tokens[($closer + 1)]['content']); + } + } + + if ($found !== 0) { + $error = 'Expected 0 spaces before semicolon; %s found'; + $data = [$found]; + $fix = $phpcsFile->addFixableError($error, $closer, 'SpaceBeforeSemicolon', $data); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($closer + 1), ''); + } + } + }//end if + + // Only want to check multi-keyword structures from here on. + if ($tokens[$stackPtr]['code'] === T_DO) { + $closer = false; + if (isset($tokens[$stackPtr]['scope_closer']) === true) { + $closer = $tokens[$stackPtr]['scope_closer']; + } + + // Do-while loops should have curly braces. This is optional in + // Javascript. + if ($closer === false && $tokens[$stackPtr]['code'] === T_DO && $phpcsFile->tokenizerType === 'JS') { + $error = 'The code block in a do-while loop should be surrounded by curly braces'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'DoWhileCurlyBraces'); + $closer = $phpcsFile->findNext(T_WHILE, $stackPtr); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + // Append an opening curly brace followed by a newline after + // the DO. + $next = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); + if ($next !== ($stackPtr + 1)) { + $phpcsFile->fixer->replaceToken(($stackPtr + 1), ''); + } + + $phpcsFile->fixer->addContent($stackPtr, ' {'.$phpcsFile->eolChar); + + // Prepend a closing curly brace before the WHILE and ensure + // it is on a new line. + $prepend = $phpcsFile->eolChar; + if ($tokens[($closer - 1)]['code'] === T_WHITESPACE) { + $prepend = ''; + if ($tokens[($closer - 1)]['content'] !== $phpcsFile->eolChar) { + $phpcsFile->fixer->replaceToken(($closer - 1), $phpcsFile->eolChar); + } + } + + $phpcsFile->fixer->addContentBefore($closer, $prepend.'} '); + $phpcsFile->fixer->endChangeset(); + }//end if + }//end if + } else if ($tokens[$stackPtr]['code'] === T_ELSE + || $tokens[$stackPtr]['code'] === T_ELSEIF + || $tokens[$stackPtr]['code'] === T_CATCH + ) { + $closer = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + if ($closer === false || $tokens[$closer]['code'] !== T_CLOSE_CURLY_BRACKET) { + return; + } + } else { + return; + }//end if + + if ($tokens[$stackPtr]['code'] === T_DO) { + // Single space after closing brace. + $found = 1; + if ($tokens[($closer + 1)]['code'] !== T_WHITESPACE) { + $found = 0; + } else if ($tokens[($closer + 1)]['content'] !== ' ') { + if (strpos($tokens[($closer + 1)]['content'], $phpcsFile->eolChar) !== false) { + $found = 'newline'; + } else { + $found = strlen($tokens[($closer + 1)]['content']); + } + } + + if ($found !== 1) { + $error = 'Expected 1 space after closing brace; %s found'; + $data = [$found]; + $fix = $phpcsFile->addFixableError($error, $closer, 'SpaceAfterCloseBrace', $data); + if ($fix === true) { + if ($found === 0) { + $phpcsFile->fixer->addContent($closer, ' '); + } else { + $phpcsFile->fixer->replaceToken(($closer + 1), ' '); + } + } + } + } else { + // New line after closing brace. + $found = 'newline'; + if ($tokens[($closer + 1)]['code'] !== T_WHITESPACE) { + $found = 'none'; + } else if (strpos($tokens[($closer + 1)]['content'], "\n") === false) { + $found = 'spaces'; + } + + if ($found !== 'newline') { + $error = 'Expected newline after closing brace'; + $fix = $phpcsFile->addFixableError($error, $closer, 'NewlineAfterCloseBrace'); + if ($fix === true) { + if ($found === 'none') { + $phpcsFile->fixer->addContent($closer, "\n"); + } else { + $phpcsFile->fixer->replaceToken(($closer + 1), "\n"); + } + } + } + }//end if + + }//end process() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/ControlStructures/ElseCatchNewlineSniff.php b/coder_sniffer/Backdrop/Sniffs/ControlStructures/ElseCatchNewlineSniff.php deleted file mode 100644 index bb929c7..0000000 --- a/coder_sniffer/Backdrop/Sniffs/ControlStructures/ElseCatchNewlineSniff.php +++ /dev/null @@ -1,72 +0,0 @@ -getTokens(); - $prevNonWhiteSpace = $phpcsFile->findPrevious( - PHP_CodeSniffer_Tokens::$emptyTokens, - ($stackPtr - 1), - null, - true - ); - - if ($tokens[$prevNonWhiteSpace]['line'] === $tokens[$stackPtr]['line']) { - $error = '%s must start on a new line'; - $data = array($tokens[$stackPtr]['content']); - $phpcsFile->addError($error, $stackPtr, 'ElseNewline', $data); - } - - }//end process() - - -}//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/ControlStructures/ElseIfSniff.php b/coder_sniffer/Backdrop/Sniffs/ControlStructures/ElseIfSniff.php index 06b5e21..7798d36 100644 --- a/coder_sniffer/Backdrop/Sniffs/ControlStructures/ElseIfSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/ControlStructures/ElseIfSniff.php @@ -2,44 +2,35 @@ /** * Verifies that control statements conform to their coding standards. * - * PHP version 5 - * - * @category PHP - * @package PHP_CodeSniffer - * @author Greg Sherwood - * @author Marc McIntyre - * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600) - * @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence - * @link http://pear.php.net/package/PHP_CodeSniffer + * @category PHP + * @package PHP_CodeSniffer + * @link http://pear.php.net/package/PHP_CodeSniffer */ -if (class_exists('PHP_CodeSniffer_Standards_AbstractPatternSniff', true) === false) { - throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_Standards_AbstractPatternSniff not found'); -} +namespace Backdrop\Sniffs\ControlStructures; + +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; /** - * Verifies that control statements conform to their coding standards. + * Checks that "elseif" is used instead of "else if". * - * @category PHP - * @package PHP_CodeSniffer - * @author Greg Sherwood - * @author Marc McIntyre - * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600) - * @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence - * @version Release: 1.2.0RC3 - * @link http://pear.php.net/package/PHP_CodeSniffer + * @category PHP + * @package PHP_CodeSniffer + * @link http://pear.php.net/package/PHP_CodeSniffer */ -class Backdrop_Sniffs_ControlStructures_ElseIfSniff implements PHP_CodeSniffer_Sniff +class ElseIfSniff implements Sniff { + /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { - return array(T_ELSE); + return [T_ELSE]; }//end register() @@ -47,33 +38,43 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token in the - * stack passed in $tokens. + * @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(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { - $tokens = $phpcsFile->getTokens(); + $tokens = $phpcsFile->getTokens(); - $nextNonWhiteSpace = $phpcsFile->findNext( - T_WHITESPACE, - $stackPtr + 1, - null, - true, - null, - true - ); + $nextNonWhiteSpace = $phpcsFile->findNext( + T_WHITESPACE, + ($stackPtr + 1), + null, + true, + null, + true + ); - if($tokens[$nextNonWhiteSpace]['code'] == T_IF){ - $phpcsFile->addError('Use "elseif" in place of "else if"', $nextNonWhiteSpace); - } + if ($tokens[$nextNonWhiteSpace]['code'] === T_IF) { + $fix = $phpcsFile->addFixableError('Use "elseif" in place of "else if"', $nextNonWhiteSpace, 'ElseIfDeclaration'); + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($stackPtr, 'elseif'); + for ($i = ($stackPtr + 1); $i < $nextNonWhiteSpace; $i++) { + if ($tokens[$i]['code'] === T_WHITESPACE) { + $phpcsFile->fixer->replaceToken($i, ''); + } + } + $phpcsFile->fixer->replaceToken($nextNonWhiteSpace, ''); + $phpcsFile->fixer->endChangeset(); + } + } }//end process() -}//end class -?> +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/ControlStructures/InlineControlStructureSniff.php b/coder_sniffer/Backdrop/Sniffs/ControlStructures/InlineControlStructureSniff.php index b9e0084..d687f1c 100644 --- a/coder_sniffer/Backdrop/Sniffs/ControlStructures/InlineControlStructureSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/ControlStructures/InlineControlStructureSniff.php @@ -1,49 +1,53 @@ getTokens(); // Check for the alternate syntax for control structures with colons (:). - if (isset($tokens[$stackPtr]['parenthesis_closer'])) { + if (isset($tokens[$stackPtr]['parenthesis_closer']) === true) { $start = $tokens[$stackPtr]['parenthesis_closer']; } else { $start = $stackPtr; } + $scopeOpener = $phpcsFile->findNext(T_WHITESPACE, ($start + 1), null, true); if ($tokens[$scopeOpener]['code'] === T_COLON) { return; @@ -55,5 +59,3 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/ControlStructures/TemplateControlStructureSniff.php b/coder_sniffer/Backdrop/Sniffs/ControlStructures/TemplateControlStructureSniff.php deleted file mode 100644 index d607a99..0000000 --- a/coder_sniffer/Backdrop/Sniffs/ControlStructures/TemplateControlStructureSniff.php +++ /dev/null @@ -1,76 +0,0 @@ -getFilename(), -8)); - if ($fileExtension !== '.tpl.php') { - return; - } - - $tokens = $phpcsFile->getTokens(); - - // If there is a scope opener, then there is a opening curly brace. - if (isset($tokens[$stackPtr]['scope_opener']) === true - && $tokens[$tokens[$stackPtr]['scope_opener']]['code'] !== T_COLON - ) { - $error = 'The control statement should use the ":" alternative syntax instead of curly braces in template files'; - $phpcsFile->addError($error, $stackPtr, 'CurlyBracket'); - } - - }//end process() - - -}//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Files/EndFileNewlineSniff.php b/coder_sniffer/Backdrop/Sniffs/Files/EndFileNewlineSniff.php new file mode 100644 index 0000000..cd038cd --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Files/EndFileNewlineSniff.php @@ -0,0 +1,130 @@ + + */ + public $supportedTokenizers = [ + 'PHP', + 'JS', + 'CSS', + ]; + + + /** + * Returns an array of tokens this test wants to listen for. + * + * @return array + */ + public function register() + { + return [ + T_OPEN_TAG, + T_INLINE_HTML, + ]; + + }//end register() + + + /** + * Processes this sniff, when one of its tokens is encountered. + * + * @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|void + */ + public function process(File $phpcsFile, $stackPtr) + { + // Skip to the end of the file. + $tokens = $phpcsFile->getTokens(); + if ($phpcsFile->tokenizerType === 'PHP') { + $lastToken = ($phpcsFile->numTokens - 1); + } else { + // JS and CSS have an artificial token at the end which we have to + // ignore. + $lastToken = ($phpcsFile->numTokens - 2); + } + + // Hard-coding the expected \n in this sniff as it is PSR-2 specific and + // PSR-2 enforces the use of unix style newlines. + if (substr($tokens[$lastToken]['content'], -1) !== "\n") { + $error = 'Expected 1 newline at end of file; 0 found'; + $fix = $phpcsFile->addFixableError($error, $lastToken, 'NoneFound'); + if ($fix === true) { + $phpcsFile->fixer->addNewline($lastToken); + } + + $phpcsFile->recordMetric($stackPtr, 'Number of newlines at EOF', '0'); + return ($phpcsFile->numTokens + 1); + } + + // Go looking for the last non-empty line. + $lastLine = $tokens[$lastToken]['line']; + if ($tokens[$lastToken]['code'] === T_WHITESPACE) { + $lastCode = $phpcsFile->findPrevious(T_WHITESPACE, ($lastToken - 1), null, true); + } else if ($tokens[$lastToken]['code'] === T_INLINE_HTML) { + $lastCode = $lastToken; + while ($lastCode > 0 && trim($tokens[$lastCode]['content']) === '') { + $lastCode--; + } + } else { + $lastCode = $lastToken; + } + + $lastCodeLine = $tokens[$lastCode]['line']; + $blankLines = (string) ($lastLine - $lastCodeLine + 1); + $phpcsFile->recordMetric($stackPtr, 'Number of newlines at EOF', $blankLines); + + if ($blankLines > 1) { + $error = 'Expected 1 newline at end of file; %s found'; + $data = [$blankLines]; + $fix = $phpcsFile->addFixableError($error, $lastCode, 'TooMany', $data); + + if ($fix === true) { + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($lastCode, rtrim($tokens[$lastCode]['content'])); + for ($i = ($lastCode + 1); $i < $lastToken; $i++) { + $phpcsFile->fixer->replaceToken($i, ''); + } + + $phpcsFile->fixer->replaceToken($lastToken, $phpcsFile->eolChar); + $phpcsFile->fixer->endChangeset(); + } + } + + // Skip the rest of the file. + return ($phpcsFile->numTokens + 1); + + }//end process() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Files/FileEncodingSniff.php b/coder_sniffer/Backdrop/Sniffs/Files/FileEncodingSniff.php new file mode 100644 index 0000000..cc9a00e --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Files/FileEncodingSniff.php @@ -0,0 +1,95 @@ + + * @copyright 2016 Klaus Purer + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + * @link http://pear.php.net/package/PHP_CodeSniffer + */ + +namespace Backdrop\Sniffs\Files; + +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; + +/** + * \Backdrop\Sniffs\Files\FileEncodingSniff. + * + * Validates the encoding of a file against a white list of allowed encodings. + * + * @category PHP + * @package PHP_CodeSniffer + * @author Klaus Purer + * @copyright 2016 Klaus Purer + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + * @version Release: @package_version@ + * @link http://pear.php.net/package/PHP_CodeSniffer + */ +class FileEncodingSniff implements Sniff +{ + + /** + * List of encodings that files may be encoded with. + * + * Any other detected encodings will throw a warning. + * + * @var array + */ + public $allowedEncodings = ['UTF-8']; + + + /** + * Returns an array of tokens this test wants to listen for. + * + * @return array + */ + public function register() + { + return [ + T_INLINE_HTML, + T_OPEN_TAG, + ]; + + }//end register() + + + /** + * Processes this sniff, when one of its tokens is encountered. + * + * @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|void + */ + public function process(File $phpcsFile, $stackPtr) + { + // Not all PHP installs have the multi byte extension - nothing we can do. + if (function_exists('mb_check_encoding') === false) { + return $phpcsFile->numTokens; + } + + $fileContent = $phpcsFile->getTokensAsString(0, $phpcsFile->numTokens); + + $validEncodingFound = false; + foreach ($this->allowedEncodings as $encoding) { + if (mb_check_encoding($fileContent, $encoding) === true) { + $validEncodingFound = true; + } + } + + if ($validEncodingFound === false) { + $warning = 'File encoding is invalid, expected %s'; + $data = [implode(' or ', $this->allowedEncodings)]; + $phpcsFile->addWarning($warning, $stackPtr, 'InvalidEncoding', $data); + } + + return $phpcsFile->numTokens; + + }//end process() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Files/LineLengthSniff.php b/coder_sniffer/Backdrop/Sniffs/Files/LineLengthSniff.php index aa3f50f..ebe6375 100644 --- a/coder_sniffer/Backdrop/Sniffs/Files/LineLengthSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Files/LineLengthSniff.php @@ -1,14 +1,18 @@ $tokens The token stack. + * @param int $stackPtr The first token on the next line. * * @return void */ - protected function checkLineLength(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $lineContent) + protected function checkLineLength($phpcsFile, $tokens, $stackPtr) { - $tokens = $phpcsFile->getTokens(); - if ($tokens[$stackPtr]['code'] === T_DOC_COMMENT || $tokens[$stackPtr]['code'] === T_COMMENT) { - if (preg_match('/^[[:space:]]*(\/\*)?\*[[:space:]]*@link.*@endlink[[:space:]]*/', $lineContent) === 1) { - // Allow @link documentation to exceed the 80 character limit. + if (isset(Tokens::$commentTokens[$tokens[($stackPtr - 1)]['code']]) === true) { + $docCommentTag = $phpcsFile->findFirstOnLine(T_DOC_COMMENT_TAG, ($stackPtr - 1)); + if ($docCommentTag !== false) { + // Allow doc comment tags such as long @param tags to exceed the 80 + // character limit. return; } - if (preg_match('/^[[:space:]]*((\/\*)?\*|\/\/)[[:space:]]*@see.*/', $lineContent) === 1) { - // Allow @see documentation to exceed the 80 character limit. + if ($tokens[($stackPtr - 1)]['code'] === T_COMMENT + // Allow @link and @see documentation to exceed the 80 character + // limit. + && (preg_match('/^[[:space:]]*\/\/ @.+/', $tokens[($stackPtr - 1)]['content']) === 1 + // Allow anything that does not contain spaces (like URLs) to be + // longer. + || strpos(trim($tokens[($stackPtr - 1)]['content'], "/ \n"), ' ') === false) + ) { return; } - parent::checkLineLength($phpcsFile, $stackPtr, $lineContent); - } + // Code examples between @code and @endcode are allowed to exceed 80 + // characters. + if (isset($tokens[$stackPtr]) === true && $tokens[$stackPtr]['code'] === T_DOC_COMMENT_WHITESPACE) { + $tag = $phpcsFile->findPrevious([T_DOC_COMMENT_TAG, T_DOC_COMMENT_OPEN_TAG], ($stackPtr - 1)); + if ($tokens[$tag]['content'] === '@code') { + return; + } + } + + // Backdrop 8 annotations can have long translatable descriptions and we + // allow them to exceed 80 characters. + if ($tokens[($stackPtr - 2)]['code'] === T_DOC_COMMENT_STRING + && (strpos($tokens[($stackPtr - 2)]['content'], '@Translation(') !== false + // Also allow anything without whitespace (like URLs) to exceed 80 + // characters. + || strpos($tokens[($stackPtr - 2)]['content'], ' ') === false + // Allow long "Contains ..." comments in @file doc blocks. + || preg_match('/^Contains [a-zA-Z_\\\\.]+$/', $tokens[($stackPtr - 2)]['content']) === 1 + // Allow long paths or namespaces in annotations such as + // "list_builder" = "Backdrop\rules\Entity\Controller\RulesReactionListBuilder" + // cardinality = \Backdrop\webform\WebformHandlerInterface::CARDINALITY_UNLIMITED. + || preg_match('#= ("|\')?\S+[\\\\/]\S+("|\')?,*$#', $tokens[($stackPtr - 2)]['content']) === 1) + // Allow @link tags in lists. + || strpos($tokens[($stackPtr - 2)]['content'], '- @link') !== false + // Allow hook implementation line to exceed 80 characters. + || preg_match('/^Implements hook_[a-zA-Z0-9_]+\(\)/', $tokens[($stackPtr - 2)]['content']) === 1 + ) { + return; + } + + parent::checkLineLength($phpcsFile, $tokens, $stackPtr); + }//end if }//end checkLineLength() @@ -70,9 +111,12 @@ protected function checkLineLength(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $ /** * Returns the length of a defined line. * - * @return integer + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $currentLine The current line. + * + * @return int */ - public function getLineLength(PHP_CodeSniffer_File $phpcsFile, $currentLine) + public function getLineLength(File $phpcsFile, $currentLine) { $tokens = $phpcsFile->getTokens(); @@ -92,5 +136,3 @@ public function getLineLength(PHP_CodeSniffer_File $phpcsFile, $currentLine) }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Files/TxtFileLineLengthSniff.php b/coder_sniffer/Backdrop/Sniffs/Files/TxtFileLineLengthSniff.php index eb64177..d03a2e8 100644 --- a/coder_sniffer/Backdrop/Sniffs/Files/TxtFileLineLengthSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Files/TxtFileLineLengthSniff.php @@ -1,38 +1,39 @@ */ public function register() { - return array(T_INLINE_HTML); + return [T_INLINE_HTML]; }//end register() @@ -40,33 +41,48 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token in the - * stack passed in $tokens. + * @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(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { $fileExtension = strtolower(substr($phpcsFile->getFilename(), -3)); if ($fileExtension === 'txt' || $fileExtension === '.md') { $tokens = $phpcsFile->getTokens(); - $content = rtrim($tokens[$stackPtr]['content']); + $content = rtrim($tokens[$stackPtr]['content']); $lineLength = mb_strlen($content, 'UTF-8'); if ($lineLength > 80) { - $data = array( - 80, - $lineLength, - ); - $warning = 'Line exceeds %s characters; contains %s characters'; - $phpcsFile->addWarning($warning, $stackPtr, 'TooLong', $data); - } - } + // Often text files contain long URLs that need to be preceded + // with certain textual elements that are significant for + // preserving the formatting of the document - e.g. a long link + // in a bulleted list. If we find that the line does not contain + // any spaces after the 40th character we'll allow it. + if (preg_match('/\s+/', mb_substr($content, 40)) === 0) { + return; + } + + // Lines without spaces are allowed to be longer. + // Markdown allowed to be longer for lines + // - without spaces + // - starting with # + // - starting with | (tables) + // - containing a link. + if (preg_match('/^([^ ]+$|#|\||.*\[.+\]\(.+\))/', $content) === 0) { + $data = [ + 80, + $lineLength, + ]; + $warning = 'Line exceeds %s characters; contains %s characters'; + $phpcsFile->addWarning($warning, $stackPtr, 'TooLong', $data); + } + }//end if + }//end if }//end process() }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Formatting/DisallowCloseTagSniff.php b/coder_sniffer/Backdrop/Sniffs/Formatting/DisallowCloseTagSniff.php deleted file mode 100644 index 682eedf..0000000 --- a/coder_sniffer/Backdrop/Sniffs/Formatting/DisallowCloseTagSniff.php +++ /dev/null @@ -1,59 +0,0 @@ -" at the end of files it not allowed. - * - * @category PHP - * @package PHP_CodeSniffer - * @link http://pear.php.net/package/PHP_CodeSniffer - */ -class Backdrop_Sniffs_Formatting_DisallowCloseTagSniff implements PHP_CodeSniffer_Sniff -{ - - - /** - * Returns an array of tokens this test wants to listen for. - * - * @return array - */ - public function register() - { - return array(T_CLOSE_TAG); - - }//end register() - - - /** - * Processes this test, when one of its tokens is encountered. - * - * @param PHP_CodeSniffer_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(PHP_CodeSniffer_File $phpcsFile, $stackPtr) - { - $tokens = $phpcsFile->getTokens(); - - $next = $phpcsFile->findNext(PHP_CodeSniffer_Tokens::$emptyTokens, $stackPtr + 1, null, true); - if ($next === false) { - $error = 'The final ?> should be omitted from all code files'; - $phpcsFile->addError($error, $stackPtr, 'FinalClose'); - } - - }//end process() - - -}//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Formatting/MultiLineAssignmentSniff.php b/coder_sniffer/Backdrop/Sniffs/Formatting/MultiLineAssignmentSniff.php index 1d3a7a0..19d5f10 100644 --- a/coder_sniffer/Backdrop/Sniffs/Formatting/MultiLineAssignmentSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Formatting/MultiLineAssignmentSniff.php @@ -1,19 +1,19 @@ - * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600) - * @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence - * @link http://pear.php.net/package/PHP_CodeSniffer + * @category PHP + * @package PHP_CodeSniffer + * @link http://pear.php.net/package/PHP_CodeSniffer */ +namespace Backdrop\Sniffs\Formatting; + +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; + /** - * Backdrop_Sniffs_Formatting_MultiLineAssignmentSniff. + * \Backdrop\Sniffs\Formatting\MultiLineAssignmentSniff. * * If an assignment goes over two lines, ensure the equal sign is indented. * @@ -25,18 +25,18 @@ * @version Release: 1.2.0RC3 * @link http://pear.php.net/package/PHP_CodeSniffer */ -class Backdrop_Sniffs_Formatting_MultiLineAssignmentSniff implements PHP_CodeSniffer_Sniff +class MultiLineAssignmentSniff implements Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { - return array(T_EQUAL); + return [T_EQUAL]; }//end register() @@ -44,13 +44,13 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. + * @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(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); @@ -61,14 +61,8 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) return; } - if ($tokens[$next]['line'] !== $tokens[$stackPtr]['line']) { - $error = 'Multi-line assignments must have the equal sign on the second line'; - $phpcsFile->addError($error, $stackPtr); - return; - } - // Make sure it is the first thing on the line, otherwise we ignore it. - $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), false, true); + $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); if ($prev === false) { // Bad assignment. return; @@ -99,11 +93,10 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) $foundIndent = strlen($tokens[$prev]['content']); if ($foundIndent !== $expectedIndent) { $error = "Multi-line assignment not indented correctly; expected $expectedIndent spaces but found $foundIndent"; - $phpcsFile->addError($error, $stackPtr); + $phpcsFile->addError($error, $stackPtr, 'MultiLineAssignmentIndent'); } }//end process() -}//end class -?> +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Formatting/MultipleStatementAlignmentSniff.php b/coder_sniffer/Backdrop/Sniffs/Formatting/MultipleStatementAlignmentSniff.php new file mode 100644 index 0000000..9db23d3 --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Formatting/MultipleStatementAlignmentSniff.php @@ -0,0 +1,350 @@ +getTokens(); + + $assignments = []; + $prevAssign = null; + $lastLine = $tokens[$stackPtr]['line']; + $maxPadding = null; + $stopped = null; + $lastCode = $stackPtr; + $lastSemi = null; + $arrayEnd = null; + + if ($end === null) { + $end = $phpcsFile->numTokens; + } + + $find = Tokens::$assignmentTokens; + unset($find[T_DOUBLE_ARROW]); + + $scopes = Tokens::$scopeOpeners; + unset($scopes[T_CLOSURE]); + unset($scopes[T_ANON_CLASS]); + unset($scopes[T_OBJECT]); + + for ($assign = $stackPtr; $assign < $end; $assign++) { + if ($tokens[$assign]['level'] < $tokens[$stackPtr]['level']) { + // Statement is in a different context, so the block is over. + break; + } + + if (isset($scopes[$tokens[$assign]['code']]) === true + && isset($tokens[$assign]['scope_opener']) === true + && $tokens[$assign]['level'] === $tokens[$stackPtr]['level'] + ) { + break; + } + + if ($assign === $arrayEnd) { + $arrayEnd = null; + } + + if (isset($find[$tokens[$assign]['code']]) === false) { + // A blank line indicates that the assignment block has ended. + if (isset(Tokens::$emptyTokens[$tokens[$assign]['code']]) === false + && ($tokens[$assign]['line'] - $tokens[$lastCode]['line']) > 1 + && $tokens[$assign]['level'] === $tokens[$stackPtr]['level'] + && $arrayEnd === null + ) { + break; + } + + if ($tokens[$assign]['code'] === T_CLOSE_TAG) { + // Breaking out of PHP ends the assignment block. + break; + } + + if ($tokens[$assign]['code'] === T_OPEN_SHORT_ARRAY + && isset($tokens[$assign]['bracket_closer']) === true + ) { + $arrayEnd = $tokens[$assign]['bracket_closer']; + } + + if ($tokens[$assign]['code'] === T_ARRAY + && isset($tokens[$assign]['parenthesis_opener']) === true + && isset($tokens[$tokens[$assign]['parenthesis_opener']]['parenthesis_closer']) === true + ) { + $arrayEnd = $tokens[$tokens[$assign]['parenthesis_opener']]['parenthesis_closer']; + } + + if (isset(Tokens::$emptyTokens[$tokens[$assign]['code']]) === false) { + $lastCode = $assign; + + if ($tokens[$assign]['code'] === T_SEMICOLON) { + if ($tokens[$assign]['conditions'] === $tokens[$stackPtr]['conditions']) { + if ($lastSemi !== null && $prevAssign !== null && $lastSemi > $prevAssign) { + // This statement did not have an assignment operator in it. + break; + } else { + $lastSemi = $assign; + } + } else if ($tokens[$assign]['level'] < $tokens[$stackPtr]['level']) { + // Statement is in a different context, so the block is over. + break; + } + } + }//end if + + continue; + } else if ($assign !== $stackPtr && $tokens[$assign]['line'] === $lastLine) { + // Skip multiple assignments on the same line. We only need to + // try and align the first assignment. + continue; + }//end if + + if ($assign !== $stackPtr) { + if ($tokens[$assign]['level'] > $tokens[$stackPtr]['level']) { + // Has to be nested inside the same conditions as the first assignment. + // We've gone one level down, so process this new block. + $assign = $this->checkAlignment($phpcsFile, $assign); + $lastCode = $assign; + continue; + } else if ($tokens[$assign]['level'] < $tokens[$stackPtr]['level']) { + // We've gone one level up, so the block we are processing is done. + break; + } else if ($arrayEnd !== null) { + // Assignments inside arrays are not part of + // the original block, so process this new block. + $assign = ($this->checkAlignment($phpcsFile, $assign, $arrayEnd) - 1); + $arrayEnd = null; + $lastCode = $assign; + continue; + } + + // Make sure it is not assigned inside a condition (eg. IF, FOR). + if (isset($tokens[$assign]['nested_parenthesis']) === true) { + foreach ($tokens[$assign]['nested_parenthesis'] as $start => $end) { + if (isset($tokens[$start]['parenthesis_owner']) === true) { + break(2); + } + } + } + }//end if + + $var = $phpcsFile->findPrevious( + Tokens::$emptyTokens, + ($assign - 1), + null, + true + ); + + // Make sure we wouldn't break our max padding length if we + // aligned with this statement, or they wouldn't break the max + // padding length if they aligned with us. + $varEnd = $tokens[($var + 1)]['column']; + $assignLen = $tokens[$assign]['length']; + if ($assign !== $stackPtr) { + if ($prevAssign === null) { + // Processing an inner block but no assignments found. + break; + } + + if (($varEnd + 1) > $assignments[$prevAssign]['assign_col']) { + $padding = 1; + $assignColumn = ($varEnd + 1); + } else { + $padding = ($assignments[$prevAssign]['assign_col'] - $varEnd + $assignments[$prevAssign]['assign_len'] - $assignLen); + if ($padding <= 0) { + $padding = 1; + } + + if ($padding > $this->maxPadding) { + $stopped = $assign; + break; + } + + $assignColumn = ($varEnd + $padding); + }//end if + + if (($assignColumn + $assignLen) > ($assignments[$maxPadding]['assign_col'] + $assignments[$maxPadding]['assign_len'])) { + $newPadding = ($varEnd - $assignments[$maxPadding]['var_end'] + $assignLen - $assignments[$maxPadding]['assign_len'] + 1); + if ($newPadding > $this->maxPadding) { + $stopped = $assign; + break; + } else { + // New alignment settings for previous assignments. + foreach ($assignments as $i => $data) { + if ($i === $assign) { + break; + } + + $newPadding = ($varEnd - $data['var_end'] + $assignLen - $data['assign_len'] + 1); + $assignments[$i]['expected'] = $newPadding; + $assignments[$i]['assign_col'] = ($data['var_end'] + $newPadding); + } + + $padding = 1; + $assignColumn = ($varEnd + 1); + } + } else if ($padding > $assignments[$maxPadding]['expected']) { + $maxPadding = $assign; + }//end if + } else { + $padding = 1; + $assignColumn = ($varEnd + 1); + $maxPadding = $assign; + }//end if + + $found = 0; + if ($tokens[($var + 1)]['code'] === T_WHITESPACE) { + $found = $tokens[($var + 1)]['length']; + if ($found === 0) { + // This means a newline was found. + $found = 1; + } + } + + $assignments[$assign] = [ + 'var_end' => $varEnd, + 'assign_len' => $assignLen, + 'assign_col' => $assignColumn, + 'expected' => $padding, + 'found' => $found, + ]; + + $lastLine = $tokens[$assign]['line']; + $prevAssign = $assign; + }//end for + + if (empty($assignments) === true) { + return $stackPtr; + } + + // If there is at least one assignment that uses more than two spaces then it + // appears that the assignments should all be aligned right. + $alignRight = false; + foreach ($assignments as $assignment => $data) { + if ($data['found'] > 2) { + $alignRight = true; + break; + } + } + + $numAssignments = count($assignments); + + $errorGenerated = false; + foreach ($assignments as $assignment => $data) { + // Missing space is already covered by + // Backdrop.WhiteSpace.OperatorSpacing.NoSpaceBefore. + if ($data['found'] === 0) { + continue; + } + + if ($alignRight === false && $data['found'] !== $data['expected']) { + $data['expected'] = 1; + } + + if ($data['found'] === $data['expected']) { + continue; + } + + $expectedText = $data['expected'].' space'; + if ($data['expected'] !== 1) { + $expectedText .= 's'; + } + + if ($data['found'] === null) { + $foundText = 'a new line'; + } else { + $foundText = $data['found'].' space'; + if ($data['found'] !== 1) { + $foundText .= 's'; + } + } + + if ($numAssignments === 1) { + $type = 'Incorrect'; + $error = 'Equals sign not aligned correctly; expected %s but found %s'; + } else { + $type = 'NotSame'; + $error = 'Equals sign not aligned with surrounding assignments; expected %s but found %s'; + } + + $errorData = [ + $expectedText, + $foundText, + ]; + + if ($this->error === true) { + $fix = $phpcsFile->addFixableError($error, $assignment, $type, $errorData); + } else { + $fix = $phpcsFile->addFixableWarning($error, $assignment, $type.'Warning', $errorData); + } + + $errorGenerated = true; + + if ($fix === true && $data['found'] !== null) { + $newContent = str_repeat(' ', $data['expected']); + if ($data['found'] === 0) { + $phpcsFile->fixer->addContentBefore($assignment, $newContent); + } else { + $phpcsFile->fixer->replaceToken(($assignment - 1), $newContent); + } + } + }//end foreach + + if ($numAssignments > 1) { + if ($errorGenerated === true) { + $phpcsFile->recordMetric($stackPtr, 'Adjacent assignments aligned', 'no'); + } else { + $phpcsFile->recordMetric($stackPtr, 'Adjacent assignments aligned', 'yes'); + } + } + + if ($stopped !== null) { + return $this->checkAlignment($phpcsFile, $stopped); + } else { + return $assign; + } + + }//end checkAlignment() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Formatting/SpaceInlineIfSniff.php b/coder_sniffer/Backdrop/Sniffs/Formatting/SpaceInlineIfSniff.php index 9fd8c59..d7f5887 100644 --- a/coder_sniffer/Backdrop/Sniffs/Formatting/SpaceInlineIfSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Formatting/SpaceInlineIfSniff.php @@ -1,36 +1,36 @@ */ public function register() { - return array( - T_INLINE_THEN, - T_INLINE_ELSE, - ); + return [T_INLINE_ELSE]; }//end register() @@ -38,16 +38,15 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token in - * the stack passed in $tokens. + * @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(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { - $tokens = $phpcsFile->getTokens(); - $operator = $tokens[$stackPtr]['content']; + $tokens = $phpcsFile->getTokens(); // Handle the short ternary operator (?:) introduced in PHP 5.3. $previous = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); @@ -56,48 +55,9 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) $error = 'There must be no space between ? and :'; $phpcsFile->addError($error, $stackPtr, 'SpaceInlineElse'); } - - if ($tokens[($stackPtr + 1)]['code'] !== T_WHITESPACE) { - $error = "Expected 1 space after \"$operator\"; 0 found"; - $phpcsFile->addError($error, $stackPtr, 'NoSpaceAfter'); - } else if (strlen($tokens[($stackPtr + 1)]['content']) !== 1) { - $found = strlen($tokens[($stackPtr + 1)]['content']); - $error = 'Expected 1 space after "%s"; %s found'; - $data = array( - $operator, - $found, - ); - $phpcsFile->addError($error, $stackPtr, 'SpacingAfter', $data); - } - - return; }//end if - $next = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); - if ($tokens[$next]['code'] === T_INLINE_ELSE) { - if ($tokens[($stackPtr - 1)]['code'] !== T_WHITESPACE) { - $error = "Expected 1 space before \"$operator\"; 0 found"; - $phpcsFile->addError($error, $stackPtr, 'NoSpaceBefore'); - } else if (strlen($tokens[($stackPtr - 1)]['content']) !== 1) { - $found = strlen($tokens[($stackPtr - 1)]['content']); - $error = 'Expected 1 space before "%s"; %s found'; - $data = array( - $operator, - $found, - ); - $phpcsFile->addError($error, $stackPtr, 'SpacingBefore', $data); - } - - return; - } - - // Reuse the standard operator sniff now. - $sniff = new Squiz_Sniffs_WhiteSpace_OperatorSpacingSniff(); - $sniff->process($phpcsFile, $stackPtr); - }//end process() }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Formatting/SpaceUnaryOperatorSniff.php b/coder_sniffer/Backdrop/Sniffs/Formatting/SpaceUnaryOperatorSniff.php index 7e49cb1..edcafe4 100644 --- a/coder_sniffer/Backdrop/Sniffs/Formatting/SpaceUnaryOperatorSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Formatting/SpaceUnaryOperatorSniff.php @@ -1,16 +1,20 @@ */ public function register() { - return array( - T_DEC, - T_INC, - T_MINUS, - T_PLUS, - T_BOOLEAN_NOT, - ); + return [ + T_DEC, + T_INC, + T_MINUS, + T_PLUS, + T_BOOLEAN_NOT, + ]; }//end register() @@ -44,61 +48,82 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token in - * the stack passed in $tokens. + * @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(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); // Check decrement / increment. if ($tokens[$stackPtr]['code'] === T_DEC || $tokens[$stackPtr]['code'] === T_INC) { - $modifyLeft = substr($tokens[($stackPtr - 1)]['content'], 0, 1) === '$' || - $tokens[($stackPtr + 1)]['content'] === ';'; + $previous = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); + $modifyLeft = in_array( + $tokens[$previous]['code'], + [ + T_VARIABLE, + T_CLOSE_SQUARE_BRACKET, + T_CLOSE_PARENTHESIS, + T_STRING, + ] + ); if ($modifyLeft === true && $tokens[($stackPtr - 1)]['code'] === T_WHITESPACE) { $error = 'There must not be a single space before a unary operator statement'; - $phpcsFile->addError($error, $stackPtr, 'IncDecLeft'); + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'IncDecLeft'); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($stackPtr - 1), ''); + } + return; } - if ($modifyLeft === false && substr($tokens[($stackPtr + 1)]['content'], 0, 1) !== '$') { + if ($modifyLeft === false && $tokens[($stackPtr + 1)]['code'] === T_WHITESPACE) { $error = 'A unary operator statement must not be followed by a single space'; - $phpcsFile->addError($error, $stackPtr, 'IncDecRight'); + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'IncDecRight'); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($stackPtr + 1), ''); + } + return; } - } + }//end if // Check "!" operator. - if ($tokens[$stackPtr]['code'] === T_BOOLEAN_NOT && $tokens[$stackPtr + 1]['code'] === T_WHITESPACE) { + if ($tokens[$stackPtr]['code'] === T_BOOLEAN_NOT && $tokens[($stackPtr + 1)]['code'] === T_WHITESPACE) { $error = 'A unary operator statement must not be followed by a space'; - $phpcsFile->addError($error, $stackPtr, 'BooleanNot'); + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'BooleanNot'); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($stackPtr + 1), ''); + } + return; } // Find the last syntax item to determine if this is an unary operator. - $lastSyntaxItem = $phpcsFile->findPrevious( - array(T_WHITESPACE), - $stackPtr - 1, - ($tokens[$stackPtr]['column']) * -1, + $lastSyntaxItem = $phpcsFile->findPrevious( + [T_WHITESPACE], + ($stackPtr - 1), + (($tokens[$stackPtr]['column']) * -1), true, null, true ); $operatorSuffixAllowed = in_array( $tokens[$lastSyntaxItem]['code'], - array( - T_LNUMBER, - T_DNUMBER, - T_CLOSE_PARENTHESIS, - T_CLOSE_CURLY_BRACKET, - T_CLOSE_SQUARE_BRACKET, - T_VARIABLE, - T_STRING, - ) + [ + T_LNUMBER, + T_DNUMBER, + T_CLOSE_PARENTHESIS, + T_CLOSE_CURLY_BRACKET, + T_CLOSE_SQUARE_BRACKET, + T_CLOSE_SHORT_ARRAY, + T_VARIABLE, + T_STRING, + ] ); // Check plus / minus value assignments or comparisons. @@ -107,7 +132,10 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) && $tokens[($stackPtr + 1)]['code'] === T_WHITESPACE ) { $error = 'A unary operator statement must not be followed by a space'; - $phpcsFile->addError($error, $stackPtr); + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'PlusMinus'); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($stackPtr + 1), ''); + } } } @@ -115,5 +143,3 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Functions/DiscouragedFunctionsSniff.php b/coder_sniffer/Backdrop/Sniffs/Functions/DiscouragedFunctionsSniff.php index 68adb6f..1dbf447 100644 --- a/coder_sniffer/Backdrop/Sniffs/Functions/DiscouragedFunctionsSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Functions/DiscouragedFunctionsSniff.php @@ -1,14 +1,16 @@ string|null) + * @var array */ - protected $forbiddenFunctions = array( + public $forbiddenFunctions = [ // Devel module debugging functions. - 'dargs' => null, - 'dcp' => null, - 'dd' => null, - 'dfb' => null, - 'dfbt' => null, - 'dpm' => null, - 'dpq' => null, - 'dpr' => null, - 'dprint_r' => null, - 'backdrop_debug' => null, - 'dsm' => null, - 'dvm' => null, - 'dvr' => null, - 'kdevel_print_object' => null, - 'kpr' => null, - 'kprint_r' => null, - 'sdpm' => null, - ); + 'dargs' => null, + 'dcp' => null, + 'dd' => null, + 'ddebug_backtrace' => null, + 'ddm' => null, + 'dfb' => null, + 'dfbt' => null, + 'dpm' => null, + 'dpq' => null, + 'dpr' => null, + 'dprint_r' => null, + 'backdrop_debug' => null, + 'dsm' => null, + 'dvm' => null, + 'dvr' => null, + 'kdevel_print_object' => null, + 'kint' => null, + 'ksm' => null, + 'kpr' => null, + 'kprint_r' => null, + 'sdpm' => null, + // Functions which are not available on all + // PHP builds. + 'fnmatch' => null, + // Functions which are a security risk. + 'eval' => null, + ]; /** * If true, an error will be thrown; otherwise a warning. * - * @var bool + * @var boolean */ public $error = false; }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Functions/FunctionDeclarationSniff.php b/coder_sniffer/Backdrop/Sniffs/Functions/FunctionDeclarationSniff.php deleted file mode 100644 index cf8195f..0000000 --- a/coder_sniffer/Backdrop/Sniffs/Functions/FunctionDeclarationSniff.php +++ /dev/null @@ -1,66 +0,0 @@ -getTokens(); - - if ($tokens[($stackPtr + 1)]['content'] !== ' ') { - $error = 'Expected exactly one space after the function keyword'; - $phpcsFile->addError($error, ($stackPtr + 1), 'SpaceAfter'); - } - - if (isset($tokens[($stackPtr + 3)]) === true - && $tokens[($stackPtr + 3)]['code'] === T_WHITESPACE - ) { - $error = 'Space before opening parenthesis of function definition prohibited'; - $phpcsFile->addError($error, ($stackPtr + 3), 'SpaceBeforeParenthesis'); - } - - }//end process() - - -}//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/InfoFiles/AutoAddedKeysSniff.php b/coder_sniffer/Backdrop/Sniffs/InfoFiles/AutoAddedKeysSniff.php new file mode 100644 index 0000000..92e27fb --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/InfoFiles/AutoAddedKeysSniff.php @@ -0,0 +1,89 @@ + + */ + public function register() + { + return [T_INLINE_HTML]; + + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @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 + */ + public function process(File $phpcsFile, $stackPtr) + { + // Only run this sniff once per info file. + if (preg_match('/\.info$/', $phpcsFile->getFilename()) === 1) { + // Backdrop 7 style info file. + $contents = file_get_contents($phpcsFile->getFilename()); + $info = ClassFilesSniff::backdropParseInfoFormat($contents); + } else if (preg_match('/\.info\.yml$/', $phpcsFile->getFilename()) === 1) { + // Backdrop 8 style info.yml file. + $contents = file_get_contents($phpcsFile->getFilename()); + try { + $info = \Symfony\Component\Yaml\Yaml::parse($contents); + } catch (\Symfony\Component\Yaml\Exception\ParseException $e) { + // If the YAML is invalid we ignore this file. + return ($phpcsFile->numTokens + 1); + } + } else { + return ($phpcsFile->numTokens + 1); + } + + if (isset($info['project']) === true) { + $warning = 'Remove "project" from the info file, it will be added by backdrop.org packaging automatically'; + $phpcsFile->addWarning($warning, $stackPtr, 'Project'); + } + + if (isset($info['datestamp']) === true) { + $warning = 'Remove "datestamp" from the info file, it will be added by backdrop.org packaging automatically'; + $phpcsFile->addWarning($warning, $stackPtr, 'Timestamp'); + } + + // "version" is special: we want to allow it in core, but not anywhere else. + if (isset($info['version']) === true && strpos($phpcsFile->getFilename(), '/core/') === false) { + $warning = 'Remove "version" from the info file, it will be added by backdrop.org packaging automatically'; + $phpcsFile->addWarning($warning, $stackPtr, 'Version'); + } + + return ($phpcsFile->numTokens + 1); + + }//end process() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/InfoFiles/ClassFilesSniff.php b/coder_sniffer/Backdrop/Sniffs/InfoFiles/ClassFilesSniff.php index 72223c7..0ccea43 100644 --- a/coder_sniffer/Backdrop/Sniffs/InfoFiles/ClassFilesSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/InfoFiles/ClassFilesSniff.php @@ -1,14 +1,17 @@ */ public function register() { - return array(T_INLINE_HTML); + return [T_INLINE_HTML]; }//end register() @@ -36,23 +39,18 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token in the - * stack passed in $tokens. + * @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 + * @return int */ - public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { + // Only run this sniff once per info file. $fileExtension = strtolower(substr($phpcsFile->getFilename(), -4)); if ($fileExtension !== 'info') { - return; - } - - $tokens = $phpcsFile->getTokens(); - // Only run this sniff once per info file. - if ($tokens[$stackPtr]['line'] !== 1) { - return; + return ($phpcsFile->numTokens + 1); } $contents = file_get_contents($phpcsFile->getFilename()); @@ -73,30 +71,35 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) // a class or interface definition. $searchTokens = token_get_all(file_get_contents($fileName)); foreach ($searchTokens as $token) { - if (is_array($token) === true && ($token[0] === T_CLASS || $token[0] === T_INTERFACE)) { + if (is_array($token) === true + && in_array($token[0], [T_CLASS, T_INTERFACE, T_TRAIT]) === true + ) { continue 2; } } + $ptr = self::getPtr('files[]', $file, $phpcsFile); $error = "It's only necessary to declare files[] if they declare a class or interface."; $phpcsFile->addError($error, $ptr, 'UnecessaryFileDeclaration'); }//end foreach }//end if + return ($phpcsFile->numTokens + 1); + }//end process() /** * Helper function that returns the position of the key in the info file. * - * @param string $key Key name to search for. - * @param string $value Corresponding value to search for. - * @param PHP_CodeSniffer_File $infoFile Info file to search in. + * @param string $key Key name to search for. + * @param string $value Corresponding value to search for. + * @param \PHP_CodeSniffer\Files\File $infoFile Info file to search in. * * @return int|false Returns the stack position if the file name is found, false - * otherwise. + * otherwise. */ - public static function getPtr($key, $value, PHP_CodeSniffer_File $infoFile) + public static function getPtr($key, $value, File $infoFile) { foreach ($infoFile->getTokens() as $ptr => $tokenInfo) { if (preg_match('@^[\s]*'.preg_quote($key).'[\s]*=[\s]*["\']?'.preg_quote($value).'["\']?@', $tokenInfo['content']) === 1) { @@ -114,14 +117,15 @@ public static function getPtr($key, $value, PHP_CodeSniffer_File $infoFile) * * @param string $data The contents of the info file to parse * - * @return array The info array. + * @return array The info array. */ public static function backdropParseInfoFormat($data) { - $info = array(); + $info = []; $constants = get_defined_constants(); - if (preg_match_all(' + if (preg_match_all( + ' @^\s* # Start at the beginning of a line, ignoring leading whitespace ((?: [^=;\[\]]| # Key names cannot contain equal signs, semi-colons or square brackets, @@ -133,43 +137,56 @@ public static function backdropParseInfoFormat($data) (\'(?:[^\']|(?<=\\\\)\')*\')| # Single-quoted string, which may contain slash-escaped quotes/slashes ([^\r\n]*?) # Non-quoted string )\s*$ # Stop at the next end of a line, ignoring trailing whitespace - @msx', $data, $matches, PREG_SET_ORDER)) { - foreach ($matches as $match) { - // Fetch the key and value string. - $i = 0; - foreach (array('key', 'value1', 'value2', 'value3') as $var) { - $$var = isset($match[++$i]) ? $match[$i] : ''; - } - $value = stripslashes(substr($value1, 1, -1)) . stripslashes(substr($value2, 1, -1)) . $value3; - - // Parse array syntax. - $keys = preg_split('/\]?\[/', rtrim($key, ']')); - $last = array_pop($keys); - $parent = &$info; - - // Create nested arrays. - foreach ($keys as $key) { - if ($key == '') { - $key = count($parent); - } - if (!isset($parent[$key]) || !is_array($parent[$key])) { - $parent[$key] = array(); - } - $parent = &$parent[$key]; - } + @msx', + $data, + $matches, + PREG_SET_ORDER + ) !== false + ) { + foreach ($matches as $match) { + // Fetch the key and value string. + $i = 0; + foreach (['key', 'value1', 'value2', 'value3'] as $var) { + if (isset($match[++$i]) === true) { + $$var = $match[$i]; + } else { + $$var = ''; + } + } - // Handle PHP constants. - if (isset($constants[$value])) { - $value = $constants[$value]; - } + $value = stripslashes(substr($value1, 1, -1)).stripslashes(substr($value2, 1, -1)).$value3; - // Insert actual value. - if ($last == '') { - $last = count($parent); - } - $parent[$last] = $value; - } - } + // Parse array syntax. + $keys = preg_split('/\]?\[/', rtrim($key, ']')); + $last = array_pop($keys); + $parent = &$info; + + // Create nested arrays. + foreach ($keys as $key) { + if ($key === '') { + $key = count($parent); + } + + if (isset($parent[$key]) === false || is_array($parent[$key]) === false) { + $parent[$key] = []; + } + + $parent = &$parent[$key]; + } + + // Handle PHP constants. + if (isset($constants[$value]) === true) { + $value = $constants[$value]; + } + + // Insert actual value. + if ($last === '') { + $last = count($parent); + } + + $parent[$last] = $value; + }//end foreach + }//end if return $info; @@ -177,5 +194,3 @@ public static function backdropParseInfoFormat($data) }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/InfoFiles/RequiredSniff.php b/coder_sniffer/Backdrop/Sniffs/InfoFiles/RequiredSniff.php index 2b04b36..fe38f6a 100644 --- a/coder_sniffer/Backdrop/Sniffs/InfoFiles/RequiredSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/InfoFiles/RequiredSniff.php @@ -1,14 +1,17 @@ */ public function register() { - return array(T_INLINE_HTML); + return [T_INLINE_HTML]; }//end register() @@ -36,27 +39,22 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token in the - * stack passed in $tokens. + * @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 + * @return int */ - public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { + // Only run this sniff once per info file. $fileExtension = strtolower(substr($phpcsFile->getFilename(), -4)); if ($fileExtension !== 'info') { - return; - } - - $tokens = $phpcsFile->getTokens(); - // Only run this sniff once per info file. - if ($tokens[$stackPtr]['line'] !== 1) { - return; + return ($phpcsFile->numTokens + 1); } $contents = file_get_contents($phpcsFile->getFilename()); - $info = Backdrop_Sniffs_InfoFiles_ClassFilesSniff::backdropParseInfoFormat($contents); + $info = ClassFilesSniff::backdropParseInfoFormat($contents); if (isset($info['name']) === false) { $error = '"name" property is missing in the info file'; $phpcsFile->addError($error, $stackPtr, 'Name'); @@ -67,20 +65,14 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) $phpcsFile->addError($error, $stackPtr, 'Description'); } - if (isset($info['core']) === false) { - $error = '"core" property is missing in the info file'; - $phpcsFile->addError($error, $stackPtr, 'Core'); - } else if ($info['core'] === '7.x' && isset($info['php']) === true - && $info['php'] <= '5.2' - ) { - $error = 'Backdrop 7 core already requires PHP 5.2'; - $ptr = Backdrop_Sniffs_InfoFiles_ClassFilesSniff::getPtr('php', $info['php'], $phpcsFile); - $phpcsFile->addError($error, $ptr, 'D7PHPVersion'); + if (isset($info['backdrop']) === false) { + $error = '"backdrop" property is missing in the info file'; + $phpcsFile->addError($error, $stackPtr, 'Core version'); } + return ($phpcsFile->numTokens + 1); + }//end process() }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Methods/MethodDeclarationSniff.php b/coder_sniffer/Backdrop/Sniffs/Methods/MethodDeclarationSniff.php new file mode 100644 index 0000000..7f06c49 --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Methods/MethodDeclarationSniff.php @@ -0,0 +1,40 @@ +getTokens(); - if ($tokens[$stackPtr]['content'] !== strtolower($tokens[$stackPtr]['content'])) { - $error = 'The PHP language keyword "%s" must be all lower case'; - $data = array(strtolower($tokens[$stackPtr]['content'])); - $phpcsFile->addError($error, $stackPtr, 'LowerCase', $data); - } - - }//end process() - - -}//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/NamingConventions/ValidClassNameSniff.php b/coder_sniffer/Backdrop/Sniffs/NamingConventions/ValidClassNameSniff.php index b25c65c..e9fa743 100644 --- a/coder_sniffer/Backdrop/Sniffs/NamingConventions/ValidClassNameSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/NamingConventions/ValidClassNameSniff.php @@ -1,8 +1,6 @@ */ public function register() { - return array( - T_CLASS, - T_INTERFACE, - ); + return [ + T_CLASS, + T_INTERFACE, + ]; }//end register() @@ -50,19 +53,19 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The current file being processed. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The current file being processed. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. * * @return void */ - public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); $className = $phpcsFile->findNext(T_STRING, $stackPtr); $name = trim($tokens[$className]['content']); - $errorData = array(ucfirst($tokens[$stackPtr]['content'])); + $errorData = [ucfirst($tokens[$stackPtr]['content'])]; // Make sure the first letter is a capital. if (preg_match('|^[A-Z]|', $name) === 0) { @@ -80,6 +83,3 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) }//end class - - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/NamingConventions/ValidFunctionNameSniff.php b/coder_sniffer/Backdrop/Sniffs/NamingConventions/ValidFunctionNameSniff.php index e8743b8..6e6ee61 100644 --- a/coder_sniffer/Backdrop/Sniffs/NamingConventions/ValidFunctionNameSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/NamingConventions/ValidFunctionNameSniff.php @@ -1,53 +1,45 @@ - * @author Greg Sherwood - * @author Marc McIntyre - * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600) - * @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence - * @link http://pear.php.net/package/PHP_CodeSniffer + * @category PHP + * @package PHP_CodeSniffer + * @link http://pear.php.net/package/PHP_CodeSniffer */ -if (class_exists('PHP_CodeSniffer_Standards_AbstractScopeSniff', true) === false) { - throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_Standards_AbstractScopeSniff not found'); -} +namespace Backdrop\Sniffs\NamingConventions; + +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions\CamelCapsFunctionNameSniff; +use PHP_CodeSniffer\Util\Common; /** - * Backdrop_Sniffs_NamingConventions_ValidFunctionNameSniff. + * \Backdrop\Sniffs\NamingConventions\ValidFunctionNameSniff. * - * Ensures method names are correct depending on whether they are public - * or private, and that functions are named correctly. + * Extends + * \PHP_CodeSniffer\Standards\Generic\Sniffs\NamingConventions\CamelCapsFunctionNameSniff + * to also check global function names outside the scope of classes and to not + * allow methods beginning with an underscore. * - * @category PHP - * @package PHP_CodeSniffer - * @author Serge Shirokov - * @author Greg Sherwood - * @author Marc McIntyre - * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600) - * @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence - * @version Release: 1.2.0RC3 - * @link http://pear.php.net/package/PHP_CodeSniffer + * @category PHP + * @package PHP_CodeSniffer + * @link http://pear.php.net/package/PHP_CodeSniffer */ -class Backdrop_Sniffs_NamingConventions_ValidFunctionNameSniff extends PEAR_Sniffs_NamingConventions_ValidFunctionNameSniff +class ValidFunctionNameSniff extends CamelCapsFunctionNameSniff { + /** * Processes the tokens within the scope. * - * @param PHP_CodeSniffer_File $phpcsFile The file being processed. - * @param int $stackPtr The position where this token was - * found. - * @param int $currScope The position of the current scope. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being processed. + * @param int $stackPtr The position where this token was + * found. + * @param int $currScope The position of the current scope. * * @return void */ - protected function processTokenWithinScope(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $currScope) + protected function processTokenWithinScope(File $phpcsFile, $stackPtr, $currScope) { $methodName = $phpcsFile->getDeclarationName($stackPtr); if ($methodName === null) { @@ -56,36 +48,39 @@ protected function processTokenWithinScope(PHP_CodeSniffer_File $phpcsFile, $sta } $className = $phpcsFile->getDeclarationName($currScope); - $errorData = array($className.'::'.$methodName); + $errorData = [$className.'::'.$methodName]; // Is this a magic method. i.e., is prefixed with "__" ? if (preg_match('|^__|', $methodName) !== 0) { $magicPart = strtolower(substr($methodName, 2)); - if (in_array($magicPart, $this->magicMethods) === false) { - $error = 'Method name "%s" is invalid; only PHP magic methods should be prefixed with a double underscore'; - $phpcsFile->addError($error, $stackPtr, 'MethodDoubleUnderscore', $errorData); + if (isset($this->magicMethods[$magicPart]) === false + && isset($this->methodsDoubleUnderscore[$magicPart]) === false + ) { + $error = 'Method name "%s" is invalid; only PHP magic methods should be prefixed with a double underscore'; + $phpcsFile->addError($error, $stackPtr, 'MethodDoubleUnderscore', $errorData); } return; } - $methodProps = $phpcsFile->getMethodProperties($stackPtr); - $scope = $methodProps['scope']; - $scopeSpecified = $methodProps['scope_specified']; - - // Methods should not contain underscores. - if (strpos($methodName, '_') !== false) { - if ($scopeSpecified === true) { - $error = '%s method name "%s" is not in lowerCamel format, it must not contain underscores'; - $data = array( - ucfirst($scope), - $errorData[0], - ); - $phpcsFile->addError($error, $stackPtr, 'ScopeNotLowerCamel', $data); - } else { - $error = 'Method name "%s" is not in lowerCamel format, it must not contain underscores'; - $phpcsFile->addError($error, $stackPtr, 'NotLowerCamel', $errorData); - } + $methodProps = $phpcsFile->getMethodProperties($stackPtr); + if (Common::isCamelCaps($methodName, false, true, $this->strict) === false) { + if ($methodProps['scope_specified'] === true) { + $error = '%s method name "%s" is not in lowerCamel format'; + $data = [ + ucfirst($methodProps['scope']), + $errorData[0], + ]; + $phpcsFile->addError($error, $stackPtr, 'ScopeNotCamelCaps', $data); + } else { + $error = 'Method name "%s" is not in lowerCamel format'; + $phpcsFile->addError($error, $stackPtr, 'NotCamelCaps', $errorData); + } + + $phpcsFile->recordMetric($stackPtr, 'CamelCase method name', 'no'); + return; + } else { + $phpcsFile->recordMetric($stackPtr, 'CamelCase method name', 'yes'); } }//end processTokenWithinScope() @@ -94,19 +89,39 @@ protected function processTokenWithinScope(PHP_CodeSniffer_File $phpcsFile, $sta /** * Processes the tokens outside the scope. * - * @param PHP_CodeSniffer_File $phpcsFile The file being processed. - * @param int $stackPtr The position where this token was - * found. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being processed. + * @param int $stackPtr The position where this token was + * found. * * @return void */ - protected function processTokenOutsideScope(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + protected function processTokenOutsideScope(File $phpcsFile, $stackPtr) { - // Empty override, does not apply to Backdrop. + $functionName = $phpcsFile->getDeclarationName($stackPtr); + if ($functionName === null) { + // Ignore closures. + return; + } + + $isApiFile = substr($phpcsFile->getFilename(), -8) === '.api.php'; + $isHookExample = substr($functionName, 0, 5) === 'hook_'; + if ($isApiFile === true && $isHookExample === true) { + // Ignore for examaple hook_ENTITY_TYPE_insert() functions in .api.php + // files. + return; + } + + if ($functionName !== strtolower($functionName)) { + $expected = strtolower(preg_replace('/([^_])([A-Z])/', '$1_$2', $functionName)); + $error = 'Invalid function name, expected %s but found %s'; + $data = [ + $expected, + $functionName, + ]; + $phpcsFile->addError($error, $stackPtr, 'InvalidName', $data); + } }//end processTokenOutsideScope() }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/NamingConventions/ValidGlobalSniff.php b/coder_sniffer/Backdrop/Sniffs/NamingConventions/ValidGlobalSniff.php index 89efa97..875b7f6 100644 --- a/coder_sniffer/Backdrop/Sniffs/NamingConventions/ValidGlobalSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/NamingConventions/ValidGlobalSniff.php @@ -1,14 +1,18 @@ + */ + public $coreGlobals = [ + '$argc', + '$argv', + '$base_insecure_url', + '$base_path', + '$base_root', + '$base_secure_url', + '$base_theme_info', + '$base_url', + '$channel', + '$conf', + '$config', + '$config_directories', + '$cookie_domain', + '$databases', + '$db_prefix', + '$db_type', + '$db_url', + '$backdrop_hash_salt', + '$backdrop_test_info', + '$element', + '$forum_topic_list_header', + '$image', + '$install_state', + '$installed_profile', + '$is_https', + '$is_https_mock', + '$item', + '$items', + '$language', + '$language_content', + '$language_url', + '$locks', + '$menu_admin', + '$multibyte', + '$pager_limits', + '$pager_page_array', + '$pager_total', + '$pager_total_items', + '$tag', + '$theme', + '$theme_engine', + '$theme_info', + '$theme_key', + '$theme_path', + '$timers', + '$update_free_access', + '$update_rewrite_settings', + '$user', + ]; /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { - return array(T_GLOBAL); + return [T_GLOBAL]; }//end register() @@ -84,24 +95,24 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The current file being processed. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The current file being processed. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. * * @return void */ - public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); $varToken = $stackPtr; // Find variable names until we hit a semicolon. - $ignore = PHP_CodeSniffer_Tokens::$emptyTokens; + $ignore = Tokens::$emptyTokens; $ignore[] = T_SEMICOLON; - while ($varToken = $phpcsFile->findNext($ignore, $varToken + 1, null, true, null, true)) { + while (($varToken = $phpcsFile->findNext($ignore, ($varToken + 1), null, true, null, true)) !== false) { if ($tokens[$varToken]['code'] === T_VARIABLE && in_array($tokens[$varToken]['content'], $this->coreGlobals) === false - && $tokens[$varToken]['content']{1} !== '_' + && $tokens[$varToken]['content'][1] !== '_' ) { $error = 'global variables should start with a single underscore followed by the module and another underscore'; $phpcsFile->addError($error, $varToken, 'GlobalUnderScore'); @@ -112,5 +123,3 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/NamingConventions/ValidVariableNameSniff.php b/coder_sniffer/Backdrop/Sniffs/NamingConventions/ValidVariableNameSniff.php index afc008c..adadd9d 100644 --- a/coder_sniffer/Backdrop/Sniffs/NamingConventions/ValidVariableNameSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/NamingConventions/ValidVariableNameSniff.php @@ -1,16 +1,19 @@ getTokens(); @@ -42,42 +43,81 @@ protected function processMemberVar(PHP_CodeSniffer_File $phpcsFile, $stackPtr) return; } + // Check if the class extends another class and get the name of the class + // that is extended. + if (empty($tokens[$stackPtr]['conditions']) === false) { + $classPtr = key($tokens[$stackPtr]['conditions']); + $extendsName = $phpcsFile->findExtendedClassName($classPtr); + + // Special case config entities: those are allowed to have underscores in + // their class property names. If a class extends something like + // ConfigEntityBase then we consider it a config entity class and allow + // underscores. + if ($extendsName !== false && strpos($extendsName, 'ConfigEntity') !== false) { + return; + } + + // Plugin annotations may have underscores in class properties. + // For example, see \Backdrop\Core\Field\Annotation\FieldFormatter. + // The only class named "Plugin" in Backdrop core is + // \Backdrop\Component\Annotation\Plugin while many Views plugins + // extend \Backdrop\views\Annotation\ViewsPluginAnnotationBase. + if ($extendsName !== false && in_array( + $extendsName, + [ + 'Plugin', + 'ViewsPluginAnnotationBase', + ] + ) !== false + ) { + return; + } + + $implementsNames = $phpcsFile->findImplementedInterfaceNames($classPtr); + if ($implementsNames !== false && in_array('AnnotationInterface', $implementsNames) !== false) { + return; + } + }//end if + + // The name of a property must start with a lowercase letter, properties + // with underscores are not allowed, except the cases handled above. $memberName = ltrim($tokens[$stackPtr]['content'], '$'); - - if (strpos($memberName, '_') !== false) { - $error = 'Class property %s should use lowerCamel naming without underscores'; - $data = array($tokens[$stackPtr]['content']); - $phpcsFile->addError($error, $stackPtr, 'LowerCamelName', $data); + if (preg_match('/^[a-z]/', $memberName) === 1 && strpos($memberName, '_') === false) { + return; } + $error = 'Class property %s should use lowerCamel naming without underscores'; + $data = [$tokens[$stackPtr]['content']]; + $phpcsFile->addError($error, $stackPtr, 'LowerCamelName', $data); + }//end processMemberVar() /** * Processes normal variables. * - * @param PHP_CodeSniffer_File $phpcsFile The file where this token was found. - * @param int $stackPtr The position where the token was found. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position where the token was found. * * @return void */ - protected function processVariable(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + protected function processVariable(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); $varName = ltrim($tokens[$stackPtr]['content'], '$'); - $phpReservedVars = array( - '_SERVER', - '_GET', - '_POST', - '_REQUEST', - '_SESSION', - '_ENV', - '_COOKIE', - '_FILES', - 'GLOBALS', - ); + $phpReservedVars = [ + '_SERVER', + '_GET', + '_POST', + '_REQUEST', + '_SESSION', + '_ENV', + '_COOKIE', + '_FILES', + 'GLOBALS', + ]; // If it's a php reserved var, then its ok. if (in_array($varName, $phpReservedVars) === true) { @@ -89,9 +129,9 @@ protected function processVariable(PHP_CodeSniffer_File $phpcsFile, $stackPtr) return; } - if (preg_match('/[A-Z]/', $varName)) { - $error = "Variable \"$varName\" is camel caps format. do not use mixed case (camelCase), use lower case and _"; - $phpcsFile->addError($error, $stackPtr); + if (preg_match('/^[A-Z]/', $varName) === 1) { + $error = "Variable \"$varName\" starts with a capital letter, but only \$lowerCamelCase or \$snake_case is allowed"; + $phpcsFile->addError($error, $stackPtr, 'LowerStart'); } }//end processVariable() @@ -100,18 +140,17 @@ protected function processVariable(PHP_CodeSniffer_File $phpcsFile, $stackPtr) /** * Processes variables in double quoted strings. * - * @param PHP_CodeSniffer_File $phpcsFile The file where this token was found. - * @param int $stackPtr The position where the token was found. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position where the token was found. * * @return void */ - protected function processVariableInString(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + protected function processVariableInString(File $phpcsFile, $stackPtr) { // We don't care about variables in strings. + return; }//end processVariableInString() }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Scope/MethodScopeSniff.php b/coder_sniffer/Backdrop/Sniffs/Scope/MethodScopeSniff.php new file mode 100644 index 0000000..8ca9cdc --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Scope/MethodScopeSniff.php @@ -0,0 +1,107 @@ +getTokens(); + + $methodName = $phpcsFile->getDeclarationName($stackPtr); + if ($methodName === null) { + // Ignore closures. + return; + } + + if ($phpcsFile->hasCondition($stackPtr, T_FUNCTION) === true) { + // Ignore nested functions. + return; + } + + $modifier = null; + for ($i = ($stackPtr - 1); $i > 0; $i--) { + if ($tokens[$i]['line'] < $tokens[$stackPtr]['line']) { + break; + } else if (isset(Tokens::$scopeModifiers[$tokens[$i]['code']]) === true) { + $modifier = $i; + break; + } + } + + if ($modifier === null) { + $error = 'Visibility must be declared on method "%s"'; + $data = [$methodName]; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'Missing', $data); + + if ($fix === true) { + // No scope modifier means the method is public in PHP, fix that + // to be explicitly public. + $phpcsFile->fixer->addContentBefore($stackPtr, 'public '); + } + } + + }//end processTokenWithinScope() + + + /** + * Processes a token that is found outside the scope that this test is + * listening to. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file where this token was found. + * @param int $stackPtr The position in the stack where this + * token was found. + * + * @return void + */ + protected function processTokenOutsideScope(File $phpcsFile, $stackPtr) + { + + }//end processTokenOutsideScope() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Semantics/ConstantNameSniff.php b/coder_sniffer/Backdrop/Sniffs/Semantics/ConstantNameSniff.php index 32b7ed3..cbe77d4 100644 --- a/coder_sniffer/Backdrop/Sniffs/Semantics/ConstantNameSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Semantics/ConstantNameSniff.php @@ -1,34 +1,36 @@ */ public function registerFunctionNames() { - return array('define'); + return ['define']; }//end registerFunctionNames() @@ -36,50 +38,46 @@ public function registerFunctionNames() /** * Processes this function call. * - * @param PHP_CodeSniffer_File $phpcsFile - * The file being scanned. - * @param int $stackPtr - * The position of the function call in the stack. - * @param int $openBracket - * The position of the opening parenthesis in the stack. - * @param int $closeBracket - * The position of the closing parenthesis in the stack. - * @param Backdrop_Sniffs_Semantics_FunctionCallSniff $sniff - * Can be used to retreive the function's arguments with the getArgument() - * method. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the function call in + * the stack. + * @param int $openBracket The position of the opening + * parenthesis in the stack. + * @param int $closeBracket The position of the closing + * parenthesis in the stack. * * @return void */ public function processFunctionCall( - PHP_CodeSniffer_File $phpcsFile, + File $phpcsFile, $stackPtr, $openBracket, - $closeBracket, - Backdrop_Sniffs_Semantics_FunctionCallSniff $sniff + $closeBracket ) { - $fileExtension = strtolower(substr($phpcsFile->getFilename(), -6)); + $nameParts = explode('.', basename($phpcsFile->getFilename())); + $fileExtension = end($nameParts); // Only check in *.module files. - if ($fileExtension !== 'module') { + if ($fileExtension !== 'module' && $fileExtension !== 'install') { return; } $tokens = $phpcsFile->getTokens(); - $argument = $sniff->getArgument(1); + $argument = $this->getArgument(1); if ($tokens[$argument['start']]['code'] !== T_CONSTANT_ENCAPSED_STRING) { // Not a string literal, so this is some obscure constant that we ignore. return; } - $moduleName = substr(basename($phpcsFile->getFilename()), 0, -7); + $moduleName = reset($nameParts); $expectedStart = strtoupper($moduleName); // Remove the quotes around the string litral. $constant = substr($tokens[$argument['start']]['content'], 1, -1); if (strpos($constant, $expectedStart) !== 0) { $warning = 'All constants defined by a module must be prefixed with the module\'s name, expected "%s" but found "%s"'; - $data = array( - $expectedStart."_$constant", - $constant, - ); + $data = [ + $expectedStart."_$constant", + $constant, + ]; $phpcsFile->addWarning($warning, $stackPtr, 'ConstantStart', $data); } @@ -87,5 +85,3 @@ public function processFunctionCall( }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Semantics/EmptyInstallSniff.php b/coder_sniffer/Backdrop/Sniffs/Semantics/EmptyInstallSniff.php index 938ec87..c4b73e8 100644 --- a/coder_sniffer/Backdrop/Sniffs/Semantics/EmptyInstallSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Semantics/EmptyInstallSniff.php @@ -1,14 +1,17 @@ getFilename(), -7)); // Only check in *.install files. @@ -46,7 +49,7 @@ public function processFunction(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $fun ) { // Check if there is a function body. $bodyPtr = $phpcsFile->findNext( - PHP_CodeSniffer_Tokens::$emptyTokens, + Tokens::$emptyTokens, ($tokens[$functionPtr]['scope_opener'] + 1), $tokens[$functionPtr]['scope_closer'], true @@ -61,5 +64,3 @@ public function processFunction(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $fun }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionAliasSniff.php b/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionAliasSniff.php index 073cdb6..43231a2 100644 --- a/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionAliasSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionAliasSniff.php @@ -1,14 +1,16 @@ */ - protected $aliases = array( - '_' => 'gettext', - 'add' => 'swfmovie_add', - 'add' => 'swfsprite_add', - 'add_root' => 'domxml_add_root', - 'addaction' => 'swfbutton_addAction', - 'addcolor' => 'swfdisplayitem_addColor', - 'addentry' => 'swfgradient_addEntry', - 'addfill' => 'swfshape_addfill', - 'addshape' => 'swfbutton_addShape', - 'addstring' => 'swftext_addString', - 'addstring' => 'swftextfield_addString', - 'align' => 'swftextfield_align', - 'attributes' => 'domxml_attributes', - 'children' => 'domxml_children', - 'chop' => 'rtrim', - 'close' => 'closedir', - 'com_get' => 'com_propget', - 'com_propset' => 'com_propput', - 'com_set' => 'com_propput', - 'die' => 'exit', - 'dir' => 'getdir', - 'diskfreespace' => 'disk_free_space', - 'domxml_getattr' => 'domxml_get_attribute', - 'domxml_setattr' => 'domxml_set_attribute', - 'doubleval' => 'floatval', - 'drawarc' => 'swfshape_drawarc', - 'drawcircle' => 'swfshape_drawcircle', - 'drawcubic' => 'swfshape_drawcubic', - 'drawcubicto' => 'swfshape_drawcubicto', - 'drawcurve' => 'swfshape_drawcurve', - 'drawcurveto' => 'swfshape_drawcurveto', - 'drawglyph' => 'swfshape_drawglyph', - 'drawline' => 'swfshape_drawline', - 'drawlineto' => 'swfshape_drawlineto', - 'dtd' => 'domxml_intdtd', - 'dumpmem' => 'domxml_dumpmem', - 'fbsql' => 'fbsql_db_query', - 'fputs' => 'fwrite', - 'get_attribute' => 'domxml_get_attribute', - 'getascent' => 'swffont_getAscent', - 'getascent' => 'swftext_getAscent', - 'getattr' => 'domxml_get_attribute', - 'getdescent' => 'swffont_getDescent', - 'getdescent' => 'swftext_getDescent', - 'getheight' => 'swfbitmap_getHeight', - 'getleading' => 'swffont_getLeading', - 'getleading' => 'swftext_getLeading', - 'getshape1' => 'swfmorph_getShape1', - 'getshape2' => 'swfmorph_getShape2', - 'getwidth' => 'swfbitmap_getWidth', - 'getwidth' => 'swffont_getWidth', - 'getwidth' => 'swftext_getWidth', - 'gzputs' => 'gzwrite', - 'i18n_convert' => 'mb_convert_encoding', - 'i18n_discover_encoding' => 'mb_detect_encoding', - 'i18n_http_input' => 'mb_http_input', - 'i18n_http_output' => 'mb_http_output', - 'i18n_internal_encoding' => 'mb_internal_encoding', - 'i18n_ja_jp_hantozen' => 'mb_convert_kana', - 'i18n_mime_header_decode' => 'mb_decode_mimeheader', - 'i18n_mime_header_encode' => 'mb_encode_mimeheader', - 'imap_create' => 'imap_createmailbox', - 'imap_fetchtext' => 'imap_body', - 'imap_getmailboxes' => 'imap_list_full', - 'imap_getsubscribed' => 'imap_lsub_full', - 'imap_header' => 'imap_headerinfo', - 'imap_listmailbox' => 'imap_list', - 'imap_listsubscribed' => 'imap_lsub', - 'imap_rename' => 'imap_renamemailbox', - 'imap_scan' => 'imap_listscan', - 'imap_scanmailbox' => 'imap_listscan', - 'ini_alter' => 'ini_set', - 'is_double' => 'is_float', - 'is_integer' => 'is_int', - 'is_long' => 'is_int', - 'is_real' => 'is_float', - 'is_writeable' => 'is_writable', - 'join' => 'implode', - 'key_exists' => 'array_key_exists', - 'labelframe' => 'swfmovie_labelFrame', - 'labelframe' => 'swfsprite_labelFrame', - 'last_child' => 'domxml_last_child', - 'lastchild' => 'domxml_last_child', - 'ldap_close' => 'ldap_unbind', - 'magic_quotes_runtime' => 'set_magic_quotes_runtime', - 'mbstrcut' => 'mb_strcut', - 'mbstrlen' => 'mb_strlen', - 'mbstrpos' => 'mb_strpos', - 'mbstrrpos' => 'mb_strrpos', - 'mbsubstr' => 'mb_substr', - 'ming_setcubicthreshold' => 'ming_setCubicThreshold', - 'ming_setscale' => 'ming_setScale', - 'move' => 'swfdisplayitem_move', - 'movepen' => 'swfshape_movepen', - 'movepento' => 'swfshape_movepento', - 'moveto' => 'swfdisplayitem_moveTo', - 'moveto' => 'swffill_moveTo', - 'moveto' => 'swftext_moveTo', - 'msql' => 'msql_db_query', - 'msql_createdb' => 'msql_create_db', - 'msql_dbname' => 'msql_result', - 'msql_dropdb' => 'msql_drop_db', - 'msql_fieldflags' => 'msql_field_flags', - 'msql_fieldlen' => 'msql_field_len', - 'msql_fieldname' => 'msql_field_name', - 'msql_fieldtable' => 'msql_field_table', - 'msql_fieldtype' => 'msql_field_type', - 'msql_freeresult' => 'msql_free_result', - 'msql_listdbs' => 'msql_list_dbs', - 'msql_listfields' => 'msql_list_fields', - 'msql_listtables' => 'msql_list_tables', - 'msql_numfields' => 'msql_num_fields', - 'msql_numrows' => 'msql_num_rows', - 'msql_regcase' => 'sql_regcase', - 'msql_selectdb' => 'msql_select_db', - 'msql_tablename' => 'msql_result', - 'mssql_affected_rows' => 'sybase_affected_rows', - 'mssql_affected_rows' => 'sybase_affected_rows', - 'mssql_close' => 'sybase_close', - 'mssql_close' => 'sybase_close', - 'mssql_connect' => 'sybase_connect', - 'mssql_connect' => 'sybase_connect', - 'mssql_data_seek' => 'sybase_data_seek', - 'mssql_data_seek' => 'sybase_data_seek', - 'mssql_fetch_array' => 'sybase_fetch_array', - 'mssql_fetch_array' => 'sybase_fetch_array', - 'mssql_fetch_field' => 'sybase_fetch_field', - 'mssql_fetch_field' => 'sybase_fetch_field', - 'mssql_fetch_object' => 'sybase_fetch_object', - 'mssql_fetch_object' => 'sybase_fetch_object', - 'mssql_fetch_row' => 'sybase_fetch_row', - 'mssql_fetch_row' => 'sybase_fetch_row', - 'mssql_field_seek' => 'sybase_field_seek', - 'mssql_field_seek' => 'sybase_field_seek', - 'mssql_free_result' => 'sybase_free_result', - 'mssql_free_result' => 'sybase_free_result', - 'mssql_get_last_message' => 'sybase_get_last_message', - 'mssql_get_last_message' => 'sybase_get_last_message', - 'mssql_min_client_severity' => 'sybase_min_client_severity', - 'mssql_min_error_severity' => 'sybase_min_error_severity', + protected $aliases = [ + '_' => 'gettext', + 'chop' => 'rtrim', + 'close' => 'closedir', + 'com_get' => 'com_propget', + 'com_propset' => 'com_propput', + 'com_set' => 'com_propput', + 'die' => 'exit', + 'diskfreespace' => 'disk_free_space', + 'doubleval' => 'floatval', + 'fbsql' => 'fbsql_db_query', + 'fputs' => 'fwrite', + 'gzputs' => 'gzwrite', + 'i18n_convert' => 'mb_convert_encoding', + 'i18n_discover_encoding' => 'mb_detect_encoding', + 'i18n_http_input' => 'mb_http_input', + 'i18n_http_output' => 'mb_http_output', + 'i18n_internal_encoding' => 'mb_internal_encoding', + 'i18n_ja_jp_hantozen' => 'mb_convert_kana', + 'i18n_mime_header_decode' => 'mb_decode_mimeheader', + 'i18n_mime_header_encode' => 'mb_encode_mimeheader', + 'imap_create' => 'imap_createmailbox', + 'imap_fetchtext' => 'imap_body', + 'imap_getmailboxes' => 'imap_list_full', + 'imap_getsubscribed' => 'imap_lsub_full', + 'imap_header' => 'imap_headerinfo', + 'imap_listmailbox' => 'imap_list', + 'imap_listsubscribed' => 'imap_lsub', + 'imap_rename' => 'imap_renamemailbox', + 'imap_scan' => 'imap_listscan', + 'imap_scanmailbox' => 'imap_listscan', + 'ini_alter' => 'ini_set', + 'is_double' => 'is_float', + 'is_integer' => 'is_int', + 'is_long' => 'is_int', + 'is_real' => 'is_float', + 'is_writeable' => 'is_writable', + 'join' => 'implode', + 'key_exists' => 'array_key_exists', + 'ldap_close' => 'ldap_unbind', + 'magic_quotes_runtime' => 'set_magic_quotes_runtime', + 'mbstrcut' => 'mb_strcut', + 'mbstrlen' => 'mb_strlen', + 'mbstrpos' => 'mb_strpos', + 'mbstrrpos' => 'mb_strrpos', + 'mbsubstr' => 'mb_substr', + 'ming_setcubicthreshold' => 'ming_setCubicThreshold', + 'ming_setscale' => 'ming_setScale', + 'msql' => 'msql_db_query', + 'msql_createdb' => 'msql_create_db', + 'msql_dbname' => 'msql_result', + 'msql_dropdb' => 'msql_drop_db', + 'msql_fieldflags' => 'msql_field_flags', + 'msql_fieldlen' => 'msql_field_len', + 'msql_fieldname' => 'msql_field_name', + 'msql_fieldtable' => 'msql_field_table', + 'msql_fieldtype' => 'msql_field_type', + 'msql_freeresult' => 'msql_free_result', + 'msql_listdbs' => 'msql_list_dbs', + 'msql_listfields' => 'msql_list_fields', + 'msql_listtables' => 'msql_list_tables', + 'msql_numfields' => 'msql_num_fields', + 'msql_numrows' => 'msql_num_rows', + 'msql_regcase' => 'sql_regcase', + 'msql_selectdb' => 'msql_select_db', + 'msql_tablename' => 'msql_result', + 'mssql_affected_rows' => 'sybase_affected_rows', + 'mssql_close' => 'sybase_close', + 'mssql_connect' => 'sybase_connect', + 'mssql_data_seek' => 'sybase_data_seek', + 'mssql_fetch_array' => 'sybase_fetch_array', + 'mssql_fetch_field' => 'sybase_fetch_field', + 'mssql_fetch_object' => 'sybase_fetch_object', + 'mssql_fetch_row' => 'sybase_fetch_row', + 'mssql_field_seek' => 'sybase_field_seek', + 'mssql_free_result' => 'sybase_free_result', + 'mssql_get_last_message' => 'sybase_get_last_message', + 'mssql_min_client_severity' => 'sybase_min_client_severity', + 'mssql_min_error_severity' => 'sybase_min_error_severity', 'mssql_min_message_severity' => 'sybase_min_message_severity', - 'mssql_min_server_severity' => 'sybase_min_server_severity', - 'mssql_num_fields' => 'sybase_num_fields', - 'mssql_num_fields' => 'sybase_num_fields', - 'mssql_num_rows' => 'sybase_num_rows', - 'mssql_num_rows' => 'sybase_num_rows', - 'mssql_pconnect' => 'sybase_pconnect', - 'mssql_pconnect' => 'sybase_pconnect', - 'mssql_query' => 'sybase_query', - 'mssql_query' => 'sybase_query', - 'mssql_result' => 'sybase_result', - 'mssql_result' => 'sybase_result', - 'mssql_select_db' => 'sybase_select_db', - 'mssql_select_db' => 'sybase_select_db', - 'multcolor' => 'swfdisplayitem_multColor', - 'mysql' => 'mysql_db_query', - 'mysql_createdb' => 'mysql_create_db', - 'mysql_db_name' => 'mysql_result', - 'mysql_dbname' => 'mysql_result', - 'mysql_dropdb' => 'mysql_drop_db', - 'mysql_fieldflags' => 'mysql_field_flags', - 'mysql_fieldlen' => 'mysql_field_len', - 'mysql_fieldname' => 'mysql_field_name', - 'mysql_fieldtable' => 'mysql_field_table', - 'mysql_fieldtype' => 'mysql_field_type', - 'mysql_freeresult' => 'mysql_free_result', - 'mysql_listdbs' => 'mysql_list_dbs', - 'mysql_listfields' => 'mysql_list_fields', - 'mysql_listtables' => 'mysql_list_tables', - 'mysql_numfields' => 'mysql_num_fields', - 'mysql_numrows' => 'mysql_num_rows', - 'mysql_selectdb' => 'mysql_select_db', - 'mysql_tablename' => 'mysql_result', - 'name' => 'domxml_attrname', - 'new_child' => 'domxml_new_child', - 'new_xmldoc' => 'domxml_new_xmldoc', - 'nextframe' => 'swfmovie_nextFrame', - 'nextframe' => 'swfsprite_nextFrame', - 'node' => 'domxml_node', - 'oci8append' => 'ocicollappend', - 'oci8assign' => 'ocicollassign', - 'oci8assignelem' => 'ocicollassignelem', - 'oci8close' => 'ocicloselob', - 'oci8free' => 'ocifreecoll', - 'oci8free' => 'ocifreedesc', - 'oci8getelem' => 'ocicollgetelem', - 'oci8load' => 'ociloadlob', - 'oci8max' => 'ocicollmax', - 'oci8ocifreecursor' => 'ocifreestatement', - 'oci8save' => 'ocisavelob', - 'oci8savefile' => 'ocisavelobfile', - 'oci8size' => 'ocicollsize', - 'oci8trim' => 'ocicolltrim', - 'oci8writetemporary' => 'ociwritetemporarylob', - 'oci8writetofile' => 'ociwritelobtofile', - 'odbc_do' => 'odbc_exec', - 'odbc_field_precision' => 'odbc_field_len', - 'output' => 'swfmovie_output', - 'parent' => 'domxml_parent', - 'pdf_add_outline' => 'pdf_add_bookmark', - 'pg_clientencoding' => 'pg_client_encoding', - 'pg_setclientencoding' => 'pg_set_client_encoding', - 'pos' => 'current', - 'recode' => 'recode_string', - 'remove' => 'swfmovie_remove', - 'remove' => 'swfsprite_remove', - 'root' => 'domxml_root', - 'rotate' => 'swfdisplayitem_rotate', - 'rotateto' => 'swfdisplayitem_rotateTo', - 'rotateto' => 'swffill_rotateTo', - 'save' => 'swfmovie_save', - 'savetofile' => 'swfmovie_saveToFile', - 'scale' => 'swfdisplayitem_scale', - 'scaleto' => 'swfdisplayitem_scaleTo', - 'scaleto' => 'swffill_scaleTo', - 'set_attribute' => 'domxml_set_attribute', - 'set_content' => 'domxml_set_content', - 'setaction' => 'swfbutton_setAction', - 'setattr' => 'domxml_set_attribute', - 'setbackground' => 'swfmovie_setBackground', - 'setbounds' => 'swftextfield_setBounds', - 'setcolor' => 'swftext_setColor', - 'setcolor' => 'swftextfield_setColor', - 'setdepth' => 'swfdisplayitem_setDepth', - 'setdimension' => 'swfmovie_setDimension', - 'setdown' => 'swfbutton_setDown', - 'setfont' => 'swftext_setFont', - 'setfont' => 'swftextfield_setFont', - 'setframes' => 'swfmovie_setFrames', - 'setframes' => 'swfsprite_setFrames', - 'setheight' => 'swftext_setHeight', - 'setheight' => 'swftextfield_setHeight', - 'sethit' => 'swfbutton_setHit', - 'setindentation' => 'swftextfield_setIndentation', - 'setleftfill' => 'swfshape_setleftfill', - 'setleftmargin' => 'swftextfield_setLeftMargin', - 'setline' => 'swfshape_setline', - 'setlinespacing' => 'swftextfield_setLineSpacing', - 'setmargins' => 'swftextfield_setMargins', - 'setmatrix' => 'swfdisplayitem_setMatrix', - 'setname' => 'swfdisplayitem_setName', - 'setname' => 'swftextfield_setName', - 'setover' => 'swfbutton_setOver', - 'setrate' => 'swfmovie_setRate', - 'setratio' => 'swfdisplayitem_setRatio', - 'setrightfill' => 'swfshape_setrightfill', - 'setrightmargin' => 'swftextfield_setRightMargin', - 'setspacing' => 'swftext_setSpacing', - 'setup' => 'swfbutton_setUp', - 'show_source' => 'highlight_file', - 'sizeof' => 'count', - 'skewx' => 'swfdisplayitem_skewX', - 'skewxto' => 'swfdisplayitem_skewXTo', - 'skewxto' => 'swffill_skewXTo', - 'skewy' => 'swfdisplayitem_skewY', - 'skewyto' => 'swfdisplayitem_skewYTo', - 'skewyto' => 'swffill_skewYTo', - 'snmpwalkoid' => 'snmprealwalk', - 'strchr' => 'strstr', - 'streammp3' => 'swfmovie_streamMp3', - 'swfaction' => 'swfaction_init', - 'swfbitmap' => 'swfbitmap_init', - 'swfbutton' => 'swfbutton_init', - 'swffill' => 'swffill_init', - 'swffont' => 'swffont_init', - 'swfgradient' => 'swfgradient_init', - 'swfmorph' => 'swfmorph_init', - 'swfmovie' => 'swfmovie_init', - 'swfshape' => 'swfshape_init', - 'swfsprite' => 'swfsprite_init', - 'swftext' => 'swftext_init', - 'swftextfield' => 'swftextfield_init', - 'xptr_new_context' => 'xpath_new_context', - ); + 'mssql_min_server_severity' => 'sybase_min_server_severity', + 'mssql_num_fields' => 'sybase_num_fields', + 'mssql_num_rows' => 'sybase_num_rows', + 'mssql_pconnect' => 'sybase_pconnect', + 'mssql_query' => 'sybase_query', + 'mssql_result' => 'sybase_result', + 'mssql_select_db' => 'sybase_select_db', + 'mysql' => 'mysql_db_query', + 'mysql_createdb' => 'mysql_create_db', + 'mysql_db_name' => 'mysql_result', + 'mysql_dbname' => 'mysql_result', + 'mysql_dropdb' => 'mysql_drop_db', + 'mysql_fieldflags' => 'mysql_field_flags', + 'mysql_fieldlen' => 'mysql_field_len', + 'mysql_fieldname' => 'mysql_field_name', + 'mysql_fieldtable' => 'mysql_field_table', + 'mysql_fieldtype' => 'mysql_field_type', + 'mysql_freeresult' => 'mysql_free_result', + 'mysql_listdbs' => 'mysql_list_dbs', + 'mysql_listfields' => 'mysql_list_fields', + 'mysql_listtables' => 'mysql_list_tables', + 'mysql_numfields' => 'mysql_num_fields', + 'mysql_numrows' => 'mysql_num_rows', + 'mysql_selectdb' => 'mysql_select_db', + 'mysql_tablename' => 'mysql_result', + 'oci8append' => 'ocicollappend', + 'oci8assign' => 'ocicollassign', + 'oci8assignelem' => 'ocicollassignelem', + 'oci8close' => 'ocicloselob', + 'oci8free' => 'ocifreedesc', + 'oci8getelem' => 'ocicollgetelem', + 'oci8load' => 'ociloadlob', + 'oci8max' => 'ocicollmax', + 'oci8ocifreecursor' => 'ocifreestatement', + 'oci8save' => 'ocisavelob', + 'oci8savefile' => 'ocisavelobfile', + 'oci8size' => 'ocicollsize', + 'oci8trim' => 'ocicolltrim', + 'oci8writetemporary' => 'ociwritetemporarylob', + 'oci8writetofile' => 'ociwritelobtofile', + 'odbc_do' => 'odbc_exec', + 'odbc_field_precision' => 'odbc_field_len', + 'pdf_add_outline' => 'pdf_add_bookmark', + 'pg_clientencoding' => 'pg_client_encoding', + 'pg_setclientencoding' => 'pg_set_client_encoding', + 'pos' => 'current', + 'recode' => 'recode_string', + 'show_source' => 'highlight_file', + 'sizeof' => 'count', + 'snmpwalkoid' => 'snmprealwalk', + 'strchr' => 'strstr', + 'xptr_new_context' => 'xpath_new_context', + ]; + /** * Returns an array of function names this test wants to listen for. * - * @return array + * @return array */ public function registerFunctionNames() { @@ -316,39 +177,32 @@ public function registerFunctionNames() /** * Processes this function call. * - * @param PHP_CodeSniffer_File $phpcsFile - * The file being scanned. - * @param int $stackPtr - * The position of the function call in the stack. - * @param int $openBracket - * The position of the opening parenthesis in the stack. - * @param int $closeBracket - * The position of the closing parenthesis in the stack. - * @param Backdrop_Sniffs_Semantics_FunctionCallSniff $sniff - * Can be used to retreive the function's arguments with the getArgument() - * method. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the function call in + * the stack. + * @param int $openBracket The position of the opening + * parenthesis in the stack. + * @param int $closeBracket The position of the closing + * parenthesis in the stack. * * @return void */ public function processFunctionCall( - PHP_CodeSniffer_File $phpcsFile, + File $phpcsFile, $stackPtr, $openBracket, - $closeBracket, - Backdrop_Sniffs_Semantics_FunctionCallSniff $sniff + $closeBracket ) { $tokens = $phpcsFile->getTokens(); $error = '%s() is a function name alias, use %s() instead'; $name = $tokens[$stackPtr]['content']; - $data = array( - $name, - $this->aliases[$name], - ); + $data = [ + $name, + $this->aliases[$name], + ]; $phpcsFile->addError($error, $stackPtr, 'FunctionAlias', $data); }//end processFunctionCall() }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionCall.php b/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionCall.php index d16a0c2..bcc8ebe 100644 --- a/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionCall.php +++ b/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionCall.php @@ -1,14 +1,18 @@ + */ + protected $arguments; + + /** + * Whether method invocations with the same function name should be processed, + * too. + * + * @var boolean + */ + protected $includeMethodCalls = false; + /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { - // We do not listen for tokens, but for specific function calls. - Backdrop_Sniffs_Semantics_FunctionCallSniff::registerListener($this); - return array(); + return [T_STRING]; }//end register() @@ -37,45 +82,153 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. + * @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(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { - // Empty default implementation, because processFunctionCall() is used. + $tokens = $phpcsFile->getTokens(); + $functionName = $tokens[$stackPtr]['content']; + if (in_array($functionName, $this->registerFunctionNames()) === false) { + // Not interested in this function. + return; + } + + if ($this->isFunctionCall($phpcsFile, $stackPtr) === false) { + return; + } + + // Find the next non-empty token. + $openBracket = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + + $this->phpcsFile = $phpcsFile; + $this->functionCall = $stackPtr; + $this->openBracket = $openBracket; + $this->closeBracket = $tokens[$openBracket]['parenthesis_closer']; + $this->arguments = []; + + $this->processFunctionCall($phpcsFile, $stackPtr, $openBracket, $this->closeBracket); }//end process() /** - * Processes this function call. + * Checks if this is a function call. * - * @param PHP_CodeSniffer_File $phpcsFile - * The file being scanned. - * @param int $stackPtr - * The position of the function call in the stack. - * @param int $openBracket - * The position of the opening parenthesis in the stack. - * @param int $closeBracket - * The position of the closing parenthesis in the stack. - * @param Backdrop_Sniffs_Semantics_FunctionCallSniff $sniff - * Can be used to retreive the function's arguments with the getArgument() - * method. + * @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 + * @return bool */ - public abstract function processFunctionCall( - PHP_CodeSniffer_File $phpcsFile, - $stackPtr, - $openBracket, - $closeBracket, - Backdrop_Sniffs_Semantics_FunctionCallSniff $sniff - ); + protected function isFunctionCall(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + // Find the next non-empty token. + $openBracket = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true); + if ($tokens[$openBracket]['code'] !== T_OPEN_PARENTHESIS) { + // Not a function call. + return false; + } -}//end class + if (isset($tokens[$openBracket]['parenthesis_closer']) === false) { + // Not a function call. + return false; + } + + // Find the previous non-empty token. + $search = Tokens::$emptyTokens; + $search[] = T_BITWISE_AND; + $previous = $phpcsFile->findPrevious($search, ($stackPtr - 1), null, true); + if ($tokens[$previous]['code'] === T_FUNCTION) { + // It's a function definition, not a function call. + return false; + } -?> + if ($tokens[$previous]['code'] === T_OBJECT_OPERATOR && $this->includeMethodCalls === false) { + // It's a method invocation, not a function call. + return false; + } + + if ($tokens[$previous]['code'] === T_DOUBLE_COLON && $this->includeMethodCalls === false) { + // It's a static method invocation, not a function call. + return false; + } + + return true; + + }//end isFunctionCall() + + + /** + * Returns start and end token for a given argument number. + * + * @param int $number Indicates which argument should be examined, starting with + * 1 for the first argument. + * + * @return array|false + */ + public function getArgument($number) + { + // Check if we already calculated the tokens for this argument. + if (isset($this->arguments[$number]) === true) { + return $this->arguments[$number]; + } + + $tokens = $this->phpcsFile->getTokens(); + // Start token of the first argument. + $start = $this->phpcsFile->findNext(Tokens::$emptyTokens, ($this->openBracket + 1), null, true); + if ($start === $this->closeBracket) { + // Function call has no arguments, so return false. + return false; + } + + // End token of the last argument. + $end = $this->phpcsFile->findPrevious(Tokens::$emptyTokens, ($this->closeBracket - 1), null, true); + $lastArgEnd = $end; + $nextSeperator = $this->openBracket; + $counter = 1; + while (($nextSeperator = $this->phpcsFile->findNext(T_COMMA, ($nextSeperator + 1), $this->closeBracket)) !== false) { + // Make sure the comma belongs directly to this function call, + // and is not inside a nested function call or array. + $brackets = $tokens[$nextSeperator]['nested_parenthesis']; + $lastBracket = array_pop($brackets); + if ($lastBracket !== $this->closeBracket) { + continue; + } + + // Update the end token of the current argument. + $end = $this->phpcsFile->findPrevious(Tokens::$emptyTokens, ($nextSeperator - 1), null, true); + // Save the calculated findings for the current argument. + $this->arguments[$counter] = [ + 'start' => $start, + 'end' => $end, + ]; + if ($counter === $number) { + break; + } + + $counter++; + $start = $this->phpcsFile->findNext(Tokens::$emptyTokens, ($nextSeperator + 1), null, true); + $end = $lastArgEnd; + }//end while + + // If the counter did not reach the passed number something is wrong. + if ($counter !== $number) { + return false; + } + + $this->arguments[$counter] = [ + 'start' => $start, + 'end' => $end, + ]; + return $this->arguments[$counter]; + + }//end getArgument() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionCallSniff.php b/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionCallSniff.php deleted file mode 100644 index 7828c9a..0000000 --- a/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionCallSniff.php +++ /dev/null @@ -1,252 +0,0 @@ -getTokens(); - $functionName = $tokens[$stackPtr]['content']; - if (isset(self::$listeners[$functionName]) === false) { - // No listener is interested in this function name, so return early. - return; - } - - if ($this->isFunctionCall($phpcsFile, $stackPtr) === false) { - return; - } - - // Find the next non-empty token. - $openBracket = $phpcsFile->findNext(PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr + 1), null, true); - - $this->phpcsFile = $phpcsFile; - $this->functionCall = $stackPtr; - $this->openBracket = $openBracket; - $this->closeBracket = $tokens[$openBracket]['parenthesis_closer']; - $this->arguments = array(); - - foreach (self::$listeners[$functionName] as $listener) { - $listener->processFunctionCall($phpcsFile, $stackPtr, $openBracket, $this->closeBracket, $this); - } - - }//end process() - - - /** - * Checks if this is a function call. - * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. - * - * @return bool - */ - protected function isFunctionCall(PHP_CodeSniffer_File $phpcsFile, $stackPtr) - { - $tokens = $phpcsFile->getTokens(); - // Find the next non-empty token. - $openBracket = $phpcsFile->findNext(PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr + 1), null, true); - - if ($tokens[$openBracket]['code'] !== T_OPEN_PARENTHESIS) { - // Not a function call. - return false; - } - - if (isset($tokens[$openBracket]['parenthesis_closer']) === false) { - // Not a function call. - return false; - } - - // Find the previous non-empty token. - $search = PHP_CodeSniffer_Tokens::$emptyTokens; - $search[] = T_BITWISE_AND; - $previous = $phpcsFile->findPrevious($search, ($stackPtr - 1), null, true); - if ($tokens[$previous]['code'] === T_FUNCTION) { - // It's a function definition, not a function call. - return false; - } - - if ($tokens[$previous]['code'] === T_OBJECT_OPERATOR) { - // It's a method invocation, not a function call. - return false; - } - - if ($tokens[$previous]['code'] === T_DOUBLE_COLON) { - // It's a static method invocation, not a function call. - return false; - } - - return true; - - }//end isFunctionCall() - - - /** - * Returns start and end token for a given argument number. - * - * @param int $number Indicates which argument should be examined, starting with - * 1 for the first argument. - * - * @return array(string => int) - */ - public function getArgument($number) - { - // Check if we already calculated the tokens for this argument. - if (isset($this->arguments[$number]) === true) { - return $this->arguments[$number]; - } - - $tokens = $this->phpcsFile->getTokens(); - // Start token of the first argument. - $start = $this->phpcsFile->findNext(PHP_CodeSniffer_Tokens::$emptyTokens, ($this->openBracket + 1), null, true); - if ($start === $this->closeBracket) { - // Function call has no arguments, so return false. - return false; - } - - // End token of the last argument. - $end = $this->phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($this->closeBracket - 1), null, true); - $lastArgEnd = $end; - $nextSeperator = $this->openBracket; - $counter = 1; - while (($nextSeperator = $this->phpcsFile->findNext(T_COMMA, ($nextSeperator + 1), $this->closeBracket)) !== false) { - // Make sure the comma belongs directly to this function call, - // and is not inside a nested function call or array. - $brackets = $tokens[$nextSeperator]['nested_parenthesis']; - $lastBracket = array_pop($brackets); - if ($lastBracket !== $this->closeBracket) { - continue; - } - - // Update the end token of the current argument. - $end = $this->phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($nextSeperator - 1), null, true); - // Save the calculated findings for the current argument. - $this->arguments[$counter] = array( - 'start' => $start, - 'end' => $end, - ); - if ($counter === $number) { - break; - } - - $counter++; - $start = $this->phpcsFile->findNext(PHP_CodeSniffer_Tokens::$emptyTokens, ($nextSeperator + 1), null, true); - $end = $lastArgEnd; - }//end while - - // If the counter did not reach the passed number something is wrong. - if ($counter !== $number) { - return false; - } - - $this->arguments[$counter] = array( - 'start' => $start, - 'end' => $end, - ); - return $this->arguments[$counter]; - - }//end getArgument() - - - /** - * Registers a listener object so that it will be called during processing. - * - * @param Backdrop_Sniffs_Semantics_FunctionCall $listener - * The listener object that should be notified. - * - * @return void - */ - public static function registerListener($listener) - { - $funtionNames = $listener->registerFunctionNames(); - foreach ($funtionNames as $name) { - self::$listeners[$name][] = $listener; - } - - }//end registerListener() - - -}//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionDefinition.php b/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionDefinition.php index c79e8ac..4b8bc39 100644 --- a/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionDefinition.php +++ b/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionDefinition.php @@ -1,14 +1,18 @@ */ public function register() { - return array(T_STRING); + return [T_STRING]; }//end register() @@ -35,18 +39,18 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. + * @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(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); // Check if this is a function definition. $functionPtr = $phpcsFile->findPrevious( - PHP_CodeSniffer_Tokens::$emptyTokens, + Tokens::$emptyTokens, ($stackPtr - 1), null, true @@ -61,17 +65,15 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) /** * Process this function definition. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the function - * name in the stack. - * @param int $functionPtr The position of the function - * keyword in the stack. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the function name in the stack. + * name in the stack. + * @param int $functionPtr The position of the function keyword in the stack. + * keyword in the stack. * * @return void */ - public abstract function processFunction(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $functionPtr); + abstract public function processFunction(File $phpcsFile, $stackPtr, $functionPtr); }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionTSniff.php b/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionTSniff.php index a084756..d016241 100644 --- a/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionTSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionTSniff.php @@ -1,14 +1,17 @@ t() calls in Backdrop 8. + * + * @var boolean + */ + protected $includeMethodCalls = true; + /** * Returns an array of function names this test wants to listen for. * - * @return array + * @return array */ public function registerFunctionNames() { - return array('t'); + return [ + 't', + 'TranslatableMarkup', + 'TranslationWrapper', + ]; }//end registerFunctionNames() @@ -36,29 +50,24 @@ public function registerFunctionNames() /** * Processes this function call. * - * @param PHP_CodeSniffer_File $phpcsFile - * The file being scanned. - * @param int $stackPtr - * The position of the function call in the stack. - * @param int $openBracket - * The position of the opening parenthesis in the stack. - * @param int $closeBracket - * The position of the closing parenthesis in the stack. - * @param Backdrop_Sniffs_Semantics_FunctionCallSniff $sniff - * Can be used to retreive the function's arguments with the getArgument() - * method. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the function call in + * the stack. + * @param int $openBracket The position of the opening + * parenthesis in the stack. + * @param int $closeBracket The position of the closing + * parenthesis in the stack. * * @return void */ public function processFunctionCall( - PHP_CodeSniffer_File $phpcsFile, + File $phpcsFile, $stackPtr, $openBracket, - $closeBracket, - Backdrop_Sniffs_Semantics_FunctionCallSniff $sniff + $closeBracket ) { $tokens = $phpcsFile->getTokens(); - $argument = $sniff->getArgument(1); + $argument = $this->getArgument(1); if ($argument === false) { $error = 'Empty calls to t() are not allowed'; @@ -80,6 +89,18 @@ public function processFunctionCall( return; } + $concatAfter = $phpcsFile->findNext(Tokens::$emptyTokens, ($closeBracket + 1), null, true, null, true); + if ($concatAfter !== false && $tokens[$concatAfter]['code'] === T_STRING_CONCAT) { + $stringAfter = $phpcsFile->findNext(Tokens::$emptyTokens, ($concatAfter + 1), null, true, null, true); + if ($stringAfter !== false + && $tokens[$stringAfter]['code'] === T_CONSTANT_ENCAPSED_STRING + && $this->checkConcatString($tokens[$stringAfter]['content']) === false + ) { + $warning = 'Do not concatenate strings to translatable strings, they should be part of the t() argument and you should use placeholders'; + $phpcsFile->addWarning($warning, $stringAfter, 'ConcatString'); + } + } + $lastChar = substr($string, -1); if ($lastChar === '"' || $lastChar === "'") { $message = substr($string, 1, -1); @@ -97,7 +118,7 @@ public function processFunctionCall( // Check if there is a backslash escaped single quote in the string and // if the string makes use of double quotes. - if ($string{0} === "'" && strpos($string, "\'") !== false + if ($string[0] === "'" && strpos($string, "\'") !== false && strpos($string, '"') === false ) { $warn = 'Avoid backslash escaping in translatable strings when possible, use "" quotes instead'; @@ -105,7 +126,7 @@ public function processFunctionCall( return; } - if ($string{0} === '"' && strpos($string, '\"') !== false + if ($string[0] === '"' && strpos($string, '\"') !== false && strpos($string, "'") === false ) { $warn = "Avoid backslash escaping in translatable strings when possible, use '' quotes instead"; @@ -115,6 +136,46 @@ public function processFunctionCall( }//end processFunctionCall() -}//end class + /** + * Checks if a string can be concatenated with a translatable string. + * + * @param string $string The string that is concatenated to a t() call. + * + * @return bool + * TRUE if the string is allowed to be concatenated with a translatable + * string, FALSE if not. + */ + protected function checkConcatString($string) + { + // Remove outer quotes, spaces and HTML tags from the original string. + $string = trim($string, '"\''); + $string = trim(strip_tags($string)); + + if ($string === '') { + return true; + } + + $allowedItems = [ + '(', + ')', + '[', + ']', + '-', + '<', + '>', + '«', + '»', + '\n', + ]; + foreach ($allowedItems as $item) { + if ($item === $string) { + return true; + } + } + + return false; -?> + }//end checkConcatString() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionTriggerErrorSniff.php b/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionTriggerErrorSniff.php new file mode 100644 index 0000000..bb88b24 --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionTriggerErrorSniff.php @@ -0,0 +1,185 @@ + + */ + public function registerFunctionNames() + { + return ['trigger_error']; + + }//end registerFunctionNames() + + + /** + * Processes this function call. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the function call in + * the stack. + * @param int $openBracket The position of the opening + * parenthesis in the stack. + * @param int $closeBracket The position of the closing + * parenthesis in the stack. + * + * @return void + */ + public function processFunctionCall( + file $phpcsFile, + $stackPtr, + $openBracket, + $closeBracket + ) { + + $tokens = $phpcsFile->getTokens(); + + // If no second argument then quit. + if ($this->getArgument(2) === false) { + return; + } + + // Only check deprecation messages. + if (strcasecmp($tokens[$this->getArgument(2)['start']]['content'], 'E_USER_DEPRECATED') !== 0) { + return; + } + + // Get the first argument passed to trigger_error(). + $argument = $this->getArgument(1); + + // Extract the message text to check. If if it formed using sprintf() + // then find the single overall string using ->findNext. + if ($tokens[$argument['start']]['code'] === T_STRING + && strcasecmp($tokens[$argument['start']]['content'], 'sprintf') === 0 + ) { + $messagePosition = $phpcsFile->findNext(T_CONSTANT_ENCAPSED_STRING, $argument['start']); + // Remove the quotes using substr, because trim would take multiple + // quotes away and possibly not report a faulty message. + $messageText = substr($tokens[$messagePosition]['content'], 1, ($tokens[$messagePosition]['length'] - 2)); + } else { + $messageParts = []; + // If not sprintf() then extract and store all the items except + // whitespace, concatenation operators and comma. This will give all + // real content such as concatenated strings and constants. + for ($i = $argument['start']; $i <= $argument['end']; $i++) { + if (in_array($tokens[$i]['code'], [T_WHITESPACE, T_STRING_CONCAT, T_COMMA]) === false) { + // For strings, remove the quotes using substr not trim. + // Simple strings are T_CONSTANT_ENCAPSED_STRING and strings + // with variable interpolation are T_DOUBLE_QUOTED_STRING. + if ($tokens[$i]['code'] === T_CONSTANT_ENCAPSED_STRING || $tokens[$i]['code'] === T_DOUBLE_QUOTED_STRING) { + $messageParts[] = substr($tokens[$i]['content'], 1, ($tokens[$i]['length'] - 2)); + } else { + $messageParts[] = $tokens[$i]['content']; + } + } + } + + $messageText = implode(' ', $messageParts); + }//end if + + // Check if there is a @deprecated tag in an associated doc comment + // block. If the @trigger_error was level 0 (entire class or file) then + // try to find a doc comment after the trigger_error also at level 0. + // If the @trigger_error was at level > 0 it means it is inside a + // function so search backwards for the function comment block, which + // will be at one level lower. + $strictStandard = false; + $triggerErrorLevel = $tokens[$stackPtr]['level']; + if ($triggerErrorLevel === 0) { + $requiredLevel = 0; + $block = $phpcsFile->findNext(T_DOC_COMMENT_OPEN_TAG, $argument['start']); + } else { + $requiredLevel = ($triggerErrorLevel - 1); + $block = $phpcsFile->findPrevious(T_DOC_COMMENT_OPEN_TAG, $argument['start']); + } + + if (isset($tokens[$block]['level']) === true + && $tokens[$block]['level'] === $requiredLevel + && isset($tokens[$block]['comment_tags']) === true + ) { + foreach ($tokens[$block]['comment_tags'] as $tag) { + $strictStandard = $strictStandard || (strtolower($tokens[$tag]['content']) === '@deprecated'); + } + } + + // The string standard format for @trigger_error() is: + // %thing% is deprecated in %deprecation-version% and is removed in + // %removal-version%. %extra-info%. See %cr-link% + // For the 'relaxed' standard the 'and is removed in' can be replaced + // with any text. + $matches = []; + if ($strictStandard === true) { + // Use (?U) 'ungreedy' before the version so that only the text up + // to the first period followed by a space is matched, as there may + // be more than one sentence in the extra-info part. + preg_match('/(.+) is deprecated in (\S+) (and is removed from) (?U)(.+)\. (.*)\. See (\S+)$/', $messageText, $matches); + $sniff = 'TriggerErrorTextLayoutStrict'; + $error = "The trigger_error message '%s' does not match the strict standard format: %%thing%% is deprecated in %%deprecation-version%% and is removed from %%removal-version%%. %%extra-info%%. See %%cr-link%%"; + } else { + // Allow %extra-info% to be empty as this is optional in the relaxed + // version. + preg_match('/(.+) is deprecated in (\S+) (?U)(.+) (\S+)\. (.*)See (\S+)$/', $messageText, $matches); + $sniff = 'TriggerErrorTextLayoutRelaxed'; + $error = "The trigger_error message '%s' does not match the relaxed standard format: %%thing%% is deprecated in %%deprecation-version%% any free text %%removal-version%%. %%extra-info%%. See %%cr-link%%"; + } + + // There should be 7 items in $matches: 0 is full text, 1 = thing, + // 2 = deprecation-version, 3 = middle text, 4 = removal-version, + // 5 = extra-info, 6 = cr-link. + if (count($matches) !== 7) { + $phpcsFile->addError($error, $argument['start'], $sniff, [$messageText]); + } else { + // The text follows the basic layout. Now check that the version + // matches backdrop:n.n.n or project:n.x-n.n. The text must be all + // lower case and numbers can be one or two digits. + foreach (['deprecation-version' => $matches[2], 'removal-version' => $matches[4]] as $name => $version) { + if (preg_match('/^backdrop:\d{1,2}\.\d{1,2}\.\d{1,2}$/', $version) === 0 + && preg_match('/^[a-z\d_]+:\d{1,2}\.x\-\d{1,2}\.\d{1,2}$/', $version) === 0 + ) { + $error = "The %s '%s' does not match the lower-case machine-name standard: backdrop:n.n.n or project:n.x-n.n"; + $phpcsFile->addWarning($error, $argument['start'], 'TriggerErrorVersion', [$name, $version]); + } + } + + // Check the 'See' link. + $crLink = $matches[6]; + // Allow for the alternative 'node' or 'project/aaa/issues' format. + preg_match('[^http(s*)://www.backdrop.org/(node|project/\w+/issues)/(\d+)(\.*)$]', $crLink, $crMatches); + // If cr_matches[4] is not blank it means that the url is correct + // but it ends with a period. As this can be a common mistake give a + // specific message to assist in fixing. + if (isset($crMatches[4]) === true && empty($crMatches[4]) === false) { + $error = "The url '%s' should not end with a period."; + $phpcsFile->addWarning($error, $argument['start'], 'TriggerErrorPeriodAfterSeeUrl', [$crLink]); + } else if (empty($crMatches) === true) { + $error = "The url '%s' does not match the standard: http(s)://www.backdrop.org/node/n or http(s)://www.backdrop.org/project/aaa/issues/n"; + $phpcsFile->addWarning($error, $argument['start'], 'TriggerErrorSeeUrlFormat', [$crLink]); + } + }//end if + + }//end processFunctionCall() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionWatchdogSniff.php b/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionWatchdogSniff.php index 23faef5..a9f009c 100644 --- a/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionWatchdogSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Semantics/FunctionWatchdogSniff.php @@ -1,14 +1,16 @@ */ public function registerFunctionNames() { - return array('watchdog'); + return ['watchdog']; }//end registerFunctionNames() @@ -35,30 +37,25 @@ public function registerFunctionNames() /** * Processes this function call. * - * @param PHP_CodeSniffer_File $phpcsFile - * The file being scanned. - * @param int $stackPtr - * The position of the function call in the stack. - * @param int $openBracket - * The position of the opening parenthesis in the stack. - * @param int $closeBracket - * The position of the closing parenthesis in the stack. - * @param Backdrop_Sniffs_Semantics_FunctionCallSniff $sniff - * Can be used to retreive the function's arguments with the getArgument() - * method. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the function call in + * the stack. + * @param int $openBracket The position of the opening + * parenthesis in the stack. + * @param int $closeBracket The position of the closing + * parenthesis in the stack. * * @return void */ public function processFunctionCall( - PHP_CodeSniffer_File $phpcsFile, + File $phpcsFile, $stackPtr, $openBracket, - $closeBracket, - Backdrop_Sniffs_Semantics_FunctionCallSniff $sniff + $closeBracket ) { $tokens = $phpcsFile->getTokens(); // Get the second argument passed to watchdog(). - $argument = $sniff->getArgument(2); + $argument = $this->getArgument(2); if ($argument === false) { $error = 'The second argument to watchdog() is missing'; $phpcsFile->addError($error, $stackPtr, 'WatchdogArgument'); @@ -72,9 +69,13 @@ public function processFunctionCall( $phpcsFile->addError($error, $argument['start'], 'WatchdogT'); } + $concatFound = $phpcsFile->findNext(T_STRING_CONCAT, $argument['start'], $argument['end']); + if ($concatFound !== false) { + $error = 'Concatenating translatable strings is not allowed, use placeholders instead and only one string literal'; + $phpcsFile->addError($error, $concatFound, 'Concat'); + } + }//end processFunctionCall() }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Semantics/InstallHooksSniff.php b/coder_sniffer/Backdrop/Sniffs/Semantics/InstallHooksSniff.php index c7f8c09..960a4be 100644 --- a/coder_sniffer/Backdrop/Sniffs/Semantics/InstallHooksSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Semantics/InstallHooksSniff.php @@ -1,14 +1,16 @@ getFilename(), -6)); // Only check in *.module files. @@ -51,7 +53,7 @@ public function processFunction(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $fun || $tokens[$stackPtr]['content'] === ($fileName.'_disable') ) { $error = '%s() is an installation hook and must be declared in an install file'; - $data = array($tokens[$stackPtr]['content']); + $data = [$tokens[$stackPtr]['content']]; $phpcsFile->addError($error, $stackPtr, 'InstallHook', $data); } @@ -59,5 +61,3 @@ public function processFunction(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $fun }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Semantics/InstallTSniff.php b/coder_sniffer/Backdrop/Sniffs/Semantics/InstallTSniff.php deleted file mode 100644 index 6033645..0000000 --- a/coder_sniffer/Backdrop/Sniffs/Semantics/InstallTSniff.php +++ /dev/null @@ -1,84 +0,0 @@ -getFilename(), -7)); - // Only check in *.install files. - if ($fileExtension !== 'install') { - return; - } - - $fileName = substr(basename($phpcsFile->getFilename()), 0, -8); - $tokens = $phpcsFile->getTokens(); - if ($tokens[$stackPtr]['content'] !== ($fileName.'_install') - && $tokens[$stackPtr]['content'] !== ($fileName.'_requirements') - ) { - return; - } - - // Search in the function body for t() calls. - $string = $phpcsFile->findNext( - T_STRING, - $tokens[$functionPtr]['scope_opener'], - $tokens[$functionPtr]['scope_closer'] - ); - while ($string !== false) { - if ($tokens[$string]['content'] === 't' || $tokens[$string]['content'] === 'st') { - $opener = $phpcsFile->findNext( - PHP_CodeSniffer_Tokens::$emptyTokens, - ($string + 1), - null, - true - ); - if ($opener !== false - && $tokens[$opener]['code'] === T_OPEN_PARENTHESIS - ) { - $error = 'Do not use t() or st() in installation phase hooks, use $t = get_t() to retrieve the appropriate localization function name'; - $phpcsFile->addError($error, $string, 'TranslationFound'); - } - } - - $string = $phpcsFile->findNext( - T_STRING, - ($string + 1), - $tokens[$functionPtr]['scope_closer'] - ); - }//end while - - }//end processFunction() - - -}//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Semantics/LStringTranslatableSniff.php b/coder_sniffer/Backdrop/Sniffs/Semantics/LStringTranslatableSniff.php index e5db9a9..6070b12 100644 --- a/coder_sniffer/Backdrop/Sniffs/Semantics/LStringTranslatableSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Semantics/LStringTranslatableSniff.php @@ -1,14 +1,16 @@ */ public function registerFunctionNames() { - return array('l'); + return ['l']; }//end registerFunctionNames() @@ -35,33 +37,28 @@ public function registerFunctionNames() /** * Processes this function call. * - * @param PHP_CodeSniffer_File $phpcsFile - * The file being scanned. - * @param int $stackPtr - * The position of the function call in the stack. - * @param int $openBracket - * The position of the opening parenthesis in the stack. - * @param int $closeBracket - * The position of the closing parenthesis in the stack. - * @param Backdrop_Sniffs_Semantics_FunctionCallSniff $sniff - * Can be used to retreive the function's arguments with the getArgument() - * method. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the function call in + * the stack. + * @param int $openBracket The position of the opening + * parenthesis in the stack. + * @param int $closeBracket The position of the closing + * parenthesis in the stack. * * @return void */ public function processFunctionCall( - PHP_CodeSniffer_File $phpcsFile, + File $phpcsFile, $stackPtr, $openBracket, - $closeBracket, - Backdrop_Sniffs_Semantics_FunctionCallSniff $sniff + $closeBracket ) { $tokens = $phpcsFile->getTokens(); // Get the first argument passed to l(). - $argument = $sniff->getArgument(1); + $argument = $this->getArgument(1); if ($tokens[$argument['start']]['code'] === T_CONSTANT_ENCAPSED_STRING // If the string starts with a HTML tag we don't complain. - && $tokens[$argument['start']]['content']{1} !== '<' + && $tokens[$argument['start']]['content'][1] !== '<' ) { $error = 'The $text argument to l() should be enclosed within t() so that it is translatable'; $phpcsFile->addError($error, $stackPtr, 'LArg'); diff --git a/coder_sniffer/Backdrop/Sniffs/Semantics/PregSecuritySniff.php b/coder_sniffer/Backdrop/Sniffs/Semantics/PregSecuritySniff.php index fbb6153..e69950e 100644 --- a/coder_sniffer/Backdrop/Sniffs/Semantics/PregSecuritySniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Semantics/PregSecuritySniff.php @@ -1,42 +1,44 @@ */ public function registerFunctionNames() { - return array( - 'preg_filter', - 'preg_grep', - 'preg_match', - 'preg_match_all', - 'preg_replace', - 'preg_replace_callback', - 'preg_split', - ); + return [ + 'preg_filter', + 'preg_grep', + 'preg_match', + 'preg_match_all', + 'preg_replace', + 'preg_replace_callback', + 'preg_split', + ]; }//end registerFunctionNames() @@ -44,29 +46,24 @@ public function registerFunctionNames() /** * Processes this function call. * - * @param PHP_CodeSniffer_File $phpcsFile - * The file being scanned. - * @param int $stackPtr - * The position of the function call in the stack. - * @param int $openBracket - * The position of the opening parenthesis in the stack. - * @param int $closeBracket - * The position of the closing parenthesis in the stack. - * @param Backdrop_Sniffs_Semantics_FunctionCallSniff $sniff - * Can be used to retreive the function's arguments with the getArgument() - * method. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the function call in + * the stack. + * @param int $openBracket The position of the opening + * parenthesis in the stack. + * @param int $closeBracket The position of the closing + * parenthesis in the stack. * * @return void */ public function processFunctionCall( - PHP_CodeSniffer_File $phpcsFile, + File $phpcsFile, $stackPtr, $openBracket, - $closeBracket, - Backdrop_Sniffs_Semantics_FunctionCallSniff $sniff + $closeBracket ) { $tokens = $phpcsFile->getTokens(); - $argument = $sniff->getArgument(1); + $argument = $this->getArgument(1); if ($argument === false) { return; @@ -79,26 +76,25 @@ public function processFunctionCall( } $pattern = $tokens[$argument['start']]['content']; - $quote = substr($pattern, 0, 1); + $quote = substr($pattern, 0, 1); // Check that the pattern is a string. - if ($quote == '"' || $quote == "'") { + if ($quote === '"' || $quote === "'") { // Get the delimiter - first char after the enclosing quotes. $delimiter = preg_quote(substr($pattern, 1, 1), '/'); // Check if there is the evil e flag. - if (preg_match('/' . $delimiter . '[\w]{0,}e[\w]{0,}$/', substr($pattern, 0, -1))) { - $warn = 'Using the e flag in %s is a possible security risk. For details see https://www.drupal.org/node/750148'; + if (preg_match('/'.$delimiter.'[\w]{0,}e[\w]{0,}$/', substr($pattern, 0, -1)) === 1) { + $warn = 'Using the e flag in %s is a possible security risk. For details see https://www.backdrop.org/node/750148'; $phpcsFile->addError( $warn, $argument['start'], 'PregEFlag', - array($tokens[$stackPtr]['content']) + [$tokens[$stackPtr]['content']] ); return; } } + }//end processFunctionCall() }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Semantics/RemoteAddressSniff.php b/coder_sniffer/Backdrop/Sniffs/Semantics/RemoteAddressSniff.php index 719a6d2..78c8203 100644 --- a/coder_sniffer/Backdrop/Sniffs/Semantics/RemoteAddressSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Semantics/RemoteAddressSniff.php @@ -1,34 +1,37 @@ getClientIp() is used instead of * $_SERVER['REMOTE_ADDR']. * * @category PHP * @package PHP_CodeSniffer * @link http://pear.php.net/package/PHP_CodeSniffer */ -class Backdrop_Sniffs_Semantics_RemoteAddressSniff implements PHP_CodeSniffer_Sniff +class RemoteAddressSniff implements Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { - return array(T_VARIABLE); + return [T_VARIABLE]; }//end register() @@ -36,17 +39,18 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The current file being processed. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The current file being processed. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. * * @return void */ - public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { - $string = $phpcsFile->getTokensAsString($stackPtr, 4); - if ($string === '$_SERVER["REMOTE_ADDR"]' || $string === '$_SERVER[\'REMOTE_ADDR\']') { - $error = 'Use the function ip_address() instead of $_SERVER[\'REMOTE_ADDR\']'; + $string = $phpcsFile->getTokensAsString($stackPtr, 4); + $startOfStatement = $phpcsFile->findStartOfStatement($stackPtr); + if (($string === '$_SERVER["REMOTE_ADDR"]' || $string === '$_SERVER[\'REMOTE_ADDR\']') && $stackPtr !== $startOfStatement) { + $error = 'Use ip_address() or Backdrop::request()->getClientIp() instead of $_SERVER[\'REMOTE_ADDR\']'; $phpcsFile->addError($error, $stackPtr, 'RemoteAddress'); } @@ -54,5 +58,3 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Semantics/TInHookMenuSniff.php b/coder_sniffer/Backdrop/Sniffs/Semantics/TInHookMenuSniff.php index a910f0e..0eabf07 100644 --- a/coder_sniffer/Backdrop/Sniffs/Semantics/TInHookMenuSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Semantics/TInHookMenuSniff.php @@ -1,14 +1,17 @@ getFilename(), -6)); // Only check in *.module files. @@ -54,7 +57,7 @@ public function processFunction(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $fun while ($string !== false) { if ($tokens[$string]['content'] === 't') { $opener = $phpcsFile->findNext( - PHP_CodeSniffer_Tokens::$emptyTokens, + Tokens::$emptyTokens, ($string + 1), null, true @@ -78,5 +81,3 @@ public function processFunction(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $fun }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Semantics/TInHookSchemaSniff.php b/coder_sniffer/Backdrop/Sniffs/Semantics/TInHookSchemaSniff.php index 83248c1..dca7619 100644 --- a/coder_sniffer/Backdrop/Sniffs/Semantics/TInHookSchemaSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Semantics/TInHookSchemaSniff.php @@ -1,14 +1,17 @@ getFilename(), -7)); // Only check in *.install files. @@ -54,7 +57,7 @@ public function processFunction(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $fun while ($string !== false) { if ($tokens[$string]['content'] === 't') { $opener = $phpcsFile->findNext( - PHP_CodeSniffer_Tokens::$emptyTokens, + Tokens::$emptyTokens, ($string + 1), null, true @@ -78,5 +81,3 @@ public function processFunction(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $fun }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Strings/ConcatenationSpacingSniff.php b/coder_sniffer/Backdrop/Sniffs/Strings/ConcatenationSpacingSniff.php deleted file mode 100644 index 1acde79..0000000 --- a/coder_sniffer/Backdrop/Sniffs/Strings/ConcatenationSpacingSniff.php +++ /dev/null @@ -1,53 +0,0 @@ - - * @link http://pear.php.net/package/PHP_CodeSniffer - */ - -/** - * Backdrop_Sniffs_Strings_ConcatenationSpacingSniff. - * - * Makes sure there are the needed spaces between the concatenation operator (.) and - * the strings being concatenated. - * - * @category PHP - * @package PHP_CodeSniffer - * @author Peter Philipp - * @link http://pear.php.net/package/PHP_CodeSniffer - */ -class Backdrop_Sniffs_Strings_ConcatenationSpacingSniff implements PHP_CodeSniffer_Sniff -{ - - - /** - * Returns an array of tokens this test wants to listen for. - * - * @return array - */ - public function register() - { - return array(T_STRING_CONCAT); - - }//end register() - - - /** - * Processes this test, when one of its tokens is encountered. - */ - public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) - { - $tokens = $phpcsFile->getTokens(); - if ($tokens[($stackPtr - 1)]['code'] !== T_WHITESPACE || $tokens[($stackPtr + 1)]['code'] !== T_WHITESPACE) { - $message = 'Concat operator must be surrounded by spaces'; - $phpcsFile->addError($message, $stackPtr, 'Missing'); - } - } -}//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/Strings/UnnecessaryStringConcatSniff.php b/coder_sniffer/Backdrop/Sniffs/Strings/UnnecessaryStringConcatSniff.php index 374920d..e10d9c1 100644 --- a/coder_sniffer/Backdrop/Sniffs/Strings/UnnecessaryStringConcatSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/Strings/UnnecessaryStringConcatSniff.php @@ -1,46 +1,46 @@ - * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600) - * @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence - * @version CVS: $Id: UnnecessaryStringConcatSniff.php 304603 2010-10-22 03:07:04Z squiz $ + * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence * @link http://pear.php.net/package/PHP_CodeSniffer */ +namespace Backdrop\Sniffs\Strings; + +use Backdrop\Sniffs\Files\LineLengthSniff; +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Standards\Generic\Sniffs\Strings\UnnecessaryStringConcatSniff as GenericUnnecessaryStringConcatSniff; +use PHP_CodeSniffer\Util\Tokens; + /** - * Generic_Sniffs_Strings_UnnecessaryStringConcatSniff. - * - * Checks that two strings are not concatenated together; suggests - * using one string instead. + * Checks that two strings are not concatenated together; suggests using one string instead. * * @category PHP * @package PHP_CodeSniffer * @author Greg Sherwood - * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600) - * @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence - * @version Release: 1.3.1 + * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600) + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence * @link http://pear.php.net/package/PHP_CodeSniffer */ -class Backdrop_Sniffs_Strings_UnnecessaryStringConcatSniff extends Generic_Sniffs_Strings_UnnecessaryStringConcatSniff +class UnnecessaryStringConcatSniff extends GenericUnnecessaryStringConcatSniff { /** * Processes this sniff, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. + * @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(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { // Work out which type of file this is for. $tokens = $phpcsFile->getTokens(); @@ -60,7 +60,7 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) return; } - $stringTokens = PHP_CodeSniffer_Tokens::$stringTokens; + $stringTokens = Tokens::$stringTokens; if (in_array($tokens[$prev]['code'], $stringTokens) === true && in_array($tokens[$next]['code'], $stringTokens) === true ) { @@ -79,9 +79,9 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) // Before we throw an error check if the string is longer than // the line length limit. - $lineLengthLimitSniff = new Backdrop_Sniffs_Files_LineLengthSniff; + $lineLengthLimitSniff = new LineLengthSniff; - $lineLenght = $lineLengthLimitSniff->getLineLength($phpcsFile, $tokens[$prev]['line']); + $lineLenght = $lineLengthLimitSniff->getLineLength($phpcsFile, $tokens[$prev]['line']); $stringLength = ($lineLenght + strlen($tokens[$next]['content']) - 4); if ($stringLength > $lineLengthLimitSniff->lineLimit) { return; @@ -100,5 +100,3 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/WhiteSpace/CloseBracketSpacingSniff.php b/coder_sniffer/Backdrop/Sniffs/WhiteSpace/CloseBracketSpacingSniff.php index 3bd023e..8eb8a94 100644 --- a/coder_sniffer/Backdrop/Sniffs/WhiteSpace/CloseBracketSpacingSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/WhiteSpace/CloseBracketSpacingSniff.php @@ -1,47 +1,43 @@ */ public function register() { - return array( - T_CLOSE_CURLY_BRACKET, - T_CLOSE_PARENTHESIS, - ); + return [ + T_CLOSE_CURLY_BRACKET, + T_CLOSE_PARENTHESIS, + T_CLOSE_SHORT_ARRAY, + ]; }//end register() @@ -49,13 +45,13 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. + * @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(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); @@ -69,15 +65,18 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) if (isset($tokens[($stackPtr - 1)]) === true && $tokens[($stackPtr - 1)]['code'] === T_WHITESPACE ) { - $before = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr - 1), null, true); + $before = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); if ($before !== false && $tokens[$stackPtr]['line'] === $tokens[$before]['line']) { $error = 'There should be no white space before a closing "%s"'; - $phpcsFile->addError( + $fix = $phpcsFile->addFixableError( $error, ($stackPtr - 1), 'ClosingWhitespace', - array($tokens[$stackPtr]['content']) + [$tokens[$stackPtr]['content']] ); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($stackPtr - 1), ''); + } } } @@ -85,5 +84,3 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/WhiteSpace/CommaSniff.php b/coder_sniffer/Backdrop/Sniffs/WhiteSpace/CommaSniff.php new file mode 100644 index 0000000..45bb8b0 --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/WhiteSpace/CommaSniff.php @@ -0,0 +1,85 @@ + + */ + public function register() + { + return [T_COMMA]; + + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @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 + 1)]) === false) { + return; + } + + if ($tokens[($stackPtr + 1)]['code'] !== T_WHITESPACE + && $tokens[($stackPtr + 1)]['code'] !== T_COMMA + && $tokens[($stackPtr + 1)]['code'] !== T_CLOSE_PARENTHESIS + ) { + $error = 'Expected one space after the comma, 0 found'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'NoSpace'); + if ($fix === true) { + $phpcsFile->fixer->addContent($stackPtr, ' '); + } + + return; + } + + if ($tokens[($stackPtr + 1)]['code'] === T_WHITESPACE + && isset($tokens[($stackPtr + 2)]) === true + && $tokens[($stackPtr + 2)]['line'] === $tokens[($stackPtr + 1)]['line'] + && $tokens[($stackPtr + 1)]['content'] !== ' ' + ) { + $error = 'Expected one space after the comma, %s found'; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'TooManySpaces', [strlen($tokens[($stackPtr + 1)]['content'])]); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($stackPtr + 1), ' '); + } + } + + }//end process() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/WhiteSpace/EmptyLinesSniff.php b/coder_sniffer/Backdrop/Sniffs/WhiteSpace/EmptyLinesSniff.php index 428d6b6..f4d2fa2 100644 --- a/coder_sniffer/Backdrop/Sniffs/WhiteSpace/EmptyLinesSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/WhiteSpace/EmptyLinesSniff.php @@ -1,48 +1,49 @@ */ - public $supportedTokenizers = array( - 'PHP', - 'JS', - 'CSS', - ); + public $supportedTokenizers = [ + 'PHP', + 'JS', + 'CSS', + ]; /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { - return array(T_WHITESPACE); + return [T_WHITESPACE]; }//end register() @@ -50,30 +51,28 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. + * @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(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); if ($tokens[$stackPtr]['content'] === $phpcsFile->eolChar - && isset($tokens[$stackPtr + 1]) === true - && $tokens[$stackPtr + 1]['content'] === $phpcsFile->eolChar - && isset($tokens[$stackPtr + 2]) === true - && $tokens[$stackPtr + 2]['content'] === $phpcsFile->eolChar - && isset($tokens[$stackPtr + 3]) === true - && $tokens[$stackPtr + 3]['content'] === $phpcsFile->eolChar + && isset($tokens[($stackPtr + 1)]) === true + && $tokens[($stackPtr + 1)]['content'] === $phpcsFile->eolChar + && isset($tokens[($stackPtr + 2)]) === true + && $tokens[($stackPtr + 2)]['content'] === $phpcsFile->eolChar + && isset($tokens[($stackPtr + 3)]) === true + && $tokens[($stackPtr + 3)]['content'] === $phpcsFile->eolChar ) { $error = 'More than 2 empty lines are not allowed'; - $phpcsFile->addError($error, $stackPtr + 3, 'EmptyLines'); + $phpcsFile->addError($error, ($stackPtr + 3), 'EmptyLines'); } }//end process() }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/WhiteSpace/FileEndSniff.php b/coder_sniffer/Backdrop/Sniffs/WhiteSpace/FileEndSniff.php deleted file mode 100644 index 6c125f0..0000000 --- a/coder_sniffer/Backdrop/Sniffs/WhiteSpace/FileEndSniff.php +++ /dev/null @@ -1,103 +0,0 @@ -getFilename()]) === false) { - $called[$phpcsFile->getFilename()] = true; - // Retrieve the raw file content, as the tokens do not work consistently - // for different file types (CSS and javascript files have additional - // artifical tokens at the end for example). - $filename = $phpcsFile->getFilename(); - - // file_get_contents requires a file path, but some programs will pass - // in info via STDIN. Change the filename to something file_get_contents - // will understand. - if ($filename == 'STDIN') { - $filename = 'php://stdin'; - } - $content = file_get_contents($filename); - $error = false; - $lastChar = substr($content, -1); - // There must be a \n character at the end of the last token. - if ($lastChar !== $phpcsFile->eolChar) { - $error = true; - } - // There must be only one \n character at the end of the file. - else if (substr($content, -2, 1) === $phpcsFile->eolChar) { - $error = true; - } - - if ($error === true) { - $error = 'Files must end in a single new line character'; - $phpcsFile->addError($error, $phpcsFile->numTokens - 1, 'FileEnd'); - } - }//end if - - }//end process() - - -}//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/WhiteSpace/NamespaceSniff.php b/coder_sniffer/Backdrop/Sniffs/WhiteSpace/NamespaceSniff.php new file mode 100644 index 0000000..a1768cb --- /dev/null +++ b/coder_sniffer/Backdrop/Sniffs/WhiteSpace/NamespaceSniff.php @@ -0,0 +1,62 @@ + + */ + public function register() + { + return [T_NAMESPACE]; + + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @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 ($tokens[($stackPtr + 1)]['content'] !== ' ') { + $error = 'There must be exactly one space after the namepace keyword'; + $fix = $phpcsFile->addFixableError($error, ($stackPtr + 1), 'OneSpace'); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($stackPtr + 1), ' '); + } + } + + }//end process() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/WhiteSpace/ObjectOperatorIndentSniff.php b/coder_sniffer/Backdrop/Sniffs/WhiteSpace/ObjectOperatorIndentSniff.php index d013c13..86a6f07 100644 --- a/coder_sniffer/Backdrop/Sniffs/WhiteSpace/ObjectOperatorIndentSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/WhiteSpace/ObjectOperatorIndentSniff.php @@ -1,8 +1,6 @@ */ public function register() { - return array(T_OBJECT_OPERATOR); + return [T_OBJECT_OPERATOR]; }//end register() @@ -45,115 +49,114 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile All the tokens found in the document. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. + * @param \PHP_CodeSniffer\Files\File $phpcsFile All the tokens found in the document. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. * * @return void */ - public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); - // Make sure this is the first object operator in a chain of them. - $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); - if ($prev === false || $tokens[$prev]['code'] !== T_VARIABLE) { + // Check that there is only whitespace before the object operator and there + // is nothing else on the line. + if ($tokens[($stackPtr - 1)]['code'] !== T_WHITESPACE || $tokens[($stackPtr - 1)]['column'] !== 1) { return; } - // Make sure this is a chained call. - $next = $phpcsFile->findNext( - T_OBJECT_OPERATOR, - ($stackPtr + 1), - null, - false, - null, - true - ); - - if ($next === false) { - // Not a chained call. + $previousLine = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 2), null, true, null, true); + + if ($previousLine === false) { return; } - // Determine correct indent. - for ($i = ($stackPtr - 1); $i >= 0; $i--) { - if ($tokens[$i]['line'] !== $tokens[$stackPtr]['line']) { - $i++; - break; + // Check if the line before is in the same scope and go back if necessary. + $scopeDiff = [$previousLine => $previousLine]; + $startOfLine = $stackPtr; + while (empty($scopeDiff) === false) { + // Find the first non whitespace character on the previous line. + $startOfLine = $this->findStartOfline($phpcsFile, $previousLine); + $startParenthesis = []; + if (isset($tokens[$startOfLine]['nested_parenthesis']) === true) { + $startParenthesis = $tokens[$startOfLine]['nested_parenthesis']; } - } - - $requiredIndent = 0; - if ($i >= 0 && $tokens[$i]['code'] === T_WHITESPACE) { - $requiredIndent = strlen($tokens[$i]['content']); - } - $requiredIndent += 2; + $operatorParenthesis = []; + if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) { + $operatorParenthesis = $tokens[$stackPtr]['nested_parenthesis']; + } - // Determine the scope of the original object operator. - $origBrackets = null; - if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) { - $origBrackets = $tokens[$stackPtr]['nested_parenthesis']; + $scopeDiff = array_diff_assoc($startParenthesis, $operatorParenthesis); + if (empty($scopeDiff) === false) { + $previousLine = key($scopeDiff); + } } - $origConditions = null; - if (isset($tokens[$stackPtr]['conditions']) === true) { - $origConditions = $tokens[$stackPtr]['conditions']; + // Closing parenthesis can be indented in several ways, so rather use the + // line that opended the parenthesis. + if ($tokens[$startOfLine]['code'] === T_CLOSE_PARENTHESIS) { + $startOfLine = $this->findStartOfline($phpcsFile, $tokens[$startOfLine]['parenthesis_opener']); } - // Check indentation of each object operator in the chain. - while ($next !== false) { - // Make sure it is in the same scope, otherwise dont check indent. - $brackets = null; - if (isset($tokens[$next]['nested_parenthesis']) === true) { - $brackets = $tokens[$next]['nested_parenthesis']; + if ($tokens[$startOfLine]['code'] === T_OBJECT_OPERATOR) { + // If there is some wrapping in function calls then there should be an + // additional level of indentation. + if (isset($tokens[$stackPtr]['nested_parenthesis']) === true + && (empty($tokens[$startOfLine]['nested_parenthesis']) === true + || $tokens[$startOfLine]['nested_parenthesis'] !== $tokens[$stackPtr]['nested_parenthesis']) + ) { + $additionalIndent = 2; + } else { + $additionalIndent = 0; } + } else { + $additionalIndent = 2; + } - $conditions = null; - if (isset($tokens[$next]['conditions']) === true) { - $conditions = $tokens[$next]['conditions']; + if ($tokens[$stackPtr]['column'] !== ($tokens[$startOfLine]['column'] + $additionalIndent)) { + $error = 'Object operator not indented correctly; expected %s spaces but found %s'; + $expectedIndent = ($tokens[$startOfLine]['column'] + $additionalIndent - 1); + $data = [ + $expectedIndent, + ($tokens[$stackPtr]['column'] - 1), + ]; + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'Indent', $data); + + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($stackPtr - 1), str_repeat(' ', $expectedIndent)); } - - if ($origBrackets === $brackets && $origConditions === $conditions) { - // Make sure it starts a line, otherwise dont check indent. - $indent = $tokens[($next - 1)]; - if ($indent['code'] === T_WHITESPACE) { - if ($indent['line'] === $tokens[$next]['line']) { - $foundIndent = strlen($indent['content']); - } else { - $foundIndent = 0; - } - - // @todo we have not established a coding standard for this, - // disabled for now. - /*if ($foundIndent !== ($requiredIndent-2)) { - $error = "Object operator not indented correctly; expected $requiredIndent spaces but found $foundIndent"; - $phpcsFile->addError($error, $next); - }*/ - } - - // It cant be the last thing on the line either. - $content = $phpcsFile->findNext(T_WHITESPACE, ($next + 1), null, true); - if ($tokens[$content]['line'] !== $tokens[$next]['line']) { - $error = 'Object operator must be at the start of the line, not the end'; - $phpcsFile->addError($error, $next); - } - }//end if - - $next = $phpcsFile->findNext( - T_OBJECT_OPERATOR, - ($next + 1), - null, - false, - null, - true - ); - }//end while + } }//end process() -}//end class + /** + * Returns the first non whitespace token on the line. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile All the tokens found in the document. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. + * + * @return int + */ + protected function findStartOfline(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + // Find the first non whitespace character on the previous line. + $startOfLine = $stackPtr; + while ($tokens[($startOfLine - 1)]['line'] === $tokens[$startOfLine]['line']) { + $startOfLine--; + } -?> + if ($tokens[$startOfLine]['code'] === T_WHITESPACE) { + $startOfLine++; + } + + return $startOfLine; + + }//end findStartOfline() + + +}//end class diff --git a/coder_sniffer/Backdrop/Sniffs/WhiteSpace/ObjectOperatorSpacingSniff.php b/coder_sniffer/Backdrop/Sniffs/WhiteSpace/ObjectOperatorSpacingSniff.php index aff088b..112e5bf 100644 --- a/coder_sniffer/Backdrop/Sniffs/WhiteSpace/ObjectOperatorSpacingSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/WhiteSpace/ObjectOperatorSpacingSniff.php @@ -1,33 +1,41 @@ */ public function register() { - return array(T_OBJECT_OPERATOR); + return [T_OBJECT_OPERATOR]; }//end register() @@ -35,34 +43,66 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. + * @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(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); + if ($tokens[($stackPtr - 1)]['code'] !== T_WHITESPACE) { + $before = 0; + } else { + if ($tokens[($stackPtr - 2)]['line'] !== $tokens[$stackPtr]['line']) { + $before = 'newline'; + } else { + $before = $tokens[($stackPtr - 1)]['length']; + } + } - $prevToken = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, $stackPtr - 1, null, true); + if ($tokens[($stackPtr + 1)]['code'] !== T_WHITESPACE) { + $after = 0; + } else { + if ($tokens[($stackPtr + 2)]['line'] !== $tokens[$stackPtr]['line']) { + $after = 'newline'; + } else { + $after = $tokens[($stackPtr + 1)]['length']; + } + } + + $phpcsFile->recordMetric($stackPtr, 'Spacing before object operator', $before); + $phpcsFile->recordMetric($stackPtr, 'Spacing after object operator', $after); + + $prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true); // Line breaks are allowed before an object operator. - if ($tokens[$stackPtr]['line'] === $tokens[$prevToken]['line'] - && $prevToken < ($stackPtr - 1) - ) { + if ($before !== 0 && $tokens[$stackPtr]['line'] === $tokens[$prevToken]['line']) { $error = 'Space found before object operator'; - $phpcsFile->addError($error, $stackPtr, 'Before'); + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'Before'); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($stackPtr - 1), ''); + } } - $nextType = $tokens[($stackPtr + 1)]['code']; - if (in_array($nextType, PHP_CodeSniffer_Tokens::$emptyTokens) === true) { + if ($after !== 0) { $error = 'Space found after object operator'; - $phpcsFile->addError($error, $stackPtr, 'After'); + $fix = $phpcsFile->addFixableError($error, $stackPtr, 'After'); + if ($fix === true) { + if ($after === 'newline') { + // Delete the operator on this line and insert it before the + // token on the next line. + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken($stackPtr, ''); + $phpcsFile->fixer->addContentBefore(($stackPtr + 2), '->'); + $phpcsFile->fixer->endChangeset(); + } else { + $phpcsFile->fixer->replaceToken(($stackPtr + 1), ''); + } + } } }//end process() }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/WhiteSpace/OpenBracketSpacingSniff.php b/coder_sniffer/Backdrop/Sniffs/WhiteSpace/OpenBracketSpacingSniff.php index 4b06d5c..58d0dcb 100644 --- a/coder_sniffer/Backdrop/Sniffs/WhiteSpace/OpenBracketSpacingSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/WhiteSpace/OpenBracketSpacingSniff.php @@ -1,47 +1,42 @@ */ public function register() { - return array( - T_OPEN_CURLY_BRACKET, - T_OPEN_PARENTHESIS, - ); + return [ + T_OPEN_CURLY_BRACKET, + T_OPEN_PARENTHESIS, + T_OPEN_SHORT_ARRAY, + ]; }//end register() @@ -49,13 +44,13 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. + * @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(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); @@ -69,19 +64,23 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) if (isset($tokens[($stackPtr + 1)]) === true && $tokens[($stackPtr + 1)]['code'] === T_WHITESPACE && strpos($tokens[($stackPtr + 1)]['content'], $phpcsFile->eolChar) === false + // Allow spaces in template files where the PHP close tag is used. + && isset($tokens[($stackPtr + 2)]) === true + && $tokens[($stackPtr + 2)]['code'] !== T_CLOSE_TAG ) { $error = 'There should be no white space after an opening "%s"'; - $phpcsFile->addError( + $fix = $phpcsFile->addFixableError( $error, ($stackPtr + 1), 'OpeningWhitespace', - array($tokens[$stackPtr]['content']) + [$tokens[$stackPtr]['content']] ); + if ($fix === true) { + $phpcsFile->fixer->replaceToken(($stackPtr + 1), ''); + } } }//end process() }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/WhiteSpace/OperatorSpacingSniff.php b/coder_sniffer/Backdrop/Sniffs/WhiteSpace/OperatorSpacingSniff.php deleted file mode 100644 index a7bd86b..0000000 --- a/coder_sniffer/Backdrop/Sniffs/WhiteSpace/OperatorSpacingSniff.php +++ /dev/null @@ -1,45 +0,0 @@ - diff --git a/coder_sniffer/Backdrop/Sniffs/WhiteSpace/ScopeClosingBraceSniff.php b/coder_sniffer/Backdrop/Sniffs/WhiteSpace/ScopeClosingBraceSniff.php index cbf22e9..920e7f6 100644 --- a/coder_sniffer/Backdrop/Sniffs/WhiteSpace/ScopeClosingBraceSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/WhiteSpace/ScopeClosingBraceSniff.php @@ -1,16 +1,20 @@ */ public function register() { - return PHP_CodeSniffer_Tokens::$scopeOpeners; + return Tokens::$scopeOpeners; }//end register() @@ -45,13 +49,13 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile All the tokens found in the document. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. + * @param \PHP_CodeSniffer\Files\File $phpcsFile All the tokens found in the document. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. * * @return void */ - public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); @@ -61,13 +65,15 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) return; } - $scopeStart = $tokens[$stackPtr]['scope_opener']; - $scopeEnd = $tokens[$stackPtr]['scope_closer']; + $scopeStart = $tokens[$stackPtr]['scope_opener']; + $scopeEnd = $tokens[$stackPtr]['scope_closer']; // If the scope closer doesn't think it belongs to this scope opener - // then the opener is sharing its closer ith other tokens. We only + // then the opener is sharing its closer with other tokens. We only // want to process the closer once, so skip this one. - if ($tokens[$scopeEnd]['scope_condition'] !== $stackPtr) { + if (isset($tokens[$scopeEnd]['scope_condition']) === false + || $tokens[$scopeEnd]['scope_condition'] !== $stackPtr + ) { return; } @@ -82,20 +88,27 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) } } - // We found a new line, now go forward and find the first non-whitespace - // token. - $lineStart= $phpcsFile->findNext( - array(T_WHITESPACE), - ($lineStart + 1), - null, - true - ); - - $startColumn = $tokens[$lineStart]['column']; + $lineStart++; + + $startColumn = 1; + if ($tokens[$lineStart]['code'] === T_WHITESPACE) { + $startColumn = $tokens[($lineStart + 1)]['column']; + } else if ($tokens[$lineStart]['code'] === T_INLINE_HTML) { + $trimmed = ltrim($tokens[$lineStart]['content']); + if ($trimmed === '') { + $startColumn = $tokens[($lineStart + 1)]['column']; + } else { + $startColumn = (strlen($tokens[$lineStart]['content']) - strlen($trimmed)); + } + } // Check that the closing brace is on it's own line. $lastContent = $phpcsFile->findPrevious( - array(T_WHITESPACE), + [ + T_WHITESPACE, + T_INLINE_HTML, + T_OPEN_TAG, + ], ($scopeEnd - 1), $scopeStart, true @@ -104,36 +117,77 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) if ($tokens[$lastContent]['line'] === $tokens[$scopeEnd]['line']) { // Only allow empty classes and methods. if (($tokens[$tokens[$scopeEnd]['scope_condition']]['code'] !== T_CLASS - && !in_array(T_CLASS, $tokens[$scopeEnd]['conditions'])) - || $tokens[$lastContent]['code'] !== T_OPEN_CURLY_BRACKET) - { + && $tokens[$tokens[$scopeEnd]['scope_condition']]['code'] !== T_INTERFACE + && in_array(T_CLASS, $tokens[$scopeEnd]['conditions']) === false + && in_array(T_INTERFACE, $tokens[$scopeEnd]['conditions']) === false) + || $tokens[$lastContent]['code'] !== T_OPEN_CURLY_BRACKET + ) { $error = 'Closing brace must be on a line by itself'; - $phpcsFile->addError($error, $scopeEnd); + $fix = $phpcsFile->addFixableError($error, $scopeEnd, 'Line'); + if ($fix === true) { + $phpcsFile->fixer->addNewlineBefore($scopeEnd); + } } + return; } // Check now that the closing brace is lined up correctly. - $braceIndent = $tokens[$scopeEnd]['column']; - if (in_array($tokens[$stackPtr]['code'], array(T_CASE, T_DEFAULT)) === true) { + $lineStart = ($scopeEnd - 1); + for ($lineStart; $lineStart > 0; $lineStart--) { + if (strpos($tokens[$lineStart]['content'], $phpcsFile->eolChar) !== false) { + break; + } + } + + $lineStart++; + + $braceIndent = 0; + if ($tokens[$lineStart]['code'] === T_WHITESPACE) { + $braceIndent = ($tokens[($lineStart + 1)]['column'] - 1); + } else if ($tokens[$lineStart]['code'] === T_INLINE_HTML) { + $trimmed = ltrim($tokens[$lineStart]['content']); + if ($trimmed === '') { + $braceIndent = ($tokens[($lineStart + 1)]['column'] - 1); + } else { + $braceIndent = (strlen($tokens[$lineStart]['content']) - strlen($trimmed) - 1); + } + } + + $fix = false; + if ($tokens[$stackPtr]['code'] === T_CASE + || $tokens[$stackPtr]['code'] === T_DEFAULT + ) { // BREAK statements should be indented n spaces from the // CASE or DEFAULT statement. - if ($braceIndent !== ($startColumn + $this->indent)) { + $expectedIndent = ($startColumn + $this->indent - 1); + if ($braceIndent !== $expectedIndent) { $error = 'Case breaking statement indented incorrectly; expected %s spaces, found %s'; - $data = array( - ($startColumn + $this->indent - 1), - ($braceIndent - 1), - ); - $phpcsFile->addError($error, $scopeEnd, 'BreakIdent', $data); + $data = [ + $expectedIndent, + $braceIndent, + ]; + $fix = $phpcsFile->addFixableError($error, $scopeEnd, 'BreakIndent', $data); } } else { - if ($braceIndent !== $startColumn) { + $expectedIndent = ($startColumn - 1); + if ($braceIndent !== $expectedIndent) { $error = 'Closing brace indented incorrectly; expected %s spaces, found %s'; - $data = array( - ($startColumn - 1), - ($braceIndent - 1), - ); - $phpcsFile->addError($error, $scopeEnd, 'Indent', $data); + $data = [ + $expectedIndent, + $braceIndent, + ]; + $fix = $phpcsFile->addFixableError($error, $scopeEnd, 'Indent', $data); + } + }//end if + + if ($fix === true) { + $spaces = str_repeat(' ', $expectedIndent); + if ($braceIndent === 0) { + $phpcsFile->fixer->addContentBefore($lineStart, $spaces); + } else { + $phpcsFile->fixer->replaceToken($lineStart, ltrim($tokens[$lineStart]['content'])); + $phpcsFile->fixer->addContentBefore($lineStart, $spaces); } } @@ -141,5 +195,3 @@ public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) }//end class - -?> diff --git a/coder_sniffer/Backdrop/Sniffs/WhiteSpace/ScopeIndentSniff.php b/coder_sniffer/Backdrop/Sniffs/WhiteSpace/ScopeIndentSniff.php index 247b979..38e01bd 100644 --- a/coder_sniffer/Backdrop/Sniffs/WhiteSpace/ScopeIndentSniff.php +++ b/coder_sniffer/Backdrop/Sniffs/WhiteSpace/ScopeIndentSniff.php @@ -1,17 +1,24 @@ + */ + public $ignoreIndentationTokens = []; + + /** + * List of tokens not needing to be checked for indentation. + * + * This is a cached copy of the public version of this var, which + * can be set in a ruleset file, and some core ignored tokens. + * + * @var array + */ + private $ignoreIndentation = []; + /** * Any scope openers that should not cause an indent. * - * @var array(int) + * @var int[] */ - protected $nonIndentingScopes = array(); + protected $nonIndentingScopes = []; + + /** + * Show debug output for this sniff. + * + * @var boolean + */ + private $debug = false; /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { - return PHP_CodeSniffer_Tokens::$scopeOpeners; + if (defined('PHP_CODESNIFFER_IN_TESTS') === true) { + $this->debug = false; + } + + return [T_OPEN_TAG]; }//end register() @@ -64,309 +122,1437 @@ public function register() /** * Processes this test, when one of its tokens is encountered. * - * @param PHP_CodeSniffer_File $phpcsFile All the tokens found in the document. - * @param int $stackPtr The position of the current token - * in the stack passed in $tokens. + * @param \PHP_CodeSniffer\Files\File $phpcsFile All the tokens found in the document. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. * - * @return void + * @return void|int */ - public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) + public function process(File $phpcsFile, $stackPtr) { - $tokens = $phpcsFile->getTokens(); - - // If this is an inline condition (ie. there is no scope opener), then - // return, as this is not a new scope. - if (isset($tokens[$stackPtr]['scope_opener']) === false) { - return; + $debug = Config::getConfigData('scope_indent_debug'); + if ($debug !== null) { + $this->debug = (bool) $debug; } - if ($tokens[$stackPtr]['code'] === T_ELSE) { - $next = $phpcsFile->findNext( - PHP_CodeSniffer_Tokens::$emptyTokens, - ($stackPtr + 1), - null, - true - ); - - // We will handle the T_IF token in another call to process. - if ($tokens[$next]['code'] === T_IF) { - return; + if ($this->tabWidth === null) { + if (isset($phpcsFile->config->tabWidth) === false || $phpcsFile->config->tabWidth === 0) { + // We have no idea how wide tabs are, so assume 4 spaces for fixing. + // It shouldn't really matter because indent checks elsewhere in the + // standard should fix things up. + $this->tabWidth = 4; + } else { + $this->tabWidth = $phpcsFile->config->tabWidth; } } - // Find the first token on this line. - $firstToken = $stackPtr; - for ($i = $stackPtr; $i >= 0; $i--) { - // Record the first code token on the line. - if (in_array($tokens[$i]['code'], PHP_CodeSniffer_Tokens::$emptyTokens) === false) { - $firstToken = $i; - } + $lastOpenTag = $stackPtr; + $lastCloseTag = null; + $openScopes = []; + $adjustments = []; + $setIndents = []; + $disableExactEnd = 0; + $tokenIndent = 0; - // It's the start of the line, so we've found our first php token. - if ($tokens[$i]['column'] === 1) { - break; - } + $tokens = $phpcsFile->getTokens(); + $first = $phpcsFile->findFirstOnLine(T_INLINE_HTML, $stackPtr); + $trimmed = ltrim($tokens[$first]['content']); + if ($trimmed === '') { + $currentIndent = ($tokens[$stackPtr]['column'] - 1); + } else { + $currentIndent = (strlen($tokens[$first]['content']) - strlen($trimmed)); } - // Based on the conditions that surround this token, determine the - // indent that we expect this current content to be. - $expectedIndent = $this->calculateExpectedIndent($tokens, $firstToken); - - // Don't process the first token if it is a closure because they have - // different indentation rules as they are often used as function arguments - // for multi-line function calls. But continue to process the content of the - // closure because it should be indented as normal. - if ($tokens[$firstToken]['code'] !== T_CLOSURE - && $tokens[$firstToken]['column'] !== $expectedIndent - ) { - $error = 'Line indented incorrectly; expected %s spaces, found %s'; - $data = array( - ($expectedIndent - 1), - ($tokens[$firstToken]['column'] - 1), - ); - $phpcsFile->addError($error, $stackPtr, 'Incorrect', $data); + if ($this->debug === true) { + $line = $tokens[$stackPtr]['line']; + echo "Start with token $stackPtr on line $line with indent $currentIndent".PHP_EOL; } - $scopeOpener = $tokens[$stackPtr]['scope_opener']; - $scopeCloser = $tokens[$stackPtr]['scope_closer']; + if (empty($this->ignoreIndentation) === true) { + $this->ignoreIndentation = [T_INLINE_HTML => true]; + foreach ($this->ignoreIndentationTokens as $token) { + if (is_int($token) === false) { + if (defined($token) === false) { + continue; + } - // Some scopes are expected not to have indents. - if (in_array($tokens[$firstToken]['code'], $this->nonIndentingScopes) === false) { - $indent = ($expectedIndent + $this->indent); - } else { - $indent = $expectedIndent; - } + $token = constant($token); + } - $newline = false; - $commentOpen = false; - $inHereDoc = false; + $this->ignoreIndentation[$token] = true; + } + }//end if - // Only loop over the content between the opening and closing brace, not - // the braces themselves. - for ($i = ($scopeOpener + 1); $i < $scopeCloser; $i++) { + $this->exact = (bool) $this->exact; + $this->tabIndent = (bool) $this->tabIndent; - // If this token is another scope, skip it as it will be handled by - // another call to this sniff. - if (in_array($tokens[$i]['code'], PHP_CodeSniffer_Tokens::$scopeOpeners) === true) { - if (isset($tokens[$i]['scope_opener']) === true) { - $i = $tokens[$i]['scope_closer']; + $checkAnnotations = $phpcsFile->config->annotations; - // If the scope closer is followed by a semi-colon, the semi-colon is part - // of the closer and should also be ignored. This most commonly happens with - // CASE statements that end with "break;", where we don't want to stop - // ignoring at the break, but rather at the semi-colon. - $nextToken = $phpcsFile->findNext(PHP_CodeSniffer_Tokens::$emptyTokens, ($i + 1), null, true); - if ($tokens[$nextToken]['code'] === T_SEMICOLON) { - $i = $nextToken; - } + for ($i = ($stackPtr + 1); $i < $phpcsFile->numTokens; $i++) { + if ($checkAnnotations === true + && $tokens[$i]['code'] === T_PHPCS_SET + && isset($tokens[$i]['sniffCode']) === true + && $tokens[$i]['sniffCode'] === 'Generic.WhiteSpace.ScopeIndent' + && $tokens[$i]['sniffProperty'] === 'exact' + ) { + $value = $tokens[$i]['sniffPropertyValue']; + if ($value === 'true') { + $value = true; + } else if ($value === 'false') { + $value = false; } else { - // If this token does not have a scope_opener indice, then - // it's probably an inline scope, so let's skip to the next - // semicolon. Inline scopes include inline if's, abstract - // methods etc. - $nextToken = $phpcsFile->findNext(T_SEMICOLON, $i, $scopeCloser); - if ($nextToken !== false) { - $i = $nextToken; - } + $value = (bool) $value; } - continue; + $this->exact = $value; + + if ($this->debug === true) { + $line = $tokens[$i]['line']; + if ($this->exact === true) { + $value = 'true'; + } else { + $value = 'false'; + } + + echo "* token $i on line $line set exact flag to $value *".PHP_EOL; + } }//end if - // If this is a HEREDOC then we need to ignore it as the - // whitespace before the contents within the HEREDOC are - // considered part of the content. - if ($tokens[$i]['code'] === T_START_HEREDOC - || $tokens[$i]['code'] === T_START_NOWDOC + $checkToken = null; + $checkIndent = null; + + /* + Don't check indents exactly between parenthesis or arrays as they + tend to have custom rules, such as with multi-line function calls + and control structure conditions. + */ + + $exact = $this->exact; + + if ($tokens[$i]['code'] === T_OPEN_PARENTHESIS + && isset($tokens[$i]['parenthesis_closer']) === true ) { - $inHereDoc = true; - continue; - } else if ($inHereDoc === true) { - if ($tokens[$i]['code'] === T_END_HEREDOC - || $tokens[$i]['code'] === T_END_NOWDOC - ) { - $inHereDoc = false; + $disableExactEnd = max($disableExactEnd, $tokens[$i]['parenthesis_closer']); + if ($this->debug === true) { + $line = $tokens[$i]['line']; + $type = $tokens[$disableExactEnd]['type']; + echo "Opening parenthesis found on line $line".PHP_EOL; + echo "\t=> disabling exact indent checking until $disableExactEnd ($type)".PHP_EOL; } + } - continue; + if ($exact === true && $i < $disableExactEnd) { + $exact = false; } + // Detect line changes and figure out where the indent is. if ($tokens[$i]['column'] === 1) { - // We started a newline. - $newline = true; + $trimmed = ltrim($tokens[$i]['content']); + if ($trimmed === '') { + if (isset($tokens[($i + 1)]) === true + && $tokens[$i]['line'] === $tokens[($i + 1)]['line'] + ) { + $checkToken = ($i + 1); + $tokenIndent = ($tokens[($i + 1)]['column'] - 1); + } + } else { + $checkToken = $i; + $tokenIndent = (strlen($tokens[$i]['content']) - strlen($trimmed)); + } } - if ($newline === true && $tokens[$i]['code'] !== T_WHITESPACE) { - // If we started a newline and we find a token that is not - // whitespace, then this must be the first token on the line that - // must be indented. - $newline = false; - $firstToken = $i; + // Closing parenthesis should just be indented to at least + // the same level as where they were opened (but can be more). + if (($checkToken !== null + && $tokens[$checkToken]['code'] === T_CLOSE_PARENTHESIS + && isset($tokens[$checkToken]['parenthesis_opener']) === true) + || ($tokens[$i]['code'] === T_CLOSE_PARENTHESIS + && isset($tokens[$i]['parenthesis_opener']) === true) + ) { + if ($checkToken !== null) { + $parenCloser = $checkToken; + } else { + $parenCloser = $i; + } - $column = $tokens[$firstToken]['column']; + if ($this->debug === true) { + $line = $tokens[$i]['line']; + echo "Closing parenthesis found on line $line".PHP_EOL; + } - // Special case for non-PHP code. - if ($tokens[$firstToken]['code'] === T_INLINE_HTML) { - $trimmedContentLength - = strlen(ltrim($tokens[$firstToken]['content'])); - if ($trimmedContentLength === 0) { - continue; + $parenOpener = $tokens[$parenCloser]['parenthesis_opener']; + if ($tokens[$parenCloser]['line'] !== $tokens[$parenOpener]['line']) { + $parens = 0; + if (isset($tokens[$parenCloser]['nested_parenthesis']) === true + && empty($tokens[$parenCloser]['nested_parenthesis']) === false + ) { + $parens = $tokens[$parenCloser]['nested_parenthesis']; + end($parens); + $parens = key($parens); + if ($this->debug === true) { + $line = $tokens[$parens]['line']; + echo "\t* token has nested parenthesis $parens on line $line *".PHP_EOL; + } } - $contentLength = strlen($tokens[$firstToken]['content']); - $column = ($contentLength - $trimmedContentLength + 1); - } + $condition = 0; + if (isset($tokens[$parenCloser]['conditions']) === true + && empty($tokens[$parenCloser]['conditions']) === false + && (isset($tokens[$parenCloser]['parenthesis_owner']) === false + || $parens > 0) + ) { + $condition = $tokens[$parenCloser]['conditions']; + end($condition); + $condition = key($condition); + if ($this->debug === true) { + $line = $tokens[$condition]['line']; + $type = $tokens[$condition]['type']; + echo "\t* token is inside condition $condition ($type) on line $line *".PHP_EOL; + } + } - // Check to see if this constant string spans multiple lines. - // If so, then make sure that the strings on lines other than the - // first line are indented appropriately, based on their whitespace. - if (in_array($tokens[$firstToken]['code'], PHP_CodeSniffer_Tokens::$stringTokens) === true) { - if (in_array($tokens[($firstToken - 1)]['code'], PHP_CodeSniffer_Tokens::$stringTokens) === true) { - // If we find a string that directly follows another string - // then its just a string that spans multiple lines, so we - // don't need to check for indenting. - continue; + if ($parens > $condition) { + if ($this->debug === true) { + echo "\t* using parenthesis *".PHP_EOL; + } + + $parenOpener = $parens; + $condition = 0; + } else if ($condition > 0) { + if ($this->debug === true) { + echo "\t* using condition *".PHP_EOL; + } + + $parenOpener = $condition; + $parens = 0; } + + $exact = false; + + $lastOpenTagConditions = array_keys($tokens[$lastOpenTag]['conditions']); + $lastOpenTagCondition = array_pop($lastOpenTagConditions); + + if ($condition > 0 && $lastOpenTagCondition === $condition) { + if ($this->debug === true) { + echo "\t* open tag is inside condition; using open tag *".PHP_EOL; + } + + $checkIndent = ($tokens[$lastOpenTag]['column'] - 1); + if (isset($adjustments[$condition]) === true) { + $checkIndent += $adjustments[$condition]; + } + + $currentIndent = $checkIndent; + + if ($this->debug === true) { + $type = $tokens[$lastOpenTag]['type']; + echo "\t=> checking indent of $checkIndent; main indent set to $currentIndent by token $lastOpenTag ($type)".PHP_EOL; + } + } else if ($condition > 0 + && isset($tokens[$condition]['scope_opener']) === true + && isset($setIndents[$tokens[$condition]['scope_opener']]) === true + ) { + $checkIndent = $setIndents[$tokens[$condition]['scope_opener']]; + if (isset($adjustments[$condition]) === true) { + $checkIndent += $adjustments[$condition]; + } + + $currentIndent = $checkIndent; + + if ($this->debug === true) { + $type = $tokens[$condition]['type']; + echo "\t=> checking indent of $checkIndent; main indent set to $currentIndent by token $condition ($type)".PHP_EOL; + } + } else { + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $parenOpener, true); + + $checkIndent = ($tokens[$first]['column'] - 1); + if (isset($adjustments[$first]) === true) { + $checkIndent += $adjustments[$first]; + } + + if ($this->debug === true) { + $line = $tokens[$first]['line']; + $type = $tokens[$first]['type']; + echo "\t* first token on line $line is $first ($type) *".PHP_EOL; + } + + if ($first === $tokens[$parenCloser]['parenthesis_opener'] + && $tokens[($first - 1)]['line'] === $tokens[$first]['line'] + ) { + // This is unlikely to be the start of the statement, so look + // back further to find it. + $first--; + if ($this->debug === true) { + $line = $tokens[$first]['line']; + $type = $tokens[$first]['type']; + echo "\t* first token is the parenthesis opener *".PHP_EOL; + echo "\t* amended first token is $first ($type) on line $line *".PHP_EOL; + } + } + + $prev = $phpcsFile->findStartOfStatement($first, T_COMMA); + if ($prev !== $first) { + // This is not the start of the statement. + if ($this->debug === true) { + $line = $tokens[$prev]['line']; + $type = $tokens[$prev]['type']; + echo "\t* previous is $type on line $line *".PHP_EOL; + } + + $first = $phpcsFile->findFirstOnLine([T_WHITESPACE, T_INLINE_HTML], $prev, true); + if ($first !== false) { + $prev = $phpcsFile->findStartOfStatement($first, T_COMMA); + $first = $phpcsFile->findFirstOnLine([T_WHITESPACE, T_INLINE_HTML], $prev, true); + } else { + $first = $prev; + } + + if ($this->debug === true) { + $line = $tokens[$first]['line']; + $type = $tokens[$first]['type']; + echo "\t* amended first token is $first ($type) on line $line *".PHP_EOL; + } + }//end if + + if (isset($tokens[$first]['scope_closer']) === true + && $tokens[$first]['scope_closer'] === $first + ) { + if ($this->debug === true) { + echo "\t* first token is a scope closer *".PHP_EOL; + } + + if (isset($tokens[$first]['scope_condition']) === true) { + $scopeCloser = $first; + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $tokens[$scopeCloser]['scope_condition'], true); + + $currentIndent = ($tokens[$first]['column'] - 1); + if (isset($adjustments[$first]) === true) { + $currentIndent += $adjustments[$first]; + } + + // Make sure it is divisible by our expected indent. + if ($tokens[$tokens[$scopeCloser]['scope_condition']]['code'] !== T_CLOSURE) { + $currentIndent = (int) (ceil($currentIndent / $this->indent) * $this->indent); + } + + $setIndents[$first] = $currentIndent; + + if ($this->debug === true) { + $type = $tokens[$first]['type']; + echo "\t=> indent set to $currentIndent by token $first ($type)".PHP_EOL; + } + }//end if + } else { + // Don't force current indent to be divisible because there could be custom + // rules in place between parenthesis, such as with arrays. + $currentIndent = ($tokens[$first]['column'] - 1); + if (isset($adjustments[$first]) === true) { + $currentIndent += $adjustments[$first]; + } + + $setIndents[$first] = $currentIndent; + + if ($this->debug === true) { + $type = $tokens[$first]['type']; + echo "\t=> checking indent of $checkIndent; main indent set to $currentIndent by token $first ($type)".PHP_EOL; + } + }//end if + }//end if + } else if ($this->debug === true) { + echo "\t * ignoring single-line definition *".PHP_EOL; + }//end if + }//end if + + // Closing short array bracket should just be indented to at least + // the same level as where it was opened (but can be more). + if ($tokens[$i]['code'] === T_CLOSE_SHORT_ARRAY + || ($checkToken !== null + && $tokens[$checkToken]['code'] === T_CLOSE_SHORT_ARRAY) + ) { + if ($checkToken !== null) { + $arrayCloser = $checkToken; + } else { + $arrayCloser = $i; } - // This is a special condition for T_DOC_COMMENT and C-style - // comments, which contain whitespace between each line. - if (in_array($tokens[$firstToken]['code'], PHP_CodeSniffer_Tokens::$commentTokens) === true) { - $content = trim($tokens[$firstToken]['content']); - if (preg_match('|^/\*|', $content) !== 0) { - // Check to see if the end of the comment is on the same line - // as the start of the comment. If it is, then we don't - // have to worry about opening a comment. - if (preg_match('|\*/$|', $content) === 0) { - // We don't have to calculate the column for the - // start of the comment as there is a whitespace - // token before it. - $commentOpen = true; + if ($this->debug === true) { + $line = $tokens[$arrayCloser]['line']; + echo "Closing short array bracket found on line $line".PHP_EOL; + } + + $arrayOpener = $tokens[$arrayCloser]['bracket_opener']; + if ($tokens[$arrayCloser]['line'] !== $tokens[$arrayOpener]['line']) { + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $arrayOpener, true); + $checkIndent = ($tokens[$first]['column'] - 1); + if (isset($adjustments[$first]) === true) { + $checkIndent += $adjustments[$first]; + } + + $exact = false; + + if ($this->debug === true) { + $line = $tokens[$first]['line']; + $type = $tokens[$first]['type']; + echo "\t* first token on line $line is $first ($type) *".PHP_EOL; + } + + if ($first === $tokens[$arrayCloser]['bracket_opener']) { + // This is unlikely to be the start of the statement, so look + // back further to find it. + $first--; + } + + $prev = $phpcsFile->findStartOfStatement($first, [T_COMMA, T_DOUBLE_ARROW]); + if ($prev !== $first) { + // This is not the start of the statement. + if ($this->debug === true) { + $line = $tokens[$prev]['line']; + $type = $tokens[$prev]['type']; + echo "\t* previous is $type on line $line *".PHP_EOL; } - } else if ($commentOpen === true) { - if ($content === '') { - // We are in a comment, but this line has nothing on it - // so let's skip it. - continue; + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $prev, true); + $prev = $phpcsFile->findStartOfStatement($first, [T_COMMA, T_DOUBLE_ARROW]); + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $prev, true); + if ($this->debug === true) { + $line = $tokens[$first]['line']; + $type = $tokens[$first]['type']; + echo "\t* amended first token is $first ($type) on line $line *".PHP_EOL; } + } else if ($tokens[$first]['code'] === T_WHITESPACE) { + $first = $phpcsFile->findNext(T_WHITESPACE, ($first + 1), null, true); + } - $contentLength = strlen($tokens[$firstToken]['content']); - $trimmedContentLength - = strlen(ltrim($tokens[$firstToken]['content'])); + if (isset($tokens[$first]['scope_closer']) === true + && $tokens[$first]['scope_closer'] === $first + ) { + // The first token is a scope closer and would have already + // been processed and set the indent level correctly, so + // don't adjust it again. + if ($this->debug === true) { + echo "\t* first token is a scope closer; ignoring closing short array bracket *".PHP_EOL; + } - $column = ($contentLength - $trimmedContentLength + 1); - if (preg_match('|\*/$|', $content) !== 0) { - $commentOpen = false; + if (isset($setIndents[$first]) === true) { + $currentIndent = $setIndents[$first]; + if ($this->debug === true) { + echo "\t=> indent reset to $currentIndent".PHP_EOL; + } + } + } else { + // Don't force current indent to be divisible because there could be custom + // rules in place for arrays. + $currentIndent = ($tokens[$first]['column'] - 1); + if (isset($adjustments[$first]) === true) { + $currentIndent += $adjustments[$first]; } - // Comments starting with a star have an extra whitespace. - if ($content{0} === '*' && $column > $indent) { - continue; + $setIndents[$first] = $currentIndent; + + if ($this->debug === true) { + $type = $tokens[$first]['type']; + echo "\t=> checking indent of $checkIndent; main indent set to $currentIndent by token $first ($type)".PHP_EOL; } }//end if + } else if ($this->debug === true) { + echo "\t * ignoring single-line definition *".PHP_EOL; + }//end if + }//end if + + // Adjust lines within scopes while auto-fixing. + if ($checkToken !== null + && $exact === false + && (empty($tokens[$checkToken]['conditions']) === false + || (isset($tokens[$checkToken]['scope_opener']) === true + && $tokens[$checkToken]['scope_opener'] === $checkToken)) + ) { + if (empty($tokens[$checkToken]['conditions']) === false) { + $condition = $tokens[$checkToken]['conditions']; + end($condition); + $condition = key($condition); + } else { + $condition = $tokens[$checkToken]['scope_condition']; + } + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $condition, true); + + if (isset($adjustments[$first]) === true + && (($adjustments[$first] < 0 && $tokenIndent > $currentIndent) + || ($adjustments[$first] > 0 && $tokenIndent < $currentIndent)) + ) { + $length = ($tokenIndent + $adjustments[$first]); + + // When fixing, we're going to adjust the indent of this line + // here automatically, so use this new padding value when + // comparing the expected padding to the actual padding. + if ($phpcsFile->fixer->enabled === true) { + $tokenIndent = $length; + $this->adjustIndent($phpcsFile, $checkToken, $length, $adjustments[$first]); + } + + if ($this->debug === true) { + $line = $tokens[$checkToken]['line']; + $type = $tokens[$checkToken]['type']; + echo "Indent adjusted to $length for $type on line $line".PHP_EOL; + } + + $adjustments[$checkToken] = $adjustments[$first]; + + if ($this->debug === true) { + $line = $tokens[$checkToken]['line']; + $type = $tokens[$checkToken]['type']; + echo "\t=> add adjustment of ".$adjustments[$checkToken]." for token $checkToken ($type) on line $line".PHP_EOL; + } + }//end if + }//end if + + // Scope closers reset the required indent to the same level as the opening condition. + if (($checkToken !== null + && isset($openScopes[$checkToken]) === true + || (isset($tokens[$checkToken]['scope_condition']) === true + && isset($tokens[$checkToken]['scope_closer']) === true + && $tokens[$checkToken]['scope_closer'] === $checkToken + && $tokens[$checkToken]['line'] !== $tokens[$tokens[$checkToken]['scope_opener']]['line'])) + || ($checkToken === null + && isset($openScopes[$i]) === true) + ) { + if ($this->debug === true) { + if ($checkToken === null) { + $type = $tokens[$tokens[$i]['scope_condition']]['type']; + $line = $tokens[$i]['line']; + } else { + $type = $tokens[$tokens[$checkToken]['scope_condition']]['type']; + $line = $tokens[$checkToken]['line']; + } + + echo "Close scope ($type) on line $line".PHP_EOL; + } + + $scopeCloser = $checkToken; + if ($scopeCloser === null) { + $scopeCloser = $i; + } + + $conditionToken = array_pop($openScopes); + if ($this->debug === true) { + $line = $tokens[$conditionToken]['line']; + $type = $tokens[$conditionToken]['type']; + echo "\t=> removed open scope $conditionToken ($type) on line $line".PHP_EOL; + } + + if (isset($tokens[$scopeCloser]['scope_condition']) === true) { + $first = $phpcsFile->findFirstOnLine([T_WHITESPACE, T_INLINE_HTML], $tokens[$scopeCloser]['scope_condition'], true); + if ($this->debug === true) { + $line = $tokens[$first]['line']; + $type = $tokens[$first]['type']; + echo "\t* first token is $first ($type) on line $line *".PHP_EOL; + } + + while ($tokens[$first]['code'] === T_CONSTANT_ENCAPSED_STRING + && $tokens[($first - 1)]['code'] === T_CONSTANT_ENCAPSED_STRING + ) { + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, ($first - 1), true); + if ($this->debug === true) { + $line = $tokens[$first]['line']; + $type = $tokens[$first]['type']; + echo "\t* found multi-line string; amended first token is $first ($type) on line $line *".PHP_EOL; + } + } + + $currentIndent = ($tokens[$first]['column'] - 1); + if (isset($adjustments[$first]) === true) { + $currentIndent += $adjustments[$first]; + } + + $setIndents[$scopeCloser] = $currentIndent; + + if ($this->debug === true) { + $type = $tokens[$scopeCloser]['type']; + echo "\t=> indent set to $currentIndent by token $scopeCloser ($type)".PHP_EOL; + } + + // We only check the indent of scope closers if they are + // curly braces because other constructs tend to have different rules. + if ($tokens[$scopeCloser]['code'] === T_CLOSE_CURLY_BRACKET) { + $exact = true; + } else { + $checkToken = null; + } }//end if + }//end if + + // Handle scope for JS object notation. + if ($phpcsFile->tokenizerType === 'JS' + && (($checkToken !== null + && $tokens[$checkToken]['code'] === T_CLOSE_OBJECT + && $tokens[$checkToken]['line'] !== $tokens[$tokens[$checkToken]['bracket_opener']]['line']) + || ($checkToken === null + && $tokens[$i]['code'] === T_CLOSE_OBJECT + && $tokens[$i]['line'] !== $tokens[$tokens[$i]['bracket_opener']]['line'])) + ) { + if ($this->debug === true) { + $line = $tokens[$i]['line']; + echo "Close JS object on line $line".PHP_EOL; + } + + $scopeCloser = $checkToken; + if ($scopeCloser === null) { + $scopeCloser = $i; + } else { + $conditionToken = array_pop($openScopes); + if ($this->debug === true) { + $line = $tokens[$conditionToken]['line']; + $type = $tokens[$conditionToken]['type']; + echo "\t=> removed open scope $conditionToken ($type) on line $line".PHP_EOL; + } + } + + $parens = 0; + if (isset($tokens[$scopeCloser]['nested_parenthesis']) === true + && empty($tokens[$scopeCloser]['nested_parenthesis']) === false + ) { + $parens = $tokens[$scopeCloser]['nested_parenthesis']; + end($parens); + $parens = key($parens); + if ($this->debug === true) { + $line = $tokens[$parens]['line']; + echo "\t* token has nested parenthesis $parens on line $line *".PHP_EOL; + } + } + + $condition = 0; + if (isset($tokens[$scopeCloser]['conditions']) === true + && empty($tokens[$scopeCloser]['conditions']) === false + ) { + $condition = $tokens[$scopeCloser]['conditions']; + end($condition); + $condition = key($condition); + if ($this->debug === true) { + $line = $tokens[$condition]['line']; + $type = $tokens[$condition]['type']; + echo "\t* token is inside condition $condition ($type) on line $line *".PHP_EOL; + } + } + + if ($parens > $condition) { + if ($this->debug === true) { + echo "\t* using parenthesis *".PHP_EOL; + } + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $parens, true); + $condition = 0; + } else if ($condition > 0) { + if ($this->debug === true) { + echo "\t* using condition *".PHP_EOL; + } + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $condition, true); + $parens = 0; + } else { + if ($this->debug === true) { + $line = $tokens[$tokens[$scopeCloser]['bracket_opener']]['line']; + echo "\t* token is not in parenthesis or condition; using opener on line $line *".PHP_EOL; + } + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $tokens[$scopeCloser]['bracket_opener'], true); + }//end if + + $currentIndent = ($tokens[$first]['column'] - 1); + if (isset($adjustments[$first]) === true) { + $currentIndent += $adjustments[$first]; + } + + if ($parens > 0 || $condition > 0) { + $checkIndent = ($tokens[$first]['column'] - 1); + if (isset($adjustments[$first]) === true) { + $checkIndent += $adjustments[$first]; + } + + if ($condition > 0) { + $checkIndent += $this->indent; + $currentIndent += $this->indent; + $exact = true; + } + } else { + $checkIndent = $currentIndent; + } + + // Make sure it is divisible by our expected indent. + $currentIndent = (int) (ceil($currentIndent / $this->indent) * $this->indent); + $checkIndent = (int) (ceil($checkIndent / $this->indent) * $this->indent); + $setIndents[$first] = $currentIndent; + + if ($this->debug === true) { + $type = $tokens[$first]['type']; + echo "\t=> checking indent of $checkIndent; main indent set to $currentIndent by token $first ($type)".PHP_EOL; + } + }//end if + + if ($checkToken !== null + && isset(Tokens::$scopeOpeners[$tokens[$checkToken]['code']]) === true + && in_array($tokens[$checkToken]['code'], $this->nonIndentingScopes, true) === false + && isset($tokens[$checkToken]['scope_opener']) === true + ) { + $exact = true; + + $lastOpener = null; + if (empty($openScopes) === false) { + end($openScopes); + $lastOpener = current($openScopes); + } + + // A scope opener that shares a closer with another token (like multiple + // CASEs using the same BREAK) needs to reduce the indent level so its + // indent is checked correctly. It will then increase the indent again + // (as all openers do) after being checked. + if ($lastOpener !== null + && isset($tokens[$lastOpener]['scope_closer']) === true + && $tokens[$lastOpener]['level'] === $tokens[$checkToken]['level'] + && $tokens[$lastOpener]['scope_closer'] === $tokens[$checkToken]['scope_closer'] + ) { + $currentIndent -= $this->indent; + $setIndents[$lastOpener] = $currentIndent; + if ($this->debug === true) { + $line = $tokens[$i]['line']; + $type = $tokens[$lastOpener]['type']; + echo "Shared closer found on line $line".PHP_EOL; + echo "\t=> indent set to $currentIndent by token $lastOpener ($type)".PHP_EOL; + } + } + + if ($tokens[$checkToken]['code'] === T_CLOSURE + && $tokenIndent > $currentIndent + ) { + // The opener is indented more than needed, which is fine. + // But just check that it is divisible by our expected indent. + $checkIndent = (int) (ceil($tokenIndent / $this->indent) * $this->indent); + $exact = false; + + if ($this->debug === true) { + $line = $tokens[$i]['line']; + echo "Closure found on line $line".PHP_EOL; + echo "\t=> checking indent of $checkIndent; main indent remains at $currentIndent".PHP_EOL; + } + } + }//end if - if ($column >= $indent) { - // Ignore code between paranthesis (multi line function calls or - // arrays) and multi line statements. - $before = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($firstToken - 1), $scopeOpener, true); - if ($before !== $scopeOpener - && $tokens[$before]['code'] !== T_SEMICOLON - && $tokens[$before]['code'] !== T_CLOSE_CURLY_BRACKET + // Method prefix indentation has to be exact or else it will break + // the rest of the function declaration, and potentially future ones. + if ($checkToken !== null + && isset(Tokens::$methodPrefixes[$tokens[$checkToken]['code']]) === true + && $tokens[($checkToken + 1)]['code'] !== T_DOUBLE_COLON + ) { + $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($checkToken + 1), null, true); + if ($next === false || $tokens[$next]['code'] !== T_CLOSURE) { + if ($this->debug === true) { + $line = $tokens[$checkToken]['line']; + $type = $tokens[$checkToken]['type']; + echo "\t* method prefix ($type) found on line $line; indent set to exact *".PHP_EOL; + } + + $exact = true; + } + } + + // JS property indentation has to be exact or else if will break + // things like function and object indentation. + if ($checkToken !== null && $tokens[$checkToken]['code'] === T_PROPERTY) { + $exact = true; + } + + // Open PHP tags needs to be indented to exact column positions + // so they don't cause problems with indent checks for the code + // within them, but they don't need to line up with the current indent + // in most cases. + if ($checkToken !== null + && ($tokens[$checkToken]['code'] === T_OPEN_TAG + || $tokens[$checkToken]['code'] === T_OPEN_TAG_WITH_ECHO) + ) { + $checkIndent = ($tokens[$checkToken]['column'] - 1); + + // If we are re-opening a block that was closed in the same + // scope as us, then reset the indent back to what the scope opener + // set instead of using whatever indent this open tag has set. + if (empty($tokens[$checkToken]['conditions']) === false) { + $close = $phpcsFile->findPrevious(T_CLOSE_TAG, ($checkToken - 1)); + if ($close !== false + && $tokens[$checkToken]['conditions'] === $tokens[$close]['conditions'] + ) { + $conditions = array_keys($tokens[$checkToken]['conditions']); + $lastCondition = array_pop($conditions); + $lastOpener = $tokens[$lastCondition]['scope_opener']; + $lastCloser = $tokens[$lastCondition]['scope_closer']; + if ($tokens[$lastCloser]['line'] !== $tokens[$checkToken]['line'] + && isset($setIndents[$lastOpener]) === true + ) { + $checkIndent = $setIndents[$lastOpener]; + } + } + } + + $checkIndent = (int) (ceil($checkIndent / $this->indent) * $this->indent); + }//end if + + // Close tags needs to be indented to exact column positions. + if ($checkToken !== null && $tokens[$checkToken]['code'] === T_CLOSE_TAG) { + $exact = true; + $checkIndent = $currentIndent; + $checkIndent = (int) (ceil($checkIndent / $this->indent) * $this->indent); + } + + // Special case for ELSE statements that are not on the same + // line as the previous IF statements closing brace. They still need + // to have the same indent or it will break code after the block. + if ($checkToken !== null && $tokens[$checkToken]['code'] === T_ELSE) { + $exact = true; + } + + // Don't perform strict checking on chained method calls since they + // are often covered by custom rules. + if ($checkToken !== null + && $tokens[$checkToken]['code'] === T_OBJECT_OPERATOR + && $exact === true + ) { + $exact = false; + } + + if ($checkIndent === null) { + $checkIndent = $currentIndent; + } + + // If the line starts with "->" we assume this is an indented chained + // method invocation, so we add one level of indentation. + if ($checkToken !== null && $tokens[$checkToken]['code'] === T_OBJECT_OPERATOR) { + $exact = true; + $checkIndent += $this->indent; + } + + // Comments starting with a star have an extra whitespace. + if ($checkToken !== null && $tokens[$checkToken]['code'] === T_COMMENT) { + $content = trim($tokens[$checkToken]['content']); + if ($content[0] === '*') { + $checkIndent += 1; + } + } + + /* + The indent of the line is checked by the following IF block. + + Up until now, we've just been figuring out what the indent + of this line should be. + + After this IF block, we adjust the indent again for + the checking of future lines + */ + + if ($checkToken !== null + && isset($this->ignoreIndentation[$tokens[$checkToken]['code']]) === false + && (($tokenIndent !== $checkIndent && $exact === true) + || ($tokenIndent < $checkIndent && $exact === false)) + ) { + if ($tokenIndent > $checkIndent) { + // Ignore multi line statements. + $before = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($checkToken - 1), null, true); + if ($before !== false && in_array( + $tokens[$before]['code'], + [ + T_SEMICOLON, + T_CLOSE_CURLY_BRACKET, + T_OPEN_CURLY_BRACKET, + T_COLON, + T_OPEN_TAG, + ] + ) === false ) { continue; } } - // The token at the start of the line, needs to have its' column - // greater than the relative indent we set above. If it is less, - // an error should be shown. - if ($column !== $indent) { - if ($this->exact === true || $column < $indent) { - $type = 'IncorrectExact'; - $error = 'Line indented incorrectly; expected '; - if ($this->exact === false) { - $error .= 'at least '; - $type = 'Incorrect'; + // Skip array closing indentation errors, this is handled by the + // ArraySniff. + if (($tokens[$checkToken]['code'] === T_CLOSE_PARENTHESIS + && isset($tokens[$checkToken]['parenthesis_owner']) === true + && $tokens[$tokens[$checkToken]['parenthesis_owner']]['code'] === T_ARRAY) + || $tokens[$checkToken]['code'] === T_CLOSE_SHORT_ARRAY + ) { + continue; + } + + $type = 'IncorrectExact'; + $error = 'Line indented incorrectly; expected '; + if ($exact === false) { + $error .= 'at least '; + $type = 'Incorrect'; + } + + if ($this->tabIndent === true) { + $error .= '%s tabs, found %s'; + $data = [ + floor($checkIndent / $this->tabWidth), + floor($tokenIndent / $this->tabWidth), + ]; + } else { + $error .= '%s spaces, found %s'; + $data = [ + $checkIndent, + $tokenIndent, + ]; + } + + if ($this->debug === true) { + $line = $tokens[$checkToken]['line']; + $message = vsprintf($error, $data); + echo "[Line $line] $message".PHP_EOL; + } + + // Assume the change would be applied and continue + // checking indents under this assumption. This gives more + // technically accurate error messages. + $adjustments[$checkToken] = ($checkIndent - $tokenIndent); + + $fix = $phpcsFile->addFixableError($error, $checkToken, $type, $data); + if ($fix === true || $this->debug === true) { + $accepted = $this->adjustIndent($phpcsFile, $checkToken, $checkIndent, ($checkIndent - $tokenIndent)); + + if ($accepted === true && $this->debug === true) { + $line = $tokens[$checkToken]['line']; + $type = $tokens[$checkToken]['type']; + echo "\t=> add adjustment of ".$adjustments[$checkToken]." for token $checkToken ($type) on line $line".PHP_EOL; + } + } + }//end if + + if ($checkToken !== null) { + $i = $checkToken; + } + + // Don't check indents exactly between arrays as they tend to have custom rules. + if ($tokens[$i]['code'] === T_OPEN_SHORT_ARRAY) { + $disableExactEnd = max($disableExactEnd, $tokens[$i]['bracket_closer']); + if ($this->debug === true) { + $line = $tokens[$i]['line']; + $type = $tokens[$disableExactEnd]['type']; + echo "Opening short array bracket found on line $line".PHP_EOL; + if ($disableExactEnd === $tokens[$i]['bracket_closer']) { + echo "\t=> disabling exact indent checking until $disableExactEnd ($type)".PHP_EOL; + } else { + echo "\t=> continuing to disable exact indent checking until $disableExactEnd ($type)".PHP_EOL; + } + } + } + + // Completely skip here/now docs as the indent is a part of the + // content itself. + if ($tokens[$i]['code'] === T_START_HEREDOC + || $tokens[$i]['code'] === T_START_NOWDOC + ) { + if ($this->debug === true) { + $line = $tokens[$i]['line']; + $type = $tokens[$disableExactEnd]['type']; + echo "Here/nowdoc found on line $line".PHP_EOL; + } + + $i = $phpcsFile->findNext([T_END_HEREDOC, T_END_NOWDOC], ($i + 1)); + $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($i + 1), null, true); + if ($tokens[$next]['code'] === T_COMMA) { + $i = $next; + } + + if ($this->debug === true) { + $line = $tokens[$i]['line']; + $type = $tokens[$i]['type']; + echo "\t* skipping to token $i ($type) on line $line *".PHP_EOL; + } + + continue; + }//end if + + // Completely skip multi-line strings as the indent is a part of the + // content itself. + if ($tokens[$i]['code'] === T_CONSTANT_ENCAPSED_STRING + || $tokens[$i]['code'] === T_DOUBLE_QUOTED_STRING + ) { + $i = $phpcsFile->findNext($tokens[$i]['code'], ($i + 1), null, true); + $i--; + continue; + } + + // Completely skip doc comments as they tend to have complex + // indentation rules. + if ($tokens[$i]['code'] === T_DOC_COMMENT_OPEN_TAG) { + $i = $tokens[$i]['comment_closer']; + continue; + } + + // Open tags reset the indent level. + if ($tokens[$i]['code'] === T_OPEN_TAG + || $tokens[$i]['code'] === T_OPEN_TAG_WITH_ECHO + ) { + if ($this->debug === true) { + $line = $tokens[$i]['line']; + echo "Open PHP tag found on line $line".PHP_EOL; + } + + if ($checkToken === null) { + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $i, true); + $currentIndent = (strlen($tokens[$first]['content']) - strlen(ltrim($tokens[$first]['content']))); + } else { + $currentIndent = ($tokens[$i]['column'] - 1); + } + + $lastOpenTag = $i; + + if (isset($adjustments[$i]) === true) { + $currentIndent += $adjustments[$i]; + } + + // Make sure it is divisible by our expected indent. + $currentIndent = (int) (ceil($currentIndent / $this->indent) * $this->indent); + $setIndents[$i] = $currentIndent; + + if ($this->debug === true) { + $type = $tokens[$i]['type']; + echo "\t=> indent set to $currentIndent by token $i ($type)".PHP_EOL; + } + + continue; + }//end if + + // Close tags reset the indent level, unless they are closing a tag + // opened on the same line. + if ($tokens[$i]['code'] === T_CLOSE_TAG) { + if ($this->debug === true) { + $line = $tokens[$i]['line']; + echo "Close PHP tag found on line $line".PHP_EOL; + } + + if ($tokens[$lastOpenTag]['line'] !== $tokens[$i]['line']) { + $currentIndent = ($tokens[$i]['column'] - 1); + $lastCloseTag = $i; + } else { + if ($lastCloseTag === null) { + $currentIndent = 0; + } else { + $currentIndent = ($tokens[$lastCloseTag]['column'] - 1); + } + } + + if (isset($adjustments[$i]) === true) { + $currentIndent += $adjustments[$i]; + } + + // Make sure it is divisible by our expected indent. + $currentIndent = (int) (ceil($currentIndent / $this->indent) * $this->indent); + $setIndents[$i] = $currentIndent; + + if ($this->debug === true) { + $type = $tokens[$i]['type']; + echo "\t=> indent set to $currentIndent by token $i ($type)".PHP_EOL; + } + + continue; + }//end if + + // Anon classes and functions set the indent based on their own indent level. + if ($tokens[$i]['code'] === T_CLOSURE || $tokens[$i]['code'] === T_ANON_CLASS) { + $closer = $tokens[$i]['scope_closer']; + if ($tokens[$i]['line'] === $tokens[$closer]['line']) { + if ($this->debug === true) { + $type = str_replace('_', ' ', strtolower(substr($tokens[$i]['type'], 2))); + $line = $tokens[$i]['line']; + echo "* ignoring single-line $type on line $line".PHP_EOL; + } + + $i = $closer; + continue; + } + + if ($this->debug === true) { + $type = str_replace('_', ' ', strtolower(substr($tokens[$i]['type'], 2))); + $line = $tokens[$i]['line']; + echo "Open $type on line $line".PHP_EOL; + } + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $i, true); + if ($this->debug === true) { + $line = $tokens[$first]['line']; + $type = $tokens[$first]['type']; + echo "\t* first token is $first ($type) on line $line *".PHP_EOL; + } + + while ($tokens[$first]['code'] === T_CONSTANT_ENCAPSED_STRING + && $tokens[($first - 1)]['code'] === T_CONSTANT_ENCAPSED_STRING + ) { + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, ($first - 1), true); + if ($this->debug === true) { + $line = $tokens[$first]['line']; + $type = $tokens[$first]['type']; + echo "\t* found multi-line string; amended first token is $first ($type) on line $line *".PHP_EOL; + } + } + + $currentIndent = (($tokens[$first]['column'] - 1) + $this->indent); + $openScopes[$tokens[$i]['scope_closer']] = $tokens[$i]['scope_condition']; + if ($this->debug === true) { + $closerToken = $tokens[$i]['scope_closer']; + $closerLine = $tokens[$closerToken]['line']; + $closerType = $tokens[$closerToken]['type']; + $conditionToken = $tokens[$i]['scope_condition']; + $conditionLine = $tokens[$conditionToken]['line']; + $conditionType = $tokens[$conditionToken]['type']; + echo "\t=> added open scope $closerToken ($closerType) on line $closerLine, pointing to condition $conditionToken ($conditionType) on line $conditionLine".PHP_EOL; + } + + if (isset($adjustments[$first]) === true) { + $currentIndent += $adjustments[$first]; + } + + // Make sure it is divisible by our expected indent. + $currentIndent = (int) (floor($currentIndent / $this->indent) * $this->indent); + $i = $tokens[$i]['scope_opener']; + $setIndents[$i] = $currentIndent; + + if ($this->debug === true) { + $type = $tokens[$i]['type']; + echo "\t=> indent set to $currentIndent by token $i ($type)".PHP_EOL; + } + + continue; + }//end if + + // Scope openers increase the indent level. + if (isset($tokens[$i]['scope_condition']) === true + && isset($tokens[$i]['scope_opener']) === true + && $tokens[$i]['scope_opener'] === $i + ) { + $closer = $tokens[$i]['scope_closer']; + if ($tokens[$i]['line'] === $tokens[$closer]['line']) { + if ($this->debug === true) { + $line = $tokens[$i]['line']; + $type = $tokens[$i]['type']; + echo "* ignoring single-line $type on line $line".PHP_EOL; + } + + $i = $closer; + continue; + } + + $condition = $tokens[$tokens[$i]['scope_condition']]['code']; + if (isset(Tokens::$scopeOpeners[$condition]) === true + && in_array($condition, $this->nonIndentingScopes, true) === false + ) { + if ($this->debug === true) { + $line = $tokens[$i]['line']; + $type = $tokens[$tokens[$i]['scope_condition']]['type']; + echo "Open scope ($type) on line $line".PHP_EOL; + } + + $currentIndent += $this->indent; + $setIndents[$i] = $currentIndent; + $openScopes[$tokens[$i]['scope_closer']] = $tokens[$i]['scope_condition']; + if ($this->debug === true) { + $closerToken = $tokens[$i]['scope_closer']; + $closerLine = $tokens[$closerToken]['line']; + $closerType = $tokens[$closerToken]['type']; + $conditionToken = $tokens[$i]['scope_condition']; + $conditionLine = $tokens[$conditionToken]['line']; + $conditionType = $tokens[$conditionToken]['type']; + echo "\t=> added open scope $closerToken ($closerType) on line $closerLine, pointing to condition $conditionToken ($conditionType) on line $conditionLine".PHP_EOL; + } + + if ($this->debug === true) { + $type = $tokens[$i]['type']; + echo "\t=> indent set to $currentIndent by token $i ($type)".PHP_EOL; + } + + continue; + }//end if + }//end if + + // JS objects set the indent level. + if ($phpcsFile->tokenizerType === 'JS' + && $tokens[$i]['code'] === T_OBJECT + ) { + $closer = $tokens[$i]['bracket_closer']; + if ($tokens[$i]['line'] === $tokens[$closer]['line']) { + if ($this->debug === true) { + $line = $tokens[$i]['line']; + echo "* ignoring single-line JS object on line $line".PHP_EOL; + } + + $i = $closer; + continue; + } + + if ($this->debug === true) { + $line = $tokens[$i]['line']; + echo "Open JS object on line $line".PHP_EOL; + } + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $i, true); + $currentIndent = (($tokens[$first]['column'] - 1) + $this->indent); + if (isset($adjustments[$first]) === true) { + $currentIndent += $adjustments[$first]; + } + + // Make sure it is divisible by our expected indent. + $currentIndent = (int) (ceil($currentIndent / $this->indent) * $this->indent); + $setIndents[$first] = $currentIndent; + + if ($this->debug === true) { + $type = $tokens[$first]['type']; + echo "\t=> indent set to $currentIndent by token $first ($type)".PHP_EOL; + } + + continue; + }//end if + + // Closing an anon class or function. + if (isset($tokens[$i]['scope_condition']) === true + && $tokens[$i]['scope_closer'] === $i + && ($tokens[$tokens[$i]['scope_condition']]['code'] === T_CLOSURE + || $tokens[$tokens[$i]['scope_condition']]['code'] === T_ANON_CLASS) + ) { + if ($this->debug === true) { + $type = str_replace('_', ' ', strtolower(substr($tokens[$tokens[$i]['scope_condition']]['type'], 2))); + $line = $tokens[$i]['line']; + echo "Close $type on line $line".PHP_EOL; + } + + $prev = false; + + $object = 0; + if ($phpcsFile->tokenizerType === 'JS') { + $conditions = $tokens[$i]['conditions']; + krsort($conditions, SORT_NUMERIC); + foreach ($conditions as $token => $condition) { + if ($condition === T_OBJECT) { + $object = $token; + break; } + } + + if ($this->debug === true && $object !== 0) { + $line = $tokens[$object]['line']; + echo "\t* token is inside JS object $object on line $line *".PHP_EOL; + } + } + + $parens = 0; + if (isset($tokens[$i]['nested_parenthesis']) === true + && empty($tokens[$i]['nested_parenthesis']) === false + ) { + $parens = $tokens[$i]['nested_parenthesis']; + end($parens); + $parens = key($parens); + if ($this->debug === true) { + $line = $tokens[$parens]['line']; + echo "\t* token has nested parenthesis $parens on line $line *".PHP_EOL; + } + } + + $condition = 0; + if (isset($tokens[$i]['conditions']) === true + && empty($tokens[$i]['conditions']) === false + ) { + $condition = $tokens[$i]['conditions']; + end($condition); + $condition = key($condition); + if ($this->debug === true) { + $line = $tokens[$condition]['line']; + $type = $tokens[$condition]['type']; + echo "\t* token is inside condition $condition ($type) on line $line *".PHP_EOL; + } + } - $error .= '%s spaces, found %s'; - $data = array( - ($indent - 1), - ($column - 1), - ); - $phpcsFile->addError($error, $firstToken, $type, $data); + if ($parens > $object && $parens > $condition) { + if ($this->debug === true) { + echo "\t* using parenthesis *".PHP_EOL; } + + $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($parens - 1), null, true); + $object = 0; + $condition = 0; + } else if ($object > 0 && $object >= $condition) { + if ($this->debug === true) { + echo "\t* using object *".PHP_EOL; + } + + $prev = $object; + $parens = 0; + $condition = 0; + } else if ($condition > 0) { + if ($this->debug === true) { + echo "\t* using condition *".PHP_EOL; + } + + $prev = $condition; + $object = 0; + $parens = 0; }//end if + + if ($prev === false) { + $prev = $phpcsFile->findPrevious([T_EQUAL, T_RETURN], ($tokens[$i]['scope_condition'] - 1), null, false, null, true); + if ($prev === false) { + $prev = $i; + if ($this->debug === true) { + echo "\t* could not find a previous T_EQUAL or T_RETURN token; will use current token *".PHP_EOL; + } + } + } + + if ($this->debug === true) { + $line = $tokens[$prev]['line']; + $type = $tokens[$prev]['type']; + echo "\t* previous token is $type on line $line *".PHP_EOL; + } + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $prev, true); + if ($this->debug === true) { + $line = $tokens[$first]['line']; + $type = $tokens[$first]['type']; + echo "\t* first token on line $line is $first ($type) *".PHP_EOL; + } + + $prev = $phpcsFile->findStartOfStatement($first); + if ($prev !== $first) { + // This is not the start of the statement. + if ($this->debug === true) { + $line = $tokens[$prev]['line']; + $type = $tokens[$prev]['type']; + echo "\t* amended previous is $type on line $line *".PHP_EOL; + } + + $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $prev, true); + if ($this->debug === true) { + $line = $tokens[$first]['line']; + $type = $tokens[$first]['type']; + echo "\t* amended first token is $first ($type) on line $line *".PHP_EOL; + } + } + + $currentIndent = ($tokens[$first]['column'] - 1); + if ($object > 0 || $condition > 0) { + $currentIndent += $this->indent; + } + + if (isset($tokens[$first]['scope_closer']) === true + && $tokens[$first]['scope_closer'] === $first + ) { + if ($this->debug === true) { + echo "\t* first token is a scope closer *".PHP_EOL; + } + + if ($condition === 0 || $tokens[$condition]['scope_opener'] < $first) { + $currentIndent = $setIndents[$first]; + } else if ($this->debug === true) { + echo "\t* ignoring scope closer *".PHP_EOL; + } + } + + // Make sure it is divisible by our expected indent. + $currentIndent = (int) (ceil($currentIndent / $this->indent) * $this->indent); + $setIndents[$first] = $currentIndent; + + if ($this->debug === true) { + $type = $tokens[$first]['type']; + echo "\t=> indent set to $currentIndent by token $first ($type)".PHP_EOL; + } }//end if }//end for + // Don't process the rest of the file. + return $phpcsFile->numTokens; + }//end process() /** - * Calculates the expected indent of a token. - * - * Returns the column at which the token should be indented to, so 1 means - * that the token should not be indented at all. + * Processes this test, when one of its tokens is encountered. * - * @param array $tokens The stack of tokens for this file. - * @param int $stackPtr The position of the token to get indent for. + * @param \PHP_CodeSniffer\Files\File $phpcsFile All the tokens found in the document. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. + * @param int $length The length of the new indent. + * @param int $change The difference in length between + * the old and new indent. * - * @return int + * @return bool */ - protected function calculateExpectedIndent(array $tokens, $stackPtr) + protected function adjustIndent(File $phpcsFile, $stackPtr, $length, $change) { - $conditionStack = array(); + $tokens = $phpcsFile->getTokens(); - $inParenthesis = false; - if (isset($tokens[$stackPtr]['nested_parenthesis']) === true - && empty($tokens[$stackPtr]['nested_parenthesis']) === false - ) { - $inParenthesis = true; + // We don't adjust indents outside of PHP. + if ($tokens[$stackPtr]['code'] === T_INLINE_HTML) { + return false; } - // Empty conditions array (top level structure). - if (empty($tokens[$stackPtr]['conditions']) === true) { - if ($inParenthesis === true) { - // Wrapped in parenthesis means it is probably in a - // function call (like a closure) so we have to assume indent - // is correct here and someone else will check it more - // carefully in another sniff. - return $tokens[$stackPtr]['column']; + $padding = ''; + if ($length > 0) { + if ($this->tabIndent === true) { + $numTabs = (int) floor($length / $this->tabWidth); + if ($numTabs > 0) { + $numSpaces = ($length - ($numTabs * $this->tabWidth)); + $padding = str_repeat("\t", $numTabs).str_repeat(' ', $numSpaces); + } } else { - return 1; + $padding = str_repeat(' ', $length); } } - $indent = 0; + if ($tokens[$stackPtr]['column'] === 1) { + $trimmed = ltrim($tokens[$stackPtr]['content']); + $accepted = $phpcsFile->fixer->replaceToken($stackPtr, $padding.$trimmed); + } else { + // Easier to just replace the entire indent. + $accepted = $phpcsFile->fixer->replaceToken(($stackPtr - 1), $padding); + } + + if ($accepted === false) { + return false; + } - $tokenConditions = $tokens[$stackPtr]['conditions']; - foreach ($tokenConditions as $id => $condition) { - // If it's an indenting scope ie. it's not in our array of - // scopes that don't indent, increase indent. - if (in_array($condition, $this->nonIndentingScopes) === false) { - if ($condition === T_CLOSURE && $inParenthesis === true) { - // Closures cause problems with indents when they are - // used as function arguments because the code inside them - // is not technically inside the function yet, so the indent - // is always off by one. So instead, use the - // indent of the closure as the base value. - $indent = ($tokens[$id]['column'] - 1); + if ($tokens[$stackPtr]['code'] === T_DOC_COMMENT_OPEN_TAG) { + // We adjusted the start of a comment, so adjust the rest of it + // as well so the alignment remains correct. + for ($x = ($stackPtr + 1); $x < $tokens[$stackPtr]['comment_closer']; $x++) { + if ($tokens[$x]['column'] !== 1) { + continue; } - $indent += $this->indent; - } - } + $length = 0; + if ($tokens[$x]['code'] === T_DOC_COMMENT_WHITESPACE) { + $length = $tokens[$x]['length']; + } - // Increase by 1 to indiciate that the code should start at a specific column. - // E.g., code indented 4 spaces should start at column 5. - $indent++; - return $indent; + $padding = ($length + $change); + if ($padding > 0) { + if ($this->tabIndent === true) { + $numTabs = (int) floor($padding / $this->tabWidth); + $numSpaces = ($padding - ($numTabs * $this->tabWidth)); + $padding = str_repeat("\t", $numTabs).str_repeat(' ', $numSpaces); + } else { + $padding = str_repeat(' ', $padding); + } + } else { + $padding = ''; + } + + $phpcsFile->fixer->replaceToken($x, $padding); + if ($this->debug === true) { + $length = strlen($padding); + $line = $tokens[$x]['line']; + $type = $tokens[$x]['type']; + echo "\t=> Indent adjusted to $length for $type on line $line".PHP_EOL; + } + }//end for + }//end if - }//end calculateExpectedIndent() + return true; + }//end adjustIndent() -}//end class -?> +}//end class diff --git a/coder_sniffer/Backdrop/drupalcs.info b/coder_sniffer/Backdrop/drupalcs.info deleted file mode 100644 index 53b4870..0000000 --- a/coder_sniffer/Backdrop/drupalcs.info +++ /dev/null @@ -1,3 +0,0 @@ -; This file is populated by the backdropcms.org packaging script with date and -; version information for downloadable releases. -name = Backdrop Code Sniffer diff --git a/coder_sniffer/Backdrop/ruleset.xml b/coder_sniffer/Backdrop/ruleset.xml index 6dbd015..d6d887d 100644 --- a/coder_sniffer/Backdrop/ruleset.xml +++ b/coder_sniffer/Backdrop/ruleset.xml @@ -2,20 +2,21 @@ Backdrop coding standard + + + + ./coder_unique_autoload_phpcs_bug_2751.php - - *.txt - *.md - *.info + + * *.txt - + *.tpl.php @@ -23,31 +24,39 @@ *.tpl.php - - - *.tpl.php - - - - *.tpl.php - - - - *.tpl.php + + + + 0 + *.tpl.php + + + + - + + 0 + + + + + + + + @@ -92,16 +101,20 @@ 0 + + 0 + + + 0 + - + + - - 0 - 0 @@ -114,11 +127,6 @@ 0 - - - 0 - 0 @@ -128,19 +136,26 @@ 0 + + 0 + 0 0 + + 0 + - - + + 0 + 0 @@ -156,8 +171,6 @@ 0 - - @@ -194,10 +207,23 @@ 0 + + + 0 + + + + 0 + + + + + + @@ -206,17 +232,60 @@ + + 0 + + + + + 0 + + + 0 + + + + 0 + + + 0 + + + 0 + - + + + + + + + + + + + + + + + + + + + + *.tpl.php + + */\.git/* */\.svn/* */\.hg/* */\.bzr/* +