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 file doc comment.
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: FileCommentSniff.php 265102 2008-08-18 23:07:22Z squiz $
14
 * @link      http://pear.php.net/package/PHP_CodeSniffer
15
 */
16
 
17
if (class_exists('PHP_CodeSniffer_CommentParser_ClassCommentParser', true) === false) {
18
    throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_CommentParser_ClassCommentParser not found');
19
}
20
 
21
/**
22
 * Parses and verifies the file doc comment.
23
 *
24
 * Verifies that :
25
 * <ul>
26
 *  <li>A file doc comment exists.</li>
27
 *  <li>There is no blank line between the open tag and the file comment.</li>
28
 *  <li>Short description ends with a full stop.</li>
29
 *  <li>There is a blank line after the short description.</li>
30
 *  <li>Each paragraph of the long description ends with a full stop.</li>
31
 *  <li>There is a blank line between the description and the tags.</li>
32
 *  <li>Check the order, indentation and content of each tag.</li>
33
 *  <li>There is exactly one blank line after the file comment.</li>
34
 * </ul>
35
 *
36
 * @category  PHP
37
 * @package   PHP_CodeSniffer
38
 * @author    Greg Sherwood <gsherwood@squiz.net>
39
 * @author    Marc McIntyre <mmcintyre@squiz.net>
40
 * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600)
41
 * @license   http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
42
 * @version   Release: 1.2.1
43
 * @link      http://pear.php.net/package/PHP_CodeSniffer
44
 */
45
 
46
class Squiz_Sniffs_Commenting_FileCommentSniff implements PHP_CodeSniffer_Sniff
47
{
48
 
49
    /**
50
     * The header comment parser for the current file.
51
     *
52
     * @var PHP_CodeSniffer_Comment_Parser_ClassCommentParser
53
     */
54
    protected $commentParser = null;
55
 
56
    /**
57
     * The current PHP_CodeSniffer_File object we are processing.
58
     *
59
     * @var PHP_CodeSniffer_File
60
     */
61
    protected $currentFile = null;
62
 
63
 
64
    /**
65
     * Returns an array of tokens this test wants to listen for.
66
     *
67
     * @return array
68
     */
69
    public function register()
70
    {
71
        return array(T_OPEN_TAG);
72
 
73
    }//end register()
74
 
75
 
76
    /**
77
     * Processes this test, when one of its tokens is encountered.
78
     *
79
     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
80
     * @param int                  $stackPtr  The position of the current token
81
     *                                        in the stack passed in $tokens.
82
     *
83
     * @return void
84
     */
85
    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
86
    {
87
        $this->currentFile = $phpcsFile;
88
 
89
        // We are only interested if this is the first open tag.
90
        if ($stackPtr !== 0) {
91
            if ($phpcsFile->findPrevious(T_OPEN_TAG, ($stackPtr - 1)) !== false) {
92
                return;
93
            }
94
        }
95
 
96
        $tokens = $phpcsFile->getTokens();
97
 
98
        $errorToken = ($stackPtr + 1);
99
        if (isset($tokens[$errorToken]) === false) {
100
            $errorToken--;
101
        }
102
 
103
        // Find the next non whitespace token.
104
        $commentStart = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
105
 
106
        if ($tokens[$commentStart]['code'] === T_CLOSE_TAG) {
107
            // We are only interested if this is the first open tag.
108
            return;
109
        } else if ($tokens[$commentStart]['code'] === T_COMMENT) {
110
            $phpcsFile->addError('You must use "/**" style comments for a file comment', $errorToken);
111
            return;
112
        } else if ($commentStart === false || $tokens[$commentStart]['code'] !== T_DOC_COMMENT) {
113
            $phpcsFile->addError('Missing file doc comment', $errorToken);
114
            return;
115
        } else {
116
 
117
            // Extract the header comment docblock.
118
            $commentEnd = ($phpcsFile->findNext(T_DOC_COMMENT, ($commentStart + 1), null, true) - 1);
119
 
120
            // Check if there is only 1 doc comment between the open tag and class token.
121
            $nextToken   = array(
122
                            T_ABSTRACT,
123
                            T_CLASS,
124
                            T_DOC_COMMENT,
125
                           );
126
 
127
            $commentNext = $phpcsFile->findNext($nextToken, ($commentEnd + 1));
128
            if ($commentNext !== false && $tokens[$commentNext]['code'] !== T_DOC_COMMENT) {
129
                // Found a class token right after comment doc block.
130
                $newlineToken = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), $commentNext, false, $phpcsFile->eolChar);
131
                if ($newlineToken !== false) {
132
                    $newlineToken = $phpcsFile->findNext(T_WHITESPACE, ($newlineToken + 1), $commentNext, false, $phpcsFile->eolChar);
133
                    if ($newlineToken === false) {
134
                        // No blank line between the class token and the doc block.
135
                        // The doc block is most likely a class comment.
136
                        $phpcsFile->addError('Missing file doc comment', $errorToken);
137
                        return;
138
                    }
139
                }
140
            }
141
 
142
            // No blank line between the open tag and the file comment.
143
            $blankLineBefore = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, false, $phpcsFile->eolChar);
