Initial Commit

This commit is contained in:
Riley Schneider
2025-12-03 16:38:10 +01:00
parent c5e26bf594
commit b732d8d4b5
17680 changed files with 5977495 additions and 2 deletions

View File

@@ -0,0 +1,243 @@
<?php
/**
* Squiz_Sniffs_Commenting_BlockCommentSniff.
*
* PHP version 5
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 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
*/
/**
* Squiz_Sniffs_Commenting_BlockCommentSniff.
*
* Verifies that block comments are used appropriately.
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 Squiz Pty Ltd (ABN 77 084 670 600)
* @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
* @version Release: 1.3.3
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class Squiz_Sniffs_Commenting_BlockCommentSniff implements PHP_CodeSniffer_Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(
T_COMMENT,
T_DOC_COMMENT,
);
}//end register()
/**
* Processes this test, when one of its tokens is encountered.
*
* @param PHP_CodeSniffer_File $phpcsFile The current 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();
// If its an inline comment return.
if (substr($tokens[$stackPtr]['content'], 0, 2) !== '/*') {
return;
}
// If this is a function/class/interface doc block comment, skip it.
// We are only interested in inline doc block comments.
if ($tokens[$stackPtr]['code'] === T_DOC_COMMENT) {
$nextToken = $phpcsFile->findNext(PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr + 1), null, true);
$ignore = array(
T_CLASS,
T_INTERFACE,
T_FUNCTION,
T_PUBLIC,
T_PRIVATE,
T_FINAL,
T_PROTECTED,
T_STATIC,
T_ABSTRACT,
T_CONST,
);
if (in_array($tokens[$nextToken]['code'], $ignore) === true) {
return;
}
$prevToken = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr - 1), null, true);
if ($tokens[$prevToken]['code'] === T_OPEN_TAG) {
return;
}
}//end if
$commentLines = array($stackPtr);
$nextComment = $stackPtr;
$lastLine = $tokens[$stackPtr]['line'];
// Construct the comment into an array.
while (($nextComment = $phpcsFile->findNext($tokens[$stackPtr]['code'], ($nextComment + 1), null, false)) !== false) {
if (($tokens[$nextComment]['line'] - 1) !== $lastLine) {
// Not part of the block.
break;
}
$lastLine = $tokens[$nextComment]['line'];
$commentLines[] = $nextComment;
}
if (count($commentLines) <= 2) {
// Small comment. Can't be right.
if (count($commentLines) === 1) {
$error = 'Single line block comment not allowed; use inline ("// text") comment instead';
$phpcsFile->addError($error, $stackPtr, 'SingleLine');
return;
}
if (trim($tokens[$commentLines[1]]['content']) === '*/') {
if (trim($tokens[$stackPtr]['content']) === '/*') {
$error = 'Empty block comment not allowed';
$phpcsFile->addError($error, $stackPtr, 'Empty');
return;
}
}
}
$content = trim($tokens[$stackPtr]['content']);
if ($content !== '/*' && $content !== '/**') {
$error = 'Block comment text must start on a new line';
$phpcsFile->addError($error, $stackPtr, 'NoNewLine');
return;
}
$starColumn = ($tokens[$stackPtr]['column'] + 3);
// Make sure first line isn't blank.
if (trim($tokens[$commentLines[1]]['content']) === '') {
$error = 'Empty line not allowed at start of comment';
$phpcsFile->addError($error, $commentLines[1], 'HasEmptyLine');
} else {
// Check indentation of first line.
$content = $tokens[$commentLines[1]]['content'];
$commentText = ltrim($content);
$leadingSpace = (strlen($content) - strlen($commentText));
if ($leadingSpace !== $starColumn) {
$expected = $starColumn;
$expected .= ($starColumn === 1) ? ' space' : ' spaces';
$data = array(
$expected,
$leadingSpace,
);
$error = 'First line of comment not aligned correctly; expected %s but found %s';
$phpcsFile->addError($error, $commentLines[1], 'FirstLineIndent', $data);
}
if (preg_match('|[A-Z]|', $commentText[0]) === 0) {
$error = 'Block comments must start with a capital letter';
$phpcsFile->addError($error, $commentLines[1], 'NoCaptial');
}
}
// Check that each line of the comment is indented past the star.
foreach ($commentLines as $line) {
$leadingSpace = (strlen($tokens[$line]['content']) - strlen(ltrim($tokens[$line]['content'])));
// First and last lines (comment opener and closer) are handled seperately.
if ($line === $commentLines[(count($commentLines) - 1)] || $line === $commentLines[0]) {
continue;
}
// First comment line was handled above.
if ($line === $commentLines[1]) {
continue;
}
// If it's empty, continue.
if (trim($tokens[$line]['content']) === '') {
continue;
}
if ($leadingSpace < $starColumn) {
$expected = $starColumn;
$expected .= ($starColumn === 1) ? ' space' : ' spaces';
$data = array(
$expected,
$leadingSpace,
);
$error = 'Comment line indented incorrectly; expected at least %s but found %s';
$phpcsFile->addError($error, $line, 'LineIndent', $data);
}
}//end foreach
// Finally, test the last line is correct.
$lastIndex = (count($commentLines) - 1);
$content = trim($tokens[$commentLines[$lastIndex]]['content']);
if ($content !== '*/' && $content !== '**/') {
$error = 'Comment closer must be on a new line';
$phpcsFile->addError($error, $commentLines[$lastIndex]);
} else {
$content = $tokens[$commentLines[$lastIndex]]['content'];
$commentText = ltrim($content);
$leadingSpace = (strlen($content) - strlen($commentText));
if ($leadingSpace !== ($tokens[$stackPtr]['column'] - 1)) {
$expected = ($tokens[$stackPtr]['column'] - 1);
$expected .= ($expected === 1) ? ' space' : ' spaces';
$data = array(
$expected,
$leadingSpace,
);
$error = 'Last line of comment aligned incorrectly; expected %s but found %s';
$phpcsFile->addError($error, $commentLines[$lastIndex], 'LastLineIndent', $data);
}
}
// Check that the lines before and after this comment are blank.
$contentBefore = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
if ($tokens[$contentBefore]['code'] === T_OPEN_CURLY_BRACKET) {
if (($tokens[$stackPtr]['line'] - $tokens[$contentBefore]['line']) < 1) {
$error = 'Empty line not required before block comment';
$phpcsFile->addError($error, $stackPtr, 'HasEmptyLineBefore');
}
} else {
if (($tokens[$stackPtr]['line'] - $tokens[$contentBefore]['line']) < 2) {
$error = 'Empty line required before block comment';
$phpcsFile->addError($error, $stackPtr, 'NoEmptyLineBefore');
}
}
$commentCloser = $commentLines[$lastIndex];
$contentAfter = $phpcsFile->findNext(T_WHITESPACE, ($commentCloser + 1), null, true);
if (($tokens[$contentAfter]['line'] - $tokens[$commentCloser]['line']) < 2) {
$error = 'Empty line required after block comment';
$phpcsFile->addError($error, $commentCloser, 'NoEmptyLineAfter');
}
}//end process()
}//end class
?>

View File

