Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 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
13
 * @version   CVS: $Id: FunctionCommentSniff.php 290854 2009-11-17 03:28:02Z squiz $
14
 * @link      http://pear.php.net/package/PHP_CodeSniffer
15
 */
16
 
17
if (class_exists('PHP_CodeSniffer_CommentParser_FunctionCommentParser', true) === false) {
18
    $error = 'Class PHP_CodeSniffer_CommentParser_FunctionCommentParser not found';
19
    throw new PHP_CodeSniffer_Exception($error);
20
}
21
 
22
/**
23
 * Parses and verifies the doc comments for functions.
24
 *
25
 * Verifies that :
26
 * <ul>
27
 *  <li>A comment exists</li>
28
 *  <li>There is a blank newline after the short description</li>
29
 *  <li>There is a blank newline between the long and short description</li>
30
 *  <li>There is a blank newline between the long description and tags</li>
31
 *  <li>Parameter names represent those in the method</li>
32
 *  <li>Parameter comments are in the correct order</li>
33
 *  <li>Parameter comments are complete</li>
34
 *  <li>A type hint is provided for array and custom class</li>
35
 *  <li>Type hint matches the actual variable/class type</li>
36
 *  <li>A blank line is present before the first and after the last parameter</li>
37
 *  <li>A return type exists</li>
38
 *  <li>Any throw tag must have a comment</li>
39
 *  <li>The tag order and indentation are correct</li>
40
 * </ul>
41
 *
42
 * @category  PHP
43
 * @package   PHP_CodeSniffer
44
 * @author    Greg Sherwood <gsherwood@squiz.net>
45
 * @author    Marc McIntyre <mmcintyre@squiz.net>
46
 * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600)
47
 * @license   http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
48
 * @version   Release: 1.2.1
49
 * @link      http://pear.php.net/package/PHP_CodeSniffer
50
 */
51
class Squiz_Sniffs_Commenting_FunctionCommentSniff implements PHP_CodeSniffer_Sniff
52
{
53
 
54
    /**
55
     * The name of the method that we are currently processing.
56
     *
57
     * @var string
58
     */
59
    private $_methodName = '';
60
 
61
    /**
62
     * The position in the stack where the fucntion token was found.
63
     *
64
     * @var int
65
     */
66
    private $_functionToken = null;
67
 
68
    /**
69
     * The position in the stack where the class token was found.
70
     *
71
     * @var int
72
     */
73
    private $_classToken = null;
74
 
75
    /**
76
     * The index of the current tag we are processing.
77
     *
78
     * @var int
79
     */
80
    private $_tagIndex = 0;
81
 
82
    /**
83
     * The function comment parser for the current method.
84
     *
85
     * @var PHP_CodeSniffer_Comment_Parser_FunctionCommentParser
86
     */
87
    protected $commentParser = null;
88
 
89
    /**
90
     * The current PHP_CodeSniffer_File object we are processing.
91
     *
92
     * @var PHP_CodeSniffer_File
93
     */
94
    protected $currentFile = null;
95
 
96
 
97
    /**
98
     * Returns an array of tokens this test wants to listen for.
99
     *
100
     * @return array
101
     */
102
    public function register()
103
    {
104
        return array(T_FUNCTION);
105
 
106
    }//end register()
107
 
108
 
109
    /**
110
     * Processes this test, when one of its tokens is encountered.
111
     *
112
     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
113
     * @param int                  $stackPtr  The position of the current token
114
     *                                        in the stack passed in $tokens.
115
     *
116
     * @return void
117
     */
118
    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
119
    {
120
        $this->currentFile = $phpcsFile;
121
 
122
        $tokens = $phpcsFile->getTokens();
123
 
124
        $find = array(
125
                 T_COMMENT,
126
                 T_DOC_COMMENT,
127
                 T_CLASS,
128
                 T_FUNCTION,
129
                 T_OPEN_TAG,
130
                );
131
 
132
        $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1));
133
 