144
            if ($blankLineBefore !== false && $blankLineBefore < $commentStart) {
145
                $error = 'Extra newline found after the open tag';
146
                $phpcsFile->addError($error, ($stackPtr + 1));
147
            }
148
 
149
            // Exactly one blank line after the file comment.
150
            $nextTokenStart = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), null, true);
151
            if ($nextTokenStart !== false) {
152
                $blankLineAfter = 0;
153
                for ($i = ($commentEnd + 1); $i < $nextTokenStart; $i++) {
154
                    if ($tokens[$i]['code'] === T_WHITESPACE && $tokens[$i]['content'] === $phpcsFile->eolChar) {
155
                        $blankLineAfter++;
156
                    }
157
                }
158
 
159
                if ($blankLineAfter !== 2) {
160
                    $error = 'There must be exactly one blank line after the file comment';
161
                    $phpcsFile->addError($error, ($commentEnd + 1));
162
                }
163
            }
164
 
165
            $comment = $phpcsFile->getTokensAsString($commentStart, ($commentEnd - $commentStart + 1));
166
 
167
            // Parse the header comment docblock.
168
            try {
169
                $this->commentParser = new PHP_CodeSniffer_CommentParser_ClassCommentParser($comment, $phpcsFile);
170
                $this->commentParser->parse();
171
            } catch (PHP_CodeSniffer_CommentParser_ParserException $e) {
172
                $line = ($e->getLineWithinComment() + $commentStart);
173
                $phpcsFile->addError($e->getMessage(), $line);
174
                return;
175
            }
176
 
177
            $comment = $this->commentParser->getComment();
178
            if (is_null($comment) === true) {
179
                $error = 'File doc comment is empty';
180
                $phpcsFile->addError($error, $commentStart);
181
                return;
182
            }
183
 
184
            // No extra newline before short description.
185
            $short        = $comment->getShortComment();
186
            $newlineCount = 0;
187
            $newlineSpan  = strspn($short, $phpcsFile->eolChar);
188
            if ($short !== '' && $newlineSpan > 0) {
189
                $line  = ($newlineSpan > 1) ? 'newlines' : 'newline';
190
                $error = "Extra $line found before file comment short description";
191
                $phpcsFile->addError($error, ($commentStart + 1));
192
            }
193
 
194
            $newlineCount = (substr_count($short, $phpcsFile->eolChar) + 1);
195
 
196
            // Exactly one blank line between short and long description.
197
            $long = $comment->getLongComment();
198
            if (empty($long) === false) {
199
                $between        = $comment->getWhiteSpaceBetween();
200
                $newlineBetween = substr_count($between, $phpcsFile->eolChar);
201
                if ($newlineBetween !== 2) {
202
                    $error = 'There must be exactly one blank line between descriptions in file comment';
203
                    $phpcsFile->addError($error, ($commentStart + $newlineCount + 1));
204
                }
205
 
206
                $newlineCount += $newlineBetween;
207
 
208
                $testLong = trim($long);
209
                if (preg_match('|[A-Z]|', $testLong[0]) === 0) {
210
                    $error = 'File comment long description must start with a capital letter';
211
                    $phpcsFile->addError($error, ($commentStart + $newlineCount));
212
                }
213
            }//end if
214
 
215
            // Exactly one blank line before tags.
216
            $tags = $this->commentParser->getTagOrders();
217
            if (count($tags) > 1) {
218
                $newlineSpan = $comment->getNewlineAfter();
219
                if ($newlineSpan !== 2) {
220
                    $error = 'There must be exactly one blank line before the tags in file comment';
221
                    if ($long !== '') {
222
                        $newlineCount += (substr_count($long, $phpcsFile->eolChar) - $newlineSpan + 1);
223
                    }
224
 
225
                    $phpcsFile->addError($error, ($commentStart + $newlineCount));
226
                    $short = rtrim($short, $phpcsFile->eolChar.' ');
227
                }
228
            }
