~ubuntu-branches/ubuntu/trusty/php-codesniffer/trusty

« back to all changes in this revision

Viewing changes to PHP_CodeSniffer-1.5.0RC2/CodeSniffer/Standards/PEAR/Sniffs/Commenting/FunctionCommentSniff.php

  • Committer: Package Import Robot
  • Author(s): Thomas Goirand
  • Date: 2013-07-12 15:16:25 UTC
  • mfrom: (1.1.3)
  • Revision ID: package-import@ubuntu.com-20130712151625-4autdc0twzbueha9
Tags: 1.5.0~rc2-1
* New upstream release.
* Refreshed patch.
* Standards-Version is now 3.9.4.
* Canonical VCS URLs.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
<?php
 
2
/**
 
3
 * Parses and verifies the doc comments for functions.
 
4
 *
 
5
 * PHP version 5
 
6
 *
 
7
 * @category  PHP
 
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
 
14
 */
 
15
 
 
16
if (class_exists('PHP_CodeSniffer_CommentParser_FunctionCommentParser', true) === false) {
 
17
    throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_CommentParser_FunctionCommentParser not found');
 
18
}
 
19
 
 
20
/**
 
21
 * Parses and verifies the doc comments for functions.
 
22
 *
 
23
 * Verifies that :
 
24
 * <ul>
 
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>
 
36
 * </ul>
 
37
 *
 
38
 * @category  PHP
 
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
 
46
 */
 
47
class PEAR_Sniffs_Commenting_FunctionCommentSniff implements PHP_CodeSniffer_Sniff
 
48
{
 
49
 
 
50
    /**
 
51
     * The name of the method that we are currently processing.
 
52
     *
 
53
     * @var string
 
54
     */
 
55
    private $_methodName = '';
 
56
 
 
57
    /**
 
58
     * The position in the stack where the function token was found.
 
59
     *
 
60
     * @var int
 
61
     */
 
62
    private $_functionToken = null;
 
63
 
 
64
    /**
 
65
     * The position in the stack where the class token was found.
 
66
     *
 
67
     * @var int
 
68
     */
 
69
    private $_classToken = null;
 
70
 
 
71
    /**
 
72
     * The function comment parser for the current method.
 
73
     *
 
74
     * @var PHP_CodeSniffer_Comment_Parser_FunctionCommentParser
 
75
     */
 
76
    protected $commentParser = null;
 
77
 
 
78
    /**
 
79
     * The current PHP_CodeSniffer_File object we are processing.
 
80
     *
 
81
     * @var PHP_CodeSniffer_File
 
82
     */
 
83
    protected $currentFile = null;
 
84
 
 
85
 
 
86
    /**
 
87
     * Returns an array of tokens this test wants to listen for.
 
88
     *
 
89
     * @return array
 
90
     */
 
91
    public function register()
 
92
    {
 
93
        return array(T_FUNCTION);
 
94
 
 
95
    }//end register()
 
96
 
 
97
 
 
98
    /**
 
99
     * Processes this test, when one of its tokens is encountered.
 
100
     *
 
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.
 
104
     *
 
105
     * @return void
 
106
     */
 
107
    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
 
108
    {
 
109
        $find = array(
 
110
                 T_COMMENT,
 
111
                 T_DOC_COMMENT,
 
112
                 T_CLASS,
 
113
                 T_FUNCTION,
 
114
                 T_OPEN_TAG,
 
115
                );
 
116
 
 
117
        $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1));
 
118
 
 
119
        if ($commentEnd === false) {
 
120
            return;
 
121
        }
 
122
 
 
123
        $this->currentFile = $phpcsFile;
 
124
        $tokens            = $phpcsFile->getTokens();
 
125
 
 
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'];
 