@@ -0,0 +1,255 @@
<?php
/**
* Parses and verifies the class doc comment.
*
* PHP version 5
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 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
*/
if (class_exists('PHP_CodeSniffer_CommentParser_ClassCommentParser', true) === false) {
throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_CommentParser_ClassCommentParser not found');
}
/**
* Parses and verifies the class doc comment.
*
* Verifies that :
* <ul>
* <li>A class doc comment exists.</li>
* <li>There is exactly one blank line before the class comment.</li>
* <li>Short description ends with a full stop.</li>
* <li>There is a blank line after the short description.</li>
* <li>Each paragraph of the long description ends with a full stop.</li>
* <li>There is a blank line between the description and the tags.</li>
* <li>Check the format of the since tag (x.x.x).</li>
* </ul>
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 Squiz Pty Ltd (ABN 77 084 670 600)
* @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
* @version Release: 1.3.3
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class Squiz_Sniffs_Commenting_ClassCommentSniff implements PHP_CodeSniffer_Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_CLASS);
}//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)
{
$this->currentFile = $phpcsFile;
$tokens = $phpcsFile->getTokens();
$find = array (
T_ABSTRACT,
T_WHITESPACE,
T_FINAL,
);
// Extract the class comment docblock.
$commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1), null, true);
if ($commentEnd !== false && $tokens[$commentEnd]['code'] === T_COMMENT) {
$phpcsFile->addError('You must use "/**" style comments for a class comment', $stackPtr, 'WrongStyle');
return;
} else if ($commentEnd === false || $tokens[$commentEnd]['code'] !== T_DOC_COMMENT) {
$phpcsFile->addError('Missing class doc comment', $stackPtr, 'Missing');
return;
}
$commentStart = ($phpcsFile->findPrevious(T_DOC_COMMENT, ($commentEnd - 1), null, true) + 1);
$commentNext = $phpcsFile->findPrevious(T_WHITESPACE, ($commentEnd + 1), $stackPtr, false, $phpcsFile->eolChar);
// Distinguish file and class comment.
$prevClassToken = $phpcsFile->findPrevious(T_CLASS, ($stackPtr - 1));
if ($prevClassToken === false) {
// This is the first class token in this file, need extra checks.
$prevNonComment = $phpcsFile->findPrevious(T_DOC_COMMENT, ($commentStart - 1), null, true);
if ($prevNonComment !== false) {
$prevComment = $phpcsFile->findPrevious(T_DOC_COMMENT, ($prevNonComment - 1));
if ($prevComment === false) {
// There is only 1 doc comment between open tag and class token.
$newlineToken = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), $stackPtr, false, $phpcsFile->eolChar);
if ($newlineToken !== false) {
$newlineToken = $phpcsFile->findNext(T_WHITESPACE, ($newlineToken + 1), $stackPtr, false, $phpcsFile->eolChar);
if ($newlineToken !== false) {
// Blank line between the class and the doc block.
// The doc block is most likely a file comment.
$phpcsFile->addError('Missing class doc comment', ($stackPtr + 1), 'Missing');
return;
}
}//end if
}//end if
// Exactly one blank line before the class comment.
$prevTokenEnd = $phpcsFile->findPrevious(T_WHITESPACE, ($commentStart - 1), null, true);
if ($prevTokenEnd !== false) {
$blankLineBefore = 0;
for ($i = ($prevTokenEnd + 1); $i < $commentStart; $i++) {
if ($tokens[$i]['code'] === T_WHITESPACE && $tokens[$i]['content'] === $phpcsFile->eolChar) {
$blankLineBefore++;
}
}
if ($blankLineBefore !== 2) {
$error = 'There must be exactly one blank line before the class comment';
$phpcsFile->addError($error, ($commentStart - 1), 'SpacingBefore');
}
}
}//end if
}//end if
$commentString = $phpcsFile->getTokensAsString($commentStart, ($commentEnd - $commentStart + 1));
// Parse the class comment docblock.
try {
$this->commentParser = new PHP_CodeSniffer_CommentParser_ClassCommentParser($commentString, $phpcsFile);
$this->commentParser->parse();
} catch (PHP_CodeSniffer_CommentParser_ParserException $e) {
$line = ($e->getLineWithinComment() + $commentStart);
$phpcsFile->addError($e->getMessage(), $line, 'FailedParse');
return;
}
$comment = $this->commentParser->getComment();
if (is_null($comment) === true) {
$error = 'Class 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, 'SpacingAfterOpen');
}
// Check for a comment description.
$short = rtrim($comment->getShortComment(), $phpcsFile->eolChar);
if (trim($short) === '') {
$error = 'Missing short description in class 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) {
$error = 'Extra newline(s) found before class comment short description';
$phpcsFile->addError($error, ($commentStart + 1), 'SpacingBeforeShort');
}
$newlineCount = (substr_count($short, $phpcsFile->eolChar) + 1);
// 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 class comment';
$phpcsFile->addError($error, ($commentStart + $newlineCount + 1), 'SpacingBetween');
}
$newlineCount += $newlineBetween;
$testLong = trim($long);
if (preg_match('|[A-Z]|', $testLong[0]) === 0) {
$error = 'Class comment long description must start with a capital letter';
$phpcsFile->addError($error, ($commentStart + $newlineCount), 'LongNotCaptial');
}
}
// Exactly one blank line before tags.
$tags = $this->commentParser->getTagOrders();
if (count($tags) > 1) {
$newlineSpan = $comment->getNewlineAfter();
if ($newlineSpan !== 2) {
$error = 'There must be exactly one blank line before the tags in class comment';
if ($long !== '') {
$newlineCount += (substr_count($long, $phpcsFile->eolChar) - $newlineSpan + 1);
}
$phpcsFile->addError($error, ($commentStart + $newlineCount), 'SpacingBeforeTags');
$short = rtrim($short, $phpcsFile->eolChar.' ');
}
}
// 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 = 'Class comment short description must be on a single line';
$phpcsFile->addError($error, ($commentStart + 1), 'ShortSingleLine');
}
if (preg_match('|[A-Z]|', $testShort[0]) === 0) {
$error = 'Class comment short description must start with a capital letter';
$phpcsFile->addError($error, ($commentStart + 1), 'ShortNotCapital');
}
if ($lastChar !== '.') {
$error = 'Class comment short description must end with a full stop';
$phpcsFile->addError($error, ($commentStart + 1), 'ShortFullStop');
}
// No tags are allowed in the class comment.
$tags = $this->commentParser->getTags();
foreach ($tags as $errorTag) {
$error = '@%s tag is not allowed in class comment';
$data = array($errorTag['tag']);
$phpcsFile->addWarning($error, ($commentStart + $errorTag['line']), 'TagNotAllowed', $data);
}
// The last content should be a newline and the content before
// that should not be blank. If there is more blank space
// then they have additional blank lines at the end of the comment.
$words = $this->commentParser->getWords();
$lastPos = (count($words) - 1);
if (trim($words[($lastPos - 1)]) !== ''
|| strpos($words[($lastPos - 1)], $this->currentFile->eolChar) === false
|| trim($words[($lastPos - 2)]) === ''
) {
$error = 'Additional blank lines found at end of class comment';
$this->currentFile->addError($error, $commentEnd, 'SpacingAfter');
}
}//end process()
}//end class
?>

View File

@@ -0,0 +1,127 @@
<?php
/**
* Squiz_Sniffs_Commenting_ClosingDeclarationCommentSniff.
*
* PHP version 5
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 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
*/
/**
* Squiz_Sniffs_Commenting_ClosingDeclarationCommentSniff.
*
* Checks the //end ... comments on classes, interfaces and functions.
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 Squiz Pty Ltd (ABN 77 084 670 600)
* @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
* @version Release: 1.3.3
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class Squiz_Sniffs_Commenting_ClosingDeclarationCommentSniff implements PHP_CodeSniffer_Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(
T_FUNCTION,
T_CLASS,
T_INTERFACE,
);
}//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();
if ($tokens[$stackPtr]['code'] === T_FUNCTION) {
$methodProps = $phpcsFile->getMethodProperties($stackPtr);
// Abstract methods do not require a closing comment.
if ($methodProps['is_abstract'] === true) {
return;
}
// Closures do not require a closing comment.
if ($methodProps['is_closure'] === true) {
return;
}
// If this function is in an interface then we don't require
// a closing comment.
if ($phpcsFile->hasCondition($stackPtr, T_INTERFACE) === true) {
return;
}
if (isset($tokens[$stackPtr]['scope_closer']) === false) {
$error = 'Possible parse error: non-abstract method defined as abstract';
$phpcsFile->addWarning($error, $stackPtr, 'Abstract');
return;
}
$decName = $phpcsFile->getDeclarationName($stackPtr);
$comment = '//end '.$decName.'()';
} else if ($tokens[$stackPtr]['code'] === T_CLASS) {
$comment = '//end class';
} else {
$comment = '//end interface';
}//end if
if (isset($tokens[$stackPtr]['scope_closer']) === false) {
$error = 'Possible parse error: %s missing opening or closing brace';
$data = array($tokens[$stackPtr]['content']);
$phpcsFile->addWarning($error, $stackPtr, 'MissingBrace', $data);
return;
}
$closingBracket = $tokens[$stackPtr]['scope_closer'];
if ($closingBracket === null) {
// Possible inline structure. Other tests will handle it.
return;
}
$error = 'Expected '.$comment;
if (isset($tokens[($closingBracket + 1)]) === false || $tokens[($closingBracket + 1)]['code'] !== T_COMMENT) {
$phpcsFile->addError($error, $closingBracket, 'Missing');
return;
}
if (rtrim($tokens[($closingBracket + 1)]['content']) !== $comment) {
$phpcsFile->addError($error, $closingBracket, 'Incorrect');
return;
}
}//end process()
}//end class
?>

View File

@@ -0,0 +1,151 @@
<?php
/**
* Squiz_Sniffs_Commenting_EmptyCatchCommentSniff.
*
* PHP version 5
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 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
*/
/**
* Squiz_Sniffs_Commenting_DocCommentAlignmentSniff.
*
* Tests that the stars in a doc comment align correctly.
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 Squiz Pty Ltd (ABN 77 084 670 600)
* @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
* @version Release: 1.3.3
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class Squiz_Sniffs_Commenting_DocCommentAlignmentSniff implements PHP_CodeSniffer_Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_DOC_COMMENT);
}//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();
// 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) {
// Could be a file comment.
$prevToken = $phpcsFile->findPrevious(PHP_CodeSniffer_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;
}
}
$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;
}
}
// 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');
} 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);
}
}
}
}//end foreach
// Check the alignment of each asterisk.
$currentColumn = strpos($content, '*');
$currentColumn += $tokens[$commentPointer]['column'];
if ($currentColumn === $requiredColumn) {
// Star is aligned correctly.
continue;
}
$error = 'Expected %s space(s) before asterisk; %s found';
$data = array(
($requiredColumn - 1),
($currentColumn - 1),
);
$phpcsFile->addError($error, $commentPointer, 'SpaceBeforeAsterisk', $data);
}//end foreach
}//end process()
}//end class
?>

View File

@@ -0,0 +1,73 @@
<?php
/**
* Squiz_Sniffs_Commenting_EmptyCatchCommentSniff.
*
* PHP version 5
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 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
*/
/**
* Squiz_Sniffs_Commenting_EmptyCatchCommentSniff.
*
* Checks for empty Catch clause. Catch clause must at least have comment
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 Squiz Pty Ltd (ABN 77 084 670 600)
* @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
* @version Release: 1.3.3
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class Squiz_Sniffs_Commenting_EmptyCatchCommentSniff implements PHP_CodeSniffer_Sniff
{
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_CATCH);
}//end 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.
*
* @return void
*/
public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$scopeStart = $tokens[$stackPtr]['scope_opener'];
$firstContent = $phpcsFile->findNext(T_WHITESPACE, ($scopeStart + 1), $tokens[$stackPtr]['scope_closer'], true);
if ($firstContent === false) {
$error = 'Empty CATCH statement must have a comment to explain why the exception is not handled';
$phpcsFile->addError($error, $scopeStart, 'Missing');
}
}//end process()
}//end class
?>

View File