134
        if ($commentEnd === false) {
135
            return;
136
        }
137
 
138
        // If the token that we found was a class or a function, then this
139
        // function has no doc comment.
140
        $code = $tokens[$commentEnd]['code'];
141
 
142
        if ($code === T_COMMENT) {
143
            $error = 'You must use "/**" style comments for a function comment';
144
            $phpcsFile->addError($error, $stackPtr);
145
            return;
146
        } else if ($code !== T_DOC_COMMENT) {
147
            $error = 'Missing function doc comment';
148
            $phpcsFile->addError($error, $stackPtr);
149
            return;
150
        }
151
 
152
        // If there is any code between the function keyword and the doc block
153
        // then the doc block is not for us.
154
        $ignore    = PHP_CodeSniffer_Tokens::$scopeModifiers;
155
        $ignore[]  = T_STATIC;
156
        $ignore[]  = T_WHITESPACE;
157
        $ignore[]  = T_ABSTRACT;
158
        $ignore[]  = T_FINAL;
159
        $prevToken = $phpcsFile->findPrevious($ignore, ($stackPtr - 1), null, true);
160
        if ($prevToken !== $commentEnd) {
161
            $phpcsFile->addError('Missing function doc comment', $stackPtr);
162
            return;
163
        }
164
 
165
        $this->_functionToken = $stackPtr;
166
 
167
        $this->_classToken = null;
168
        foreach ($tokens[$stackPtr]['conditions'] as $condPtr => $condition) {
169
            if ($condition === T_CLASS || $condition === T_INTERFACE) {
170
                $this->_classToken = $condPtr;
171
                break;
172
            }
173
        }
174
 
175
        // Find the first doc comment.
176
        $commentStart      = ($phpcsFile->findPrevious(T_DOC_COMMENT, ($commentEnd - 1), null, true) + 1);
177
        $comment           = $phpcsFile->getTokensAsString($commentStart, ($commentEnd - $commentStart + 1));
178
        $this->_methodName = $phpcsFile->getDeclarationName($stackPtr);
179
 
180
        try {
181
            $this->commentParser = new PHP_CodeSniffer_CommentParser_FunctionCommentParser($comment, $phpcsFile);
182
            $this->commentParser->parse();
183
        } catch (PHP_CodeSniffer_CommentParser_ParserException $e) {
184
            $line = ($e->getLineWithinComment() + $commentStart);
185
            $phpcsFile->addError($e->getMessage(), $line);
186
            return;
187
        }
188
 
189
        $comment = $this->commentParser->getComment();
190
        if (is_null($comment) === true) {
191
            $error = 'Function doc comment is empty';
192
            $phpcsFile->addError($error, $commentStart);
193
            return;
194
        }
195
 
196
        $this->processParams($commentStart, $commentEnd);
197
        $this->processSince($commentStart, $commentEnd);
198
        $this->processSees($commentStart);
199
        $this->processReturn($commentStart, $commentEnd);
200
        $this->processThrows($commentStart);
201
 
202
        // Check for a comment description.
203
        $short = $comment->getShortComment();
204
        if (trim($short) === '') {
205
            $error = 'Missing short description in function doc comment';
206
            $phpcsFile->addError($error, $commentStart);
207
            return;
208
        }
209
 
210
        // No extra newline before short description.
211
        $newlineCount = 0;
212
        $newlineSpan  = strspn($short, $phpcsFile->eolChar);
213
        if ($short !== '' && $newlineSpan > 0) {
214
            $line  = ($newlineSpan > 1) ? 'newlines' : 'newline';
215
            $error = "Extra $line found before function comment short description";
216
            $phpcsFile->addError($error, ($commentStart + 1));
217
        }
218
 
219
        $newlineCount = (substr_count($short, $phpcsFile->eolChar) + 1);
220
 
221
        // Exactly one blank line between short and long description.
222
        $long = $comment->getLongComment();