129
 
 
130
        if ($code === T_COMMENT) {
 
131
            $error = 'You must use "/**" style comments for a function comment';
 
132
            $phpcsFile->addError($error, $stackPtr, 'WrongStyle');
 
133
            return;
 
134
        } else if ($code !== T_DOC_COMMENT) {
 
135
            $phpcsFile->addError('Missing function doc comment', $stackPtr, 'Missing');
 
136
            return;
 
137
        }
 
138
 
 
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;
 
145
        $ignore[]  = T_FINAL;
 
146
        $prevToken = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true);
 
147
        if ($prevToken !== $commentEnd) {
 
148
            $phpcsFile->addError('Missing function doc comment', $stackPtr, 'Missing');
 
149
            return;
 
150
        }
 
151
 
 
152
        $this->_functionToken = $stackPtr;
 
153
 
 
154
        $this->_classToken = null;
 
155
        foreach ($tokens[$stackPtr]['conditions'] as $condPtr => $condition) {
 
156
            if ($condition === T_CLASS || $condition === T_INTERFACE) {
 
157
                $this->_classToken = $condPtr;
 
158
                break;
 
159
            }
 
160
        }
 
161
 
 
162
        // If the first T_OPEN_TAG is right before the comment, it is probably
 
163
        // a file comment.
 
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');
 
170
                return;
 
171
            }
 
172
        }
 
173
 
 
174
        $comment           = $phpcsFile->getTokensAsString($commentStart, ($commentEnd - $commentStart + 1));
 
175
        $this->_methodName = $phpcsFile->getDeclarationName($stackPtr);
 
176
 
 
177
        try {
 
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');
 
183
            return;
 
184
        }
 
185
 
 
186
        $comment = $this->commentParser->getComment();
 
187
        if (is_null($comment) === true) {
 
188
            $error = 'Function doc comment is empty';
 
189
            $phpcsFile->addError($error, $commentStart, 'Empty');
 
190
            return;
 
191
        }
 
192
 
 
193
        $this->processParams($commentStart);
 
194
        $this->processReturn($commentStart, $commentEnd);
 
195
        $this->processThrows($commentStart);
 
196
 
 
197
        // No extra newline before short description.
 
198
        $short        = $comment->getShortComment();
 
199
        $newlineCount = 0;
 
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');
 
204
        }
 
205
 
 
206
        $newlineCount = (substr_count($short, $phpcsFile->eolChar) + 1);
 
207
 
 
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');
 
216
            }
 
217
 
 
218
            $newlineCount += $newlineBetween;
 
219
        }
 
220
 
 
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';
 
227
                if ($long !== '') {
 
228
                    $newlineCount += (substr_count($long, $phpcsFile->eolChar) - $newlineSpan + 1);
 
229
                }
 
230
 
 
231
                $phpcsFile->addError($error, ($commentStart + $newlineCount), 'SpacingBeforeTags');
 
232
                $short = rtrim($short, $phpcsFile->eolChar.' ');
 
233
            }
 
234
        }
 
235
 
 
236
    }//end process()
 
237
 
 
238
 
 
239
    /**
 
240
     * Process any throw tags that this function comment has.
 
241
     *
 
242
     * @param int $commentStart The position in the stack where the
 
243
     *                          comment started.
 
244
     *
 
245
     * @return void
 
246
     */
 
247
    protected function processThrows($commentStart)
 
248
    {
 
249
        if (count($this->commentParser->getThrows()) === 0) {
 
250
            return;
 
251
        }
 
252
 
 
253
        foreach ($this->commentParser->getThrows() as $throw) {
 
254
 
 
255
            $exception = $throw->getValue();
 
256
            $errorPos  = ($commentStart + $throw->getLine());
 
257
 
 
258
            if ($exception === '') {
 
259
                $error = '@throws tag must contain the exception class name';
 
260
                $this->currentFile->addError($error, $errorPos, 'EmptyThrows');
 
261
            }
 
262
        }
 
263
 
 
264
    }//end processThrows()
 
