Chris@0: getTokens(); Chris@0: Chris@0: // If this is a function/class/interface doc block comment, skip it. Chris@0: // We are only interested in inline doc block comments, which are Chris@0: // not allowed. Chris@0: if ($tokens[$stackPtr]['code'] === T_DOC_COMMENT_OPEN_TAG) { Chris@0: $nextToken = $phpcsFile->findNext( Chris@17: Tokens::$emptyTokens, Chris@0: ($stackPtr + 1), Chris@0: null, Chris@0: true Chris@0: ); Chris@0: Chris@0: $ignore = array( Chris@0: T_CLASS, Chris@0: T_INTERFACE, Chris@0: T_TRAIT, Chris@0: T_FUNCTION, Chris@0: T_CLOSURE, Chris@0: T_PUBLIC, Chris@0: T_PRIVATE, Chris@0: T_PROTECTED, Chris@0: T_FINAL, Chris@0: T_STATIC, Chris@0: T_ABSTRACT, Chris@0: T_CONST, Chris@0: T_PROPERTY, Chris@0: T_VAR, Chris@0: ); Chris@0: Chris@0: // Also ignore all doc blocks defined in the outer scope (no scope Chris@0: // conditions are set). Chris@0: if (in_array($tokens[$nextToken]['code'], $ignore) === true Chris@0: || empty($tokens[$stackPtr]['conditions']) === true Chris@0: ) { Chris@0: return; Chris@0: } Chris@0: Chris@0: if ($phpcsFile->tokenizerType === 'JS') { Chris@0: // We allow block comments if a function or object Chris@0: // is being assigned to a variable. Chris@17: $ignore = Tokens::$emptyTokens; Chris@0: $ignore[] = T_EQUAL; Chris@0: $ignore[] = T_STRING; Chris@0: $ignore[] = T_OBJECT_OPERATOR; Chris@0: $nextToken = $phpcsFile->findNext($ignore, ($nextToken + 1), null, true); Chris@0: if ($tokens[$nextToken]['code'] === T_FUNCTION Chris@0: || $tokens[$nextToken]['code'] === T_CLOSURE Chris@0: || $tokens[$nextToken]['code'] === T_OBJECT Chris@0: || $tokens[$nextToken]['code'] === T_PROTOTYPE Chris@0: ) { Chris@0: return; Chris@0: } Chris@0: } Chris@0: Chris@0: $prevToken = $phpcsFile->findPrevious( Chris@17: Tokens::$emptyTokens, Chris@0: ($stackPtr - 1), Chris@0: null, Chris@0: true Chris@0: ); Chris@0: Chris@0: if ($tokens[$prevToken]['code'] === T_OPEN_TAG) { Chris@0: return; Chris@0: } Chris@0: Chris@0: // Inline doc blocks are allowed in JSDoc. Chris@0: if ($tokens[$stackPtr]['content'] === '/**' && $phpcsFile->tokenizerType !== 'JS') { Chris@0: // The only exception to inline doc blocks is the /** @var */ Chris@0: // declaration. Chris@0: $content = $phpcsFile->getTokensAsString($stackPtr, ($tokens[$stackPtr]['comment_closer'] - $stackPtr + 1)); Chris@0: if (preg_match('#^/\*\* @var [a-zA-Z0-9_\\\\\[\]|]+ \$[a-zA-Z0-9_]+ \*/$#', $content) !== 1) { Chris@0: $error = 'Inline doc block comments are not allowed; use "/* Comment */" or "// Comment" instead'; Chris@0: $phpcsFile->addError($error, $stackPtr, 'DocBlock'); Chris@0: } Chris@0: } Chris@0: }//end if Chris@0: Chris@0: if ($tokens[$stackPtr]['content']{0} === '#') { Chris@0: $error = 'Perl-style comments are not allowed; use "// Comment" instead'; Chris@0: $fix = $phpcsFile->addFixableError($error, $stackPtr, 'WrongStyle'); Chris@0: if ($fix === true) { Chris@0: $comment = ltrim($tokens[$stackPtr]['content'], "# \t"); Chris@0: $phpcsFile->fixer->replaceToken($stackPtr, "// $comment"); Chris@0: } Chris@0: } Chris@0: Chris@0: // We don't want end of block comments. If the last comment is a closing Chris@0: // curly brace. Chris@0: $previousContent = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true); Chris@0: if ($tokens[$previousContent]['line'] === $tokens[$stackPtr]['line']) { Chris@0: if ($tokens[$previousContent]['code'] === T_CLOSE_CURLY_BRACKET) { Chris@0: return; Chris@0: } Chris@0: Chris@0: // Special case for JS files. Chris@0: if ($tokens[$previousContent]['code'] === T_COMMA Chris@0: || $tokens[$previousContent]['code'] === T_SEMICOLON Chris@0: ) { Chris@0: $lastContent = $phpcsFile->findPrevious(T_WHITESPACE, ($previousContent - 1), null, true); Chris@0: if ($tokens[$lastContent]['code'] === T_CLOSE_CURLY_BRACKET) { Chris@0: return; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: $comment = rtrim($tokens[$stackPtr]['content']); Chris@0: Chris@0: // Only want inline comments. Chris@0: if (substr($comment, 0, 2) !== '//') { Chris@0: return; Chris@0: } Chris@0: Chris@0: // Ignore code example lines. Chris@0: if ($this->isInCodeExample($phpcsFile, $stackPtr) === true) { Chris@0: return; Chris@0: } Chris@0: Chris@0: // Verify the indentation of this comment line. Chris@0: $this->processIndentation($phpcsFile, $stackPtr); Chris@0: Chris@0: // If the current line starts with a tag such as "@see" then the trailing dot Chris@0: // rules and upper case start rules don't apply. Chris@0: if (strpos(trim(substr($tokens[$stackPtr]['content'], 2)), '@') === 0) { Chris@0: return; Chris@0: } Chris@0: Chris@0: // The below section determines if a comment block is correctly capitalised, Chris@0: // and ends in a full-stop. It will find the last comment in a block, and Chris@0: // work its way up. Chris@0: $nextComment = $phpcsFile->findNext(array(T_COMMENT), ($stackPtr + 1), null, false); Chris@0: if (($nextComment !== false) Chris@0: && (($tokens[$nextComment]['line']) === ($tokens[$stackPtr]['line'] + 1)) Chris@0: // A tag such as @todo means a separate comment block. Chris@0: && strpos(trim(substr($tokens[$nextComment]['content'], 2)), '@') !== 0 Chris@0: ) { Chris@0: return; Chris@0: } Chris@0: Chris@0: $topComment = $stackPtr; Chris@0: $lastComment = $stackPtr; Chris@0: while (($topComment = $phpcsFile->findPrevious(array(T_COMMENT), ($lastComment - 1), null, false)) !== false) { Chris@0: if ($tokens[$topComment]['line'] !== ($tokens[$lastComment]['line'] - 1)) { Chris@0: break; Chris@0: } Chris@0: Chris@0: $lastComment = $topComment; Chris@0: } Chris@0: Chris@0: $topComment = $lastComment; Chris@0: $commentText = ''; Chris@0: Chris@0: for ($i = $topComment; $i <= $stackPtr; $i++) { Chris@0: if ($tokens[$i]['code'] === T_COMMENT) { Chris@0: $commentText .= trim(substr($tokens[$i]['content'], 2)); Chris@0: } Chris@0: } Chris@0: Chris@0: if ($commentText === '') { Chris@0: $error = 'Blank comments are not allowed'; Chris@0: $fix = $phpcsFile->addFixableError($error, $stackPtr, 'Empty'); Chris@0: if ($fix === true) { Chris@0: $phpcsFile->fixer->replaceToken($stackPtr, ''); Chris@0: } Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@0: $words = preg_split('/\s+/', $commentText); Chris@0: if (preg_match('|\p{Lu}|u', $commentText[0]) === 0 && $commentText[0] !== '@') { Chris@0: // Allow special lower cased words that contain non-alpha characters Chris@0: // (function references, machine names with underscores etc.). Chris@0: $matches = array(); Chris@0: preg_match('/[a-z]+/', $words[0], $matches); Chris@0: if (isset($matches[0]) === true && $matches[0] === $words[0]) { Chris@0: $error = 'Inline comments must start with a capital letter'; Chris@0: $fix = $phpcsFile->addFixableError($error, $topComment, 'NotCapital'); Chris@0: if ($fix === true) { Chris@0: $newComment = preg_replace("/$words[0]/", ucfirst($words[0]), $tokens[$topComment]['content'], 1); Chris@0: $phpcsFile->fixer->replaceToken($topComment, $newComment); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: $commentCloser = $commentText[(strlen($commentText) - 1)]; Chris@0: $acceptedClosers = array( Chris@0: 'full-stops' => '.', Chris@0: 'exclamation marks' => '!', Chris@0: 'colons' => ':', Chris@0: 'question marks' => '?', Chris@0: 'or closing parentheses' => ')', Chris@0: ); Chris@0: Chris@0: // Allow @tag style comments without punctuation. Chris@0: if (in_array($commentCloser, $acceptedClosers) === false && $commentText[0] !== '@') { Chris@0: // Allow special last words like URLs or function references Chris@0: // without punctuation. Chris@0: $lastWord = $words[(count($words) - 1)]; Chris@0: $matches = array(); Chris@0: preg_match('/https?:\/\/.+/', $lastWord, $matches); Chris@0: $isUrl = isset($matches[0]) === true; Chris@0: preg_match('/[$a-zA-Z_]+\([$a-zA-Z_]*\)/', $lastWord, $matches); Chris@0: $isFunction = isset($matches[0]) === true; Chris@0: Chris@0: // Also allow closing tags like @endlink or @endcode. Chris@0: $isEndTag = $lastWord[0] === '@'; Chris@0: Chris@0: if ($isUrl === false && $isFunction === false && $isEndTag === false) { Chris@0: $error = 'Inline comments must end in %s'; Chris@0: $ender = ''; Chris@0: foreach ($acceptedClosers as $closerName => $symbol) { Chris@0: $ender .= ' '.$closerName.','; Chris@0: } Chris@0: Chris@0: $ender = trim($ender, ' ,'); Chris@0: $data = array($ender); Chris@0: $fix = $phpcsFile->addFixableError($error, $stackPtr, 'InvalidEndChar', $data); Chris@0: if ($fix === true) { Chris@0: $newContent = preg_replace('/(\s+)$/', '.$1', $tokens[$stackPtr]['content']); Chris@0: $phpcsFile->fixer->replaceToken($stackPtr, $newContent); Chris@0: } Chris@0: } Chris@0: }//end if Chris@0: Chris@0: // Finally, the line below the last comment cannot be empty if this inline Chris@0: // comment is on a line by itself. Chris@0: if ($tokens[$previousContent]['line'] < $tokens[$stackPtr]['line'] && ($stackPtr + 1) < $phpcsFile->numTokens) { Chris@0: for ($i = ($stackPtr + 1); $i < $phpcsFile->numTokens; $i++) { Chris@0: if ($tokens[$i]['line'] === ($tokens[$stackPtr]['line'] + 1)) { Chris@0: if ($tokens[$i]['code'] !== T_WHITESPACE || $i === ($phpcsFile->numTokens - 1)) { Chris@0: return; Chris@0: } Chris@0: } else if ($tokens[$i]['line'] > ($tokens[$stackPtr]['line'] + 1)) { Chris@0: break; Chris@0: } Chris@0: } Chris@0: Chris@0: $warning = 'There must be no blank line following an inline comment'; Chris@0: $fix = $phpcsFile->addFixableWarning($warning, $stackPtr, 'SpacingAfter'); Chris@0: if ($fix === true) { Chris@0: $next = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); Chris@0: if ($next === ($phpcsFile->numTokens - 1)) { Chris@0: return; Chris@0: } Chris@0: Chris@0: $phpcsFile->fixer->beginChangeset(); Chris@0: for ($i = ($stackPtr + 1); $i < $next; $i++) { Chris@0: if ($tokens[$i]['line'] === $tokens[$next]['line']) { Chris@0: break; Chris@0: } Chris@0: Chris@0: $phpcsFile->fixer->replaceToken($i, ''); Chris@0: } Chris@0: Chris@0: $phpcsFile->fixer->endChangeset(); Chris@0: } Chris@0: }//end if Chris@0: Chris@0: }//end process() Chris@0: Chris@0: Chris@0: /** Chris@0: * Determines if a comment line is part of an @code/@endcode example. Chris@0: * Chris@17: * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. Chris@17: * @param int $stackPtr The position of the current token Chris@17: * in the stack passed in $tokens. Chris@0: * Chris@0: * @return boolean Returns true if the comment line is within a @code block, Chris@0: * false otherwise. Chris@0: */ Chris@17: protected function isInCodeExample(File $phpcsFile, $stackPtr) Chris@0: { Chris@0: $tokens = $phpcsFile->getTokens(); Chris@0: $prevComment = $stackPtr; Chris@0: $lastComment = $stackPtr; Chris@0: while (($prevComment = $phpcsFile->findPrevious(array(T_COMMENT), ($lastComment - 1), null, false)) !== false) { Chris@0: if ($tokens[$prevComment]['line'] !== ($tokens[$lastComment]['line'] - 1)) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: if ($tokens[$prevComment]['content'] === '// @code'.$phpcsFile->eolChar) { Chris@0: return true; Chris@0: } Chris@0: Chris@0: if ($tokens[$prevComment]['content'] === '// @endcode'.$phpcsFile->eolChar) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: $lastComment = $prevComment; Chris@0: } Chris@0: Chris@0: return false; Chris@0: Chris@0: }//end isInCodeExample() Chris@0: Chris@0: Chris@0: /** Chris@0: * Checks the indentation level of the comment contents. Chris@0: * Chris@17: * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. Chris@17: * @param int $stackPtr The position of the current token Chris@17: * in the stack passed in $tokens. Chris@0: * Chris@0: * @return void Chris@0: */ Chris@17: protected function processIndentation(File $phpcsFile, $stackPtr) Chris@0: { Chris@0: $tokens = $phpcsFile->getTokens(); Chris@0: $comment = rtrim($tokens[$stackPtr]['content']); Chris@0: $spaceCount = 0; Chris@0: $tabFound = false; Chris@0: Chris@0: $commentLength = strlen($comment); Chris@0: for ($i = 2; $i < $commentLength; $i++) { Chris@0: if ($comment[$i] === "\t") { Chris@0: $tabFound = true; Chris@0: break; Chris@0: } Chris@0: Chris@0: if ($comment[$i] !== ' ') { Chris@0: break; Chris@0: } Chris@0: Chris@0: $spaceCount++; Chris@0: } Chris@0: Chris@0: $fix = false; Chris@0: if ($tabFound === true) { Chris@0: $error = 'Tab found before comment text; expected "// %s" but found "%s"'; Chris@0: $data = array( Chris@0: ltrim(substr($comment, 2)), Chris@0: $comment, Chris@0: ); Chris@0: $fix = $phpcsFile->addFixableError($error, $stackPtr, 'TabBefore', $data); Chris@0: } else if ($spaceCount === 0 && strlen($comment) > 2) { Chris@0: $error = 'No space found before comment text; expected "// %s" but found "%s"'; Chris@0: $data = array( Chris@0: substr($comment, 2), Chris@0: $comment, Chris@0: ); Chris@0: $fix = $phpcsFile->addFixableError($error, $stackPtr, 'NoSpaceBefore', $data); Chris@0: }//end if Chris@0: Chris@0: if ($fix === true) { Chris@0: $newComment = '// '.ltrim($tokens[$stackPtr]['content'], "/\t "); Chris@0: $phpcsFile->fixer->replaceToken($stackPtr, $newComment); Chris@0: } Chris@0: Chris@0: if ($spaceCount > 1) { Chris@0: // Check if there is a comment on the previous line that justifies the Chris@0: // indentation. Chris@0: $prevComment = $phpcsFile->findPrevious(array(T_COMMENT), ($stackPtr - 1), null, false); Chris@0: if (($prevComment !== false) && (($tokens[$prevComment]['line']) === ($tokens[$stackPtr]['line'] - 1))) { Chris@0: $prevCommentText = rtrim($tokens[$prevComment]['content']); Chris@0: $prevSpaceCount = 0; Chris@0: for ($i = 2; $i < strlen($prevCommentText); $i++) { Chris@0: if ($prevCommentText[$i] !== ' ') { Chris@0: break; Chris@0: } Chris@0: Chris@0: $prevSpaceCount++; Chris@0: } Chris@0: Chris@0: if ($spaceCount > $prevSpaceCount && $prevSpaceCount > 0) { Chris@0: // A previous comment could be a list item or @todo. Chris@0: $indentationStarters = array( Chris@0: '-', Chris@0: '@todo', Chris@0: ); Chris@0: $words = preg_split('/\s+/', $prevCommentText); Chris@0: $numberedList = (bool) preg_match('/^[0-9]+\./', $words[1]); Chris@0: if (in_array($words[1], $indentationStarters) === true) { Chris@0: if ($spaceCount !== ($prevSpaceCount + 2)) { Chris@0: $error = 'Comment indentation error after %s element, expected %s spaces'; Chris@0: $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpacingBefore', array($words[1], $prevSpaceCount + 2)); Chris@0: if ($fix === true) { Chris@0: $newComment = '//'.str_repeat(' ', ($prevSpaceCount + 2)).ltrim($tokens[$stackPtr]['content'], "/\t "); Chris@0: $phpcsFile->fixer->replaceToken($stackPtr, $newComment); Chris@0: } Chris@0: } Chris@0: } else if ($numberedList === true) { Chris@0: $expectedSpaceCount = ($prevSpaceCount + strlen($words[1]) + 1); Chris@0: if ($spaceCount !== $expectedSpaceCount) { Chris@0: $error = 'Comment indentation error, expected %s spaces'; Chris@0: $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpacingBefore', array($expectedSpaceCount)); Chris@0: if ($fix === true) { Chris@0: $newComment = '//'.str_repeat(' ', $expectedSpaceCount).ltrim($tokens[$stackPtr]['content'], "/\t "); Chris@0: $phpcsFile->fixer->replaceToken($stackPtr, $newComment); Chris@0: } Chris@0: } Chris@0: } else { Chris@0: $error = 'Comment indentation error, expected only %s spaces'; Chris@0: $phpcsFile->addError($error, $stackPtr, 'SpacingBefore', array($prevSpaceCount)); Chris@0: }//end if Chris@0: }//end if Chris@0: } else { Chris@0: $error = '%s spaces found before inline comment; expected "// %s" but found "%s"'; Chris@0: $data = array( Chris@0: $spaceCount, Chris@0: substr($comment, (2 + $spaceCount)), Chris@0: $comment, Chris@0: ); Chris@0: $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpacingBefore', $data); Chris@0: if ($fix === true) { Chris@0: $phpcsFile->fixer->replaceToken($stackPtr, '// '.substr($comment, (2 + $spaceCount)).$phpcsFile->eolChar); Chris@0: } Chris@0: }//end if Chris@0: }//end if Chris@0: Chris@0: }//end processIndentation() Chris@0: Chris@0: Chris@0: }//end class