@@ -0,0 +1,609 @@
<?php
/**
* Parses and verifies the file doc comment.
*
* PHP version 5
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 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
*/
if (class_exists('PHP_CodeSniffer_CommentParser_ClassCommentParser', true) === false) {
throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_CommentParser_ClassCommentParser not found');
}
/**
* Parses and verifies the file doc comment.
*
* Verifies that :
* <ul>
* <li>A file doc comment exists.</li>
* <li>There is no blank line between the open tag and the file comment.</li>
* <li>Short description ends with a full stop.</li>
* <li>There is a blank line after the short description.</li>
* <li>Each paragraph of the long description ends with a full stop.</li>
* <li>There is a blank line between the description and the tags.</li>
* <li>Check the order, indentation and content of each tag.</li>
* <li>There is exactly one blank line after the file comment.</li>
* </ul>
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 Squiz Pty Ltd (ABN 77 084 670 600)
* @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
* @version Release: 1.3.3
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class Squiz_Sniffs_Commenting_FileCommentSniff implements PHP_CodeSniffer_Sniff
{
/**
* A list of tokenizers this sniff supports.
*
* @var array
*/
public $supportedTokenizers = array(
'PHP',
'JS',
);
/**
* The header comment parser for the current file.
*
* @var PHP_CodeSniffer_Comment_Parser_ClassCommentParser
*/
protected $commentParser = null;
/**
* The current PHP_CodeSniffer_File object we are processing.
*
* @var PHP_CodeSniffer_File
*/
protected $currentFile = null;
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_OPEN_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)
{
$this->currentFile = $phpcsFile;
// 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();
$errorToken = ($stackPtr + 1);
if (isset($tokens[$errorToken]) === false) {
$errorToken--;
}
// Find the next non whitespace token.
$commentStart = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
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) {
$phpcsFile->addError('You must use "/**" style comments for a file comment', $errorToken, 'WrongStyle');
return;
} else if ($commentStart === false || $tokens[$commentStart]['code'] !== T_DOC_COMMENT) {
$phpcsFile->addError('Missing file doc comment', $errorToken, 'Missing');
return;
}
// Extract the header comment docblock.
$commentEnd = ($phpcsFile->findNext(T_DOC_COMMENT, ($commentStart + 1), null, true) - 1);
// Check if there is only 1 doc comment between the open tag and class token.
$nextToken = array(
T_ABSTRACT,
T_CLASS,
T_DOC_COMMENT,
);
$commentNext = $phpcsFile->findNext($nextToken, ($commentEnd + 1));
if ($commentNext !== false && $tokens[$commentNext]['code'] !== T_DOC_COMMENT) {
// Found a class token right after comment doc block.
$newlineToken = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), $commentNext, false, $phpcsFile->eolChar);
if ($newlineToken !== false) {
$newlineToken = $phpcsFile->findNext(T_WHITESPACE, ($newlineToken + 1), $commentNext, false, $phpcsFile->eolChar);
if ($newlineToken === false) {
// No blank line between the class token and the doc block.
// The doc block is most likely a class comment.
$phpcsFile->addError('Missing file doc comment', $errorToken, 'Missing');
return;
}
}
}
// No blank line between the open tag and the file comment.
$blankLineBefore = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, false, $phpcsFile->eolChar);
if ($blankLineBefore !== false && $blankLineBefore < $commentStart) {
$error = 'Extra newline found after the open tag';
$phpcsFile->addError($error, $stackPtr, 'SpacingAfterOpen');
}
// Exactly one blank line after the file comment.
$nextTokenStart = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), null, true);
if ($nextTokenStart !== false) {
$blankLineAfter = 0;
for ($i = ($commentEnd + 1); $i < $nextTokenStart; $i++) {
if ($tokens[$i]['code'] === T_WHITESPACE && $tokens[$i]['content'] === $phpcsFile->eolChar) {
$blankLineAfter++;
}
}
if ($blankLineAfter !== 2) {
$error = 'There must be exactly one blank line after the file comment';
$phpcsFile->addError($error, ($commentEnd + 1), 'SpacingAfterComment');
}
}
$commentString = $phpcsFile->getTokensAsString($commentStart, ($commentEnd - $commentStart + 1));
// Parse the header comment docblock.
try {
$this->commentParser = new PHP_CodeSniffer_CommentParser_ClassCommentParser($commentString, $phpcsFile);
$this->commentParser->parse();
} catch (PHP_CodeSniffer_CommentParser_ParserException $e) {
$line = ($e->getLineWithinComment() + $commentStart);
$phpcsFile->addError($e->getMessage(), $line, 'Exception');
return;
}
$comment = $this->commentParser->getComment();
if (is_null($comment) === true) {
$error = 'File 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');
}
// No extra newline before short description.
$short = $comment->getShortComment();
$newlineCount = 0;
$newlineSpan = strspn($short, $phpcsFile->eolChar);
if ($short !== '' && $newlineSpan > 0) {
$error = 'Extra newline(s) found before file comment short description';
$phpcsFile->addError($error, ($commentStart + 1), 'SpacingBeforeShort');
}
$newlineCount = (substr_count($short, $phpcsFile->eolChar) + 1);
// 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 file comment';
$phpcsFile->addError($error, ($commentStart + $newlineCount + 1), 'SpacingBetween');
}
$newlineCount += $newlineBetween;
$testLong = trim($long);
if (preg_match('|[A-Z]|', $testLong[0]) === 0) {
$error = 'File comment long description must start with a capital letter';
$phpcsFile->addError($error, ($commentStart + $newlineCount), 'LongNotCaptial');
}
}//end if
// Exactly one blank line before tags.
$tags = $this->commentParser->getTagOrders();
if (count($tags) > 1) {
$newlineSpan = $comment->getNewlineAfter();
if ($newlineSpan !== 2) {
$error = 'There must be exactly one blank line before the tags in file comment';
if ($long !== '') {
$newlineCount += (substr_count($long, $phpcsFile->eolChar) - $newlineSpan + 1);
}
$phpcsFile->addError($error, ($commentStart + $newlineCount), 'SpacingBeforeTags');
$short = rtrim($short, $phpcsFile->eolChar.' ');
}
}
// 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 = 'File comment short description must be on a single line';
$phpcsFile->addError($error, ($commentStart + 1), 'ShortSingleLine');
}
if (preg_match('|[A-Z]|', $testShort[0]) === 0) {
$error = 'File comment short description must start with a capital letter';
$phpcsFile->addError($error, ($commentStart + 1), 'ShortNotCapital');
}
if ($lastChar !== '.') {
$error = 'File comment short description must end with a full stop';
$phpcsFile->addError($error, ($commentStart + 1), 'ShortFullStop');
}
// Check for unknown/deprecated tags.
$unknownTags = $this->commentParser->getUnknown();
foreach ($unknownTags as $errorTag) {
// Unknown tags are not parsed, do not process further.
$error = '@%s tag is not allowed in file comment';
$data = array($errorTag['tag']);
$phpcsFile->addWarning($error, ($commentStart + $errorTag['line']), 'TagNotAllowed', $data);
}
// Check each tag.
$this->processTags($commentStart, $commentEnd);
// The last content should be a newline and the content before
// that should not be blank. If there is more blank space
// then they have additional blank lines at the end of the comment.
$words = $this->commentParser->getWords();
$lastPos = (count($words) - 1);
if (trim($words[($lastPos - 1)]) !== ''
|| strpos($words[($lastPos - 1)], $this->currentFile->eolChar) === false
|| trim($words[($lastPos - 2)]) === ''
) {
$error = 'Additional blank lines found at end of file comment';
$this->currentFile->addError($error, $commentEnd, 'SpacingAfter');
}
}//end process()
/**
* Processes each required or optional tag.
*
* @param int $commentStart The position in the stack where the comment started.
* @param int $commentEnd The position in the stack where the comment ended.
*
* @return void
*/
protected function processTags($commentStart, $commentEnd)
{
// Required tags in correct order.
$tags = array(
'package' => 'precedes @subpackage',
'subpackage' => 'follows @package',
'author' => 'follows @subpackage',
'copyright' => 'follows @author',
'license' => 'follows @copyright',
);
$foundTags = $this->commentParser->getTagOrders();
$errorPos = 0;
$orderIndex = 0;
$longestTag = 0;
$indentation = array();
foreach ($tags as $tag => $orderText) {
// Required tag missing.
if (in_array($tag, $foundTags) === false) {
$error = 'Missing @%s tag in file comment';
$data = array($tag);
$this->currentFile->addError($error, $commentEnd, 'MissingTag', $data);
continue;
}
// Get the line number for current tag.
$tagName = ucfirst($tag);
if ($tagName === 'Author' || $tagName === 'Copyright') {
// These tags are different because they return an array.
$tagName .= 's';
}
// Work out the line number for this tag.
$getMethod = 'get'.$tagName;
$tagElement = $this->commentParser->$getMethod();
if (is_null($tagElement) === true || empty($tagElement) === true) {
continue;
} else if (is_array($tagElement) === true && empty($tagElement) === false) {
$tagElement = $tagElement[0];
}
$errorPos = ($commentStart + $tagElement->getLine());
// Make sure there is no duplicate tag.
$foundIndexes = array_keys($foundTags, $tag);
if (count($foundIndexes) > 1) {
$error = 'Only 1 @%s tag is allowed in file comment';
$data = array($tag);
$this->currentFile->addError($error, $errorPos, 'DuplicateTag', $data);
}
// Check tag order.
if ($foundIndexes[0] > $orderIndex) {
$orderIndex = $foundIndexes[0];
} else {
$error = 'The @%s tag is in the wrong order; the tag %s';
$data = array(
$tag,
$orderText,
);
$this->currentFile->addError($error, $errorPos, 'TagOrder', $data);
}
// Store the indentation of each tag.
$len = strlen($tag);
if ($len > $longestTag) {
$longestTag = $len;
}
$indentation[] = array(
'tag' => $tag,
'errorPos' => $errorPos,
'space' => $this->getIndentation($tag, $tagElement),
);
$method = 'process'.$tagName;
if (method_exists($this, $method) === true) {
// Process each tag if a method is defined.
call_user_func(array($this, $method), $errorPos);
} else {
$tagElement->process($this->currentFile, $commentStart, 'file');
}
}//end foreach
// Check tag indentation.
foreach ($indentation as $indentInfo) {
$tagName = ucfirst($indentInfo['tag']);
if ($tagName === 'Author') {
$tagName .= 's';
}
if ($indentInfo['space'] !== 0 && $indentInfo['space'] !== ($longestTag + 1)) {
$expected = ($longestTag - strlen($indentInfo['tag']) + 1);
$space = ($indentInfo['space'] - strlen($indentInfo['tag']));
$error = '@%s tag comment indented incorrectly; expected %s spaces but found %s';
$data = array(
$indentInfo['tag'],
$expected,
$space,
);
$this->currentFile->addError($error, $indentInfo['errorPos'], 'TagIndent', $data);
}
}
}//end processTags()
/**
* Get the indentation information of each tag.
*
* @param string $tagName The name of the doc comment element.
* @param PHP_CodeSniffer_CommentParser_DocElement $tagElement The doc comment element.
*
* @return void
*/
protected function getIndentation($tagName, $tagElement)
{
if ($tagElement instanceof PHP_CodeSniffer_CommentParser_SingleElement) {
if ($tagElement->getContent() !== '') {
return (strlen($tagName) + substr_count($tagElement->getWhitespaceBeforeContent(), ' '));
}
} else if ($tagElement instanceof PHP_CodeSniffer_CommentParser_PairElement) {
if ($tagElement->getValue() !== '') {
return (strlen($tagName) + substr_count($tagElement->getWhitespaceBeforeValue(), ' '));
}
}
return 0;
}//end getIndentation()
/**
* The package name must be camel-cased.
*
* @param int $errorPos The line number where the error occurs.
*
* @return void
*/
protected function processPackage($errorPos)
{
$package = $this->commentParser->getPackage();
if ($package !== null) {
$content = $package->getContent();
if (empty($content) === true) {
$error = 'Content missing for @package tag in file comment';
$this->currentFile->addError($error, $errorPos, 'MissingPackage');
} else if (PHP_CodeSniffer::isUnderscoreName($content) !== true) {
// Package name must be properly camel-cased.
$nameBits = explode('_', str_replace(' ', '', $content));
$firstBit = array_shift($nameBits);
$newName = strtoupper($firstBit{0}).substr($firstBit, 1).'_';
foreach ($nameBits as $bit) {
$newName .= strtoupper($bit{0}).substr($bit, 1).'_';
}
$error = 'Package name "%s" is not valid; consider "%s" instead';
$data = array(
$content,
trim($newName, '_'),
);
$this->currentFile->addError($error, $errorPos, 'IncorrectPackage', $data);
} else if (strpos($content, 'Squiz') === 0) {
// Package name must not start with Squiz.
$newName = substr($content, 5);
$error = 'Package name "%s" is not valid; consider "%s" instead';
$data = array(
$content,
$newName,
);
$this->currentFile->addError($error, $errorPos, 'SquizPackage', $data);
}
}
}//end processPackage()
/**
* The subpackage name must be camel-cased.
*
* @param int $errorPos The line number where the error occurs.
*
* @return void
*/
protected function processSubpackage($errorPos)
{
$subpackage = $this->commentParser->getSubpackage();
if ($subpackage !== null) {
$content = $subpackage->getContent();
if (empty($content) === true) {
$error = 'Content missing for @subpackage tag in file comment';
$this->currentFile->addError($error, $errorPos, 'MissingSubpackage');
} else if (PHP_CodeSniffer::isUnderscoreName($content) !== true) {
// Subpackage name must be properly camel-cased.
$nameBits = explode('_', $content);
$firstBit = array_shift($nameBits);
$newName = strtoupper($firstBit{0}).substr($firstBit, 1).'_';
foreach ($nameBits as $bit) {
$newName .= strtoupper($bit{0}).substr($bit, 1).'_';
}
$error = 'Subpackage name "%s" is not valid; consider "%s" instead';
$data = array(
$content,
trim($newName, '_'),
);
$this->currentFile->addError($error, $errorPos, 'IncorrectSubpackage', $data);
}
}
}//end processSubpackage()
/**
* Author tag must be 'Squiz Pty Ltd <mysource4@squiz.net>'.
*
* @param int $errorPos The line number where the error occurs.
*
* @return void
*/
protected function processAuthors($errorPos)
{
$authors = $this->commentParser->getAuthors();
if (empty($authors) === false) {
$author = $authors[0];
$content = $author->getContent();
if (empty($content) === true) {
$error = 'Content missing for @author tag in file comment';
$this->currentFile->addError($error, $errorPos, 'MissingAuthor');
} else if ($content !== 'Squiz Pty Ltd <products@squiz.net>') {
$error = 'Expected "Squiz Pty Ltd <products@squiz.net>" for author tag';
$this->currentFile->addError($error, $errorPos, 'IncorrectAuthor');
}
}
}//end processAuthors()
/**
* Copyright tag must be in the form '2006-YYYY Squiz Pty Ltd (ABN 77 084 670 600)'.
*
* @param int $errorPos The line number where the error occurs.
*
* @return void
*/
protected function processCopyrights($errorPos)
{
$copyrights = $this->commentParser->getCopyrights();
$copyright = $copyrights[0];
if ($copyright !== null) {
$content = $copyright->getContent();
if (empty($content) === true) {
$error = 'Content missing for @copyright tag in file comment';
$this->currentFile->addError($error, $errorPos, 'MissingCopyright');
} else if (preg_match('/^([0-9]{4})(-[0-9]{4})? (Squiz Pty Ltd \(ACN 084 670 600\))$/', $content) === 0) {
$error = 'Expected "xxxx-xxxx Squiz Pty Ltd (ACN 084 670 600)" for copyright declaration';
$this->currentFile->addError($error, $errorPos, 'IncorrectCopyright');
}
}
}//end processCopyrights()
/**
* License tag must be 'http://matrix.squiz.net/licence Squiz.Net Open Source Licence'.
*
* @param int $errorPos The line number where the error occurs.
*
* @return void
*/
protected function processLicense($errorPos)
{
$license = $this->commentParser->getLicense();
if ($license !== null) {
$url = $license->getValue();
$content = $license->getComment();
if (empty($url) === true && empty($content) === true) {
$error = 'Content missing for @license tag in file comment';
$this->currentFile->addError($error, $errorPos, 'MissingLicense');
} else {
// Check for license URL.
if (empty($url) === true) {
$error = 'License URL missing for @license tag in file comment';
$this->currentFile->addError($error, $errorPos, 'MissingLinceseURL');
} else if ($url !== 'http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt') {
$error = 'Expected "http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt" for license URL';
$this->currentFile->addError($error, $errorPos, 'IncorrectLicenseURL');
}
// Check for license name.
if (empty($content) === true) {
$error = 'License name missing for @license tag in file comment';
$this->currentFile->addError($error, $errorPos, 'MissingLinceseName');
} else if ($content !== 'GPLv2') {
$error = 'Expected "GPLv2" for license name';
$this->currentFile->addError($error, $errorPos, 'IncorrectLicenseName');
}
}//end if
}//end if
}//end processLicense()
}//end class
?>

