Subversion Repositories Applications.framework

Rev

Rev 5 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
5 aurelien 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 Squiz Pty Ltd (ABN 77 084 670 600)
12
 * @license   http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
34 aurelien 13
 * @version   CVS: $Id: FunctionCommentSniff.php 34 2009-04-09 07:34:39Z aurelien $
5 aurelien 14
 * @link      http://pear.php.net/package/PHP_CodeSniffer
15
 */
16
 
17
if (class_exists('PHP_CodeSniffer_CommentParser_FunctionCommentParser', true) === false) {
18
    throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_CommentParser_FunctionCommentParser not found');
19
}
20
 
21
/**
22
 * Parses and verifies the doc comments for functions.
23
 *
24
 * Verifies that :
25
 * <ul>
26
 *  <li>A comment exists</li>
27
 *  <li>There is a blank newline after the short description.</li>
28
 *  <li>There is a blank newline between the long and short description.</li>
29
 *  <li>There is a blank newline between the long description and tags.</li>
30
 *  <li>Parameter names represent those in the method.</li>
31
 *  <li>Parameter comments are in the correct order</li>
32
 *  <li>Parameter comments are complete</li>
33
 *  <li>A space is present before the first and after the last parameter</li>
34
 *  <li>A return type exists</li>
35
 *  <li>There must be one blank line between body and headline comments.</li>
36
 *  <li>Any throw tag must have an exception class.</li>
37
 * </ul>
38
 *
39
 * @category  PHP
40
 * @package   PHP_CodeSniffer
41
 * @author    Greg Sherwood <gsherwood@squiz.net>
42
 * @author    Marc McIntyre <mmcintyre@squiz.net>
43
 * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600)
44
 * @license   http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
45
 * @version   Release: 1.2.0RC1
46
 * @link      http://pear.php.net/package/PHP_CodeSniffer
47
 */
48
class PEAR_Sniffs_Commenting_FunctionCommentSniff implements PHP_CodeSniffer_Sniff
49
{
50
 
51
    /**
52
     * The name of the method that we are currently processing.
53
     *
54
     * @var string
55
     */
56
    private $_methodName = '';
57
 
58
    /**
59
     * The position in the stack where the fucntion token was found.
60
     *
61
     * @var int
62
     */
63
    private $_functionToken = null;
64
 
65
    /**
66
     * The position in the stack where the class token was found.
67
     *
68
     * @var int
69
     */
70
    private $_classToken = null;
71
 
72
    /**
73
     * The function comment parser for the current method.
74
     *
75
     * @var PHP_CodeSniffer_Comment_Parser_FunctionCommentParser
76
     */
77
    protected $commentParser = null;
78
 
79
    /**
80
     * The current PHP_CodeSniffer_File object we are processing.
81
     *
82
     * @var PHP_CodeSniffer_File
83
     */
84
    protected $currentFile = null;
85
 
86
 
87
    /**
88
     * Returns an array of tokens this test wants to listen for.
89
     *
90
     * @return array
91
     */
92
    public function register()
93
    {
94
        return array(T_FUNCTION);
95
 
96
    }//end register()
97
 
98
 
99
    /**
100
     * Processes this test, when one of its tokens is encountered.
101
     *
102
     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
103
     * @param int                  $stackPtr  The position of the current token
104
     *                                        in the stack passed in $tokens.
105
     *
106
     * @return void
107
     */
108
    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
109
    {
110
        $find = array(
111
                 T_COMMENT,
112
                 T_DOC_COMMENT,
113
                 T_CLASS,
114
                 T_FUNCTION,
115
                 T_OPEN_TAG,
116
                );
117
 
118
        $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1));
119
 
120
        if ($commentEnd === false) {
121
            return;
122
        }
123
 
124
        $this->currentFile = $phpcsFile;
125
        $tokens            = $phpcsFile->getTokens();
126
 
127
        // If the token that we found was a class or a function, then this
128
        // function has no doc comment.
129
        $code = $tokens[$commentEnd]['code'];
130
 
131
        if ($code === T_COMMENT) {
132
            $error = 'You must use "/**" style comments for a function comment';
133
            $phpcsFile->addError($error, $stackPtr);
134
            return;
135
        } else if ($code !== T_DOC_COMMENT) {
136
            $phpcsFile->addError('Missing function doc comment', $stackPtr);
137
            return;
138
        }
139
 
140
        // If there is any code between the function keyword and the doc block