223
        if (empty($long) === false) {
224
            $between        = $comment->getWhiteSpaceBetween();
225
            $newlineBetween = substr_count($between, $phpcsFile->eolChar);
226
            if ($newlineBetween !== 2) {
227
                $error = 'There must be exactly one blank line between descriptions in function comment';
228
                $phpcsFile->addError($error, ($commentStart + $newlineCount + 1));
229
            }
230
 
231
            $newlineCount += $newlineBetween;
232
 
233
            $testLong = trim($long);
234
            if (preg_match('|[A-Z]|', $testLong[0]) === 0) {
235
                $error = 'Function comment long description must start with a capital letter';
236
                $phpcsFile->addError($error, ($commentStart + $newlineCount));
237
            }
238
        }//end if
239
 
240
        // Exactly one blank line before tags.
241
        $params = $this->commentParser->getTagOrders();
242
        if (count($params) > 1) {
243
            $newlineSpan = $comment->getNewlineAfter();
244
            if ($newlineSpan !== 2) {
245
                $error = 'There must be exactly one blank line before the tags in function comment';
246
                if ($long !== '') {
247
                    $newlineCount += (substr_count($long, $phpcsFile->eolChar) - $newlineSpan + 1);
248
                }
249
 
250
                $phpcsFile->addError($error, ($commentStart + $newlineCount));
251
                $short = rtrim($short, $phpcsFile->eolChar.' ');
252
            }
253
        }
254
 
255
        // Short description must be single line and end with a full stop.
256
        $testShort = trim($short);
257
        $lastChar  = $testShort[(strlen($testShort) - 1)];
258
        if (substr_count($testShort, $phpcsFile->eolChar) !== 0) {
259
            $error = 'Function comment short description must be on a single line';
260
            $phpcsFile->addError($error, ($commentStart + 1));
261
        }
262
 
263
        if (preg_match('|[A-Z]|', $testShort[0]) === 0) {
264
            $error = 'Function comment short description must start with a capital letter';
265
            $phpcsFile->addError($error, ($commentStart + 1));
266
        }
267
 
268
        if ($lastChar !== '.') {
269
            $error = 'Function comment short description must end with a full stop';
270
            $phpcsFile->addError($error, ($commentStart + 1));
271
        }
272
 
273
        // Check for unknown/deprecated tags.
274
        $unknownTags = $this->commentParser->getUnknown();
275
        foreach ($unknownTags as $errorTag) {
276
            $error = "@$errorTag[tag] tag is not allowed in function comment";
277
            $phpcsFile->addWarning($error, ($commentStart + $errorTag['line']));
278
        }
279
 
280
    }//end process()
281
 
282
 
283
    /**
284
     * Process the since tag.
285
     *
286
     * @param int $commentStart The position in the stack where the comment started.
287
     * @param int $commentEnd   The position in the stack where the comment ended.
288
     *
289
     * @return void
290
     */
291
    protected function processSince($commentStart, $commentEnd)