265
 
 
266
 
 
267
    /**
 
268
     * Process the return comment of this function comment.
 
269
     *
 
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.
 
272
     *
 
273
     * @return void
 
274
     */
 
275
    protected function processReturn($commentStart, $commentEnd)
 
276
    {
 
277
        // Skip constructor and destructor.
 
278
        $className = '';
 
279
        if ($this->_classToken !== null) {
 
280
            $className = $this->currentFile->getDeclarationName($this->_classToken);
 
281
            $className = strtolower(ltrim($className, '_'));
 
282
        }
 
283
 
 
284
        $methodName      = strtolower(ltrim($this->_methodName, '_'));
 
285
        $isSpecialMethod = ($this->_methodName === '__construct' || $this->_methodName === '__destruct');
 
286
 
 
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');
 
296
            }
 
297
        }
 
298
 
 
299
    }//end processReturn()
 
300
 
 
301
 
 
302
    /**
 
303
     * Process the function parameter comments.
 
304
     *
 
305
     * @param int $commentStart The position in the stack where
 
306
     *                          the comment started.
 
307
     *
 
308
     * @return void
 
309
     */
 
310
    protected function processParams($commentStart)
 
311
    {
 
312
        $realParams = $this->currentFile->getMethodParameters($this->_functionToken);
 
313
 
 
314
        $params      = $this->commentParser->getParams();
 
315
        $foundParams = array();
 
316
 
 
317
        if (empty($params) === false) {
 
318
 
 
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');
 
324
            }
 
325
 
 
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');
 
331
            }
 
332
 
 
333
            $previousParam      = null;
 
334
            $spaceBeforeVar     = 10000;
 
335
            $spaceBeforeComment = 10000;
 
336
            $longestType        = 0;
 
337
            $longestVar         = 0;
 
338
 
 
339
            foreach ($params as $param) {
 
340
 
 
341
                $paramComment = trim($param->getComment());
 
342
                $errorPos     = ($param->getLine() + $commentStart);
 
343
 
 
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');
 
348
                }
 
349
 
 
350
                $spaceCount = substr_count($param->getWhitespaceBeforeVarName(), ' ');
 
351
                if ($spaceCount < $spaceBeforeVar) {
 
352
                    $spaceBeforeVar = $spaceCount;
 
353
                    $longestType    = $errorPos;
 
354
                }
 
355
 
 
356
                $spaceCount = substr_count($param->getWhitespaceBeforeComment(), ' ');
 
357
 
 
358
                if ($spaceCount < $spaceBeforeComment && $paramComment !== '') {
 
359
                    $spaceBeforeComment = $spaceCount;
 
360
                    $longestVar         = $errorPos;
 
361
                }
 
362
 
 
363
                // Make sure they are in the correct order,
 
364
                // and have the correct name.
 
365
                $pos = $param->getPosition();
 
366
 
 
367
                $paramName = ($param->getVarName() !== '') ? $param->getVarName() : '[ UNKNOWN ]';
 
368
 
 
369
                if ($previousParam !== null) {
 
370
                    $previousName = ($previousParam->getVarName() !== '') ? $previousParam->getVarName() : 'UNKNOWN';
 
371
 
 
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';
 
375
                        $data  = array(
 
376
                                  $previousName,
 
377
                                  ($pos - 1),
 
378
                                  $paramName,
 
379
                                  $pos,
 
380
                                 );
 
381
                        $this->currentFile->addError($error, $errorPos, 'ParameterNamesNotAligned', $data);
 
382
                    }
 
383
 
 
384
                    if ($param->alignsCommentWith($previousParam) === false) {
 
385
                        $error = 'The comments for parameters %s (%s) and %s (%s) do not align';
 
386
                        $data  = array(
 
387
                                  $previousName,
 
388
                                  ($pos - 1),
 
389
                                  $paramName,
 
390
                                  $pos,
 
391
                                 );
 
392
                        $this->currentFile->addError($error, $errorPos, 'ParameterCommentsNotAligned', $data);
 
393
                    }
 
