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 class 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: ClassCommentSniff.php 245547 2007-11-04 22:29:53Z 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 class doc comment.
23
 *
24
 * Verifies that :
25
 * <ul>
26
 *  <li>A class doc comment exists.</li>
27
 *  <li>There is exactly one blank line before the class 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 format of the since tag (x.x.x).</li>
33
 * </ul>
34
 *
35
 * @category  PHP
36
 * @package   PHP_CodeSniffer
37
 * @author    Greg Sherwood <gsherwood@squiz.net>
38
 * @author    Marc McIntyre <mmcintyre@squiz.net>
39
 * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600)
40
 * @license   http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
41
 * @version   Release: 1.2.1
42
 * @link      http://pear.php.net/package/PHP_CodeSniffer
43
 */
44
class Squiz_Sniffs_Commenting_ClassCommentSniff implements PHP_CodeSniffer_Sniff
45
{
46
 
47
 
48
    /**
49
     * Returns an array of tokens this test wants to listen for.
50
     *
51
     * @return array
52
     */
53
    public function register()
54
    {
55
        return array(T_CLASS);
56
 
57
    }//end register()
58
 
59
 
60
    /**
61
     * Processes this test, when one of its tokens is encountered.
62
     *
63
     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
64
     * @param int                  $stackPtr  The position of the current token
65
     *                                        in the stack passed in $tokens.
66
     *
67
     * @return void
68
     */
69
    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
70
    {
71
        $this->currentFile = $phpcsFile;
72
 
73
        $tokens = $phpcsFile->getTokens();
74
        $find   = array (
75
                   T_ABSTRACT,
76
                   T_WHITESPACE,
77
                   T_FINAL,
78
                  );
79
 
80
        // Extract the class comment docblock.
81
        $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1), null, true);
82
 
83
        if ($commentEnd !== false && $tokens[$commentEnd]['code'] === T_COMMENT) {
84
            $phpcsFile->addError('You must use "/**" style comments for a class comment', $stackPtr);
85
            return;
86
        } else if ($commentEnd === false || $tokens[$commentEnd]['code'] !== T_DOC_COMMENT) {
87
            $phpcsFile->addError('Missing class doc comment', $stackPtr);
88
            return;
89
        }
90
 
91
        $commentStart = ($phpcsFile->findPrevious(T_DOC_COMMENT, ($commentEnd - 1), null, true) + 1);
92
        $commentNext  = $phpcsFile->findPrevious(T_WHITESPACE, ($commentEnd + 1), $stackPtr, false, $phpcsFile->eolChar);
93
 
94
        // Distinguish file and class comment.
95
        $prevClassToken = $phpcsFile->findPrevious(T_CLASS, ($stackPtr - 1));
96
        if ($prevClassToken === false) {
97
            // This is the first class token in this file, need extra checks.
98
            $prevNonComment = $phpcsFile->findPrevious(T_DOC_COMMENT, ($commentStart - 1), null, true);
99
            if ($prevNonComment !== false) {
100
                $prevComment = $phpcsFile->findPrevious(T_DOC_COMMENT, ($prevNonComment - 1));
101
                if ($prevComment === false) {
102
                    // There is only 1 doc comment between open tag and class token.
103
                    $newlineToken = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), $stackPtr, false, $phpcsFile->eolChar);
104
                    if ($newlineToken !== false) {
105
                        $newlineToken = $phpcsFile->findNext(T_WHITESPACE, ($newlineToken + 1), $stackPtr, false, $phpcsFile->eolChar);
106
                        if ($newlineToken !== false) {
107
                            // Blank line between the class and the doc block.
108
                            // The doc block is most likely a file comment.
109
                            $phpcsFile->addError('Missing class doc comment', ($stackPtr + 1));
110
                            return;
111
                        }
112
                    }//end if
113
                }//end if
114
 
115
                // Exactly one blank line before the class comment.
116
                $prevTokenEnd = $phpcsFile->findPrevious(T_WHITESPACE, ($commentStart - 1), null, true);