View File

@@ -0,0 +1,806 @@
<?php
/**
* Parses and verifies the doc comments for functions.
*
* PHP version 5
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 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
*/
if (class_exists('PHP_CodeSniffer_CommentParser_FunctionCommentParser', true) === false) {
$error = 'Class PHP_CodeSniffer_CommentParser_FunctionCommentParser not found';
throw new PHP_CodeSniffer_Exception($error);
}
/**
* Parses and verifies the doc comments for functions.
*
* Verifies that :
* <ul>
* <li>A comment exists</li>
* <li>There is a blank newline after the short description</li>
* <li>There is a blank newline between the long and short description</li>
* <li>There is a blank newline between the long description and tags</li>
* <li>Parameter names represent those in the method</li>
* <li>Parameter comments are in the correct order</li>
* <li>Parameter comments are complete</li>
* <li>A type hint is provided for array and custom class</li>
* <li>Type hint matches the actual variable/class type</li>
* <li>A blank line is present before the first and after the last parameter</li>
* <li>A return type exists</li>
* <li>Any throw tag must have a comment</li>
* <li>The tag order and indentation are correct</li>
* </ul>
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 Squiz Pty Ltd (ABN 77 084 670 600)
* @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
* @version Release: 1.3.3
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class Squiz_Sniffs_Commenting_FunctionCommentSniff implements PHP_CodeSniffer_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 index of the current tag we are processing.
*
* @var int
*/
private $_tagIndex = 0;
/**
* The function comment parser for the current method.
*
* @var PHP_CodeSniffer_Comment_Parser_FunctionCommentParser
*/
protected $commentParser = null;
/**
* The current PHP_CodeSniffer_File object we are processing.
*
* @var PHP_CodeSniffer_File
*/
protected $currentFile = null;
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_FUNCTION);
}//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)
{
$this->currentFile = $phpcsFile;
$tokens = $phpcsFile->getTokens();
$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');
}
return;
} else if ($code !== T_DOC_COMMENT) {
$error = 'Missing function doc comment';
$phpcsFile->addError($error, $stackPtr, 'Missing');
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;
$this->_classToken = null;
foreach ($tokens[$stackPtr]['conditions'] as $condPtr => $condition) {
if ($condition === T_CLASS || $condition === T_INTERFACE) {
$this->_classToken = $condPtr;
break;
}
}
// Find the first doc comment.
$commentStart = ($phpcsFile->findPrevious(T_DOC_COMMENT, ($commentEnd - 1), null, true) + 1);
$commentString = $phpcsFile->getTokensAsString($commentStart, ($commentEnd - $commentStart + 1));
$this->_methodName = $phpcsFile->getDeclarationName($stackPtr);
try {
$this->commentParser = new PHP_CodeSniffer_CommentParser_FunctionCommentParser($commentString, $phpcsFile);
$this->commentParser->parse();
} catch (PHP_CodeSniffer_CommentParser_ParserException $e) {
$line = ($e->getLineWithinComment() + $commentStart);
$phpcsFile->addError($e->getMessage(), $line, 'FailedParse');
return;
}
$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');
}
$this->processParams($commentStart, $commentEnd);
$this->processSees($commentStart);
$this->processReturn($commentStart, $commentEnd);
$this->processThrows($commentStart);
// 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) {
$error = 'Extra newline(s) found before function comment short description';
$phpcsFile->addError($error, ($commentStart + 1), 'SpacingBeforeShort');
}
$newlineCount = (substr_count($short, $phpcsFile->eolChar) + 1);
// 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), 'SpacingBetween');
}
$newlineCount += $newlineBetween;
$testLong = trim($long);
if (preg_match('|[A-Z]|', $testLong[0]) === 0) {
$error = 'Function comment long description must start with a capital letter';
$phpcsFile->addError($error, ($commentStart + $newlineCount), 'LongNotCapital');
}
}//end if
// Exactly one blank line before tags.
$params = $this->commentParser->getTagOrders();
if (count($params) > 1) {
$newlineSpan = $comment->getNewlineAfter();
if ($newlineSpan !== 2) {
$error = 'There must be exactly one blank line before the tags in function comment';
if ($long !== '') {
$newlineCount += (substr_count($long, $phpcsFile->eolChar) - $newlineSpan + 1);
}
$phpcsFile->addError($error, ($commentStart + $newlineCount), 'SpacingBeforeTags');
$short = rtrim($short, $phpcsFile->eolChar.' ');
}
}
// 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';
$phpcsFile->addError($error, ($commentStart + 1), 'ShortSingleLine');
}
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 ($lastChar !== '.') {
$error = 'Function comment short description must end with a full stop';
$phpcsFile->addError($error, ($commentStart + 1), 'ShortFullStop');
}
// Check for unknown/deprecated tags.
$this->processUnknownTags($commentStart, $commentEnd);
// The last content should be a newline and the content before
// that should not be blank. If there is more blank space
// then they have additional blank lines at the end of the comment.
$words = $this->commentParser->getWords();
$lastPos = (count($words) - 1);
if (trim($words[($lastPos - 1)]) !== ''
|| strpos($words[($lastPos - 1)], $this->currentFile->eolChar) === false
|| trim($words[($lastPos - 2)]) === ''
) {
$error = 'Additional blank lines found at end of function comment';
$this->currentFile->addError($error, $commentEnd, 'SpacingAfter');
}
}//end process()
/**
* Process the see tags.
*
* @param int $commentStart The position in the stack where the comment started.
*
* @return void
*/
protected function processSees($commentStart)
{
$sees = $this->commentParser->getSees();
if (empty($sees) === false) {
$tagOrder = $this->commentParser->getTagOrders();
$index = array_keys($this->commentParser->getTagOrders(), 'see');
foreach ($sees as $i => $see) {
$errorPos = ($commentStart + $see->getLine());
$since = array_keys($tagOrder, 'since');
if (count($since) === 1 && $this->_tagIndex !== 0) {
$this->_tagIndex++;
if ($index[$i] !== $this->_tagIndex) {
$error = 'The @see tag is in the wrong order; the tag precedes @return';
$this->currentFile->addError($error, $errorPos, 'SeeOrder');
}
}
$content = $see->getContent();
if (empty($content) === true) {
$error = 'Content missing for @see tag in function comment';
$this->currentFile->addError($error, $errorPos, 'EmptySee');
continue;
}
$spacing = substr_count($see->getWhitespaceBeforeContent(), ' ');
if ($spacing !== 4) {
$error = '@see tag indented incorrectly; expected 4 spaces but found %s';
$data = array($spacing);
$this->currentFile->addError($error, $errorPos, 'SeeIndent', $data);
}
}//end foreach
}//end if
}//end processSees()
/**
* Process the return comment of this function comment.
*
* @param int $commentStart The position in the stack where the comment started.
* @param int $commentEnd The position in the stack where the comment ended.
*
* @return void
*/
protected function processReturn($commentStart, $commentEnd)
{
// Skip constructor and destructor.
$className = '';
if ($this->_classToken !== null) {
$className = $this->currentFile->getDeclarationName($this->_classToken);
$className = strtolower(ltrim($className, '_'));
}
$methodName = strtolower(ltrim($this->_methodName, '_'));
$isSpecialMethod = ($this->_methodName === '__construct' || $this->_methodName === '__destruct');
$return = $this->commentParser->getReturn();
if ($isSpecialMethod === false && $methodName !== $className) {
if ($return !== null) {
$tagOrder = $this->commentParser->getTagOrders();
$index = array_keys($tagOrder, 'return');
$errorPos = ($commentStart + $return->getLine());
$content = trim($return->getRawContent());
if (count($index) > 1) {
$error = 'Only 1 @return tag is allowed in function comment';
$this->currentFile->addError($error, $errorPos, 'DuplicateReturn');
return;
}
$since = array_keys($tagOrder, 'since');
if (count($since) === 1 && $this->_tagIndex !== 0) {
$this->_tagIndex++;
if ($index[0] !== $this->_tagIndex) {
$error = 'The @return tag is in the wrong order; the tag follows @see (if used)';
$this->currentFile->addError($error, $errorPos, 'ReturnOrder');
}
}
if (empty($content) === true) {
$error = 'Return type missing for @return tag in function comment';
$this->currentFile->addError($error, $errorPos, 'MissingReturnType');
} else {
// Check return type (can be multiple, separated by '|').
$typeNames = explode('|', $content);
$suggestedNames = array();
foreach ($typeNames as $i => $typeName) {
$suggestedName = PHP_CodeSniffer::suggestType($typeName);
if (in_array($suggestedName, $suggestedNames) === false) {
$suggestedNames[] = $suggestedName;
}
}
$suggestedType = implode('|', $suggestedNames);
if ($content !== $suggestedType) {
$error = 'Function return type "%s" is invalid';
$data = array($content);
$this->currentFile->addError($error, $errorPos, 'InvalidReturn', $data);
}
$tokens = $this->currentFile->getTokens();
// If the return type is void, make sure there is
// no return statement in the function.
if ($content === 'void') {
if (isset($tokens[$this->_functionToken]['scope_closer']) === true) {
$endToken = $tokens[$this->_functionToken]['scope_closer'];
$returnToken = $this->currentFile->findNext(T_RETURN, $this->_functionToken, $endToken);
if ($returnToken !== false) {
// If the function is not returning anything, just
// exiting, then there is no problem.
$semicolon = $this->currentFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true);
if ($tokens[$semicolon]['code'] !== T_SEMICOLON) {
$error = 'Function return type is void, but function contains return statement';
$this->currentFile->addError($error, $errorPos, 'InvalidReturnVoid');
}
}
}
} else if ($content !== 'mixed') {
// If return type is not void, there needs to be a
// returns statement somewhere in the function that
// returns something.
if (isset($tokens[$this->_functionToken]['scope_closer']) === true) {
$endToken = $tokens[$this->_functionToken]['scope_closer'];
$returnToken = $this->currentFile->findNext(T_RETURN, $this->_functionToken, $endToken);
if ($returnToken === false) {
$error = 'Function return type is not void, but function has no return statement';
$this->currentFile->addError($error, $errorPos, 'InvalidNoReturn');
} else {
$semicolon = $this->currentFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true);
if ($tokens[$semicolon]['code'] === T_SEMICOLON) {
$error = 'Function return type is not void, but function is returning void here';
$this->currentFile->addError($error, $returnToken, 'InvalidReturnNotVoid');
}
}
}
}//end if
$spacing = substr_count($return->getWhitespaceBeforeValue(), ' ');
if ($spacing !== 1) {
$error = '@return tag indented incorrectly; expected 1 space but found %s';
$data = array($spacing);
$this->currentFile->addError($error, $errorPos, 'ReturnIndent', $data);
}
}//end if
} else {
$error = 'Missing @return tag in function comment';
$this->currentFile->addError($error, $commentEnd, 'MissingReturn');
}//end if
} else {
// No return tag for constructor and destructor.
if ($return !== null) {
$errorPos = ($commentStart + $return->getLine());
$error = '@return tag is not required for constructor and destructor';
$this->currentFile->addError($error, $errorPos, 'ReturnNotRequired');
}
}//end if
}//end processReturn()
/**
* Process any throw tags that this function comment has.
*
* @param int $commentStart The position in the stack where the comment started.
*
* @return void
*/
protected function processThrows($commentStart)
{
if (count($this->commentParser->getThrows()) === 0) {
return;
}
$tagOrder = $this->commentParser->getTagOrders();
$index = array_keys($this->commentParser->getTagOrders(), 'throws');
foreach ($this->commentParser->getThrows() as $i => $throw) {
$exception = $throw->getValue();
$content = trim($throw->getComment());
$errorPos = ($commentStart + $throw->getLine());
if (empty($exception) === true) {
$error = 'Exception type and comment missing for @throws tag in function comment';
$this->currentFile->addError($error, $errorPos, 'InvalidThrows');
} else if (empty($content) === true) {
$error = 'Comment missing for @throws tag in function comment';
$this->currentFile->addError($error, $errorPos, 'EmptyThrows');
} else {
// Starts with a capital letter and ends with a fullstop.
$firstChar = $content{0};
if (strtoupper($firstChar) !== $firstChar) {
$error = '@throws tag comment must start with a capital letter';
$this->currentFile->addError($error, $errorPos, 'ThrowsNotCapital');
}
$lastChar = $content[(strlen($content) - 1)];
if ($lastChar !== '.') {
$error = '@throws tag comment must end with a full stop';
$this->currentFile->addError($error, $errorPos, 'ThrowsNoFullStop');
}
}
$since = array_keys($tagOrder, 'since');
if (count($since) === 1 && $this->_tagIndex !== 0) {
$this->_tagIndex++;
if ($index[$i] !== $this->_tagIndex) {
$error = 'The @throws tag is in the wrong order; the tag follows @return';
$this->currentFile->addError($error, $errorPos, 'ThrowsOrder');
}
}
}//end foreach
}//end processThrows()
/**
* 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.
*
* @return void
*/
protected function processParams($commentStart, $commentEnd)
{
$realParams = $this->currentFile->getMethodParameters($this->_functionToken);
$params = $this->commentParser->getParams();
$foundParams = array();
if (empty($params) === false) {
if (substr_count($params[(count($params) - 1)]->getWhitespaceAfter(), $this->currentFile->eolChar) !== 2) {
$error = 'Last parameter comment requires a blank newline after it';
$errorPos = ($params[(count($params) - 1)]->getLine() + $commentStart);
$this->currentFile->addError($error, $errorPos, 'SpacingAfterParams');
}
// Parameters must appear immediately after the comment.
if ($params[0]->getOrder() !== 2) {
$error = 'Parameters must appear immediately after the comment';
$errorPos = ($params[0]->getLine() + $commentStart);
$this->currentFile->addError($error, $errorPos, 'SpacingBeforeParams');
}
$previousParam = null;
$spaceBeforeVar = 10000;
$spaceBeforeComment = 10000;
$longestType = 0;
$longestVar = 0;
foreach ($params as $param) {
$paramComment = trim($param->getComment());
$errorPos = ($param->getLine() + $commentStart);
// 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');
}
$spaceCount = substr_count($param->getWhitespaceBeforeVarName(), ' ');
if ($spaceCount < $spaceBeforeVar) {
$spaceBeforeVar = $spaceCount;
$longestType = $errorPos;
}
$spaceCount = substr_count($param->getWhitespaceBeforeComment(), ' ');
if ($spaceCount < $spaceBeforeComment && $paramComment !== '') {
$spaceBeforeComment = $spaceCount;
$longestVar = $errorPos;
}
// Make sure they are in the correct order, and have the correct name.
$pos = $param->getPosition();
$paramName = ($param->getVarName() !== '') ? $param->getVarName() : '[ UNKNOWN ]';
if ($previousParam !== null) {
$previousName = ($previousParam->getVarName() !== '') ? $previousParam->getVarName() : 'UNKNOWN';
// Check to see if the parameters align properly.
if ($param->alignsVariableWith($previousParam) === false) {
$error = 'The variable names for parameters %s (%s) and %s (%s) do not align';
$data = array(
$previousName,
($pos - 1),
$paramName,
$pos,
);
$this->currentFile->addError($error, $errorPos, 'ParameterNamesNotAligned', $data);
}
if ($param->alignsCommentWith($previousParam) === false) {
$error = 'The comments for parameters %s (%s) and %s (%s) do not align';
$data = array(
$previousName,
($pos - 1),
$paramName,
$pos,
);
$this->currentFile->addError($error, $errorPos, 'ParameterCommentsNotAligned', $data);
}
}
// Variable must be one of the supported standard type.
$typeNames = explode('|', $param->getType());
foreach ($typeNames as $typeName) {
$suggestedName = PHP_CodeSniffer::suggestType($typeName);
if ($typeName !== $suggestedName) {
$error = 'Expected "%s"; found "%s" for %s at position %s';
$data = array(
$suggestedName,
$typeName,
$paramName,
$pos,
);
$this->currentFile->addError($error, $errorPos, 'IncorrectParamVarName', $data);
} else if (count($typeNames) === 1) {
// Check type hint for array and custom type.
$suggestedTypeHint = '';
if (strpos($suggestedName, 'array') !== false) {
$suggestedTypeHint = 'array';
} else if (in_array($typeName, PHP_CodeSniffer::$allowedTypes) === false) {
$suggestedTypeHint = $suggestedName;
}
if ($suggestedTypeHint !== '' && isset($realParams[($pos - 1)]) === true) {
$typeHint = $realParams[($pos - 1)]['type_hint'];
if ($typeHint === '') {
$error = 'Type hint "%s" missing for %s at position %s';
$data = array(
$suggestedTypeHint,
$paramName,
$pos,
);
$this->currentFile->addError($error, ($commentEnd + 2), 'TypeHintMissing', $data);
} else if ($typeHint !== $suggestedTypeHint) {
$error = 'Expected type hint "%s"; found "%s" for %s at position %s';
$data = array(
$suggestedTypeHint,
$typeHint,
$paramName,
$pos,
);
$this->currentFile->addError($error, ($commentEnd + 2), 'IncorrectTypeHint', $data);
}
} else if ($suggestedTypeHint === '' && isset($realParams[($pos - 1)]) === true) {
$typeHint = $realParams[($pos - 1)]['type_hint'];
if ($typeHint !== '') {
$error = 'Unknown type hint "%s" found for %s at position %s';
$data = array(
$typeHint,
$paramName,
$pos,
);
$this->currentFile->addError($error, ($commentEnd + 2), 'InvalidTypeHint', $data);
}
}
}//end if
}//end foreach
// Make sure the names of the parameter comment matches the
// actual parameter.
if (isset($realParams[($pos - 1)]) === true) {
$realName = $realParams[($pos - 1)]['name'];
$foundParams[] = $realName;
// Append ampersand to name if passing by reference.
if ($realParams[($pos - 1)]['pass_by_reference'] === true) {
$realName = '&'.$realName;
}
if ($realName !== $paramName) {
$code = 'ParamNameNoMatch';
$data = array(
$paramName,
$realName,
$pos,
);
$error = 'Doc comment for var %s does not match ';
if (strtolower($paramName) === strtolower($realName)) {
$error .= 'case of ';
$code = 'ParamNameNoCaseMatch';
}
$error .= 'actual variable name %s at position %s';
$this->currentFile->addError($error, $errorPos, $code, $data);
}
} else {
// We must have an extra parameter comment.
$error = 'Superfluous doc comment at position '.$pos;
$this->currentFile->addError($error, $errorPos, 'ExtraParamComment');
}
if ($param->getVarName() === '') {
$error = 'Missing parameter name at position '.$pos;
$this->currentFile->addError($error, $errorPos, 'MissingParamName');
}
if ($param->getType() === '') {
$error = 'Missing type at position '.$pos;
$this->currentFile->addError($error, $errorPos, 'MissingParamType');
}
if ($paramComment === '') {
$error = 'Missing comment for param "%s" at position %s';
$data = array(
$paramName,
$pos,
);
$this->currentFile->addError($error, $errorPos, 'MissingParamComment', $data);
} else {
// Param comments must start with a capital letter and
// end with the full stop.
$firstChar = $paramComment{0};
if (preg_match('|[A-Z]|', $firstChar) === 0) {
$error = 'Param comment must start with a capital letter';
$this->currentFile->addError($error, $errorPos, 'ParamCommentNotCapital');
}
$lastChar = $paramComment[(strlen($paramComment) - 1)];
if ($lastChar !== '.') {
$error = 'Param comment must end with a full stop';
$this->currentFile->addError($error, $errorPos, 'ParamCommentFullStop');
}
}
$previousParam = $param;
}//end foreach
if ($spaceBeforeVar !== 1 && $spaceBeforeVar !== 10000 && $spaceBeforeComment !== 10000) {
$error = 'Expected 1 space after the longest type';
$this->currentFile->addError($error, $longestType, 'SpacingAfterLongType');
}
if ($spaceBeforeComment !== 1 && $spaceBeforeComment !== 10000) {
$error = 'Expected 1 space after the longest variable name';
$this->currentFile->addError($error, $longestVar, 'SpacingAfterLongName');
}
}//end if
$realNames = array();
foreach ($realParams as $realParam) {
$realNames[] = $realParam['name'];
}
// Report missing comments.
$diff = array_diff($realNames, $foundParams);
foreach ($diff as $neededParam) {
if (count($params) !== 0) {
$errorPos = ($params[(count($params) - 1)]->getLine() + $commentStart);
} else {
$errorPos = $commentStart;
}
$error = 'Doc comment for "%s" missing';
$data = array($neededParam);
$this->currentFile->addError($error, $errorPos, 'MissingParamTag', $data);
}
}//end processParams()
/**
* Process a list of unknown tags.
*
* @param int $commentStart The position in the stack where the comment started.
* @param int $commentEnd The position in the stack where the comment ended.
*
* @return void
*/
protected function processUnknownTags($commentStart, $commentEnd)
{
$unknownTags = $this->commentParser->getUnknown();
foreach ($unknownTags as $errorTag) {
$error = '@%s tag is not allowed in function comment';
$data = array($errorTag['tag']);
$this->currentFile->addWarning($error, ($commentStart + $errorTag['line']), 'TagNotAllowed', $data);
}
}//end processUnknownTags
}//end class
?>