292
    {
293
        $since = $this->commentParser->getSince();
294
 
295
        if ($since !== null) {
296
            $errorPos = ($commentStart + $since->getLine());
297
            $tagOrder = $this->commentParser->getTagOrders();
298
            $firstTag = 0;
299
 
300
            while ($tagOrder[$firstTag] === 'comment' || $tagOrder[$firstTag] === 'param') {
301
                $firstTag++;
302
            }
303
 
304
            $this->_tagIndex = $firstTag;
305
            $index           = array_keys($this->commentParser->getTagOrders(), 'since');
306
            if (count($index) > 1) {
307
                $error = 'Only 1 @since tag is allowed in function comment';
308
                $this->currentFile->addError($error, $errorPos);
309
                return;
310
            }
311
 
312
            if ($index[0] !== $firstTag) {
313
                $error = 'The @since tag is in the wrong order; the tag preceds @see (if used) or @return';
314
                $this->currentFile->addError($error, $errorPos);
315
            }
316
 
317
            $content = $since->getContent();
318
            if (empty($content) === true) {
319
                $error = 'Version number missing for @since tag in function comment';
320
                $this->currentFile->addError($error, $errorPos);
321
                return;
322
            } else if ($content !== '%release_version%') {
323
                if (preg_match('/^([0-9]+)\.([0-9]+)\.([0-9]+)/', $content) === 0) {
324
                    $error = 'Expected version number to be in the form x.x.x in @since tag';
325
                    $this->currentFile->addError($error, $errorPos);
326
                }
327
            }
328
 
329
            $spacing        = substr_count($since->getWhitespaceBeforeContent(), ' ');
330
            $return         = $this->commentParser->getReturn();
331
            $throws         = $this->commentParser->getThrows();
332
            $correctSpacing = ($return !== null || empty($throws) === false) ? 2 : 1;
333
 
334
            if ($spacing !== $correctSpacing) {
335
                $error  = '@since tag indented incorrectly; ';
336
                $error .= "expected $correctSpacing spaces but found $spacing.";
337
                $this->currentFile->addError($error, $errorPos);
338
            }
339
        } else {
340
            $error = 'Missing @since tag in function comment';
341
            $this->currentFile->addError($error, $commentEnd);
342
        }//end if
343
 
344
    }//end processSince()
345
 
346
 
347
    /**
348
     * Process the see tags.
349
     *
350
     * @param int $commentStart The position in the stack where the comment started.
351
     *
352
     * @return void
353
     */
354
    protected function processSees($commentStart)
355
    {
356
        $sees = $this->commentParser->getSees();
357
        if (empty($sees) === false) {
358
            $tagOrder = $this->commentParser->getTagOrders();
359
            $index    = array_keys($this->commentParser->getTagOrders(), 'see');
360
            foreach ($sees as $i => $see) {
361
                $errorPos = ($commentStart + $see->getLine());
362
                $since    = array_keys($tagOrder, 'since');
363
                if (count($since) === 1 && $this->_tagIndex !== 0) {
364
                    $this->_tagIndex++;
365
                    if ($index[$i] !== $this->_tagIndex) {
366
                        $error = 'The @see tag is in the wrong order; the tag follows @since';
367
                        $this->currentFile->addError($error, $errorPos);
368
                    }
369
                }
370
 
371
                $content = $see->getContent();
372
                if (empty($content) === true) {
373
                    $error = 'Content missing for @see tag in function comment';
374
                    $this->currentFile->addError($error, $errorPos);
375
                    continue;
376
                }
377
 
378
                $spacing = substr_count($see->getWhitespaceBeforeContent(), ' ');
379
                if ($spacing !== 4) {
380
                    $error  = '@see tag indented incorrectly; ';
381
                    $error .= "expected 4 spaces but found $spacing";
382
                    $this->currentFile->addError($error, $errorPos);
383
                }
384
            }//end foreach
385
        }//end if
386
 
387
    }//end processSees()
388
 
389
 
390
    /**
391
     * Process the return comment of this function comment.
392
     *
393
     * @param int $commentStart The position in the stack where the comment started.
394
     * @param int $commentEnd   The position in the stack where the comment ended.
395
     *
396
     * @return void
397
     */
398
    protected function processReturn($commentStart, $commentEnd)
