3
* Parses and verifies the doc comments for functions.
8
* @package PHP_CodeSniffer
9
* @author Greg Sherwood <gsherwood@squiz.net>
10
* @author Marc McIntyre <mmcintyre@squiz.net>
11
* @copyright 2006-2012 Squiz Pty Ltd (ABN 77 084 670 600)
12
* @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
13
* @link http://pear.php.net/package/PHP_CodeSniffer
16
if (class_exists('PHP_CodeSniffer_CommentParser_FunctionCommentParser', true) === false) {
17
throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_CommentParser_FunctionCommentParser not found');
21
* Parses and verifies the doc comments for functions.
25
* <li>A comment exists</li>
26
* <li>There is a blank newline after the short description.</li>
27
* <li>There is a blank newline between the long and short description.</li>
28
* <li>There is a blank newline between the long description and tags.</li>
29
* <li>Parameter names represent those in the method.</li>
30
* <li>Parameter comments are in the correct order</li>
31
* <li>Parameter comments are complete</li>
32
* <li>A space is present before the first and after the last parameter</li>
33
* <li>A return type exists</li>
34
* <li>There must be one blank line between body and headline comments.</li>
35
* <li>Any throw tag must have an exception class.</li>
39
* @package PHP_CodeSniffer
40
* @author Greg Sherwood <gsherwood@squiz.net>
41
* @author Marc McIntyre <mmcintyre@squiz.net>
42
* @copyright 2006-2012 Squiz Pty Ltd (ABN 77 084 670 600)
43
* @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
44
* @version Release: 1.5.0RC2
45
* @link http://pear.php.net/package/PHP_CodeSniffer
47
class PEAR_Sniffs_Commenting_FunctionCommentSniff implements PHP_CodeSniffer_Sniff
51
* The name of the method that we are currently processing.
55
private $_methodName = '';
58
* The position in the stack where the function token was found.
62
private $_functionToken = null;
65
* The position in the stack where the class token was found.
69
private $_classToken = null;
72
* The function comment parser for the current method.
74
* @var PHP_CodeSniffer_Comment_Parser_FunctionCommentParser
76
protected $commentParser = null;
79
* The current PHP_CodeSniffer_File object we are processing.
81
* @var PHP_CodeSniffer_File
83
protected $currentFile = null;
87
* Returns an array of tokens this test wants to listen for.
91
public function register()
93
return array(T_FUNCTION);
99
* Processes this test, when one of its tokens is encountered.
101
* @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
102
* @param int $stackPtr The position of the current token
103
* in the stack passed in $tokens.
107
public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
117
$commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1));
119
if ($commentEnd === false) {
123
$this->currentFile = $phpcsFile;
124
$tokens = $phpcsFile->getTokens();
126
// If the token that we found was a class or a function, then this
127
// function has no doc comment.
128
$code = $tokens[$commentEnd]['code'];
130
if ($code === T_COMMENT) {
131
$error = 'You must use "/**" style comments for a function comment';
132
$phpcsFile->addError($error, $stackPtr, 'WrongStyle');
134
} else if ($code !== T_DOC_COMMENT) {
135
$phpcsFile->addError('Missing function doc comment', $stackPtr, 'Missing');
139
// If there is any code between the function keyword and the doc block
140
// then the doc block is not for us.
141
$ignore = PHP_CodeSniffer_Tokens::$scopeModifiers;
142
$ignore[] = T_STATIC;
143
$ignore[] = T_WHITESPACE;
144
$ignore[] = T_ABSTRACT;
146
$prevToken = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true);
147
if ($prevToken !== $commentEnd) {
148
$phpcsFile->addError('Missing function doc comment', $stackPtr, 'Missing');
152
$this->_functionToken = $stackPtr;
154
$this->_classToken = null;
155
foreach ($tokens[$stackPtr]['conditions'] as $condPtr => $condition) {
156
if ($condition === T_CLASS || $condition === T_INTERFACE) {
157
$this->_classToken = $condPtr;
162
// If the first T_OPEN_TAG is right before the comment, it is probably
164
$commentStart = ($phpcsFile->findPrevious(T_DOC_COMMENT, ($commentEnd - 1), null, true) + 1);
165
$prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($commentStart - 1), null, true);
166
if ($tokens[$prevToken]['code'] === T_OPEN_TAG) {
167
// Is this the first open tag?
168
if ($stackPtr === 0 || $phpcsFile->findPrevious(T_OPEN_TAG, ($prevToken - 1)) === false) {
169
$phpcsFile->addError('Missing function doc comment', $stackPtr, 'Missing');
174
$comment = $phpcsFile->getTokensAsString($commentStart, ($commentEnd - $commentStart + 1));
175
$this->_methodName = $phpcsFile->getDeclarationName($stackPtr);
178
$this->commentParser = new PHP_CodeSniffer_CommentParser_FunctionCommentParser($comment, $phpcsFile);
179
$this->commentParser->parse();
180
} catch (PHP_CodeSniffer_CommentParser_ParserException $e) {
181
$line = ($e->getLineWithinComment() + $commentStart);
182
$phpcsFile->addError($e->getMessage(), $line, 'FailedParse');
186
$comment = $this->commentParser->getComment();
187
if (is_null($comment) === true) {
188
$error = 'Function doc comment is empty';
189
$phpcsFile->addError($error, $commentStart, 'Empty');
193
$this->processParams($commentStart);
194
$this->processReturn($commentStart, $commentEnd);
195
$this->processThrows($commentStart);
197
// No extra newline before short description.
198
$short = $comment->getShortComment();
200
$newlineSpan = strspn($short, $phpcsFile->eolChar);
201
if ($short !== '' && $newlineSpan > 0) {
202
$error = 'Extra newline(s) found before function comment short description';
203
$phpcsFile->addError($error, ($commentStart + 1), 'SpacingBeforeShort');
206
$newlineCount = (substr_count($short, $phpcsFile->eolChar) + 1);
208
// Exactly one blank line between short and long description.
209
$long = $comment->getLongComment();
210
if (empty($long) === false) {
211
$between = $comment->getWhiteSpaceBetween();
212
$newlineBetween = substr_count($between, $phpcsFile->eolChar);
213
if ($newlineBetween !== 2) {
214
$error = 'There must be exactly one blank line between descriptions in function comment';
215
$phpcsFile->addError($error, ($commentStart + $newlineCount + 1), 'SpacingAfterShort');
218
$newlineCount += $newlineBetween;
221
// Exactly one blank line before tags.
222
$params = $this->commentParser->getTagOrders();
223
if (count($params) > 1) {
224
$newlineSpan = $comment->getNewlineAfter();
225
if ($newlineSpan !== 2) {
226
$error = 'There must be exactly one blank line before the tags in function comment';
228
$newlineCount += (substr_count($long, $phpcsFile->eolChar) - $newlineSpan + 1);
231
$phpcsFile->addError($error, ($commentStart + $newlineCount), 'SpacingBeforeTags');
232
$short = rtrim($short, $phpcsFile->eolChar.' ');
240
* Process any throw tags that this function comment has.
242
* @param int $commentStart The position in the stack where the
247
protected function processThrows($commentStart)
249
if (count($this->commentParser->getThrows()) === 0) {
253
foreach ($this->commentParser->getThrows() as $throw) {
255
$exception = $throw->getValue();
256
$errorPos = ($commentStart + $throw->getLine());
258
if ($exception === '') {
259
$error = '@throws tag must contain the exception class name';
260
$this->currentFile->addError($error, $errorPos, 'EmptyThrows');
264
}//end processThrows()
268
* Process the return comment of this function comment.
270
* @param int $commentStart The position in the stack where the comment started.
271
* @param int $commentEnd The position in the stack where the comment ended.
275
protected function processReturn($commentStart, $commentEnd)
277
// Skip constructor and destructor.
279
if ($this->_classToken !== null) {
280
$className = $this->currentFile->getDeclarationName($this->_classToken);
281
$className = strtolower(ltrim($className, '_'));
284
$methodName = strtolower(ltrim($this->_methodName, '_'));
285
$isSpecialMethod = ($this->_methodName === '__construct' || $this->_methodName === '__destruct');
287
if ($isSpecialMethod === false && $methodName !== $className) {
288
// Report missing return tag.
289
if ($this->commentParser->getReturn() === null) {
290
$error = 'Missing @return tag in function comment';
291
$this->currentFile->addError($error, $commentEnd, 'MissingReturn');
292
} else if (trim($this->commentParser->getReturn()->getRawContent()) === '') {
293
$error = '@return tag is empty in function comment';
294
$errorPos = ($commentStart + $this->commentParser->getReturn()->getLine());
295
$this->currentFile->addError($error, $errorPos, 'EmptyReturn');
299
}//end processReturn()
303
* Process the function parameter comments.
305
* @param int $commentStart The position in the stack where
306
* the comment started.
310
protected function processParams($commentStart)
312
$realParams = $this->currentFile->getMethodParameters($this->_functionToken);
314
$params = $this->commentParser->getParams();
315
$foundParams = array();
317
if (empty($params) === false) {
319
$lastParm = (count($params) - 1);
320
if (substr_count($params[$lastParm]->getWhitespaceAfter(), $this->currentFile->eolChar) !== 2) {
321
$error = 'Last parameter comment requires a blank newline after it';
322
$errorPos = ($params[$lastParm]->getLine() + $commentStart);
323
$this->currentFile->addError($error, $errorPos, 'SpacingAfterParams');
326
// Parameters must appear immediately after the comment.
327
if ($params[0]->getOrder() !== 2) {
328
$error = 'Parameters must appear immediately after the comment';
329
$errorPos = ($params[0]->getLine() + $commentStart);
330
$this->currentFile->addError($error, $errorPos, 'SpacingBeforeParams');
333
$previousParam = null;
334
$spaceBeforeVar = 10000;
335
$spaceBeforeComment = 10000;
339
foreach ($params as $param) {
341
$paramComment = trim($param->getComment());
342
$errorPos = ($param->getLine() + $commentStart);
344
// Make sure that there is only one space before the var type.
345
if ($param->getWhitespaceBeforeType() !== ' ') {
346
$error = 'Expected 1 space before variable type';
347
$this->currentFile->addError($error, $errorPos, 'SpacingBeforeParamType');
350
$spaceCount = substr_count($param->getWhitespaceBeforeVarName(), ' ');
351
if ($spaceCount < $spaceBeforeVar) {
352
$spaceBeforeVar = $spaceCount;
353
$longestType = $errorPos;
356
$spaceCount = substr_count($param->getWhitespaceBeforeComment(), ' ');
358
if ($spaceCount < $spaceBeforeComment && $paramComment !== '') {
359
$spaceBeforeComment = $spaceCount;
360
$longestVar = $errorPos;
363
// Make sure they are in the correct order,
364
// and have the correct name.
365
$pos = $param->getPosition();
367
$paramName = ($param->getVarName() !== '') ? $param->getVarName() : '[ UNKNOWN ]';
369
if ($previousParam !== null) {
370
$previousName = ($previousParam->getVarName() !== '') ? $previousParam->getVarName() : 'UNKNOWN';
372
// Check to see if the parameters align properly.
373
if ($param->alignsVariableWith($previousParam) === false) {
374
$error = 'The variable names for parameters %s (%s) and %s (%s) do not align';
381
$this->currentFile->addError($error, $errorPos, 'ParameterNamesNotAligned', $data);
384
if ($param->alignsCommentWith($previousParam) === false) {
385
$error = 'The comments for parameters %s (%s) and %s (%s) do not align';
392
$this->currentFile->addError($error, $errorPos, 'ParameterCommentsNotAligned', $data);
396
// Make sure the names of the parameter comment matches the
398
if (isset($realParams[($pos - 1)]) === true) {
399
$realName = $realParams[($pos - 1)]['name'];
400
$foundParams[] = $realName;
402
// Append ampersand to name if passing by reference.
403
if ($realParams[($pos - 1)]['pass_by_reference'] === true) {
404
$realName = '&'.$realName;
407
if ($realName !== $paramName) {
408
$code = 'ParamNameNoMatch';
415
$error = 'Doc comment for var %s does not match ';
416
if (strtolower($paramName) === strtolower($realName)) {
417
$error .= 'case of ';
418
$code = 'ParamNameNoCaseMatch';
421
$error .= 'actual variable name %s at position %s';
423
$this->currentFile->addError($error, $errorPos, $code, $data);
426
// We must have an extra parameter comment.
427
$error = 'Superfluous doc comment at position '.$pos;
428
$this->currentFile->addError($error, $errorPos, 'ExtraParamComment');
431
if ($param->getVarName() === '') {
432
$error = 'Missing parameter name at position '.$pos;
433
$this->currentFile->addError($error, $errorPos, 'MissingParamName');
436
if ($param->getType() === '') {
437
$error = 'Missing type at position '.$pos;
438
$this->currentFile->addError($error, $errorPos, 'MissingParamType');
441
if ($paramComment === '') {
442
$error = 'Missing comment for param "%s" at position %s';
447
$this->currentFile->addError($error, $errorPos, 'MissingParamComment', $data);
450
$previousParam = $param;
454
if ($spaceBeforeVar !== 1 && $spaceBeforeVar !== 10000 && $spaceBeforeComment !== 10000) {
455
$error = 'Expected 1 space after the longest type';
456
$this->currentFile->addError($error, $longestType, 'SpacingAfterLongType');
459
if ($spaceBeforeComment !== 1 && $spaceBeforeComment !== 10000) {
460
$error = 'Expected 1 space after the longest variable name';
461
$this->currentFile->addError($error, $longestVar, 'SpacingAfterLongName');
466
$realNames = array();
467
foreach ($realParams as $realParam) {
468
$realNames[] = $realParam['name'];
471
// Report and missing comments.
472
$diff = array_diff($realNames, $foundParams);
473
foreach ($diff as $neededParam) {
474
if (count($params) !== 0) {
475
$errorPos = ($params[(count($params) - 1)]->getLine() + $commentStart);
477
$errorPos = $commentStart;
480
$error = 'Doc comment for "%s" missing';
481
$data = array($neededParam);
482
$this->currentFile->addError($error, $errorPos, 'MissingParamTag', $data);
485
}//end processParams()