Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 493 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
FunctionCommentSniff | |
0.00% |
0 / 493 |
|
0.00% |
0 / 7 |
19182 | |
0.00% |
0 / 1 |
register | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
process | |
0.00% |
0 / 67 |
|
0.00% |
0 / 1 |
756 | |||
processReturn | |
0.00% |
0 / 88 |
|
0.00% |
0 / 1 |
342 | |||
functionReturnsValue | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
156 | |||
processThrows | |
0.00% |
0 / 53 |
|
0.00% |
0 / 1 |
182 | |||
processParams | |
0.00% |
0 / 258 |
|
0.00% |
0 / 1 |
4032 | |||
replaceParamComment | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | /** |
3 | * This file was copied from PHP_CodeSniffer before being modified |
4 | * File: Standards/PEAR/Sniffs/Commenting/FunctionCommentSniff.php |
5 | * From repository: https://github.com/squizlabs/PHP_CodeSniffer |
6 | * |
7 | * Parses and verifies the doc comments for functions. |
8 | * |
9 | * PHP version 5 |
10 | * |
11 | * @category PHP |
12 | * @package PHP_CodeSniffer |
13 | * @author Greg Sherwood <gsherwood@squiz.net> |
14 | * @author Marc McIntyre <mmcintyre@squiz.net> |
15 | * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) |
16 | * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD-3-Clause |
17 | * @link http://pear.php.net/package/PHP_CodeSniffer |
18 | */ |
19 | |
20 | namespace MediaWiki\Sniffs\Commenting; |
21 | |
22 | use MediaWiki\Sniffs\PHPUnit\PHPUnitTestTrait; |
23 | use PHP_CodeSniffer\Files\File; |
24 | use PHP_CodeSniffer\Sniffs\Sniff; |
25 | use PHP_CodeSniffer\Util\Tokens; |
26 | |
27 | class FunctionCommentSniff implements Sniff { |
28 | |
29 | use DocumentationTypeTrait; |
30 | use PHPUnitTestTrait; |
31 | |
32 | /** |
33 | * Standard class methods that |
34 | * don't require documentation |
35 | */ |
36 | private const SKIP_STANDARD_METHODS = [ |
37 | '__toString', |
38 | '__destruct', |
39 | '__sleep', |
40 | '__wakeup', |
41 | '__clone', |
42 | '__invoke', |
43 | '__call', |
44 | '__callStatic', |
45 | '__get', |
46 | '__set', |
47 | '__isset', |
48 | '__unset', |
49 | '__serialize', |
50 | '__unserialize', |
51 | '__set_state', |
52 | '__debugInfo', |
53 | ]; |
54 | |
55 | /** |
56 | * @inheritDoc |
57 | */ |
58 | public function register(): array { |
59 | return [ T_FUNCTION ]; |
60 | } |
61 | |
62 | /** |
63 | * Processes this test, when one of its tokens is encountered. |
64 | * |
65 | * @param File $phpcsFile The file being scanned. |
66 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. |
67 | * |
68 | * @return void |
69 | */ |
70 | public function process( File $phpcsFile, $stackPtr ) { |
71 | $funcName = $phpcsFile->getDeclarationName( $stackPtr ); |
72 | if ( $funcName === null || in_array( $funcName, self::SKIP_STANDARD_METHODS ) ) { |
73 | // Don't require documentation for an obvious method |
74 | return; |
75 | } |
76 | |
77 | $tokens = $phpcsFile->getTokens(); |
78 | $find = Tokens::$methodPrefixes; |
79 | $find[] = T_WHITESPACE; |
80 | $searchBefore = $stackPtr; |
81 | $linesBetweenDocAndFunction = 1; |
82 | do { |
83 | $commentEnd = $phpcsFile->findPrevious( $find, $searchBefore - 1, null, true ); |
84 | // Allow attributes between doc block and function, T306941 |
85 | if ( isset( $tokens[$commentEnd]['attribute_opener'] ) ) { |
86 | $searchBefore = $tokens[$commentEnd]['attribute_opener']; |
87 | // Attributes should be on their own lines |
88 | $linesBetweenDocAndFunction++; |
89 | continue; |
90 | } |
91 | break; |
92 | } while ( true ); |
93 | if ( $tokens[$commentEnd]['code'] === T_COMMENT ) { |
94 | // Inline comments might just be closing comments for |
95 | // control structures or functions instead of function comments |
96 | // using the wrong comment type. If there is other code on the line, |
97 | // assume they relate to that code. |
98 | $prev = $phpcsFile->findPrevious( $find, $commentEnd - 1, null, true ); |
99 | if ( $prev !== false && $tokens[$prev]['line'] === $tokens[$commentEnd]['line'] ) { |
100 | $commentEnd = $prev; |
101 | } |
102 | } |
103 | if ( $tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG |
104 | && $tokens[$commentEnd]['code'] !== T_COMMENT |
105 | ) { |
106 | // Function has no documentation; check if this is allowed or not |
107 | $methodProps = $phpcsFile->getMethodProperties( $stackPtr ); |
108 | $methodParams = $phpcsFile->getMethodParameters( $stackPtr ); |
109 | $hasReturnType = $methodProps['return_type'] !== '' || $funcName === '__construct'; |
110 | $hasParams = $methodParams !== []; |
111 | $getterWithoutParams = !$hasParams && preg_match( '/^(get|is)[A-Z]/', $funcName ); |
112 | $allParamsTyped = true; |
113 | foreach ( $methodParams as $parameter ) { |
114 | if ( $parameter['type_hint'] === '' ) { |
115 | $allParamsTyped = false; |
116 | break; |
117 | } |
118 | } |
119 | // Enforce strict return type or @return documentation for interfaces/abstract methods, |
120 | // but only if they are entirely undocumented at the moment |
121 | $returnsValue = $this->functionReturnsValue( $phpcsFile, $stackPtr ) ?? true; |
122 | $isFullyTyped = $allParamsTyped && ( $hasReturnType || !$returnsValue ); |
123 | $isTestFile = $this->isTestFile( $phpcsFile, $stackPtr ); |
124 | // A function is *allowed* to omit the documentation comment |
125 | // (but in many cases, documentation comments still make sense, and are not discouraged) |
126 | // if it is fully typed (parameter and return type declarations), or in a test file, |
127 | // or has no parameters and is not a getter. |
128 | // The last exception, allowing parameterless non-getters to omit their return type, may be removed later. |
129 | if ( !$isFullyTyped && !$isTestFile && ( $getterWithoutParams || $hasParams ) ) { |
130 | $phpcsFile->addError( |
131 | 'Missing function doc comment', |
132 | $stackPtr, |
133 | // Messages used: MissingDocumentationPublic, MissingDocumentationProtected, |
134 | // MissingDocumentationPrivate |
135 | 'MissingDocumentation' . ucfirst( $methodProps['scope'] ) |
136 | ); |
137 | } |
138 | return; |
139 | } |
140 | if ( $tokens[$commentEnd]['code'] === T_COMMENT ) { |
141 | $phpcsFile->addError( |
142 | 'You must use "/**" style comments for a function comment', |
143 | $stackPtr, |
144 | 'WrongStyle' |
145 | ); |
146 | return; |
147 | } |
148 | if ( $tokens[$commentEnd]['line'] !== $tokens[$stackPtr]['line'] - $linesBetweenDocAndFunction ) { |
149 | $phpcsFile->addError( |
150 | 'There must be no blank lines after the function comment', |
151 | $commentEnd, |
152 | 'SpacingAfter' |
153 | ); |
154 | } |
155 | $commentStart = $tokens[$commentEnd]['comment_opener']; |
156 | |
157 | foreach ( $tokens[$commentStart]['comment_tags'] as $tag ) { |
158 | $tagText = $tokens[$tag]['content']; |
159 | if ( strcasecmp( $tagText, '@inheritDoc' ) === 0 || $tagText === '@deprecated' ) { |
160 | // No need to validate deprecated functions or those that inherit |
161 | // their documentation |
162 | return; |
163 | } |
164 | } |
165 | |
166 | // Don't validate functions with {@inheritDoc}, per T270830 |
167 | // Not available in comment_tags, need to check manually |
168 | $end = reset( $tokens[$commentStart]['comment_tags'] ) ?: $commentEnd; |
169 | $rawComment = $phpcsFile->getTokensAsString( $commentStart + 1, $end - $commentStart - 1 ); |
170 | if ( stripos( $rawComment, '{@inheritDoc}' ) !== false ) { |
171 | return; |
172 | } |
173 | |
174 | if ( $funcName !== '__construct' ) { |
175 | $this->processReturn( $phpcsFile, $stackPtr, $commentStart ); |
176 | } |
177 | $this->processThrows( $phpcsFile, $commentStart ); |
178 | $this->processParams( $phpcsFile, $stackPtr, $commentStart ); |
179 | } |
180 | |
181 | /** |
182 | * Process the return comment of this function comment. |
183 | * |
184 | * @param File $phpcsFile The file being scanned. |
185 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. |
186 | * @param int $commentStart The position in the stack where the comment started. |
187 | */ |
188 | protected function processReturn( File $phpcsFile, int $stackPtr, int $commentStart ): void { |
189 | $tokens = $phpcsFile->getTokens(); |
190 | |
191 | $hasReturnType = $phpcsFile->getMethodProperties( $stackPtr )['return_type'] !== ''; |
192 | // Assume interfaces/abstract methods don't return anything when they have some comment |
193 | // already, no matter what the comment says |
194 | $returnsValue = $this->functionReturnsValue( $phpcsFile, $stackPtr ) ?? false; |
195 | |
196 | $returnPtr = null; |
197 | foreach ( $tokens[$commentStart]['comment_tags'] as $ptr ) { |
198 | if ( $tokens[$ptr]['content'] !== '@return' ) { |
199 | continue; |
200 | } |
201 | if ( $returnPtr ) { |
202 | $phpcsFile->addError( 'Only 1 @return tag is allowed in a function comment', $ptr, 'DuplicateReturn' ); |
203 | return; |
204 | } |
205 | $returnPtr = $ptr; |
206 | } |
207 | if ( $returnPtr !== null ) { |
208 | $retTypeSpacingPtr = $returnPtr + 1; |
209 | // Check spaces before type |
210 | if ( $tokens[$retTypeSpacingPtr]['code'] === T_DOC_COMMENT_WHITESPACE ) { |
211 | $expectedSpaces = 1; |
212 | $currentSpaces = strlen( $tokens[$retTypeSpacingPtr]['content'] ); |
213 | if ( $currentSpaces !== $expectedSpaces ) { |
214 | $fix = $phpcsFile->addFixableWarning( |
215 | 'Expected %s spaces before return type; %s found', |
216 | $retTypeSpacingPtr, |
217 | 'SpacingBeforeReturnType', |
218 | [ $expectedSpaces, $currentSpaces ] |
219 | ); |
220 | if ( $fix ) { |
221 | $phpcsFile->fixer->replaceToken( $retTypeSpacingPtr, ' ' ); |
222 | } |
223 | } |
224 | } |
225 | $retTypePtr = $returnPtr + 2; |
226 | $content = ''; |
227 | if ( $tokens[$retTypePtr]['code'] === T_DOC_COMMENT_STRING ) { |
228 | $content = $tokens[$retTypePtr]['content']; |
229 | } |
230 | if ( $content === '' ) { |
231 | $phpcsFile->addError( |
232 | 'Return type missing for @return tag in function comment', |
233 | $returnPtr, |
234 | 'MissingReturnType' |
235 | ); |
236 | return; |
237 | } |
238 | [ $type, $separatorLength, $comment ] = $this->splitTypeAndComment( $content ); |
239 | $fixType = false; |
240 | // Check for unneeded punctuation |
241 | $type = $this->fixTrailingPunctuation( |
242 | $phpcsFile, |
243 | $retTypePtr, |
244 | $type, |
245 | $fixType, |
246 | 'return type' |
247 | ); |
248 | $type = $this->fixWrappedParenthesis( |
249 | $phpcsFile, |
250 | $retTypePtr, |
251 | $type, |
252 | $fixType, |
253 | 'return type' |
254 | ); |
255 | // Check the type for short types |
256 | $type = $this->fixShortTypes( $phpcsFile, $retTypePtr, $type, $fixType, 'return' ); |
257 | $this->maybeAddObjectTypehintError( |
258 | $phpcsFile, |
259 | $retTypePtr, |
260 | $type, |
261 | 'return' |
262 | ); |
263 | $this->maybeAddTypeTypehintError( |
264 | $phpcsFile, |
265 | $retTypePtr, |
266 | $type, |
267 | 'return' |
268 | ); |
269 | // Check spacing after type |
270 | if ( $comment !== '' ) { |
271 | $expectedSpaces = 1; |
272 | if ( $separatorLength !== $expectedSpaces ) { |
273 | $fix = $phpcsFile->addFixableWarning( |
274 | 'Expected %s spaces after return type; %s found', |
275 | $retTypePtr, |
276 | 'SpacingAfterReturnType', |
277 | [ $expectedSpaces, $separatorLength ] |
278 | ); |
279 | if ( $fix ) { |
280 | $fixType = true; |
281 | $separatorLength = $expectedSpaces; |
282 | } |
283 | } |
284 | } |
285 | if ( $fixType ) { |
286 | $phpcsFile->fixer->replaceToken( |
287 | $retTypePtr, |
288 | $type . ( $comment !== '' ? str_repeat( ' ', $separatorLength ) . $comment : '' ) |
289 | ); |
290 | } |
291 | } elseif ( $returnsValue && !$hasReturnType && !$this->isTestFunction( $phpcsFile, $stackPtr ) ) { |
292 | $phpcsFile->addError( |
293 | 'Missing return type or @return tag in function comment', |
294 | $tokens[$commentStart]['comment_closer'], |
295 | 'MissingReturn' |
296 | ); |
297 | } |
298 | } |
299 | |
300 | private function functionReturnsValue( File $phpcsFile, int $stackPtr ): ?bool { |
301 | $tokens = $phpcsFile->getTokens(); |
302 | |
303 | // Interfaces or abstract functions don't have a body |
304 | if ( !isset( $tokens[$stackPtr]['scope_closer'] ) ) { |
305 | return null; |
306 | } |
307 | |
308 | for ( $i = $tokens[$stackPtr]['scope_closer'] - 1; $i > $stackPtr; $i-- ) { |
309 | $token = $tokens[$i]; |
310 | if ( isset( $token['scope_condition'] ) ) { |
311 | $scope = $tokens[$token['scope_condition']]['code']; |
312 | if ( $scope === T_FUNCTION || $scope === T_CLOSURE || $scope === T_ANON_CLASS ) { |
313 | // Skip to the other side of the closure/inner function and continue |
314 | $i = $token['scope_condition']; |
315 | continue; |
316 | } |
317 | } |
318 | if ( $token['code'] === T_YIELD || $token['code'] === T_YIELD_FROM ) { |
319 | return true; |
320 | } elseif ( $token['code'] === T_RETURN && |
321 | // Ignore empty `return;` and continue searching |
322 | isset( $tokens[$i + 1] ) && $tokens[$i + 1]['code'] !== T_SEMICOLON |
323 | ) { |
324 | return true; |
325 | } |
326 | } |
327 | return false; |
328 | } |
329 | |
330 | /** |
331 | * Process any throw tags that this function comment has. |
332 | * |
333 | * @param File $phpcsFile The file being scanned. |
334 | * @param int $commentStart The position in the stack where the comment started. |
335 | */ |
336 | protected function processThrows( File $phpcsFile, int $commentStart ): void { |
337 | $tokens = $phpcsFile->getTokens(); |
338 | foreach ( $tokens[$commentStart]['comment_tags'] as $tag ) { |
339 | if ( $tokens[$tag]['content'] !== '@throws' ) { |
340 | continue; |
341 | } |
342 | // Check spaces before exception |
343 | if ( $tokens[$tag + 1]['code'] === T_DOC_COMMENT_WHITESPACE ) { |
344 | $expectedSpaces = 1; |
345 | $currentSpaces = strlen( $tokens[$tag + 1]['content'] ); |
346 | if ( $currentSpaces !== $expectedSpaces ) { |
347 | $fix = $phpcsFile->addFixableWarning( |
348 | 'Expected %s spaces before exception type; %s found', |
349 | $tag + 1, |
350 | 'SpacingBeforeExceptionType', |
351 | [ $expectedSpaces, $currentSpaces ] |
352 | ); |
353 | if ( $fix ) { |
354 | $phpcsFile->fixer->replaceToken( $tag + 1, ' ' ); |
355 | } |
356 | } |
357 | } |
358 | $exception = ''; |
359 | $comment = ''; |
360 | $separatorLength = null; |
361 | if ( $tokens[$tag + 2]['code'] === T_DOC_COMMENT_STRING ) { |
362 | [ $exception, $separatorLength, $comment ] = $this->splitTypeAndComment( $tokens[$tag + 2]['content'] ); |
363 | } |
364 | if ( $exception === '' ) { |
365 | $phpcsFile->addError( |
366 | 'Exception type missing for @throws tag in function comment', |
367 | $tag, |
368 | 'InvalidThrows' |
369 | ); |
370 | continue; |
371 | } |
372 | $fixType = false; |
373 | $exception = $this->fixWrappedParenthesis( |
374 | $phpcsFile, |
375 | $tag, |
376 | $exception, |
377 | $fixType, |
378 | 'exception type' |
379 | ); |
380 | // Check spacing after exception |
381 | if ( $comment !== '' ) { |
382 | $expectedSpaces = 1; |
383 | if ( $separatorLength !== $expectedSpaces ) { |
384 | $fix = $phpcsFile->addFixableWarning( |
385 | 'Expected %s spaces after exception type; %s found', |
386 | $tag + 2, |
387 | 'SpacingAfterExceptionType', |
388 | [ $expectedSpaces, $separatorLength ] |
389 | ); |
390 | if ( $fix ) { |
391 | $fixType = true; |
392 | $separatorLength = $expectedSpaces; |
393 | } |
394 | } |
395 | } |
396 | if ( $fixType ) { |
397 | $phpcsFile->fixer->replaceToken( |
398 | $tag + 2, |
399 | $exception . ( $comment !== '' ? str_repeat( ' ', $separatorLength ) . $comment : '' ) |
400 | ); |
401 | } |
402 | } |
403 | } |
404 | |
405 | /** |
406 | * Process the function parameter comments. |
407 | * |
408 | * @param File $phpcsFile The file being scanned. |
409 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. |
410 | * @param int $commentStart The position in the stack where the comment started. |
411 | */ |
412 | protected function processParams( File $phpcsFile, int $stackPtr, int $commentStart ): void { |
413 | $tokens = $phpcsFile->getTokens(); |
414 | $params = []; |
415 | foreach ( $tokens[$commentStart]['comment_tags'] as $pos => $tag ) { |
416 | if ( $tokens[$tag]['content'] !== '@param' ) { |
417 | continue; |
418 | } |
419 | |
420 | $paramSpace = 0; |
421 | $type = ''; |
422 | $typeSpace = 0; |
423 | $var = ''; |
424 | $varSpace = 0; |
425 | $comment = ''; |
426 | $commentFirst = ''; |
427 | if ( $tokens[$tag + 1]['code'] === T_DOC_COMMENT_WHITESPACE ) { |
428 | $paramSpace = strlen( $tokens[$tag + 1]['content'] ); |
429 | } |
430 | if ( $tokens[$tag + 2]['code'] === T_DOC_COMMENT_STRING ) { |
431 | preg_match( '/^ |
432 | # Match parameter type and separator as a group of |
433 | ((?: |
434 | # plain letters |
435 | [^&$.\{\[] |
436 | | |
437 | # or pairs of braces around plain letters, never single braces |
438 | \{ [^&$.\{\}]* \} |
439 | | |
440 | # or pairs of brackets around plain letters, never single brackets |
441 | \[ [^&$.\[\]]* \] |
442 | | |
443 | # allow & on intersect types, but not as pass-by-ref |
444 | & [^$.\[\]] |
445 | )*) (?: |
446 | # Match parameter name with variadic arg or surround by {} or [] |
447 | ( (?: \.\.\. | [\[\{] )? [&$] \S+ ) |
448 | # Match optional rest of line |
449 | (?: (\s+) (.*) )? |
450 | )? /x', |
451 | $tokens[$tag + 2]['content'], |
452 | $matches |
453 | ); |
454 | $untrimmedType = $matches[1] ?? ''; |
455 | $type = rtrim( $untrimmedType ); |
456 | $typeSpace = strlen( $untrimmedType ) - strlen( $type ); |
457 | if ( isset( $matches[2] ) ) { |
458 | $var = $matches[2]; |
459 | if ( isset( $matches[4] ) ) { |
460 | $varSpace = strlen( $matches[3] ); |
461 | $commentFirst = $matches[4]; |
462 | $comment = $commentFirst; |
463 | // Any strings until the next tag belong to this comment. |
464 | $end = $tokens[$commentStart]['comment_tags'][$pos + 1] ?? |
465 | $tokens[$commentStart]['comment_closer']; |
466 | for ( $i = $tag + 3; $i < $end; $i++ ) { |
467 | if ( $tokens[$i]['code'] === T_DOC_COMMENT_STRING ) { |
468 | $comment .= ' ' . $tokens[$i]['content']; |
469 | } |
470 | } |
471 | } |
472 | } else { |
473 | $phpcsFile->addError( 'Missing parameter name', $tag, 'MissingParamName' ); |
474 | } |
475 | } else { |
476 | $phpcsFile->addError( 'Missing parameter type', $tag, 'MissingParamType' ); |
477 | } |
478 | |
479 | $isPassByReference = str_starts_with( $var, '&' ); |
480 | // Remove the pass by reference to allow compare with varargs |
481 | if ( $isPassByReference ) { |
482 | $var = substr( $var, 1 ); |
483 | } |
484 | |
485 | $isLegacyVariadicArg = str_ends_with( $var, ',...' ); |
486 | $isVariadicArg = str_starts_with( $var, '...$' ); |
487 | // Remove the variadic indicator from the doc name to compare it against the real |
488 | // name, so that we can allow both formats. |
489 | if ( $isLegacyVariadicArg ) { |
490 | $var = substr( $var, 0, -4 ); |
491 | } elseif ( $isVariadicArg ) { |
492 | $var = substr( $var, 3 ); |
493 | } |
494 | |
495 | $params[] = [ |
496 | 'tag' => $tag, |
497 | 'type' => $type, |
498 | 'var' => $var, |
499 | 'variadic_arg' => $isVariadicArg, |
500 | 'legacy_variadic_arg' => $isLegacyVariadicArg, |
501 | 'pass_by_reference' => $isPassByReference, |
502 | 'comment' => $comment, |
503 | 'comment_first' => $commentFirst, |
504 | 'param_space' => $paramSpace, |
505 | 'type_space' => $typeSpace, |
506 | 'var_space' => $varSpace, |
507 | ]; |
508 | } |
509 | $realParams = $phpcsFile->getMethodParameters( $stackPtr ); |
510 | $foundParams = []; |
511 | // We want to use ... for all variable length arguments, so added |
512 | // this prefix to the variable name so comparisons are easier. |
513 | foreach ( $realParams as $pos => $param ) { |
514 | if ( $param['variable_length'] === true ) { |
515 | $realParams[$pos]['name'] = '...' . $param['name']; |
516 | } |
517 | } |
518 | foreach ( $params as $pos => $param ) { |
519 | if ( $param['var'] === '' ) { |
520 | continue; |
521 | } |
522 | // Check number of spaces before type (after @param) |
523 | $spaces = 1; |
524 | if ( $param['param_space'] !== $spaces ) { |
525 | $fix = $phpcsFile->addFixableWarning( |
526 | 'Expected %s spaces before parameter type; %s found', |
527 | $param['tag'], |
528 | 'SpacingBeforeParamType', |
529 | [ $spaces, $param['param_space'] ] |
530 | ); |
531 | if ( $fix ) { |
532 | $phpcsFile->fixer->replaceToken( $param['tag'] + 1, str_repeat( ' ', $spaces ) ); |
533 | } |
534 | } |
535 | // Check if type is provided |
536 | if ( $param['type'] === '' ) { |
537 | $phpcsFile->addError( |
538 | 'Expected parameter type before parameter name "%s"', |
539 | $param['tag'], |
540 | 'NoParamType', |
541 | [ $param['var'] ] |
542 | ); |
543 | } else { |
544 | // Check number of spaces after the type. |
545 | if ( $param['type_space'] !== $spaces ) { |
546 | $fix = $phpcsFile->addFixableWarning( |
547 | 'Expected %s spaces after parameter type; %s found', |
548 | $param['tag'], |
549 | 'SpacingAfterParamType', |
550 | [ $spaces, $param['type_space'] ] |
551 | ); |
552 | if ( $fix ) { |
553 | $this->replaceParamComment( |
554 | $phpcsFile, |
555 | $param, |
556 | [ 'type_space' => $spaces ] |
557 | ); |
558 | } |
559 | } |
560 | |
561 | } |
562 | $fixVar = false; |
563 | $var = $this->fixTrailingPunctuation( |
564 | $phpcsFile, |
565 | $param['tag'], |
566 | $param['var'], |
567 | $fixVar, |
568 | 'param name' |
569 | ); |
570 | $var = $this->fixWrappedParenthesis( |
571 | $phpcsFile, |
572 | $param['tag'], |
573 | $var, |
574 | $fixVar, |
575 | 'param name' |
576 | ); |
577 | if ( $fixVar ) { |
578 | $this->replaceParamComment( |
579 | $phpcsFile, |
580 | $param, |
581 | [ 'var' => $var ] |
582 | ); |
583 | } |
584 | // Make sure the param name is correct. |
585 | $defaultNull = false; |
586 | if ( isset( $realParams[$pos] ) ) { |
587 | $realName = $realParams[$pos]['name']; |
588 | // If difference is pass by reference, add or remove & from documentation |
589 | if ( $param['pass_by_reference'] !== $realParams[$pos]['pass_by_reference'] ) { |
590 | $fix = $phpcsFile->addFixableError( |
591 | 'Pass-by-reference for parameter %s does not match ' . |
592 | 'pass-by-reference of variable name %s', |
593 | $param['tag'], |
594 | 'ParamPassByReference', |
595 | [ $var, $realName ] |
596 | ); |
597 | if ( $fix ) { |
598 | $this->replaceParamComment( |
599 | $phpcsFile, |
600 | $param, |
601 | [ 'pass_by_reference' => $realParams[$pos]['pass_by_reference'] ] |
602 | ); |
603 | } |
604 | $param['pass_by_reference'] = $realParams[$pos]['pass_by_reference']; |
605 | } |
606 | if ( $realName !== $var ) { |
607 | if ( |
608 | str_starts_with( $realName, '...$' ) && |
609 | ( $param['legacy_variadic_arg'] || $param['variadic_arg'] ) |
610 | ) { |
611 | // Mark all variants as found |
612 | $foundParams[] = "...$var"; |
613 | $foundParams[] = "$var,..."; |
614 | } else { |
615 | $code = 'ParamNameNoMatch'; |
616 | $error = 'Doc comment for parameter %s does not match '; |
617 | if ( strcasecmp( $var, $realName ) === 0 ) { |
618 | $error .= 'case of '; |
619 | $code = 'ParamNameNoCaseMatch'; |
620 | } |
621 | $error .= 'actual variable name %s'; |
622 | $phpcsFile->addError( $error, $param['tag'], $code, [ $var, $realName ] ); |
623 | } |
624 | } |
625 | $defaultNull = ( $realParams[$pos]['default'] ?? '' ) === 'null'; |
626 | } elseif ( $param['variadic_arg'] || $param['legacy_variadic_arg'] ) { |
627 | $phpcsFile->addError( |
628 | 'Variadic parameter documented but not present in the signature', |
629 | $param['tag'], |
630 | 'VariadicDocNotSignature' |
631 | ); |
632 | } else { |
633 | $phpcsFile->addError( 'Superfluous parameter comment', $param['tag'], 'ExtraParamComment' ); |
634 | } |
635 | $foundParams[] = $var; |
636 | $fixType = false; |
637 | // Check for unneeded punctuation on parameter type |
638 | $type = $this->fixWrappedParenthesis( |
639 | $phpcsFile, |
640 | $param['tag'], |
641 | $param['type'], |
642 | $fixType, |
643 | 'param type' |
644 | ); |
645 | // Check the short type of boolean and integer |
646 | $type = $this->fixShortTypes( $phpcsFile, $param['tag'], $type, $fixType, 'param' ); |
647 | $this->maybeAddObjectTypehintError( |
648 | $phpcsFile, |
649 | $param['tag'], |
650 | $type, |
651 | 'param' |
652 | ); |
653 | $this->maybeAddTypeTypehintError( |
654 | $phpcsFile, |
655 | $param['tag'], |
656 | $type, |
657 | 'param' |
658 | ); |
659 | $explodedType = $type === '' ? [] : explode( '|', $type ); |
660 | $nullableDoc = str_starts_with( $type, '?' ); |
661 | $nullFound = false; |
662 | foreach ( $explodedType as $index => $singleType ) { |
663 | $singleType = lcfirst( $singleType ); |
664 | // Either an explicit null, or mixed, which null is a |
665 | // part of (T218324) |
666 | if ( $singleType === 'null' || $singleType === 'mixed' ) { |
667 | $nullFound = true; |
668 | } elseif ( str_ends_with( $singleType, '[optional]' ) ) { |
669 | $fix = $phpcsFile->addFixableError( |
670 | 'Key word "[optional]" on "%s" should not be used', |
671 | $param['tag'], |
672 | 'NoOptionalKeyWord', |
673 | [ $param['type'] ] |
674 | ); |
675 | if ( $fix ) { |
676 | $explodedType[$index] = substr( $singleType, 0, -10 ); |
677 | $fixType = true; |
678 | } |
679 | } |
680 | } |
681 | if ( |
682 | isset( $realParams[$pos] ) && $nullableDoc && $defaultNull && |
683 | !$realParams[$pos]['nullable_type'] |
684 | ) { |
685 | // Don't offer autofix, as changing a signature is somewhat delicate |
686 | $phpcsFile->addError( |
687 | 'Use nullable type("%s") for parameters documented as nullable', |
688 | $realParams[$pos]['token'], |
689 | 'PHP71NullableDocOptionalArg', |
690 | [ $type ] |
691 | ); |
692 | } elseif ( $defaultNull && !( $nullFound || $nullableDoc ) ) { |
693 | // Check if the default of null is in the type list |
694 | $fix = $phpcsFile->addFixableError( |
695 | 'Default of null should be declared in @param tag', |
696 | $param['tag'], |
697 | 'DefaultNullTypeParam' |
698 | ); |
699 | if ( $fix ) { |
700 | $explodedType[] = 'null'; |
701 | $fixType = true; |
702 | } |
703 | } |
704 | |
705 | if ( $fixType ) { |
706 | $this->replaceParamComment( |
707 | $phpcsFile, |
708 | $param, |
709 | [ 'type' => implode( '|', $explodedType ) ] |
710 | ); |
711 | } |
712 | if ( $param['comment'] === '' ) { |
713 | continue; |
714 | } |
715 | // Check number of spaces after the var name. |
716 | if ( $param['var_space'] !== $spaces && |
717 | ltrim( $param['comment'] ) !== '' |
718 | ) { |
719 | $fix = $phpcsFile->addFixableWarning( |
720 | 'Expected %s spaces after parameter name; %s found', |
721 | $param['tag'], |
722 | 'SpacingAfterParamName', |
723 | [ $spaces, $param['var_space'] ] |
724 | ); |
725 | if ( $fix ) { |
726 | $this->replaceParamComment( |
727 | $phpcsFile, |
728 | $param, |
729 | [ 'var_space' => $spaces ] |
730 | ); |
731 | } |
732 | } |
733 | // Warn if the parameter is documented as variadic, but the signature doesn't have |
734 | // the splat operator |
735 | if ( |
736 | ( $param['variadic_arg'] || $param['legacy_variadic_arg'] ) && |
737 | isset( $realParams[$pos] ) && |
738 | $realParams[$pos]['variable_length'] === false |
739 | ) { |
740 | $legacyName = $param['legacy_variadic_arg'] ? "$var,..." : "...$var"; |
741 | $phpcsFile->addError( |
742 | 'Splat operator missing for documented variadic parameter "%s"', |
743 | $realParams[$pos]['token'], |
744 | 'MissingSplatVariadicArg', |
745 | [ $legacyName ] |
746 | ); |
747 | } |
748 | } |
749 | $missingParams = []; |
750 | $hasUntypedParams = false; |
751 | foreach ( $realParams as $param ) { |
752 | $hasUntypedParams = $hasUntypedParams || $param['type_hint'] === ''; |
753 | if ( !in_array( $param['name'], $foundParams ) ) { |
754 | $missingParams[] = $param['name']; |
755 | } |
756 | } |
757 | $isTestFunction = $this->isTestFunction( $phpcsFile, $stackPtr ); |
758 | // Report missing comments, unless *all* parameters have types. |
759 | // As an exception, tests are allowed to omit comments an long as they omit *all* comments. |
760 | if ( $hasUntypedParams && ( !$isTestFunction || $foundParams !== [] ) ) { |
761 | foreach ( $missingParams as $neededParam ) { |
762 | $phpcsFile->addError( |
763 | 'Doc comment for parameter "%s" missing', |
764 | $commentStart, |
765 | 'MissingParamTag', |
766 | [ $neededParam ] |
767 | ); |
768 | } |
769 | } |
770 | } |
771 | |
772 | /** |
773 | * Replace a {@}param comment |
774 | * |
775 | * @param File $phpcsFile The file being scanned. |
776 | * @param array $param Array of the @param |
777 | * @param array $fixParam Array with fixes to @param. Only provide keys to replace |
778 | */ |
779 | protected function replaceParamComment( File $phpcsFile, array $param, array $fixParam ): void { |
780 | // Use the old value for unchanged keys |
781 | $fixParam += $param; |
782 | |
783 | // Build the new line |
784 | $content = $fixParam['type'] . |
785 | str_repeat( ' ', $fixParam['type_space'] ) . |
786 | ( $fixParam['pass_by_reference'] ? '&' : '' ) . |
787 | ( $fixParam['variadic_arg'] ? '...' : '' ) . |
788 | $fixParam['var'] . |
789 | ( $fixParam['legacy_variadic_arg'] ? ',...' : '' ) . |
790 | str_repeat( ' ', $fixParam['var_space'] ) . |
791 | $fixParam['comment_first']; |
792 | $phpcsFile->fixer->replaceToken( $fixParam['tag'] + 2, $content ); |
793 | } |
794 | |
795 | } |