229
 
230
            // Short description must be single line and end with a full stop.
231
            $testShort = trim($short);
232
            $lastChar  = $testShort[(strlen($testShort) - 1)];
233
            if (substr_count($testShort, $phpcsFile->eolChar) !== 0) {
234
                $error = 'File comment short description must be on a single line';
235
                $phpcsFile->addError($error, ($commentStart + 1));
236
            }
237
 
238
            if (preg_match('|[A-Z]|', $testShort[0]) === 0) {
239
                $error = 'File comment short description must start with a capital letter';
240
                $phpcsFile->addError($error, ($commentStart + 1));
241
            }
242
 
243
            if ($lastChar !== '.') {
244
                $error = 'File comment short description must end with a full stop';
245
                $phpcsFile->addError($error, ($commentStart + 1));
246
            }
247
 
248
            // Check for unknown/deprecated tags.
249
            $unknownTags = $this->commentParser->getUnknown();
250
            foreach ($unknownTags as $errorTag) {
251
                // Unknown tags are not parsed, do not process further.
252
                $error = "@$errorTag[tag] tag is not allowed in file comment";
253
                $phpcsFile->addWarning($error, ($commentStart + $errorTag['line']));
254
            }
255
 
256
            // Check each tag.
257
            $this->processTags($commentStart, $commentEnd);
258
        }//end if
259
 
260
    }//end process()
261
 
262
 
263
    /**
264
     * Processes each required or optional tag.
265
     *
266
     * @param int $commentStart The position in the stack where the comment started.
267
     * @param int $commentEnd   The position in the stack where the comment ended.
268
     *
269
     * @return void
270
     */
271
    protected function processTags($commentStart, $commentEnd)
272
    {
273
        // Required tags in correct order.
274
        $tags = array(
275
                 'version'    => 'precedes @package',
276
                 'package'    => 'follows @version',
277
                 'subpackage' => 'follows @package',
278
                 'author'     => 'follows @subpackage',
279
                 'copyright'  => 'follows @author',
280
                 'license'    => 'follows @copyright',
281
                );
282
 
283
        $foundTags   = $this->commentParser->getTagOrders();
284
        $errorPos    = 0;
285
        $orderIndex  = 0;
286
        $longestTag  = 0;
287
        $indentation = array();
288
        foreach ($tags as $tag => $orderText) {
289
 
290
            // Required tag missing.
291
            if (in_array($tag, $foundTags) === false) {
292
                $error = "Missing @$tag tag in file comment";
293
                $this->currentFile->addError($error, $commentEnd);
294
                continue;
295
            }
296
 
297
            // Get the line number for current tag.
298
            $tagName = ucfirst($tag);
299
            if ($tagName === 'Author' || $tagName === 'Copyright') {
300
                // These tags are different because they return an array.
301
                $tagName .= 's';
302
            }
303
 
304
            // Work out the line number for this tag.
305
            $getMethod  = 'get'.$tagName;
306
            $tagElement = $this->commentParser->$getMethod();
307
            if (is_null($tagElement) === true || empty($tagElement) === true) {
308
                continue;
309
            } else if (is_array($tagElement) === true && empty($tagElement) === false) {
310
                $tagElement = $tagElement[0];
311
            }
312
 
313
            $errorPos = ($commentStart + $tagElement->getLine());
314
 
315
            // Make sure there is no duplicate tag.
316
            $foundIndexes = array_keys($foundTags, $tag);
317
            if (count($foundIndexes) > 1) {
318
                $error = "Only 1 @$tag tag is allowed in file comment";
319
                $this->currentFile->addError($error, $errorPos);
320
            }
321
 
322
            // Check tag order.
323
            if ($foundIndexes[0] > $orderIndex) {
324
                $orderIndex = $foundIndexes[0];
325
            } else {
326
                $error = "The @$tag tag is in the wrong order; the tag $orderText";;
327
                $this->currentFile->addError($error, $errorPos);
328
            }
329
 
330
            // Store the indentation of each tag.
331
            $len = strlen($tag);
332
            if ($len > $longestTag) {
333
                $longestTag = $len;
334
            }
335
 
336
            $indentation[] = array(
337
                              'tag'      => $tag,
338
                              'errorPos' => $errorPos,
339
                              'space'    => $this->getIndentation($tag, $tagElement),
340
                             );
341
 
342
            $method = 'process'.$tagName;
343
            if (method_exists($this, $method) === true) {
344
                // Process each tag if a method is defined.
345
                call_user_func(array($this, $method), $errorPos);
346
            } else {
347
                $tagElement->process($this->currentFile, $commentStart, 'file');
348
            }
349
        }//end foreach
350
 
351
        // Check tag indentation.
352
        foreach ($indentation as $indentInfo) {
353
            $tagName = ucfirst($indentInfo['tag']);
354
            if ($tagName === 'Author') {
355
                $tagName .= 's';
356
            }
357
 
358
            if ($indentInfo['space'] !== 0 && $indentInfo['space'] !== ($longestTag + 1)) {
359
                $expected = ($longestTag - strlen($indentInfo['tag']) + 1);
360
                $space    = ($indentInfo['space'] - strlen($indentInfo['tag']));
361
                $error    = "@$indentInfo[tag] tag comment indented incorrectly. ";
362
                $error   .= "Expected $expected spaces but found $space.";
363
                $this->currentFile->addError($error, $indentInfo['errorPos']);
364
            }
365
        }
366
 
367
    }//end processTags()