View File

@@ -0,0 +1,215 @@
<?php
/**
* Verifies that a @throws tag exists for a function that throws exceptions.
* Verifies the number of @throws tags and the number of throw tokens matches.
* Verifies the exception type.
*
* PHP version 5
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 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
*/
if (class_exists('PHP_CodeSniffer_Standards_AbstractScopeSniff', true) === false) {
$error = 'Class PHP_CodeSniffer_Standards_AbstractScopeSniff not found';
throw new PHP_CodeSniffer_Exception($error);
}
/**
* Verifies that a @throws tag exists for a function that throws exceptions.
* Verifies the number of @throws tags and the number of throw tokens matches.
* Verifies the exception type.
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 Squiz Pty Ltd (ABN 77 084 670 600)
* @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
* @version Release: 1.3.3
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class Squiz_Sniffs_Commenting_FunctionCommentThrowTagSniff extends PHP_CodeSniffer_Standards_AbstractScopeSniff
{
/**
* Constructs a Squiz_Sniffs_Commenting_FunctionCommentThrowTagSniff.
*/
public function __construct()
{
parent::__construct(array(T_FUNCTION), array(T_THROW));
}//end __construct()
/**
* Processes the function tokens within the class.
*
* @param PHP_CodeSniffer_File $phpcsFile The file where this token was found.
* @param int $stackPtr The position where the token was found.
* @param int $currScope The current scope opener token.
*
* @return void
*/
protected function processTokenWithinScope(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $currScope)
{
// Is this the first throw token within the current function scope?
// If so, we have to validate other throw tokens within the same scope.
$previousThrow = $phpcsFile->findPrevious(T_THROW, ($stackPtr - 1), $currScope);
if ($previousThrow !== false) {
return;
}
$tokens = $phpcsFile->getTokens();
$find = array(
T_COMMENT,
T_DOC_COMMENT,
T_CLASS,
T_FUNCTION,
T_OPEN_TAG,
);
$commentEnd = $phpcsFile->findPrevious($find, ($currScope - 1));
if ($commentEnd === false) {
return;
}
if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT) {
// Function doesn't have a comment. Let someone else warn about that.
return;
}
$commentStart = ($phpcsFile->findPrevious(T_DOC_COMMENT, ($commentEnd - 1), null, true) + 1);
$comment = $phpcsFile->getTokensAsString($commentStart, ($commentEnd - $commentStart + 1));
try {
$this->commentParser = new PHP_CodeSniffer_CommentParser_FunctionCommentParser($comment, $phpcsFile);
$this->commentParser->parse();
} catch (PHP_CodeSniffer_CommentParser_ParserException $e) {
$line = ($e->getLineWithinComment() + $commentStart);
$phpcsFile->addError($e->getMessage(), $line, 'FailedParse');
return;
}
// Find the position where the current function scope ends.
$currScopeEnd = 0;
if (isset($tokens[$currScope]['scope_closer']) === true) {
$currScopeEnd = $tokens[$currScope]['scope_closer'];
}
// Find all the exception type token within the current scope.
$throwTokens = array();
$currPos = $stackPtr;
if ($currScopeEnd !== 0) {
while ($currPos < $currScopeEnd && $currPos !== false) {
/*
If we can't find a NEW, we are probably throwing
a variable, so we ignore it, but they still need to
provide at least one @throws tag, even through we
don't know the exception class.
*/
$nextToken = $phpcsFile->findNext(T_WHITESPACE, ($currPos + 1), null, true);
if ($tokens[$nextToken]['code'] === T_NEW) {
$currException = $phpcsFile->findNext(
array(
T_NS_SEPARATOR,
T_STRING,
),
$currPos,
$currScopeEnd,
false,
null,
true
);
if ($currException !== false) {
$endException = $phpcsFile->findNext(
array(
T_NS_SEPARATOR,
T_STRING,
),
($currException + 1),
$currScopeEnd,
true,
null,
true
);
if ($endException === false) {
$throwTokens[] = $tokens[$currException]['content'];
} else {
$throwTokens[] = $phpcsFile->getTokensAsString($currException, ($endException - $currException));
}
}//end if
}//end if
$currPos = $phpcsFile->findNext(T_THROW, ($currPos + 1), $currScopeEnd);
}//end while
}//end if
// Only need one @throws tag for each type of exception thrown.
$throwTokens = array_unique($throwTokens);
sort($throwTokens);
$throws = $this->commentParser->getThrows();
if (empty($throws) === true) {
$error = 'Missing @throws tag in function comment';
$phpcsFile->addError($error, $commentEnd, 'Missing');
} else if (empty($throwTokens) === true) {
// If token count is zero, it means that only variables are being
// thrown, so we need at least one @throws tag (checked above).
// Nothing more to do.
return;
} else {
$throwTags = array();
$lineNumber = array();
foreach ($throws as $throw) {
$throwTags[] = $throw->getValue();
$lineNumber[$throw->getValue()] = $throw->getLine();
}
$throwTags = array_unique($throwTags);
sort($throwTags);
// Make sure @throws tag count matches throw token count.
$tokenCount = count($throwTokens);
$tagCount = count($throwTags);
if ($tokenCount !== $tagCount) {
$error = 'Expected %s @throws tag(s) in function comment; %s found';
$data = array(
$tokenCount,
$tagCount,
);
$phpcsFile->addError($error, $commentEnd, 'WrongNumber', $data);
return;
} else {
// Exception type in @throws tag must be thrown in the function.
foreach ($throwTags as $i => $throwTag) {
$errorPos = ($commentStart + $lineNumber[$throwTag]);
if (empty($throwTag) === false && $throwTag !== $throwTokens[$i]) {
$error = 'Expected "%s" but found "%s" for @throws tag exception';
$data = array(
$throwTokens[$i],
$throwTag,
);
$phpcsFile->addError($error, $errorPos, 'WrongType', $data);
}
}
}
}//end if
}//end processTokenWithinScope()
}//end class
?>