141
        // then the doc block is not for us.
142
        $ignore    = PHP_CodeSniffer_Tokens::$scopeModifiers;
143
        $ignore[]  = T_STATIC;
144
        $ignore[]  = T_WHITESPACE;
145
        $ignore[]  = T_ABSTRACT;
146
        $ignore[]  = T_FINAL;
147
        $prevToken = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true);
148
        if ($prevToken !== $commentEnd) {
149
            $phpcsFile->addError('Missing function doc comment', $stackPtr);
150
            return;
151
        }
152
 
153
        $this->_functionToken = $stackPtr;
154
 
155
        $this->_classToken = null;
156
        foreach ($tokens[$stackPtr]['conditions'] as $condPtr => $condition) {
157
            if ($condition === T_CLASS || $condition === T_INTERFACE) {
158
                $this->_classToken = $condPtr;
159
                break;
160
            }
161
        }
162
 
163
        // If the first T_OPEN_TAG is right before the comment, it is probably
164
        // a file comment.
165
        $commentStart = ($phpcsFile->findPrevious(T_DOC_COMMENT, ($commentEnd - 1), null, true) + 1);
166
        $prevToken    = $phpcsFile->findPrevious(T_WHITESPACE, ($commentStart - 1), null, true);
167
        if ($tokens[$prevToken]['code'] === T_OPEN_TAG) {
168
            // Is this the first open tag?
169
            if ($stackPtr === 0 || $phpcsFile->findPrevious(T_OPEN_TAG, ($prevToken - 1)) === false) {
170
                $phpcsFile->addError('Missing function doc comment', $stackPtr);
171
                return;
172
            }
173
        }
174
 
175
        $comment           = $phpcsFile->getTokensAsString($commentStart, ($commentEnd - $commentStart + 1));
176
        $this->_methodName = $phpcsFile->getDeclarationName($stackPtr);
177
 
178
        try {
179
            $this->commentParser = new PHP_CodeSniffer_CommentParser_FunctionCommentParser($comment, $phpcsFile);
180
            $this->commentParser->parse();
181
        } catch (PHP_CodeSniffer_CommentParser_ParserException $e) {
182
            $line = ($e->getLineWithinComment() + $commentStart);
183
            $phpcsFile->addError($e->getMessage(), $line);
184
            return;
185
        }
186
 
187
        $comment = $this->commentParser->getComment();
188
        if (is_null($comment) === true) {
189
            $error = 'Function doc comment is empty';
190
            $phpcsFile->addError($error, $commentStart);
191
            return;
192
        }
193
 
194
        $this->processParams($commentStart);
195
        $this->processReturn($commentStart, $commentEnd);
196
        $this->processThrows($commentStart);
197
 
198
        // No extra newline before short description.
199
        $short        = $comment->getShortComment();
200
        $newlineCount = 0;
201
        $newlineSpan  = strspn($short, $phpcsFile->eolChar);
202
        if ($short !== '' && $newlineSpan > 0) {
203
            $line  = ($newlineSpan > 1) ? 'newlines' : 'newline';
204
            $error = "Extra $line found before function comment short description";
205
            $phpcsFile->addError($error, ($commentStart + 1));
206
        }
207
 
208
        $newlineCount = (substr_count($short, $phpcsFile->eolChar) + 1);
209
 
210
        // Exactly one blank line between short and long description.
211
        $long = $comment->getLongComment();
212
        if (empty($long) === false) {
213
            $between        = $comment->getWhiteSpaceBetween();
214
            $newlineBetween = substr_count($between, $phpcsFile->eolChar);
215
            if ($newlineBetween !== 2) {
216
                $error = 'There must be exactly one blank line between descriptions in function comment';
217
                $phpcsFile->addError($error, ($commentStart + $newlineCount + 1));
218
            }
219
 
220
            $newlineCount += $newlineBetween;
221
        }
222
 
223
        // Exactly one blank line before tags.
224
        $params = $this->commentParser->getTagOrders();
225
        if (count($params) > 1) {
226
            $newlineSpan = $comment->getNewlineAfter();
227
            if ($newlineSpan !== 2) {
228
                $error = 'There must be exactly one blank line before the tags in function comment';
229
                if ($long !== '') {
230
                    $newlineCount += (substr_count($long, $phpcsFile->eolChar) - $newlineSpan + 1);
231
                }
232
 
233
                $phpcsFile->addError($error, ($commentStart + $newlineCount));
234
                $short = rtrim($short, $phpcsFile->eolChar.' ');
235
            }
236
        }