117
                if ($prevTokenEnd !== false) {
118
                    $blankLineBefore = 0;
119
                    for ($i = ($prevTokenEnd + 1); $i < $commentStart; $i++) {
120
                        if ($tokens[$i]['code'] === T_WHITESPACE && $tokens[$i]['content'] === $phpcsFile->eolChar) {
121
                            $blankLineBefore++;
122
                        }
123
                    }
124
 
125
                    if ($blankLineBefore !== 2) {
126
                        $error = 'There must be exactly one blank line before the class comment';
127
                        $phpcsFile->addError($error, ($commentStart - 1));
128
                    }
129
                }
130
 
131
            }//end if
132
        }//end if
133
 
134
        $comment = $phpcsFile->getTokensAsString($commentStart, ($commentEnd - $commentStart + 1));
135
 
136
        // Parse the class comment docblock.
137
        try {
138
            $this->commentParser = new PHP_CodeSniffer_CommentParser_ClassCommentParser($comment, $phpcsFile);
139
            $this->commentParser->parse();
140
        } catch (PHP_CodeSniffer_CommentParser_ParserException $e) {
141
            $line = ($e->getLineWithinComment() + $commentStart);
142
            $phpcsFile->addError($e->getMessage(), $line);
143
            return;
144
        }
145
 
146
        $comment = $this->commentParser->getComment();
147
        if (is_null($comment) === true) {
148
            $error = 'Class doc comment is empty';
149
            $phpcsFile->addError($error, $commentStart);
150
            return;
151
        }
152
 
153
        // Check for a comment description.
154
        $short = rtrim($comment->getShortComment(), $phpcsFile->eolChar);
155
        if (trim($short) === '') {
156
            $error = 'Missing short description in class doc comment';
157
            $phpcsFile->addError($error, $commentStart);
158
            return;
159
        }
160
 
161
        // No extra newline before short description.
162
        $newlineCount = 0;
163
        $newlineSpan  = strspn($short, $phpcsFile->eolChar);
164
        if ($short !== '' && $newlineSpan > 0) {
165
            $line  = ($newlineSpan > 1) ? 'newlines' : 'newline';
166
            $error = "Extra $line found before class comment short description";
167
            $phpcsFile->addError($error, ($commentStart + 1));
168
        }
169
 
170
        $newlineCount = (substr_count($short, $phpcsFile->eolChar) + 1);
171
 
172
        // Exactly one blank line between short and long description.
173
        $long = $comment->getLongComment();
174
        if (empty($long) === false) {
175
            $between        = $comment->getWhiteSpaceBetween();
176
            $newlineBetween = substr_count($between, $phpcsFile->eolChar);
177
            if ($newlineBetween !== 2) {
178
                $error = 'There must be exactly one blank line between descriptions in class comment';
179
                $phpcsFile->addError($error, ($commentStart + $newlineCount + 1));
180
            }
181
 
182
            $newlineCount += $newlineBetween;
183
 
184
            $testLong = trim($long);
185
            if (preg_match('|[A-Z]|', $testLong[0]) === 0) {
186
                $error = 'Class comment long description must start with a capital letter';
187
                $phpcsFile->addError($error, ($commentStart + $newlineCount));
188
            }
189
        }
190
 
191
        // Exactly one blank line before tags.
192
        $tags = $this->commentParser->getTagOrders();
193
        if (count($tags) > 1) {
194
            $newlineSpan = $comment->getNewlineAfter();
195
            if ($newlineSpan !== 2) {
196
                $error = 'There must be exactly one blank line before the tags in class comment';
197
                if ($long !== '') {
198
                    $newlineCount += (substr_count($long, $phpcsFile->eolChar) - $newlineSpan + 1);
199
                }
200
 
201
                $phpcsFile->addError($error, ($commentStart + $newlineCount));
202
                $short = rtrim($short, $phpcsFile->eolChar.' ');
203
            }
204
        }
205
 
206
        // Short description must be single line and end with a full stop.
207
        $testShort = trim($short);
208
        $lastChar  = $testShort[(strlen($testShort) - 1)];
209
        if (substr_count($testShort, $phpcsFile->eolChar) !== 0) {
210
            $error = 'Class comment short description must be on a single line';
211
            $phpcsFile->addError($error, ($commentStart + 1));
212
        }