View File

@@ -0,0 +1,271 @@
<?php
/**
* Squiz_Sniffs_Commenting_InlineCommentSniff.
*
* PHP version 5
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 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
*/
/**
* Squiz_Sniffs_Commenting_InlineCommentSniff.
*
* Checks that there is adequate spacing between comments.
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 Squiz Pty Ltd (ABN 77 084 670 600)
* @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
* @version Release: 1.3.3
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class Squiz_Sniffs_Commenting_InlineCommentSniff implements PHP_CodeSniffer_Sniff
{
/**
* A list of tokenizers this sniff supports.
*
* @var array
*/
public $supportedTokenizers = array(
'PHP',
'JS',
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(
T_COMMENT,
T_DOC_COMMENT,
);
}//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();
// 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) {
$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_FINAL,
T_STATIC,
T_ABSTRACT,
T_CONST,
T_OBJECT,
T_PROPERTY,
);
if (in_array($tokens[$nextToken]['code'], $ignore) === true) {
return;
} else {
if ($phpcsFile->tokenizerType === 'JS') {
// We allow block comments if a function is being assigned
// to a variable.
$ignore = PHP_CodeSniffer_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) {
return;
}
}
$prevToken = $phpcsFile->findPrevious(
PHP_CodeSniffer_Tokens::$emptyTokens,
($stackPtr - 1),
null,
true
);
if ($tokens[$prevToken]['code'] === T_OPEN_TAG) {
return;
}
// Only error once per comment.
if (substr($tokens[$stackPtr]['content'], 0, 3) === '/**') {
$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} === '#') {
$error = 'Perl-style comments are not allowed; use "// Comment" instead';
$phpcsFile->addError($error, $stackPtr, 'WrongStyle');
}
// We don't want end of block comments. If the last 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;
}
}
}
$comment = rtrim($tokens[$stackPtr]['content']);
// Only want inline comments.
if (substr($comment, 0, 2) !== '//') {
return;
}
$spaceCount = 0;
for ($i = 2; $i < strlen($comment); $i++) {
if ($comment[$i] !== ' ') {
break;
}
$spaceCount++;
}
if ($spaceCount === 0) {
$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) {
$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);
}
// 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);
if (($nextComment !== false) && (($tokens[$nextComment]['line']) === ($tokens[$stackPtr]['line'] + 1))) {
return;
}
$topComment = $stackPtr;
$lastComment = $stackPtr;
while (($topComment = $phpcsFile->findPrevious(array(T_COMMENT), ($lastComment - 1), null, false)) !== false) {
if ($tokens[$topComment]['line'] !== ($tokens[$lastComment]['line'] - 1)) {
break;
}
$lastComment = $topComment;
}
$topComment = $lastComment;
$commentText = '';
for ($i = $topComment; $i <= $stackPtr; $i++) {
if ($tokens[$i]['code'] === T_COMMENT) {
$commentText .= trim(substr($tokens[$i]['content'], 2));
}
}
if ($commentText === '') {
$error = 'Blank comments are not allowed';
$phpcsFile->addError($error, $stackPtr, 'Empty');
return;
}
if (preg_match('|[A-Z]|', $commentText[0]) === 0) {
$error = 'Inline comments must start with a capital letter';
$phpcsFile->addError($error, $topComment, 'NotCapital');
}
$commentCloser = $commentText[(strlen($commentText) - 1)];
$acceptedClosers = array(
'full-stops' => '.',
'exclamation marks' => '!',
'or question marks' => '?',
);
if (in_array($commentCloser, $acceptedClosers) === false) {
$error = 'Inline comments must end in %s';
$ender = '';
foreach ($acceptedClosers as $closerName => $symbol) {
$ender .= ' '.$closerName.',';
}
$ender = rtrim($ender, ',');
$data = array($ender);
$phpcsFile->addError($error, $stackPtr, 'InvalidEndChar', $data);
}
// Finally, the line below the last comment cannot be empty.
$start = false;
for ($i = ($stackPtr + 1); $i < $phpcsFile->numTokens; $i++) {
if ($tokens[$i]['line'] === ($tokens[$stackPtr]['line'] + 1)) {
if ($tokens[$i]['code'] !== T_WHITESPACE) {
return;
}
} else if ($tokens[$i]['line'] > ($tokens[$stackPtr]['line'] + 1)) {
break;
}
}
$error = 'There must be no blank line following an inline comment';
$phpcsFile->addError($error, $stackPtr, 'SpacingAfter');
}//end process()
}//end class
?>