237
 
238
    }//end process()
239
 
240
 
241
    /**
242
     * Process any throw tags that this function comment has.
243
     *
244
     * @param int $commentStart The position in the stack where the
245
     *                          comment started.
246
     *
247
     * @return void
248
     */
249
    protected function processThrows($commentStart)
250
    {
251
        if (count($this->commentParser->getThrows()) === 0) {
252
            return;
253
        }
254
 
255
        foreach ($this->commentParser->getThrows() as $throw) {
256
 
257
            $exception = $throw->getValue();
258
            $errorPos  = ($commentStart + $throw->getLine());
259
 
260
            if ($exception === '') {
261
                $error = '@throws tag must contain the exception class name';
262
                $this->currentFile->addError($error, $errorPos);
263
            }
264
        }
265
 
266
    }//end processThrows()
267
 
268
 
269
    /**
270
     * Process the return comment of this function comment.
271
     *
272
     * @param int $commentStart The position in the stack where the comment started.
273
     * @param int $commentEnd   The position in the stack where the comment ended.
274
     *
275
     * @return void
276
     */
277
    protected function processReturn($commentStart, $commentEnd)
278
    {
279
        // Skip constructor and destructor.
280
        $className = '';
281
        if ($this->_classToken !== null) {
282
            $className = $this->currentFile->getDeclarationName($this->_classToken);
283
            $className = strtolower(ltrim($className, '_'));
284
        }
285
 
286
        $methodName      = strtolower(ltrim($this->_methodName, '_'));
287
        $isSpecialMethod = ($this->_methodName === '__construct' || $this->_methodName === '__destruct');
288
 
289
        if ($isSpecialMethod === false && $methodName !== $className) {
290
            // Report missing return tag.
291
            if ($this->commentParser->getReturn() === null) {
292
                $error = 'Missing @return tag in function comment';
293
                $this->currentFile->addError($error, $commentEnd);
294
            } else if (trim($this->commentParser->getReturn()->getRawContent()) === '') {
295
                $error    = '@return tag is empty in function comment';
296
                $errorPos = ($commentStart + $this->commentParser->getReturn()->getLine());
297
                $this->currentFile->addError($error, $errorPos);
298
            }
299
        }
300
 
301
    }//end processReturn()
302
 
303
 
304
    /**
305
     * Process the function parameter comments.
306
     *
307
     * @param int $commentStart The position in the stack where
308
     *                          the comment started.
309
     *
310
     * @return void
311
     */
312
    protected function processParams($commentStart)
