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