399
    {
400
        // Skip constructor and destructor.
401
        $className = '';
402
        if ($this->_classToken !== null) {
403
            $className = $this->currentFile->getDeclarationName($this->_classToken);
404
            $className = strtolower(ltrim($className, '_'));
405
        }
406
 
407
        $methodName      = strtolower(ltrim($this->_methodName, '_'));
408
        $isSpecialMethod = ($this->_methodName === '__construct' || $this->_methodName === '__destruct');
409
        $return          = $this->commentParser->getReturn();
410
 
411
        if ($isSpecialMethod === false && $methodName !== $className) {
412
            if ($return !== null) {
413
                $tagOrder = $this->commentParser->getTagOrders();
414
                $index    = array_keys($tagOrder, 'return');
415
                $errorPos = ($commentStart + $return->getLine());
416
                $content  = trim($return->getRawContent());
417
 
418
                if (count($index) > 1) {
419
                    $error = 'Only 1 @return tag is allowed in function comment';
420
                    $this->currentFile->addError($error, $errorPos);
421
                    return;
422
                }
423
 
424
                $since = array_keys($tagOrder, 'since');
425
                if (count($since) === 1 && $this->_tagIndex !== 0) {
426
                    $this->_tagIndex++;
427
                    if ($index[0] !== $this->_tagIndex) {
428
                        $error = 'The @return tag is in the wrong order; the tag follows @see (if used) or @since';
429
                        $this->currentFile->addError($error, $errorPos);
430
                    }
431
                }
432
 
433
                if (empty($content) === true) {
434
                    $error = 'Return type missing for @return tag in function comment';
435
                    $this->currentFile->addError($error, $errorPos);
436
                } else {
437
                    // Check return type (can be multiple, separated by '|').
438
                    $typeNames      = explode('|', $content);
439
                    $suggestedNames = array();
440
                    foreach ($typeNames as $i => $typeName) {
441
                        $suggestedName = PHP_CodeSniffer::suggestType($typeName);
442
                        if (in_array($suggestedName, $suggestedNames) === false) {
443
                            $suggestedNames[] = $suggestedName;
444
                        }
445
                    }
446
 
447
                    $suggestedType = implode('|', $suggestedNames);
448
                    if ($content !== $suggestedType) {
449
                        $error = "Function return type \"$content\" is invalid";
450
                        $this->currentFile->addError($error, $errorPos);
451
                    }
452
 
453
                    $tokens = $this->currentFile->getTokens();
454
 
455
                    // If the return type is void, make sure there is
456
                    // no return statement in the function.
457
                    if ($content === 'void') {
458
                        if (isset($tokens[$this->_functionToken]['scope_closer']) === true) {
459
                            $endToken = $tokens[$this->_functionToken]['scope_closer'];
460
                            $return   = $this->currentFile->findNext(T_RETURN, $this->_functionToken, $endToken);
461
                            if ($return !== false) {
462
                                // If the function is not returning anything, just
463
                                // exiting, then there is no problem.
464
                                $semicolon = $this->currentFile->findNext(T_WHITESPACE, ($return + 1), null, true);
465
                                if ($tokens[$semicolon]['code'] !== T_SEMICOLON) {
466
                                    $error = 'Function return type is void, but function contains return statement';
467
                                    $this->currentFile->addError($error, $errorPos);
468
                                }
469
                            }
470
                        }
471
                    } else if ($content !== 'mixed') {
472
                        // If return type is not void, there needs to be a
473
                        // returns statement somewhere in the function that
474
                        // returns something.
475
                        if (isset($tokens[$this->_functionToken]['scope_closer']) === true) {
476
                            $endToken = $tokens[$this->_functionToken]['scope_closer'];
477
                            $return   = $this->currentFile->findNext(T_RETURN, $this->_functionToken, $endToken);
478
                            if ($return === false) {
479
                                $error = 'Function return type is not void, but function has no return statement';
480
                                $this->currentFile->addError($error, $errorPos);
481
                            } else {
482
                                $semicolon = $this->currentFile->findNext(T_WHITESPACE, ($return + 1), null, true);
483
                                if ($tokens[$semicolon]['code'] === T_SEMICOLON) {
484
                                    $error = 'Function return type is not void, but function is returning void here';
485
                                    $this->currentFile->addError($error, $return);
486
                                }
487
                            }
488
                        }
489
                    }//end if
490
                }//end if
491
            } else {
492
                $error = 'Missing @return tag in function comment';
493
                $this->currentFile->addError($error, $commentEnd);
494
            }//end if
495
 
496
        } else {
497
            // No return tag for constructor and destructor.
498
            if ($return !== null) {
499
                $errorPos = ($commentStart + $return->getLine());
500
                $error    = '@return tag is not required for constructor and destructor';
501
                $this->currentFile->addError($error, $errorPos);
502
            }
503
        }//end if
504
 
505
    }//end processReturn()