368
 
369
 
370
    /**
371
     * Get the indentation information of each tag.
372
     *
373
     * @param string                                   $tagName    The name of the doc comment element.
374
     * @param PHP_CodeSniffer_CommentParser_DocElement $tagElement The doc comment element.
375
     *
376
     * @return void
377
     */
378
    protected function getIndentation($tagName, $tagElement)
379
    {
380
        if ($tagElement instanceof PHP_CodeSniffer_CommentParser_SingleElement) {
381
            if ($tagElement->getContent() !== '') {
382
                return (strlen($tagName) + substr_count($tagElement->getWhitespaceBeforeContent(), ' '));
383
            }
384
        } else if ($tagElement instanceof PHP_CodeSniffer_CommentParser_PairElement) {
385
            if ($tagElement->getValue() !== '') {
386
                return (strlen($tagName) + substr_count($tagElement->getWhitespaceBeforeValue(), ' '));
387
            }
388
        }
389
 
390
        return 0;
391
 
392
    }//end getIndentation()
393
 
394
 
395
    /**
396
     * The version tag must have the exact keyword 'release_version'.
397
     *
398
     * @param int $errorPos The line number where the error occurs.
399
     *
400
     * @return void
401
     */
402
    protected function processVersion($errorPos)
403
    {
404
        $version = $this->commentParser->getVersion();
405
        if ($version !== null) {
406
            $content = $version->getContent();
407
            if (empty($content) === true) {
408
                $error = 'Content missing for @version tag in file comment';
409
                $this->currentFile->addError($error, $errorPos);
410
            } else if ($content !== '%release_version%') {
411
                if (preg_match('/^([0-9]+)\.([0-9]+)\.([0-9]+)/', $content) === 0) {
412
                    // Separate keyword so it does not get replaced when we commit.
413
                    $error = 'Expected keyword "%'.'release_version%" for version number';
414
                    $this->currentFile->addError($error, $errorPos);
415
                }
416
            }
417
        }
418
 
419
    }//end processVersion()
420
 
421
 
422
    /**
423
     * The package name must be 'MySource4'.
424
     *
425
     * @param int $errorPos The line number where the error occurs.
426
     *
427
     * @return void
428
     */
429
    protected function processPackage($errorPos)
430
    {
431
        $package = $this->commentParser->getPackage();
432
        if ($package !== null) {
433
            $content = $package->getContent();
434
            if (empty($content) === true) {
435
                $error = 'Content missing for @package tag in file comment';
436
                $this->currentFile->addError($error, $errorPos);
437
            } else if ($content !== 'MySource4') {
438
                $error = 'Expected "MySource4" for package name';
439
                $this->currentFile->addError($error, $errorPos);
440
            }
441
        }
442
 
443
    }//end processPackage()
444
 
445
 
446
    /**
447
     * The subpackage name must be camel-cased.
448
     *
449
     * @param int $errorPos The line number where the error occurs.
450
     *
451
     * @return void
452
     */
453
    protected function processSubpackage($errorPos)