394
                }//end if
 
395
 
 
396
                // Make sure the names of the parameter comment matches the
 
397
                // actual parameter.
 
398
                if (isset($realParams[($pos - 1)]) === true) {
 
399
                    $realName      = $realParams[($pos - 1)]['name'];
 
400
                    $foundParams[] = $realName;
 
401
 
 
402
                    // Append ampersand to name if passing by reference.
 
403
                    if ($realParams[($pos - 1)]['pass_by_reference'] === true) {
 
404
                        $realName = '&'.$realName;
 
405
                    }
 
406
 
 
407
                    if ($realName !== $paramName) {
 
408
                        $code = 'ParamNameNoMatch';
 
409
                        $data = array(
 
410
                                    $paramName,
 
411
                                    $realName,
 
412
                                    $pos,
 
413
                                );
 
414
 
 
415
                        $error  = 'Doc comment for var %s does not match ';
 
416
                        if (strtolower($paramName) === strtolower($realName)) {
 
417
                            $error .= 'case of ';
 
418
                            $code   = 'ParamNameNoCaseMatch';
 
419
                        }
 
420
 
 
421
                        $error .= 'actual variable name %s at position %s';
 
422
 
 
423
                        $this->currentFile->addError($error, $errorPos, $code, $data);
 
424
                    }
 
425
                } else {
 
426
                    // We must have an extra parameter comment.
 
427
                    $error = 'Superfluous doc comment at position '.$pos;
 
428
                    $this->currentFile->addError($error, $errorPos, 'ExtraParamComment');
 
429
                }
 
430
 
 
431
                if ($param->getVarName() === '') {
 
432
                    $error = 'Missing parameter name at position '.$pos;
 
433
                     $this->currentFile->addError($error, $errorPos, 'MissingParamName');
 
434
                }
 
435
 
 
436
                if ($param->getType() === '') {
 
437
                    $error = 'Missing type at position '.$pos;
 
438
                    $this->currentFile->addError($error, $errorPos, 'MissingParamType');
 
439
                }
 
440
 
 
441
                if ($paramComment === '') {
 
442
                    $error = 'Missing comment for param "%s" at position %s';
 
443
                    $data  = array(
 
444
                              $paramName,
 
445
                              $pos,
 
446
                             );
 
447
                    $this->currentFile->addError($error, $errorPos, 'MissingParamComment', $data);
 
448
                }
 
449
 
 
450
                $previousParam = $param;
 
451
 
 
452
            }//end foreach
 
453
 
 
454
            if ($spaceBeforeVar !== 1 && $spaceBeforeVar !== 10000 && $spaceBeforeComment !== 10000) {
 
455
                $error = 'Expected 1 space after the longest type';
 
456
                $this->currentFile->addError($error, $longestType, 'SpacingAfterLongType');
 
457
            }
 
458
 
 
459
            if ($spaceBeforeComment !== 1 && $spaceBeforeComment !== 10000) {
 
460
                $error = 'Expected 1 space after the longest variable name';
 
461
                $this->currentFile->addError($error, $longestVar, 'SpacingAfterLongName');
 
462
            }
 
463
 
 
464
        }//end if
 
465
 
 
466
        $realNames = array();
 
467
        foreach ($realParams as $realParam) {
 
468
            $realNames[] = $realParam['name'];
 
469
        }
 
470
 
 
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);
 
476
            } else {
 
477
                $errorPos = $commentStart;
 
478
            }
 
479
 
 
480
            $error = 'Doc comment for "%s" missing';
 
481
            $data  = array($neededParam);
 
482
            $this->currentFile->addError($error, $errorPos, 'MissingParamTag', $data);
 
483
        }
 
484
 
 
485
    }//end processParams()
 
486
 
 
487
 
 
488
}//end class
 
489
 
 
490
?>