506
 
507
 
508
    /**
509
     * Process any throw tags that this function comment has.
510
     *
511
     * @param int $commentStart The position in the stack where the comment started.
512
     *
513
     * @return void
514
     */
515
    protected function processThrows($commentStart)
516
    {
517
        if (count($this->commentParser->getThrows()) === 0) {
518
            return;
519
        }
520
 
521
        $tagOrder = $this->commentParser->getTagOrders();
522
        $index    = array_keys($this->commentParser->getTagOrders(), 'throws');
523
 
524
        foreach ($this->commentParser->getThrows() as $i => $throw) {
525
            $exception = $throw->getValue();
526
            $content   = trim($throw->getComment());
527
            $errorPos  = ($commentStart + $throw->getLine());
528
            if (empty($exception) === true) {
529
                $error = 'Exception type and comment missing for @throws tag in function comment';
530
                $this->currentFile->addError($error, $errorPos);
531
            } else if (empty($content) === true) {
532
                $error = 'Comment missing for @throws tag in function comment';
533
                $this->currentFile->addError($error, $errorPos);
534
            } else {
535
                // Starts with a capital letter and ends with a fullstop.
536
                $firstChar = $content{0};
537
                if (strtoupper($firstChar) !== $firstChar) {
538
                    $error = '@throws tag comment must start with a capital letter';
539
                    $this->currentFile->addError($error, $errorPos);
540
                }
541
 
542
                $lastChar = $content[(strlen($content) - 1)];
543
                if ($lastChar !== '.') {
544
                    $error = '@throws tag comment must end with a full stop';
545
                    $this->currentFile->addError($error, $errorPos);
546
                }
547
            }
548
 
549
            $since = array_keys($tagOrder, 'since');
550
            if (count($since) === 1 && $this->_tagIndex !== 0) {
551
                $this->_tagIndex++;
552
                if ($index[$i] !== $this->_tagIndex) {
553
                    $error = 'The @throws tag is in the wrong order; the tag follows @return';
554
                    $this->currentFile->addError($error, $errorPos);
555
                }
556
            }
557
        }//end foreach
558
 
559
    }//end processThrows()
560
 
561
 
562
    /**
563
     * Process the function parameter comments.
564
     *
565
     * @param int $commentStart The position in the stack where
566
     *                          the comment started.
567
     * @param int $commentEnd   The position in the stack where
568
     *                          the comment ended.
569
     *
570
     * @return void
571
     */
572
    protected function processParams($commentStart, $commentEnd)
