Chris@0:
Chris@0: *
A doc comment exists.
Chris@0: * There is a blank newline after the @file statement.
Chris@0: *
Chris@0: *
Chris@0: * @category PHP
Chris@0: * @package PHP_CodeSniffer
Chris@0: * @link http://pear.php.net/package/PHP_CodeSniffer
Chris@0: */
Chris@0:
Chris@17: class FileCommentSniff implements Sniff
Chris@0: {
Chris@0:
Chris@0:
Chris@0: /**
Chris@0: * A list of tokenizers this sniff supports.
Chris@0: *
Chris@0: * @var array
Chris@0: */
Chris@0: public $supportedTokenizers = array(
Chris@0: 'PHP',
Chris@0: 'JS',
Chris@0: );
Chris@0:
Chris@0:
Chris@0: /**
Chris@0: * Returns an array of tokens this test wants to listen for.
Chris@0: *
Chris@0: * @return array
Chris@0: */
Chris@0: public function register()
Chris@0: {
Chris@0: return array(T_OPEN_TAG);
Chris@0:
Chris@0: }//end register()
Chris@0:
Chris@0:
Chris@0: /**
Chris@0: * Processes this test, when one of its tokens is encountered.
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 int
Chris@0: */
Chris@17: public function process(File $phpcsFile, $stackPtr)
Chris@0: {
Chris@0: $this->currentFile = $phpcsFile;
Chris@0:
Chris@0: $tokens = $phpcsFile->getTokens();
Chris@0: $commentStart = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
Chris@0:
Chris@0: // Files containing exactly one class, interface or trait are allowed to
Chris@0: // ommit a file doc block. If a namespace is used then the file comment must
Chris@0: // be omitted.
Chris@0: $oopKeyword = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT], $stackPtr);
Chris@0: if ($oopKeyword !== false) {
Chris@0: $namespace = $phpcsFile->findNext(T_NAMESPACE, $stackPtr);
Chris@0: // Check if the file contains multiple classes/interfaces/traits - then a
Chris@0: // file doc block is allowed.
Chris@0: $secondOopKeyword = $phpcsFile->findNext([T_CLASS, T_INTERFACE, T_TRAIT], ($oopKeyword + 1));
Chris@0: // Namespaced classes, interfaces and traits should not have an @file doc
Chris@0: // block.
Chris@0: if (($tokens[$commentStart]['code'] === T_DOC_COMMENT_OPEN_TAG
Chris@0: || $tokens[$commentStart]['code'] === T_COMMENT)
Chris@0: && $secondOopKeyword === false
Chris@0: && $namespace !== false
Chris@0: ) {
Chris@0: $fix = $phpcsFile->addFixableError('Namespaced classes, interfaces and traits should not begin with a file doc comment', $commentStart, 'NamespaceNoFileDoc');
Chris@0: if ($fix === true) {
Chris@0: $phpcsFile->fixer->beginChangeset();
Chris@0:
Chris@0: for ($i = $commentStart; $i <= ($tokens[$commentStart]['comment_closer'] + 1); $i++) {
Chris@0: $phpcsFile->fixer->replaceToken($i, '');
Chris@0: }
Chris@0:
Chris@0: // If, after removing the comment, there are two new lines
Chris@0: // remove them.
Chris@0: if ($tokens[($commentStart - 1)]['content'] === "\n" && $tokens[$i]['content'] === "\n") {
Chris@0: $phpcsFile->fixer->replaceToken($i, '');
Chris@0: }
Chris@0:
Chris@0: $phpcsFile->fixer->endChangeset();
Chris@0: }
Chris@0: }
Chris@0:
Chris@0: if ($namespace !== false) {
Chris@0: return ($phpcsFile->numTokens + 1);
Chris@0: }
Chris@0:
Chris@0: // Search for global functions before and after the class.
Chris@0: $function = $phpcsFile->findPrevious(T_FUNCTION, ($oopKeyword - 1));
Chris@0: if ($function === false) {
Chris@0: $function = $phpcsFile->findNext(T_FUNCTION, ($tokens[$oopKeyword]['scope_closer'] + 1));
Chris@0: }
Chris@0:
Chris@0: $fileTag = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($commentStart + 1), null, false, '@file');
Chris@0:
Chris@0: // No other classes, no other global functions and no explicit @file tag
Chris@0: // anywhere means it is ok to skip the file comment.
Chris@0: if ($secondOopKeyword === false && $function === false && $fileTag === false) {
Chris@0: return ($phpcsFile->numTokens + 1);
Chris@0: }
Chris@0: }//end if
Chris@0:
Chris@0: if ($tokens[$commentStart]['code'] === T_COMMENT) {
Chris@0: $fix = $phpcsFile->addFixableError('You must use "/**" style comments for a file comment', $commentStart, 'WrongStyle');
Chris@0: if ($fix === true) {
Chris@0: $content = $tokens[$commentStart]['content'];
Chris@0:
Chris@0: // If the comment starts with something like "/**" then we just
Chris@0: // insert a space after the stars.
Chris@0: if (strpos($content, '/**') === 0) {
Chris@0: $phpcsFile->fixer->replaceToken($commentStart, str_replace('/**', '/** ', $content));
Chris@0: } else if (strpos($content, '/*') === 0) {
Chris@0: // Just turn the /* ... */ style comment into a /** ... */ style
Chris@0: // comment.
Chris@0: $phpcsFile->fixer->replaceToken($commentStart, str_replace('/*', '/**', $content));
Chris@0: } else {
Chris@0: $content = trim(ltrim($tokens[$commentStart]['content'], '/# '));
Chris@0: $phpcsFile->fixer->replaceToken($commentStart, "/**\n * @file\n * $content\n */\n");
Chris@0: }
Chris@0: }
Chris@0:
Chris@0: return ($phpcsFile->numTokens + 1);
Chris@0: } else if ($commentStart === false || $tokens[$commentStart]['code'] !== T_DOC_COMMENT_OPEN_TAG) {
Chris@0: $fix = $phpcsFile->addFixableError('Missing file doc comment', 0, 'Missing');
Chris@0: if ($fix === true) {
Chris@0: // Only PHP has a real opening tag, additional newline at the
Chris@0: // beginning here.
Chris@0: if ($phpcsFile->tokenizerType === 'PHP') {
Chris@0: // In templates add the file doc block to the very beginning of
Chris@0: // the file.
Chris@0: if ($tokens[0]['code'] === T_INLINE_HTML) {
Chris@0: $phpcsFile->fixer->addContentBefore(0, "\n");
Chris@0: } else {
Chris@0: $phpcsFile->fixer->addContent($stackPtr, "\n/**\n * @file\n */\n");
Chris@0: }
Chris@0: } else {
Chris@0: $phpcsFile->fixer->addContent($stackPtr, "/**\n * @file\n */\n");
Chris@0: }
Chris@0: }
Chris@0:
Chris@0: return ($phpcsFile->numTokens + 1);
Chris@0: }//end if
Chris@0:
Chris@0: $commentEnd = $tokens[$commentStart]['comment_closer'];
Chris@0: $fileTag = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($commentStart + 1), $commentEnd, false, '@file');
Chris@0: $next = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), null, true);
Chris@0:
Chris@0: // If there is no @file tag and the next line is a function or class
Chris@0: // definition then the file docblock is mising.
Chris@0: if ($tokens[$next]['line'] === ($tokens[$commentEnd]['line'] + 1)
Chris@0: && $tokens[$next]['code'] === T_FUNCTION
Chris@0: ) {
Chris@0: if ($fileTag === false) {
Chris@0: $fix = $phpcsFile->addFixableError('Missing file doc comment', $stackPtr, 'Missing');
Chris@0: if ($fix === true) {
Chris@0: // Only PHP has a real opening tag, additional newline at the
Chris@0: // beginning here.
Chris@0: if ($phpcsFile->tokenizerType === 'PHP') {
Chris@0: $phpcsFile->fixer->addContent($stackPtr, "\n/**\n * @file\n */\n");
Chris@0: } else {
Chris@0: $phpcsFile->fixer->addContent($stackPtr, "/**\n * @file\n */\n");
Chris@0: }
Chris@0: }
Chris@0:
Chris@0: return ($phpcsFile->numTokens + 1);
Chris@0: }
Chris@0: }//end if
Chris@0:
Chris@0: if ($fileTag === false || $tokens[$fileTag]['line'] !== ($tokens[$commentStart]['line'] + 1)) {
Chris@0: $second_line = $phpcsFile->findNext(array(T_DOC_COMMENT_STAR, T_DOC_COMMENT_CLOSE_TAG), ($commentStart + 1), $commentEnd);
Chris@0: $fix = $phpcsFile->addFixableError('The second line in the file doc comment must be "@file"', $second_line, 'FileTag');
Chris@0: if ($fix === true) {
Chris@0: if ($fileTag === false) {
Chris@0: $phpcsFile->fixer->addContent($commentStart, "\n * @file");
Chris@0: } else {
Chris@0: // Delete the @file tag at its current position and insert one
Chris@0: // after the beginning of the comment.
Chris@0: $phpcsFile->fixer->beginChangeset();
Chris@0: $phpcsFile->fixer->addContent($commentStart, "\n * @file");
Chris@0: $phpcsFile->fixer->replaceToken($fileTag, '');
Chris@0: $phpcsFile->fixer->endChangeset();
Chris@0: }
Chris@0: }
Chris@0:
Chris@0: return ($phpcsFile->numTokens + 1);
Chris@0: }
Chris@0:
Chris@0: // Exactly one blank line after the file comment.
Chris@0: if ($tokens[$next]['line'] !== ($tokens[$commentEnd]['line'] + 2)
Chris@0: && $next !== false && $tokens[$next]['code'] !== T_CLOSE_TAG
Chris@0: ) {
Chris@0: $error = 'There must be exactly one blank line after the file comment';
Chris@0: $fix = $phpcsFile->addFixableError($error, $commentEnd, 'SpacingAfterComment');
Chris@0: if ($fix === true) {
Chris@0: $phpcsFile->fixer->beginChangeset();
Chris@0: $uselessLine = ($commentEnd + 1);
Chris@0: while ($uselessLine < $next) {
Chris@0: $phpcsFile->fixer->replaceToken($uselessLine, '');
Chris@0: $uselessLine++;
Chris@0: }
Chris@0:
Chris@0: $phpcsFile->fixer->addContent($commentEnd, "\n\n");
Chris@0: $phpcsFile->fixer->endChangeset();
Chris@0: }
Chris@0:
Chris@0: return ($phpcsFile->numTokens + 1);
Chris@0: }
Chris@0:
Chris@0: // Template file: no blank line after the file comment.
Chris@0: if ($tokens[$next]['line'] !== ($tokens[$commentEnd]['line'] + 1)
Chris@0: && $tokens[$next]['line'] > $tokens[$commentEnd]['line']
Chris@0: && $tokens[$next]['code'] === T_CLOSE_TAG
Chris@0: ) {
Chris@0: $error = 'There must be no blank line after the file comment in a template';
Chris@0: $fix = $phpcsFile->addFixableError($error, $commentEnd, 'TeamplateSpacingAfterComment');
Chris@0: if ($fix === true) {
Chris@0: $phpcsFile->fixer->beginChangeset();
Chris@0: $uselessLine = ($commentEnd + 1);
Chris@0: while ($uselessLine < $next) {
Chris@0: $phpcsFile->fixer->replaceToken($uselessLine, '');
Chris@0: $uselessLine++;
Chris@0: }
Chris@0:
Chris@0: $phpcsFile->fixer->addContent($commentEnd, "\n");
Chris@0: $phpcsFile->fixer->endChangeset();
Chris@0: }
Chris@0: }
Chris@0:
Chris@0: // Ignore the rest of the file.
Chris@0: return ($phpcsFile->numTokens + 1);
Chris@0:
Chris@0: }//end process()
Chris@0:
Chris@0:
Chris@0: }//end class