313
    {
314
        $realParams = $this->currentFile->getMethodParameters($this->_functionToken);
315
 
316
        $params      = $this->commentParser->getParams();
317
        $foundParams = array();
318
 
319
        if (empty($params) === false) {
320
 
321
            $lastParm = (count($params) - 1);
322
            if (substr_count($params[$lastParm]->getWhitespaceAfter(), $this->currentFile->eolChar) !== 2) {
323
                $error    = 'Last parameter comment requires a blank newline after it';
324
                $errorPos = ($params[$lastParm]->getLine() + $commentStart);
325
                $this->currentFile->addError($error, $errorPos);
326
            }
327
 
328
            // Parameters must appear immediately after the comment.
329
            if ($params[0]->getOrder() !== 2) {
330
                $error    = 'Parameters must appear immediately after the comment';
331
                $errorPos = ($params[0]->getLine() + $commentStart);
332
                $this->currentFile->addError($error, $errorPos);
333
            }
334
 
335
            $previousParam      = null;
336
            $spaceBeforeVar     = 10000;
337
            $spaceBeforeComment = 10000;
338
            $longestType        = 0;
339
            $longestVar         = 0;
340
 
341
            foreach ($params as $param) {
342
 
343
                $paramComment = trim($param->getComment());
344
                $errorPos     = ($param->getLine() + $commentStart);
345
 
346
                // Make sure that there is only one space before the var type.
347
                if ($param->getWhitespaceBeforeType() !== ' ') {
348
                    $error = 'Expected 1 space before variable type';
349
                    $this->currentFile->addError($error, $errorPos);
350
                }
351
 
352
                $spaceCount = substr_count($param->getWhitespaceBeforeVarName(), ' ');
353
                if ($spaceCount < $spaceBeforeVar) {
354
                    $spaceBeforeVar = $spaceCount;
355
                    $longestType    = $errorPos;
356
                }
357
 
358
                $spaceCount = substr_count($param->getWhitespaceBeforeComment(), ' ');
359
 
360
                if ($spaceCount < $spaceBeforeComment && $paramComment !== '') {
361
                    $spaceBeforeComment = $spaceCount;
362
                    $longestVar         = $errorPos;
363
                }
364
 
365
                // Make sure they are in the correct order,
366
                // and have the correct name.
367
                $pos = $param->getPosition();
368
 
369
                $paramName = ($param->getVarName() !== '') ? $param->getVarName() : '[ UNKNOWN ]';
370
 
371
                if ($previousParam !== null) {
372
                    $previousName = ($previousParam->getVarName() !== '') ? $previousParam->getVarName() : 'UNKNOWN';
373
 
374
                    // Check to see if the parameters align properly.
375
                    if ($param->alignsVariableWith($previousParam) === false) {
376
                        $error = 'The variable names for parameters '.$previousName.' ('.($pos - 1).') and '.$paramName.' ('.$pos.') do not align';
377
                        $this->currentFile->addError($error, $errorPos);
378
                    }
379
 
380
                    if ($param->alignsCommentWith($previousParam) === false) {
381
                        $error = 'The comments for parameters '.$previousName.' ('.($pos - 1).') and '.$paramName.' ('.$pos.') do not align';
382
                        $this->currentFile->addError($error, $errorPos);
383
                    }
384
                }//end if
385
 
386
                // Make sure the names of the parameter comment matches the
387
                // actual parameter.
388
                if (isset($realParams[($pos - 1)]) === true) {
389
                    $realName      = $realParams[($pos - 1)]['name'];
390
                    $foundParams[] = $realName;
391
                    // Append ampersand to name if passing by reference.
392
                    if ($realParams[($pos - 1)]['pass_by_reference'] === true) {
393
                        $realName = '&'.$realName;
394
                    }
395
 
396
                    if ($realName !== $param->getVarName()) {
397
                        $error  = 'Doc comment var "'.$paramName;
398
                        $error .= '" does not match actual variable name "'.$realName;
399
                        $error .= '" at position '.$pos;
400
 
401
                        $this->currentFile->addError($error, $errorPos);
402
                    }
403
                } else {
404
                    // We must have an extra parameter comment.
405
                    $error = 'Superfluous doc comment at position '.$pos;
406
                    $this->currentFile->addError($error, $errorPos);
407
                }
408
 
409
                if ($param->getVarName() === '') {
410
                    $error = 'Missing parameter name at position '.$pos;
411
                     $this->currentFile->addError($error, $errorPos);
412
                }
413
 
414
                if ($param->getType() === '') {
415
                    $error = 'Missing type at position '.$pos;
416
                    $this->currentFile->addError($error, $errorPos);
417
                }
418
 
419
                if ($paramComment === '') {
420
                    $error = 'Missing comment for param "'.$paramName.'" at position '.$pos;
421
                    $this->currentFile->addError($error, $errorPos);
422
                }
423
 
424
                $previousParam = $param;
425
 
426
            }//end foreach
427
 
428
            if ($spaceBeforeVar !== 1 && $spaceBeforeVar !== 10000 && $spaceBeforeComment !== 10000) {
429
                $error = 'Expected 1 space after the longest type';
430
                $this->currentFile->addError($error, $longestType);
431
            }
432
 
433
            if ($spaceBeforeComment !== 1 && $spaceBeforeComment !== 10000) {
434
                $error = 'Expected 1 space after the longest variable name';
435
                $this->currentFile->addError($error, $longestVar);
436
            }
437
 
438
        }//end if
439
 
440
        $realNames = array();
441
        foreach ($realParams as $realParam) {
442
            $realNames[] = $realParam['name'];
443
        }
444
 
445
        // Report and missing comments.
446
        $diff = array_diff($realNames, $foundParams);
447
        foreach ($diff as $neededParam) {
448
            if (count($params) !== 0) {
449
                $errorPos = ($params[(count($params) - 1)]->getLine() + $commentStart);
450
            } else {
451
                $errorPos = $commentStart;
452
            }
453
 
454
            $error = 'Doc comment for "'.$neededParam.'" missing';
455
            $this->currentFile->addError($error, $errorPos);
456
        }
457
 
458
    }//end processParams()
459
 
460
 
461
}//end class
462
 
463
?>