213
 
214
        if (preg_match('|[A-Z]|', $testShort[0]) === 0) {
215
            $error = 'Class comment short description must start with a capital letter';
216
            $phpcsFile->addError($error, ($commentStart + 1));
217
        }
218
 
219
        if ($lastChar !== '.') {
220
            $error = 'Class comment short description must end with a full stop';
221
            $phpcsFile->addError($error, ($commentStart + 1));
222
        }
223
 
224
        // Check for unknown/deprecated tags.
225
        $unknownTags = $this->commentParser->getUnknown();
226
        foreach ($unknownTags as $errorTag) {
227
            $error = "@$errorTag[tag] tag is not allowed in class comment";
228
            $phpcsFile->addWarning($error, ($commentStart + $errorTag['line']));
229
            return;
230
        }
231
 
232
        // Check each tag.
233
        $this->processTags($commentStart, $commentEnd);
234
 
235
    }//end process()
236
 
237
 
238
    /**
239
     * Processes each required or optional tag.
240
     *
241
     * @param int $commentStart The position in the stack where the comment started.
242
     * @param int $commentEnd   The position in the stack where the comment ended.
243
     *
244
     * @return void
245
     */
246
    protected function processTags($commentStart, $commentEnd)
247
    {
248
        $foundTags = $this->commentParser->getTagOrders();
249
 
250
        // Other tags found.
251
        foreach ($foundTags as $tagName) {
252
            if ($tagName !== 'comment' && $tagName !== 'since') {
253
                $error = 'Only @since tag is allowed in class comment';
254
                $this->currentFile->addWarning($error, $commentEnd);
255
                break;
256
            }
257
        }
258
 
259
        // Since tag missing.
260
        if (in_array('since', $foundTags) === false) {
261
            $error = 'Missing @since tag in class comment';
262
            $this->currentFile->addError($error, $commentEnd);
263
            return;
264
        }
265
 
266
        // Get the line number for current tag.
267
        $since = $this->commentParser->getSince();
268
        if (is_null($since) === true || empty($since) === true) {
269
            return;
270
        }
271
 
272
        $errorPos = ($commentStart + $since->getLine());
273
 
274
        // Make sure there is no duplicate tag.
275
        $foundIndexes = array_keys($foundTags, 'since');
276
        if (count($foundIndexes) > 1) {
277
            $error = 'Only 1 @since tag is allowed in class comment';
278
            $this->currentFile->addError($error, $errorPos);
279
        }
280
 
281
        // Check spacing.
282
        if ($since->getContent() !== '') {
283
            $spacing = substr_count($since->getWhitespaceBeforeContent(), ' ');
284
            if ($spacing !== 1) {
285
                $error = "Expected 1 space but found $spacing before version number in @since tag";
286
                $this->currentFile->addError($error, $errorPos);
287
            }
288
        }
289
 
290
        // Check content.
291
        $this->processSince($errorPos);
292
 
293
    }//end processTags()
294
 
295
 
296
    /**
297
     * Processes the since tag.
298
     *
299
     * The since tag must have the exact keyword 'release_version'
300
     * or is in the form x.x.x
301
     *
302
     * @param int $errorPos The line number where the error occurs.
303
     *
304
     * @return void
305
     */
306
    protected function processSince($errorPos)
307
    {
308
        $since = $this->commentParser->getSince();
309
        if ($since !== null) {
310
            $content = $since->getContent();
311
            if (empty($content) === true) {
312
                $error = 'Content missing for @since tag in class comment';
313
                $this->currentFile->addError($error, $errorPos);
314
 
315
            } else if ($content !== '%release_version%') {
316
                if (preg_match('/^([0-9]+)\.([0-9]+)\.([0-9]+)/', $content) === 0) {
317
                    $error = 'Expected version number to be in the form x.x.x in @since tag';
318
                    $this->currentFile->addError($error, $errorPos);
319
                }
320
            }
321
        }
322
 
323
    }//end processSince()
324
 
325
 
326
}//end class
327
?>