573
    {
574
        $realParams  = $this->currentFile->getMethodParameters($this->_functionToken);
575
        $params      = $this->commentParser->getParams();
576
        $foundParams = array();
577
 
578
        if (empty($params) === false) {
579
 
580
            if (substr_count($params[(count($params) - 1)]->getWhitespaceAfter(), $this->currentFile->eolChar) !== 2) {
581
                $error    = 'Last parameter comment requires a blank newline after it';
582
                $errorPos = ($params[(count($params) - 1)]->getLine() + $commentStart);
583
                $this->currentFile->addError($error, $errorPos);
584
            }
585
 
586
            // Parameters must appear immediately after the comment.
587
            if ($params[0]->getOrder() !== 2) {
588
                $error    = 'Parameters must appear immediately after the comment';
589
                $errorPos = ($params[0]->getLine() + $commentStart);
590
                $this->currentFile->addError($error, $errorPos);
591
            }
592
 
593
            $previousParam      = null;
594
            $spaceBeforeVar     = 10000;
595
            $spaceBeforeComment = 10000;
596
            $longestType        = 0;
597
            $longestVar         = 0;
598
 
599
            foreach ($params as $param) {
600
 
601
                $paramComment = trim($param->getComment());
602
                $errorPos     = ($param->getLine() + $commentStart);
603
 
604
                // Make sure that there is only one space before the var type.
605
                if ($param->getWhitespaceBeforeType() !== ' ') {
606
                    $error = 'Expected 1 space before variable type';
607
                    $this->currentFile->addError($error, $errorPos);
608
                }
609
 
610
                $spaceCount = substr_count($param->getWhitespaceBeforeVarName(), ' ');
611
                if ($spaceCount < $spaceBeforeVar) {
612
                    $spaceBeforeVar = $spaceCount;
613
                    $longestType    = $errorPos;
614
                }
615
 
616
                $spaceCount = substr_count($param->getWhitespaceBeforeComment(), ' ');
617
 
618
                if ($spaceCount < $spaceBeforeComment && $paramComment !== '') {
619
                    $spaceBeforeComment = $spaceCount;
620
                    $longestVar         = $errorPos;
621
                }
622
 
623
                // Make sure they are in the correct order, and have the correct name.
624
                $pos       = $param->getPosition();
625
                $paramName = ($param->getVarName() !== '') ? $param->getVarName() : '[ UNKNOWN ]';
626
 
627
                if ($previousParam !== null) {
628
                    $previousName = ($previousParam->getVarName() !== '') ? $previousParam->getVarName() : 'UNKNOWN';
629
 
630
                    // Check to see if the parameters align properly.
631
                    if ($param->alignsVariableWith($previousParam) === false) {
632
                        $error = 'The variable names for parameters '.$previousName.' ('.($pos - 1).') and '.$paramName.' ('.$pos.') do not align';
633
                        $this->currentFile->addError($error, $errorPos);
634
                    }
635
 
636
                    if ($param->alignsCommentWith($previousParam) === false) {
637
                        $error = 'The comments for parameters '.$previousName.' ('.($pos - 1).') and '.$paramName.' ('.$pos.') do not align';
638
                        $this->currentFile->addError($error, $errorPos);
639
                    }
640
                }
641
 
642
                // Variable must be one of the supported standard type.
643
                $typeNames = explode('|', $param->getType());
644
                foreach ($typeNames as $typeName) {
645
                    $suggestedName = PHP_CodeSniffer::suggestType($typeName);
646
                    if ($typeName !== $suggestedName) {
647
                        $error = "Expected \"$suggestedName\"; found \"$typeName\" for $paramName at position $pos";
648
                        $this->currentFile->addError($error, $errorPos);
649
                    } else if (count($typeNames) === 1) {
650
                        // Check type hint for array and custom type.
651
                        $suggestedTypeHint = '';
652
                        if (strpos($suggestedName, 'array') !== false) {
653
                            $suggestedTypeHint = 'array';
654
                        } else if (in_array($typeName, PHP_CodeSniffer::$allowedTypes) === false) {
655
                            $suggestedTypeHint = $suggestedName;
656
                        }
657
 
658
                        if ($suggestedTypeHint !== '' && isset($realParams[($pos - 1)]) === true) {
659
                            $typeHint = $realParams[($pos - 1)]['type_hint'];
660
                            if ($typeHint === '') {
661
                                $error = "Type hint \"$suggestedTypeHint\" missing for $paramName at position $pos";
662
                                $this->currentFile->addError($error, ($commentEnd + 2));
663
                            } else if ($typeHint !== $suggestedTypeHint) {
664
                                $error = "Expected type hint \"$suggestedTypeHint\"; found \"$typeHint\" for $paramName at position $pos";
665
                                $this->currentFile->addError($error, ($commentEnd + 2));
666
                            }
667
                        } else if ($suggestedTypeHint === '' && isset($realParams[($pos - 1)]) === true) {
668
                            $typeHint = $realParams[($pos - 1)]['type_hint'];
669
                            if ($typeHint !== '') {
670
                                $error = "Unknown type hint \"$typeHint\" found for $paramName at position $pos";
671
                                $this->currentFile->addError($error, ($commentEnd + 2));
672
                            }
673
                        }
674
                    }//end if
675
                }//end foreach
676
 
677
                // Make sure the names of the parameter comment matches the
678
                // actual parameter.
679
                if (isset($realParams[($pos - 1)]) === true) {
680
                    $realName      = $realParams[($pos - 1)]['name'];
681
                    $foundParams[] = $realName;
682
 
683
                    // Append ampersand to name if passing by reference.
684
                    if ($realParams[($pos - 1)]['pass_by_reference'] === true) {
685
                        $realName = '&'.$realName;
686
                    }
687
 
688
                    if ($realName !== $paramName) {
689
                        $error  = 'Doc comment for var '.$paramName;
690
                        $error .= ' does not match ';
691
                        if (strtolower($paramName) === strtolower($realName)) {
692
                            $error .= 'case of ';
693
                        }
694
 
695
                        $error .= 'actual variable name '.$realName;
696
                        $error .= ' at position '.$pos;
697
 
698
                        $this->currentFile->addError($error, $errorPos);
699
                    }
700
                } else {
701
                    // We must have an extra parameter comment.
702
                    $error = 'Superfluous doc comment at position '.$pos;
703
                    $this->currentFile->addError($error, $errorPos);
704
                }
705
 
706
                if ($param->getVarName() === '') {
707
                    $error = 'Missing parameter name at position '.$pos;
708
                     $this->currentFile->addError($error, $errorPos);
709
                }
710
 
711
                if ($param->getType() === '') {
712
                    $error = 'Missing type at position '.$pos;
713
                    $this->currentFile->addError($error, $errorPos);
714
                }
715
 
716
                if ($paramComment === '') {
717
                    $error = 'Missing comment for param "'.$paramName.'" at position '.$pos;
718
                    $this->currentFile->addError($error, $errorPos);
719
                } else {
720
                    // Param comments must start with a capital letter and
721
                    // end with the full stop.
722
                    $firstChar = $paramComment{0};
723
                    if (preg_match('|[A-Z]|', $firstChar) === 0) {
724
                        $error = 'Param comment must start with a capital letter';
725
                        $this->currentFile->addError($error, $errorPos);
726
                    }
727
 
728
                    $lastChar = $paramComment[(strlen($paramComment) - 1)];
729
                    if ($lastChar !== '.') {
730
                        $error = 'Param comment must end with a full stop';
731
                        $this->currentFile->addError($error, $errorPos);
732
                    }
733
                }
734
 
735
                $previousParam = $param;
736
 
737
            }//end foreach
738
 
739
            if ($spaceBeforeVar !== 1 && $spaceBeforeVar !== 10000 && $spaceBeforeComment !== 10000) {
740
                $error = 'Expected 1 space after the longest type';
741
                $this->currentFile->addError($error, $longestType);
742
            }
743
 
744
            if ($spaceBeforeComment !== 1 && $spaceBeforeComment !== 10000) {
745
                $error = 'Expected 1 space after the longest variable name';
746
                $this->currentFile->addError($error, $longestVar);
747
            }
748
 
749
        }//end if
750
 
751
        $realNames = array();
752
        foreach ($realParams as $realParam) {
753
            $realNames[] = $realParam['name'];
754
        }
755
 
756
        // Report missing comments.
757
        $diff = array_diff($realNames, $foundParams);
758
        foreach ($diff as $neededParam) {
759
            if (count($params) !== 0) {
760
                $errorPos = ($params[(count($params) - 1)]->getLine() + $commentStart);
761
            } else {
762
                $errorPos = $commentStart;
763
            }
764
 
765
            $error = 'Doc comment for "'.$neededParam.'" missing';
766
            $this->currentFile->addError($error, $errorPos);
767
        }
768
 
769
    }//end processParams()
770
 
771
 
772
}//end class
773
 
774
?>