454
    {
455
        $subpackage = $this->commentParser->getSubpackage();
456
        if ($subpackage !== null) {
457
            $content = $subpackage->getContent();
458
            if (empty($content) === true) {
459
                $error = 'Content missing for @subpackage tag in file comment';
460
                $this->currentFile->addError($error, $errorPos);
461
            } else if (PHP_CodeSniffer::isUnderscoreName($content) !== true) {
462
                // Subpackage name must be properly camel-cased.
463
                $nameBits = explode('_', $content);
464
                $firstBit = array_shift($nameBits);
465
                $newName  = strtoupper($firstBit{0}).substr($firstBit, 1).'_';
466
                foreach ($nameBits as $bit) {
467
                    $newName .= strtoupper($bit{0}).substr($bit, 1).'_';
468
                }
469
 
470
                $validName = trim($newName, '_');
471
                $error     = "Subpackage name \"$content\" is not valid; ";
472
                $error    .= "consider \"$validName\" instead";
473
                $this->currentFile->addError($error, $errorPos);
474
            }
475
        }
476
 
477
    }//end processSubpackage()
478
 
479
 
480
    /**
481
     * Author tag must be 'Squiz Pty Ltd <mysource4@squiz.net>'.
482
     *
483
     * @param int $errorPos The line number where the error occurs.
484
     *
485
     * @return void
486
     */
487
    protected function processAuthors($errorPos)
488
    {
489
        $authors = $this->commentParser->getAuthors();
490
        if (empty($authors) === false) {
491
            $author  = $authors[0];
492
            $content = $author->getContent();
493
            if (empty($content) === true) {
494
                $error = 'Content missing for @author tag in file comment';
495
                $this->currentFile->addError($error, $errorPos);
496
            } else if ($content !== 'Squiz Pty Ltd <mysource4@squiz.net>') {
497
                $error = 'Expected "Squiz Pty Ltd <mysource4@squiz.net>" for author tag';
498
                $this->currentFile->addError($error, $errorPos);
499
            }
500
        }
501
 
502
    }//end processAuthors()
503
 
504
 
505
    /**
506
     * Copyright tag must be in the form '2006-YYYY Squiz Pty Ltd (ABN 77 084 670 600)'.
507
     *
508
     * @param int $errorPos The line number where the error occurs.
509
     *
510
     * @return void
511
     */
512
    protected function processCopyrights($errorPos)
513
    {
514
        $copyrights = $this->commentParser->getCopyrights();
515
        $copyright  = $copyrights[0];
516
 
517
        if ($copyright !== null) {
518
            $content = $copyright->getContent();
519
            if (empty($content) === true) {
520
                $error = 'Content missing for @copyright tag in file comment';
521
                $this->currentFile->addError($error, $errorPos);
522
 
523
            } else if (preg_match('/^([0-9]{4})-([0-9]{4})? (Squiz Pty Ltd \(ABN 77 084 670 600\))$/', $content) === 0) {
524
                $error = 'Expected "2006-2007 Squiz Pty Ltd (ABN 77 084 670 600)" for copyright declaration';
525
                $this->currentFile->addError($error, $errorPos);
526
            }
527
        }
528
 
529
    }//end processCopyrights()
530
 
531
 
532
    /**
533
     * License tag must be 'http://matrix.squiz.net/licence Squiz.Net Open Source Licence'.
534
     *
535
     * @param int $errorPos The line number where the error occurs.
536
     *
537
     * @return void
538
     */
539
    protected function processLicense($errorPos)
540
    {
541
        $license = $this->commentParser->getLicense();
542
        if ($license !== null) {
543
            $url     = $license->getValue();
544
            $content = $license->getComment();
545
            if (empty($url) === true && empty($content) === true) {
546
                $error = 'Content missing for @license tag in file comment';
547
                $this->currentFile->addError($error, $errorPos);
548
            } else {
549
                // Check for license URL.
550
                if (empty($url) === true) {
551
                    $error = 'License URL missing for @license tag in file comment';
552
                    $this->currentFile->addError($error, $errorPos);
553
                } else if ($url !== 'http://matrix.squiz.net/licence') {
554
                    $error = 'Expected "http://matrix.squiz.net/licence" for license URL';
555
                    $this->currentFile->addError($error, $errorPos);
556
                }
557
 
558
                // Check for license name.
559
                if (empty($content) === true) {
560
                    $error = 'License name missing for @license tag in file comment';
561
                    $this->currentFile->addError($error, $errorPos);
562
                } else if ($content !== 'Squiz.Net Open Source Licence') {
563
                    $error = 'Expected "Squiz.Net Open Source Licence" for license name';
564
                    $this->currentFile->addError($error, $errorPos);
565
                }
566
            }//end if
567
        }//end if
568
 
569
    }//end processLicense()
570
 
571
 
572
}//end class
573
 
574
 
575
?>