Chris@0
|
1 <?php
|
Chris@0
|
2 /**
|
Chris@0
|
3 * Ensures doc blocks follow basic formatting.
|
Chris@0
|
4 *
|
Chris@0
|
5 * @category PHP
|
Chris@0
|
6 * @package PHP_CodeSniffer
|
Chris@0
|
7 * @link http://pear.php.net/package/PHP_CodeSniffer
|
Chris@0
|
8 */
|
Chris@0
|
9
|
Chris@0
|
10 /**
|
Chris@0
|
11 * Ensures doc blocks follow basic formatting.
|
Chris@0
|
12 *
|
Chris@0
|
13 * Largely copied from Generic_Sniffs_Commenting_DocCommentSniff, but Drupal @file
|
Chris@0
|
14 * comments are different.
|
Chris@0
|
15 *
|
Chris@0
|
16 * @category PHP
|
Chris@0
|
17 * @package PHP_CodeSniffer
|
Chris@0
|
18 * @link http://pear.php.net/package/PHP_CodeSniffer
|
Chris@0
|
19 */
|
Chris@0
|
20 class Drupal_Sniffs_Commenting_DocCommentSniff implements PHP_CodeSniffer_Sniff
|
Chris@0
|
21 {
|
Chris@0
|
22
|
Chris@0
|
23 /**
|
Chris@0
|
24 * A list of tokenizers this sniff supports.
|
Chris@0
|
25 *
|
Chris@0
|
26 * @var array
|
Chris@0
|
27 */
|
Chris@0
|
28 public $supportedTokenizers = array(
|
Chris@0
|
29 'PHP',
|
Chris@0
|
30 'JS',
|
Chris@0
|
31 );
|
Chris@0
|
32
|
Chris@0
|
33
|
Chris@0
|
34 /**
|
Chris@0
|
35 * Returns an array of tokens this test wants to listen for.
|
Chris@0
|
36 *
|
Chris@0
|
37 * @return array
|
Chris@0
|
38 */
|
Chris@0
|
39 public function register()
|
Chris@0
|
40 {
|
Chris@0
|
41 return array(T_DOC_COMMENT_OPEN_TAG);
|
Chris@0
|
42
|
Chris@0
|
43 }//end register()
|
Chris@0
|
44
|
Chris@0
|
45
|
Chris@0
|
46 /**
|
Chris@0
|
47 * Processes this test, when one of its tokens is encountered.
|
Chris@0
|
48 *
|
Chris@0
|
49 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
|
Chris@0
|
50 * @param int $stackPtr The position of the current token
|
Chris@0
|
51 * in the stack passed in $tokens.
|
Chris@0
|
52 *
|
Chris@0
|
53 * @return void
|
Chris@0
|
54 */
|
Chris@0
|
55 public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
|
Chris@0
|
56 {
|
Chris@0
|
57 $tokens = $phpcsFile->getTokens();
|
Chris@0
|
58 $commentEnd = $phpcsFile->findNext(T_DOC_COMMENT_CLOSE_TAG, ($stackPtr + 1));
|
Chris@0
|
59 $commentStart = $tokens[$commentEnd]['comment_opener'];
|
Chris@0
|
60
|
Chris@0
|
61 $empty = array(
|
Chris@0
|
62 T_DOC_COMMENT_WHITESPACE,
|
Chris@0
|
63 T_DOC_COMMENT_STAR,
|
Chris@0
|
64 );
|
Chris@0
|
65
|
Chris@0
|
66 $short = $phpcsFile->findNext($empty, ($stackPtr + 1), $commentEnd, true);
|
Chris@0
|
67 if ($short === false) {
|
Chris@0
|
68 // No content at all.
|
Chris@0
|
69 $error = 'Doc comment is empty';
|
Chris@0
|
70 $phpcsFile->addError($error, $stackPtr, 'Empty');
|
Chris@0
|
71 return;
|
Chris@0
|
72 }
|
Chris@0
|
73
|
Chris@0
|
74 // Ignore doc blocks in functions, this is handled by InlineCommentSniff.
|
Chris@0
|
75 if (empty($tokens[$stackPtr]['conditions']) === false && in_array(T_FUNCTION, $tokens[$stackPtr]['conditions']) === true) {
|
Chris@0
|
76 return;
|
Chris@0
|
77 }
|
Chris@0
|
78
|
Chris@0
|
79 // The first line of the comment should just be the /** code.
|
Chris@0
|
80 // In JSDoc there are cases with @lends that are on the same line as code.
|
Chris@0
|
81 if ($tokens[$short]['line'] === $tokens[$stackPtr]['line'] && $phpcsFile->tokenizerType !== 'JS') {
|
Chris@0
|
82 $error = 'The open comment tag must be the only content on the line';
|
Chris@0
|
83 $fix = $phpcsFile->addFixableError($error, $stackPtr, 'ContentAfterOpen');
|
Chris@0
|
84 if ($fix === true) {
|
Chris@0
|
85 $phpcsFile->fixer->beginChangeset();
|
Chris@0
|
86 $phpcsFile->fixer->addNewline($stackPtr);
|
Chris@0
|
87 $phpcsFile->fixer->addContentBefore($short, '* ');
|
Chris@0
|
88 $phpcsFile->fixer->endChangeset();
|
Chris@0
|
89 }
|
Chris@0
|
90 }
|
Chris@0
|
91
|
Chris@0
|
92 // The last line of the comment should just be the */ code.
|
Chris@0
|
93 $prev = $phpcsFile->findPrevious($empty, ($commentEnd - 1), $stackPtr, true);
|
Chris@0
|
94 if ($tokens[$commentEnd]['content'] !== '*/') {
|
Chris@0
|
95 $error = 'Wrong function doc comment end; expected "*/", found "%s"';
|
Chris@0
|
96 $phpcsFile->addError($error, $commentEnd, 'WrongEnd', array($tokens[$commentEnd]['content']));
|
Chris@0
|
97 }
|
Chris@0
|
98
|
Chris@0
|
99 // Check for additional blank lines at the end of the comment.
|
Chris@0
|
100 if ($tokens[$prev]['line'] < ($tokens[$commentEnd]['line'] - 1)) {
|
Chris@0
|
101 $error = 'Additional blank lines found at end of doc comment';
|
Chris@0
|
102 $fix = $phpcsFile->addFixableError($error, $commentEnd, 'SpacingAfter');
|
Chris@0
|
103 if ($fix === true) {
|
Chris@0
|
104 $phpcsFile->fixer->beginChangeset();
|
Chris@0
|
105 for ($i = ($prev + 1); $i < $commentEnd; $i++) {
|
Chris@0
|
106 if ($tokens[($i + 1)]['line'] === $tokens[$commentEnd]['line']) {
|
Chris@0
|
107 break;
|
Chris@0
|
108 }
|
Chris@0
|
109
|
Chris@0
|
110 $phpcsFile->fixer->replaceToken($i, '');
|
Chris@0
|
111 }
|
Chris@0
|
112
|
Chris@0
|
113 $phpcsFile->fixer->endChangeset();
|
Chris@0
|
114 }
|
Chris@0
|
115 }
|
Chris@0
|
116
|
Chris@0
|
117 // The short description of @file comments is one line below.
|
Chris@0
|
118 if ($tokens[$short]['code'] === T_DOC_COMMENT_TAG && $tokens[$short]['content'] === '@file') {
|
Chris@0
|
119 $next = $phpcsFile->findNext($empty, ($short + 1), $commentEnd, true);
|
Chris@0
|
120 if ($next !== false) {
|
Chris@0
|
121 $fileShort = $short;
|
Chris@0
|
122 $short = $next;
|
Chris@0
|
123 }
|
Chris@0
|
124 }
|
Chris@0
|
125
|
Chris@0
|
126 // Do not check defgroup sections, they have no short description. Also don't
|
Chris@0
|
127 // check PHPUnit tests doc blocks because they might not have a description.
|
Chris@0
|
128 if (in_array($tokens[$short]['content'], array('@defgroup', '@addtogroup', '@}', '@coversDefaultClass')) === true) {
|
Chris@0
|
129 return;
|
Chris@0
|
130 }
|
Chris@0
|
131
|
Chris@0
|
132 // Check for a comment description.
|
Chris@0
|
133 if ($tokens[$short]['code'] !== T_DOC_COMMENT_STRING) {
|
Chris@0
|
134 // JSDoc has many cases of @type declaration that don't have a
|
Chris@0
|
135 // description.
|
Chris@0
|
136 if ($phpcsFile->tokenizerType === 'JS') {
|
Chris@0
|
137 return;
|
Chris@0
|
138 }
|
Chris@0
|
139
|
Chris@0
|
140 // PHPUnit test methods are allowed to skip the short description and
|
Chris@0
|
141 // only provide an @covers annotation.
|
Chris@0
|
142 if ($tokens[$short]['content'] === '@covers') {
|
Chris@0
|
143 return;
|
Chris@0
|
144 }
|
Chris@0
|
145
|
Chris@0
|
146 $error = 'Missing short description in doc comment';
|
Chris@0
|
147 $phpcsFile->addError($error, $stackPtr, 'MissingShort');
|
Chris@0
|
148 return;
|
Chris@0
|
149 }
|
Chris@0
|
150
|
Chris@0
|
151 if (isset($fileShort) === true) {
|
Chris@0
|
152 $start = $fileShort;
|
Chris@0
|
153 } else {
|
Chris@0
|
154 $start = $stackPtr;
|
Chris@0
|
155 }
|
Chris@0
|
156
|
Chris@0
|
157 // No extra newline before short description.
|
Chris@0
|
158 if ($tokens[$short]['line'] !== ($tokens[$start]['line'] + 1)) {
|
Chris@0
|
159 $error = 'Doc comment short description must be on the first line';
|
Chris@0
|
160 $fix = $phpcsFile->addFixableError($error, $short, 'SpacingBeforeShort');
|
Chris@0
|
161 if ($fix === true) {
|
Chris@0
|
162 $phpcsFile->fixer->beginChangeset();
|
Chris@0
|
163 for ($i = $start; $i < $short; $i++) {
|
Chris@0
|
164 if ($tokens[$i]['line'] === $tokens[$start]['line']) {
|
Chris@0
|
165 continue;
|
Chris@0
|
166 } else if ($tokens[$i]['line'] === $tokens[$short]['line']) {
|
Chris@0
|
167 break;
|
Chris@0
|
168 }
|
Chris@0
|
169
|
Chris@0
|
170 $phpcsFile->fixer->replaceToken($i, '');
|
Chris@0
|
171 }
|
Chris@0
|
172
|
Chris@0
|
173 $phpcsFile->fixer->endChangeset();
|
Chris@0
|
174 }
|
Chris@0
|
175 }
|
Chris@0
|
176
|
Chris@0
|
177 if ($tokens[($short - 1)]['content'] !== ' '
|
Chris@0
|
178 && strpos($tokens[($short - 1)]['content'], $phpcsFile->eolChar) === false
|
Chris@0
|
179 ) {
|
Chris@0
|
180 $error = 'Function comment short description must start with exactly one space';
|
Chris@0
|
181 $fix = $phpcsFile->addFixableError($error, $short, 'ShortStartSpace');
|
Chris@0
|
182 if ($fix === true) {
|
Chris@0
|
183 if ($tokens[($short - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) {
|
Chris@0
|
184 $phpcsFile->fixer->replaceToken(($short - 1), ' ');
|
Chris@0
|
185 } else {
|
Chris@0
|
186 $phpcsFile->fixer->addContent(($short - 1), ' ');
|
Chris@0
|
187 }
|
Chris@0
|
188 }
|
Chris@0
|
189 }
|
Chris@0
|
190
|
Chris@0
|
191 // Account for the fact that a short description might cover
|
Chris@0
|
192 // multiple lines.
|
Chris@0
|
193 $shortContent = $tokens[$short]['content'];
|
Chris@0
|
194 $shortEnd = $short;
|
Chris@0
|
195 for ($i = ($short + 1); $i < $commentEnd; $i++) {
|
Chris@0
|
196 if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
|
Chris@0
|
197 if ($tokens[$i]['line'] === ($tokens[$shortEnd]['line'] + 1)) {
|
Chris@0
|
198 $shortContent .= $tokens[$i]['content'];
|
Chris@0
|
199 $shortEnd = $i;
|
Chris@0
|
200 } else {
|
Chris@0
|
201 break;
|
Chris@0
|
202 }
|
Chris@0
|
203 }
|
Chris@0
|
204
|
Chris@0
|
205 if ($tokens[$i]['code'] === T_DOC_COMMENT_TAG) {
|
Chris@0
|
206 break;
|
Chris@0
|
207 }
|
Chris@0
|
208 }
|
Chris@0
|
209
|
Chris@0
|
210 // Remove any trailing white spaces which are detected by other sniffs.
|
Chris@0
|
211 $shortContent = trim($shortContent);
|
Chris@0
|
212
|
Chris@0
|
213 if (preg_match('|\p{Lu}|u', $shortContent[0]) === 0 && $shortContent !== '{@inheritdoc}'
|
Chris@0
|
214 // Ignore Features module export files that just use the file name as
|
Chris@0
|
215 // comment.
|
Chris@0
|
216 && $shortContent !== basename($phpcsFile->getFilename())
|
Chris@0
|
217 ) {
|
Chris@0
|
218 $error = 'Doc comment short description must start with a capital letter';
|
Chris@0
|
219 // If we cannot capitalize the first character then we don't have a
|
Chris@0
|
220 // fixable error.
|
Chris@0
|
221 if ($tokens[$short]['content'] === ucfirst($tokens[$short]['content'])) {
|
Chris@0
|
222 $phpcsFile->addError($error, $short, 'ShortNotCapital');
|
Chris@0
|
223 } else {
|
Chris@0
|
224 $fix = $phpcsFile->addFixableError($error, $short, 'ShortNotCapital');
|
Chris@0
|
225 if ($fix === true) {
|
Chris@0
|
226 $phpcsFile->fixer->replaceToken($short, ucfirst($tokens[$short]['content']));
|
Chris@0
|
227 }
|
Chris@0
|
228 }
|
Chris@0
|
229 }
|
Chris@0
|
230
|
Chris@0
|
231 $lastChar = substr($shortContent, -1);
|
Chris@0
|
232 if (in_array($lastChar, array('.', '!', '?', ')')) === false && $shortContent !== '{@inheritdoc}'
|
Chris@0
|
233 // Ignore Features module export files that just use the file name as
|
Chris@0
|
234 // comment.
|
Chris@0
|
235 && $shortContent !== basename($phpcsFile->getFilename())
|
Chris@0
|
236 ) {
|
Chris@0
|
237 $error = 'Doc comment short description must end with a full stop';
|
Chris@0
|
238 $fix = $phpcsFile->addFixableError($error, $shortEnd, 'ShortFullStop');
|
Chris@0
|
239 if ($fix === true) {
|
Chris@0
|
240 $phpcsFile->fixer->addContent($shortEnd, '.');
|
Chris@0
|
241 }
|
Chris@0
|
242 }
|
Chris@0
|
243
|
Chris@0
|
244 if ($tokens[$short]['line'] !== $tokens[$shortEnd]['line']) {
|
Chris@0
|
245 $error = 'Doc comment short description must be on a single line, further text should be a separate paragraph';
|
Chris@0
|
246 $phpcsFile->addError($error, $shortEnd, 'ShortSingleLine');
|
Chris@0
|
247 }
|
Chris@0
|
248
|
Chris@0
|
249 $long = $phpcsFile->findNext($empty, ($shortEnd + 1), ($commentEnd - 1), true);
|
Chris@0
|
250 if ($long === false) {
|
Chris@0
|
251 return;
|
Chris@0
|
252 }
|
Chris@0
|
253
|
Chris@0
|
254 if ($tokens[$long]['code'] === T_DOC_COMMENT_STRING) {
|
Chris@0
|
255 if ($tokens[$long]['line'] !== ($tokens[$shortEnd]['line'] + 2)) {
|
Chris@0
|
256 $error = 'There must be exactly one blank line between descriptions in a doc comment';
|
Chris@0
|
257 $fix = $phpcsFile->addFixableError($error, $long, 'SpacingBetween');
|
Chris@0
|
258 if ($fix === true) {
|
Chris@0
|
259 $phpcsFile->fixer->beginChangeset();
|
Chris@0
|
260 for ($i = ($shortEnd + 1); $i < $long; $i++) {
|
Chris@0
|
261 if ($tokens[$i]['line'] === $tokens[$shortEnd]['line']) {
|
Chris@0
|
262 continue;
|
Chris@0
|
263 } else if ($tokens[$i]['line'] === ($tokens[$long]['line'] - 1)) {
|
Chris@0
|
264 break;
|
Chris@0
|
265 }
|
Chris@0
|
266
|
Chris@0
|
267 $phpcsFile->fixer->replaceToken($i, '');
|
Chris@0
|
268 }
|
Chris@0
|
269
|
Chris@0
|
270 $phpcsFile->fixer->endChangeset();
|
Chris@0
|
271 }
|
Chris@0
|
272 }
|
Chris@0
|
273
|
Chris@0
|
274 if (preg_match('|\p{Lu}|u', $tokens[$long]['content'][0]) === 0
|
Chris@0
|
275 && $tokens[$long]['content'] !== ucfirst($tokens[$long]['content'])
|
Chris@0
|
276 ) {
|
Chris@0
|
277 $error = 'Doc comment long description must start with a capital letter';
|
Chris@0
|
278 $fix = $phpcsFile->addFixableError($error, $long, 'LongNotCapital');
|
Chris@0
|
279 if ($fix === true) {
|
Chris@0
|
280 $phpcsFile->fixer->replaceToken($long, ucfirst($tokens[$long]['content']));
|
Chris@0
|
281 }
|
Chris@0
|
282 }
|
Chris@0
|
283
|
Chris@0
|
284 // Account for the fact that a description might cover multiple lines.
|
Chris@0
|
285 $longContent = $tokens[$long]['content'];
|
Chris@0
|
286 $longEnd = $long;
|
Chris@0
|
287 for ($i = ($long + 1); $i < $commentEnd; $i++) {
|
Chris@0
|
288 if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
|
Chris@0
|
289 if ($tokens[$i]['line'] <= ($tokens[$longEnd]['line'] + 1)) {
|
Chris@0
|
290 $longContent .= $tokens[$i]['content'];
|
Chris@0
|
291 $longEnd = $i;
|
Chris@0
|
292 } else {
|
Chris@0
|
293 break;
|
Chris@0
|
294 }
|
Chris@0
|
295 }
|
Chris@0
|
296
|
Chris@0
|
297 if ($tokens[$i]['code'] === T_DOC_COMMENT_TAG) {
|
Chris@0
|
298 if ($tokens[$i]['line'] <= ($tokens[$longEnd]['line'] + 1)
|
Chris@0
|
299 // Allow link tags within the long comment itself.
|
Chris@0
|
300 && ($tokens[$i]['content'] === '@link' || $tokens[$i]['content'] === '@endlink')
|
Chris@0
|
301 ) {
|
Chris@0
|
302 $longContent .= $tokens[$i]['content'];
|
Chris@0
|
303 $longEnd = $i;
|
Chris@0
|
304 } else {
|
Chris@0
|
305 break;
|
Chris@0
|
306 }
|
Chris@0
|
307 }
|
Chris@0
|
308 }//end for
|
Chris@0
|
309
|
Chris@0
|
310 // Remove any trailing white spaces which are detected by other sniffs.
|
Chris@0
|
311 $longContent = trim($longContent);
|
Chris@0
|
312
|
Chris@0
|
313 if (preg_match('/[a-zA-Z]$/', $longContent) === 1) {
|
Chris@0
|
314 $error = 'Doc comment long description must end with a full stop';
|
Chris@0
|
315 $fix = $phpcsFile->addFixableError($error, $longEnd, 'LongFullStop');
|
Chris@0
|
316 if ($fix === true) {
|
Chris@0
|
317 $phpcsFile->fixer->addContent($longEnd, '.');
|
Chris@0
|
318 }
|
Chris@0
|
319 }
|
Chris@0
|
320 }//end if
|
Chris@0
|
321
|
Chris@0
|
322 if (empty($tokens[$commentStart]['comment_tags']) === true) {
|
Chris@0
|
323 // No tags in the comment.
|
Chris@0
|
324 return;
|
Chris@0
|
325 }
|
Chris@0
|
326
|
Chris@0
|
327 $firstTag = $tokens[$commentStart]['comment_tags'][0];
|
Chris@0
|
328 $prev = $phpcsFile->findPrevious($empty, ($firstTag - 1), $stackPtr, true);
|
Chris@0
|
329 // This does not apply to @file, @code, @link and @endlink tags.
|
Chris@0
|
330 if ($tokens[$firstTag]['line'] !== ($tokens[$prev]['line'] + 2)
|
Chris@0
|
331 && isset($fileShort) === false
|
Chris@0
|
332 && in_array($tokens[$firstTag]['content'], array('@code', '@link', '@endlink')) === false
|
Chris@0
|
333 ) {
|
Chris@0
|
334 $error = 'There must be exactly one blank line before the tags in a doc comment';
|
Chris@0
|
335 $fix = $phpcsFile->addFixableError($error, $firstTag, 'SpacingBeforeTags');
|
Chris@0
|
336 if ($fix === true) {
|
Chris@0
|
337 $phpcsFile->fixer->beginChangeset();
|
Chris@0
|
338 for ($i = ($prev + 1); $i < $firstTag; $i++) {
|
Chris@0
|
339 if ($tokens[$i]['line'] === $tokens[$firstTag]['line']) {
|
Chris@0
|
340 break;
|
Chris@0
|
341 }
|
Chris@0
|
342
|
Chris@0
|
343 $phpcsFile->fixer->replaceToken($i, '');
|
Chris@0
|
344 }
|
Chris@0
|
345
|
Chris@0
|
346 $indent = str_repeat(' ', $tokens[$stackPtr]['column']);
|
Chris@0
|
347 $phpcsFile->fixer->addContent($prev, $phpcsFile->eolChar.$indent.'*'.$phpcsFile->eolChar);
|
Chris@0
|
348 $phpcsFile->fixer->endChangeset();
|
Chris@0
|
349 }
|
Chris@0
|
350 }
|
Chris@0
|
351
|
Chris@0
|
352 // Break out the tags into groups and check alignment within each.
|
Chris@0
|
353 // A tag group is one where there are no blank lines between tags.
|
Chris@0
|
354 // The param tag group is special as it requires all @param tags to be inside.
|
Chris@0
|
355 $tagGroups = array();
|
Chris@0
|
356 $groupid = 0;
|
Chris@0
|
357 $paramGroupid = null;
|
Chris@0
|
358 $currentTag = null;
|
Chris@0
|
359 $previousTag = null;
|
Chris@0
|
360 $isNewGroup = null;
|
Chris@0
|
361 foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) {
|
Chris@0
|
362 if ($pos > 0) {
|
Chris@0
|
363 $prev = $phpcsFile->findPrevious(
|
Chris@0
|
364 T_DOC_COMMENT_STRING,
|
Chris@0
|
365 ($tag - 1),
|
Chris@0
|
366 $tokens[$commentStart]['comment_tags'][($pos - 1)]
|
Chris@0
|
367 );
|
Chris@0
|
368
|
Chris@0
|
369 if ($prev === false) {
|
Chris@0
|
370 $prev = $tokens[$commentStart]['comment_tags'][($pos - 1)];
|
Chris@0
|
371 }
|
Chris@0
|
372
|
Chris@0
|
373 $isNewGroup = $tokens[$prev]['line'] !== ($tokens[$tag]['line'] - 1);
|
Chris@0
|
374 if ($isNewGroup === true) {
|
Chris@0
|
375 $groupid++;
|
Chris@0
|
376 }
|
Chris@0
|
377 }
|
Chris@0
|
378
|
Chris@0
|
379 $currentTag = $tokens[$tag]['content'];
|
Chris@0
|
380 if ($currentTag === '@param') {
|
Chris@0
|
381 if (($paramGroupid === null
|
Chris@0
|
382 && empty($tagGroups[$groupid]) === false)
|
Chris@0
|
383 || ($paramGroupid !== null
|
Chris@0
|
384 && $paramGroupid !== $groupid)
|
Chris@0
|
385 ) {
|
Chris@0
|
386 $error = 'Parameter tags must be grouped together in a doc comment';
|
Chris@0
|
387 $phpcsFile->addError($error, $tag, 'ParamGroup');
|
Chris@0
|
388 }
|
Chris@0
|
389
|
Chris@0
|
390 if ($paramGroupid === null) {
|
Chris@0
|
391 $paramGroupid = $groupid;
|
Chris@0
|
392 }
|
Chris@0
|
393
|
Chris@0
|
394 // The @param, @return and @throws tag sections should be
|
Chris@0
|
395 // separated by a blank line both before and after these sections.
|
Chris@0
|
396 } else if ($isNewGroup === false
|
Chris@0
|
397 && (in_array($currentTag, array('@param', '@return', '@throws')) === true
|
Chris@0
|
398 || in_array($previousTag, array('@param', '@return', '@throws')) === true)
|
Chris@0
|
399 && $previousTag !== $currentTag
|
Chris@0
|
400 ) {
|
Chris@0
|
401 $error = 'Separate the %s and %s sections by a blank line.';
|
Chris@0
|
402 $fix = $phpcsFile->addFixableError($error, $tag, 'TagGroupSpacing', array($previousTag, $currentTag));
|
Chris@0
|
403 if ($fix === true) {
|
Chris@0
|
404 $phpcsFile->fixer->replaceToken(($tag - 1), "\n".str_repeat(' ', ($tokens[$tag]['column'] - 3)).'* ');
|
Chris@0
|
405 }
|
Chris@0
|
406 }//end if
|
Chris@0
|
407
|
Chris@0
|
408 $previousTag = $currentTag;
|
Chris@0
|
409 $tagGroups[$groupid][] = $tag;
|
Chris@0
|
410 }//end foreach
|
Chris@0
|
411
|
Chris@0
|
412 foreach ($tagGroups as $group) {
|
Chris@0
|
413 $maxLength = 0;
|
Chris@0
|
414 $paddings = array();
|
Chris@0
|
415 foreach ($group as $pos => $tag) {
|
Chris@0
|
416 $tagLength = strlen($tokens[$tag]['content']);
|
Chris@0
|
417 if ($tagLength > $maxLength) {
|
Chris@0
|
418 $maxLength = $tagLength;
|
Chris@0
|
419 }
|
Chris@0
|
420
|
Chris@0
|
421 // Check for a value. No value means no padding needed.
|
Chris@0
|
422 $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd);
|
Chris@0
|
423 if ($string !== false && $tokens[$string]['line'] === $tokens[$tag]['line']) {
|
Chris@0
|
424 $paddings[$tag] = strlen($tokens[($tag + 1)]['content']);
|
Chris@0
|
425 }
|
Chris@0
|
426 }
|
Chris@0
|
427
|
Chris@0
|
428 // Check that there was single blank line after the tag block
|
Chris@0
|
429 // but account for a multi-line tag comments.
|
Chris@0
|
430 $lastTag = $group[$pos];
|
Chris@0
|
431 $next = $phpcsFile->findNext(T_DOC_COMMENT_TAG, ($lastTag + 3), $commentEnd);
|
Chris@0
|
432 if ($next !== false) {
|
Chris@0
|
433 $prev = $phpcsFile->findPrevious(array(T_DOC_COMMENT_TAG, T_DOC_COMMENT_STRING), ($next - 1), $commentStart);
|
Chris@0
|
434 if ($tokens[$next]['line'] !== ($tokens[$prev]['line'] + 2)) {
|
Chris@0
|
435 $error = 'There must be a single blank line after a tag group';
|
Chris@0
|
436 $fix = $phpcsFile->addFixableError($error, $lastTag, 'SpacingAfterTagGroup');
|
Chris@0
|
437 if ($fix === true) {
|
Chris@0
|
438 $phpcsFile->fixer->beginChangeset();
|
Chris@0
|
439 for ($i = ($prev + 1); $i < $next; $i++) {
|
Chris@0
|
440 if ($tokens[$i]['line'] === $tokens[$next]['line']) {
|
Chris@0
|
441 break;
|
Chris@0
|
442 }
|
Chris@0
|
443
|
Chris@0
|
444 $phpcsFile->fixer->replaceToken($i, '');
|
Chris@0
|
445 }
|
Chris@0
|
446
|
Chris@0
|
447 $indent = str_repeat(' ', $tokens[$stackPtr]['column']);
|
Chris@0
|
448 $phpcsFile->fixer->addContent($prev, $phpcsFile->eolChar.$indent.'*'.$phpcsFile->eolChar);
|
Chris@0
|
449 $phpcsFile->fixer->endChangeset();
|
Chris@0
|
450 }
|
Chris@0
|
451 }
|
Chris@0
|
452 }//end if
|
Chris@0
|
453
|
Chris@0
|
454 // Now check paddings.
|
Chris@0
|
455 foreach ($paddings as $tag => $padding) {
|
Chris@0
|
456 if ($padding !== 1) {
|
Chris@0
|
457 $error = 'Tag value indented incorrectly; expected 1 space but found %s';
|
Chris@0
|
458 $data = array($padding);
|
Chris@0
|
459
|
Chris@0
|
460 $fix = $phpcsFile->addFixableError($error, ($tag + 1), 'TagValueIndent', $data);
|
Chris@0
|
461 if ($fix === true) {
|
Chris@0
|
462 $phpcsFile->fixer->replaceToken(($tag + 1), ' ');
|
Chris@0
|
463 }
|
Chris@0
|
464 }
|
Chris@0
|
465 }
|
Chris@0
|
466 }//end foreach
|
Chris@0
|
467
|
Chris@0
|
468 // If there is a param group, it needs to be first; with the exception of
|
Chris@0
|
469 // @code, @todo and link tags.
|
Chris@0
|
470 if ($paramGroupid !== null && $paramGroupid !== 0
|
Chris@0
|
471 && in_array($tokens[$tokens[$commentStart]['comment_tags'][0]]['content'], array('@code', '@todo', '@link', '@endlink', '@codingStandardsIgnoreStart')) === false
|
Chris@0
|
472 // In JSDoc we can have many other valid tags like @function or
|
Chris@0
|
473 // @constructor before the param tags.
|
Chris@0
|
474 && $phpcsFile->tokenizerType !== 'JS'
|
Chris@0
|
475 ) {
|
Chris@0
|
476 $error = 'Parameter tags must be defined first in a doc comment';
|
Chris@0
|
477 $phpcsFile->addError($error, $tagGroups[$paramGroupid][0], 'ParamNotFirst');
|
Chris@0
|
478 }
|
Chris@0
|
479
|
Chris@0
|
480 $foundTags = array();
|
Chris@0
|
481 foreach ($tokens[$stackPtr]['comment_tags'] as $pos => $tag) {
|
Chris@0
|
482 $tagName = $tokens[$tag]['content'];
|
Chris@0
|
483 if (isset($foundTags[$tagName]) === true) {
|
Chris@0
|
484 $lastTag = $tokens[$stackPtr]['comment_tags'][($pos - 1)];
|
Chris@0
|
485 if ($tokens[$lastTag]['content'] !== $tagName) {
|
Chris@0
|
486 $error = 'Tags must be grouped together in a doc comment';
|
Chris@0
|
487 $phpcsFile->addError($error, $tag, 'TagsNotGrouped');
|
Chris@0
|
488 }
|
Chris@0
|
489
|
Chris@0
|
490 continue;
|
Chris@0
|
491 }
|
Chris@0
|
492
|
Chris@0
|
493 $foundTags[$tagName] = true;
|
Chris@0
|
494 }
|
Chris@0
|
495
|
Chris@0
|
496 }//end process()
|
Chris@0
|
497
|
Chris@0
|
498
|
Chris@0
|
499 }//end class
|