View File

@@ -0,0 +1,191 @@
<?php
/**
* Squiz_Sniffs_ControlStructures_LongConditionClosingCommentSniff.
*
* PHP version 5
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 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
*/
/**
* Squiz_Sniffs_ControlStructures_LongConditionClosingCommentSniff.
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 Squiz Pty Ltd (ABN 77 084 670 600)
* @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
* @version Release: 1.3.3
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class Squiz_Sniffs_Commenting_LongConditionClosingCommentSniff implements PHP_CodeSniffer_Sniff
{
/**
* A list of tokenizers this sniff supports.
*
* @var array
*/
public $supportedTokenizers = array(
'PHP',
'JS',
);
/**
* The openers that we are interested in.
*
* @var array(int)
*/
private static $_openers = array(
T_SWITCH,
T_IF,
T_FOR,
T_FOREACH,
T_WHILE,
T_TRY,
T_CASE,
);
/**
* The length that a code block must be before
* requiring a closing comment.
*
* @var int
*/
protected $lineLimit = 20;
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_CLOSE_CURLY_BRACKET);
}//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();
if (isset($tokens[$stackPtr]['scope_condition']) === false) {
// No scope condition. It is a function closer.
return;
}
$startCondition = $tokens[$tokens[$stackPtr]['scope_condition']];
$startBrace = $tokens[$tokens[$stackPtr]['scope_opener']];
$endBrace = $tokens[$stackPtr];
// We are only interested in some code blocks.
if (in_array($startCondition['code'], self::$_openers) === false) {
return;
}
if ($startCondition['code'] === T_IF) {
// If this is actually and ELSE IF, skip it as the brace
// will be checked by the original IF.
$else = $phpcsFile->findPrevious(T_WHITESPACE, ($tokens[$stackPtr]['scope_condition'] - 1), null, true);
if ($tokens[$else]['code'] === T_ELSE) {
return;
}
// IF statements that have an ELSE block need to use
// "end if" rather than "end else" or "end elseif".
do {
$nextToken = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
if ($tokens[$nextToken]['code'] === T_ELSE || $tokens[$nextToken]['code'] === T_ELSEIF) {
// Check for ELSE IF (2 tokens) as opposed to ELSEIF (1 token).
if ($tokens[$nextToken]['code'] === T_ELSE
&& isset($tokens[$nextToken]['scope_closer']) === false
) {
$nextToken = $phpcsFile->findNext(T_WHITESPACE, ($nextToken + 1), null, true);
if ($tokens[$nextToken]['code'] !== T_IF
|| isset($tokens[$nextToken]['scope_closer']) === false
) {
// Not an ELSE IF or is an inline ELSE IF.
break;
}
}
// The end brace becomes the ELSE's end brace.
$stackPtr = $tokens[$nextToken]['scope_closer'];
$endBrace = $tokens[$stackPtr];
} else {
break;
}
} while (isset($tokens[$nextToken]['scope_closer']) === true);
}//end if
if ($startCondition['code'] === T_TRY) {
// TRY statements need to check until the end of all CATCH statements.
do {
$nextToken = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
if ($tokens[$nextToken]['code'] === T_CATCH) {
// The end brace becomes the CATCH's end brace.
$stackPtr = $tokens[$nextToken]['scope_closer'];
$endBrace = $tokens[$stackPtr];
} else {
break;
}
} while (isset($tokens[$nextToken]['scope_closer']) === true);
}
$lineDifference = ($endBrace['line'] - $startBrace['line']);
$expected = '//end '.$startCondition['content'];
$comment = $phpcsFile->findNext(array(T_COMMENT), $stackPtr, null, false);
if (($comment === false) || ($tokens[$comment]['line'] !== $endBrace['line'])) {
if ($lineDifference >= $this->lineLimit) {
$error = 'End comment for long condition not found; expected "%s"';
$data = array($expected);
$phpcsFile->addError($error, $stackPtr, 'Missing', $data);
}
return;
}
if (($comment - $stackPtr) !== 1) {
$error = 'Space found before closing comment; expected "%s"';
$data = array($expected);
$phpcsFile->addError($error, $stackPtr, 'SpacingBefore', $data);
}
if (trim($tokens[$comment]['content']) !== $expected) {
$found = trim($tokens[$comment]['content']);
$error = 'Incorrect closing comment; expected "%s" but found "%s"';
$data = array(
$expected,
$found,
);
$phpcsFile->addError($error, $stackPtr, 'Invalid', $data);
return;
}
}//end process()
}//end class
?>

View File

@@ -0,0 +1,103 @@
<?php
/**
* Squiz_Sniffs_Commenting_PostStatementCommentSniff.
*
* PHP version 5
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 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
*/
/**
* Squiz_Sniffs_Commenting_PostStatementCommentSniff.
*
* Checks to ensure that there are no comments after statements.
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 Squiz Pty Ltd (ABN 77 084 670 600)
* @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
* @version Release: 1.3.3
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class Squiz_Sniffs_Commenting_PostStatementCommentSniff implements PHP_CodeSniffer_Sniff
{
/**
* A list of tokenizers this sniff supports.
*
* @var array
*/
public $supportedTokenizers = array(
'PHP',
'JS',
);
/**
* Returns an array of tokens this test wants to listen for.
*
* @return array
*/
public function register()
{
return array(T_COMMENT);
}//end register()
/**
* 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.
*
* @return void
*/
public function process(PHP_CodeSniffer_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.';
$phpcsFile->addError($error, $stackPtr, 'Found');
}//end process()
}//end class
?>

View File

@@ -0,0 +1,347 @@
<?php
/**
* Parses and verifies the variable doc comment.
*
* PHP version 5
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 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
*/
if (class_exists('PHP_CodeSniffer_Standards_AbstractVariableSniff', true) === false) {
throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_Standards_AbstractVariableSniff not found');
}
if (class_exists('PHP_CodeSniffer_CommentParser_MemberCommentParser', true) === false) {
throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_CommentParser_MemberCommentParser not found');
}
/**
* Parses and verifies the variable doc comment.
*
* Verifies that :
* <ul>
* <li>A variable doc comment exists.</li>
* <li>Short description ends with a full stop.</li>
* <li>There is a blank line after the short description.</li>
* <li>There is a blank line between the description and the tags.</li>
* <li>Check the order, indentation and content of each tag.</li>
* </ul>
*
* @category PHP
* @package PHP_CodeSniffer
* @author Greg Sherwood <gsherwood@squiz.net>
* @author Marc McIntyre <mmcintyre@squiz.net>
* @copyright 2006-2011 Squiz Pty Ltd (ABN 77 084 670 600)
* @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
* @version Release: 1.3.3
* @link http://pear.php.net/package/PHP_CodeSniffer
*/
class Squiz_Sniffs_Commenting_VariableCommentSniff extends PHP_CodeSniffer_Standards_AbstractVariableSniff
{
/**
* The header comment parser for the current file.
*
* @var PHP_CodeSniffer_Comment_Parser_ClassCommentParser
*/
protected $commentParser = null;
/**
* Called to process class member vars.
*
* @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 processMemberVar(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
{
$this->currentFile = $phpcsFile;
$tokens = $phpcsFile->getTokens();
$commentToken = array(
T_COMMENT,
T_DOC_COMMENT,
);
// Extract the var comment docblock.
$commentEnd = $phpcsFile->findPrevious($commentToken, ($stackPtr - 3));
if ($commentEnd !== false && $tokens[$commentEnd]['code'] === T_COMMENT) {
$phpcsFile->addError('You must use "/**" style comments for a variable comment', $stackPtr, 'WrongStyle');
return;
} else if ($commentEnd === false || $tokens[$commentEnd]['code'] !== T_DOC_COMMENT) {
$phpcsFile->addError('Missing variable doc comment', $stackPtr, 'Missing');
return;
} else {
// Make sure the comment we have found belongs to us.
$commentFor = $phpcsFile->findNext(array(T_VARIABLE, T_CLASS, T_INTERFACE), ($commentEnd + 1));
if ($commentFor !== $stackPtr) {
$phpcsFile->addError('Missing variable doc comment', $stackPtr, 'Missing');
return;
}
}
$commentStart = ($phpcsFile->findPrevious(T_DOC_COMMENT, ($commentEnd - 1), null, true) + 1);
$commentString = $phpcsFile->getTokensAsString($commentStart, ($commentEnd - $commentStart + 1));
// Parse the header comment docblock.
try {
$this->commentParser = new PHP_CodeSniffer_CommentParser_MemberCommentParser($commentString, $phpcsFile);
$this->commentParser->parse();
} catch (PHP_CodeSniffer_CommentParser_ParserException $e) {
$line = ($e->getLineWithinComment() + $commentStart);
$phpcsFile->addError($e->getMessage(), $line, 'ErrorParsing');
return;
}
$comment = $this->commentParser->getComment();
if (is_null($comment) === true) {
$error = 'Variable 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');
}
// Check for a comment description.
$short = $comment->getShortComment();
$long = '';
if (trim($short) === '') {
$error = 'Missing short description in variable doc comment';
$phpcsFile->addError($error, $commentStart, 'MissingShort');
$newlineCount = 1;
} else {
// No extra newline before short description.
$newlineCount = 0;
$newlineSpan = strspn($short, $phpcsFile->eolChar);
if ($short !== '' && $newlineSpan > 0) {
$error = 'Extra newline(s) found before variable comment short description';
$phpcsFile->addError($error, ($commentStart + 1), 'SpacingBeforeShort');
}
$newlineCount = (substr_count($short, $phpcsFile->eolChar) + 1);
// 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 variable comment';
$phpcsFile->addError($error, ($commentStart + $newlineCount + 1), 'SpacingBetween');
}
$newlineCount += $newlineBetween;
$testLong = trim($long);
if (preg_match('|[A-Z]|', $testLong[0]) === 0) {
$error = 'Variable comment long description must start with a capital letter';
$phpcsFile->addError($error, ($commentStart + $newlineCount), 'LongNotCapital');
}
}//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 = 'Variable comment short description must be on a single line';
$phpcsFile->addError($error, ($commentStart + 1), 'ShortSingleLine');
}
if (preg_match('|[A-Z]|', $testShort[0]) === 0) {
$error = 'Variable comment short description must start with a capital letter';
$phpcsFile->addError($error, ($commentStart + 1), 'ShortNotCapital');
}
if ($lastChar !== '.') {
$error = 'Variable comment short description must end with a full stop';
$phpcsFile->addError($error, ($commentStart + 1), 'ShortFullStop');
}
}//end if
// Exactly one blank line before tags.
$tags = $this->commentParser->getTagOrders();
if (count($tags) > 1) {
$newlineSpan = $comment->getNewlineAfter();
if ($newlineSpan !== 2) {
$error = 'There must be exactly one blank line before the tags in variable comment';
if ($long !== '') {
$newlineCount += (substr_count($long, $phpcsFile->eolChar) - $newlineSpan + 1);
}
$phpcsFile->addError($error, ($commentStart + $newlineCount), 'SpacingBeforeTags');
$short = rtrim($short, $phpcsFile->eolChar.' ');
}
}
// Check for unknown/deprecated tags.
$unknownTags = $this->commentParser->getUnknown();
foreach ($unknownTags as $errorTag) {
// Unknown tags are not parsed, do not process further.
$error = '@%s tag is not allowed in variable comment';
$data = array($errorTag['tag']);
$phpcsFile->addWarning($error, ($commentStart + $errorTag['line']), 'TagNotAllowed', $data);
}
// Check each tag.
$this->processVar($commentStart, $commentEnd);
$this->processSees($commentStart);
// The last content should be a newline and the content before
// that should not be blank. If there is more blank space
// then they have additional blank lines at the end of the comment.
$words = $this->commentParser->getWords();
$lastPos = (count($words) - 1);
if (trim($words[($lastPos - 1)]) !== ''
|| strpos($words[($lastPos - 1)], $this->currentFile->eolChar) === false
|| trim($words[($lastPos - 2)]) === ''
) {
$error = 'Additional blank lines found at end of variable comment';
$this->currentFile->addError($error, $commentEnd, 'SpacingAfter');
}
}//end processMemberVar()
/**
* Process the var tag.
*
* @param int $commentStart The position in the stack where the comment started.
* @param int $commentEnd The position in the stack where the comment ended.
*
* @return void
*/
protected function processVar($commentStart, $commentEnd)
{
$var = $this->commentParser->getVar();
if ($var !== null) {
$errorPos = ($commentStart + $var->getLine());
$index = array_keys($this->commentParser->getTagOrders(), 'var');
if (count($index) > 1) {
$error = 'Only 1 @var tag is allowed in variable comment';
$this->currentFile->addError($error, $errorPos, 'DuplicateVar');
return;
}
if ($index[0] !== 1) {
$error = 'The @var tag must be the first tag in a variable comment';
$this->currentFile->addError($error, $errorPos, 'VarOrder');
}
$content = $var->getContent();
if (empty($content) === true) {
$error = 'Var type missing for @var tag in variable comment';
$this->currentFile->addError($error, $errorPos, 'MissingVarType');
return;
} else {
$suggestedType = PHP_CodeSniffer::suggestType($content);
if ($content !== $suggestedType) {
$error = 'Expected "%s"; found "%s" for @var tag in variable comment';
$data = array(
$suggestedType,
$content,
);
$this->currentFile->addError($error, $errorPos, 'IncorrectVarType', $data);
}
}
$spacing = substr_count($var->getWhitespaceBeforeContent(), ' ');
if ($spacing !== 1) {
$error = '@var tag indented incorrectly; expected 1 space but found %s';
$data = array($spacing);
$this->currentFile->addError($error, $errorPos, 'VarIndent', $data);
}
} else {
$error = 'Missing @var tag in variable comment';
$this->currentFile->addError($error, $commentEnd, 'MissingVar');
}//end if
}//end processVar()
/**
* Process the see tags.
*
* @param int $commentStart The position in the stack where the comment started.
*
* @return void
*/
protected function processSees($commentStart)
{
$sees = $this->commentParser->getSees();
if (empty($sees) === false) {
foreach ($sees as $see) {
$errorPos = ($commentStart + $see->getLine());
$content = $see->getContent();
if (empty($content) === true) {
$error = 'Content missing for @see tag in variable comment';
$this->currentFile->addError($error, $errorPos, 'EmptySees');
continue;
}
$spacing = substr_count($see->getWhitespaceBeforeContent(), ' ');
if ($spacing !== 1) {
$error = '@see tag indented incorrectly; expected 1 spaces but found %s';
$data = array($spacing);
$this->currentFile->addError($error, $errorPos, 'SeesIndent', $data);
}
}
}
}//end processSees()
/**
* Called to process a normal variable.
*
* Not required for this sniff.
*
* @param PHP_CodeSniffer_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(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
{
return;
}//end processVariable()
/**
* Called to process variables found in duoble quoted strings.
*
* Not required for this sniff.
*
* @param PHP_CodeSniffer_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(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
{
return;
}//end processVariableInString()
}//end class
?>