• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPCSStandards / PHP_CodeSniffer / 17174734458

23 Aug 2025 11:06AM UTC coverage: 76.88% (-2.1%) from 78.934%
17174734458

push

github

jrfnl
TEMP/TESTING PHPUnit 6331

19187 of 24957 relevant lines covered (76.88%)

60.25 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.65
/src/Standards/Squiz/Sniffs/Commenting/FunctionCommentSniff.php
1
<?php
2
/**
3
 * Parses and verifies the doc comments for functions.
4
 *
5
 * @author    Greg Sherwood <gsherwood@squiz.net>
6
 * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
7
 * @license   https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
8
 */
9

10
namespace PHP_CodeSniffer\Standards\Squiz\Sniffs\Commenting;
11

12
use PHP_CodeSniffer\Config;
13
use PHP_CodeSniffer\Files\File;
14
use PHP_CodeSniffer\Standards\PEAR\Sniffs\Commenting\FunctionCommentSniff as PEARFunctionCommentSniff;
15
use PHP_CodeSniffer\Util\Common;
16

17
class FunctionCommentSniff extends PEARFunctionCommentSniff
18
{
19

20
    /**
21
     * Whether to skip inheritdoc comments.
22
     *
23
     * @var boolean
24
     */
25
    public $skipIfInheritdoc = false;
26

27
    /**
28
     * The current PHP version.
29
     *
30
     * @var integer|string|null
31
     */
32
    private $phpVersion = null;
33

34

35
    /**
36
     * Process the return comment of this function comment.
37
     *
38
     * @param \PHP_CodeSniffer\Files\File $phpcsFile    The file being scanned.
39
     * @param int                         $stackPtr     The position of the current token
40
     *                                                  in the stack passed in $tokens.
41
     * @param int                         $commentStart The position in the stack where the comment started.
42
     *
43
     * @return void
44
     */
45
    protected function processReturn(File $phpcsFile, $stackPtr, $commentStart)
2✔
46
    {
47
        $tokens = $phpcsFile->getTokens();
2✔
48
        $return = null;
2✔
49

50
        if ($this->skipIfInheritdoc === true) {
2✔
51
            if ($this->checkInheritdoc($phpcsFile, $stackPtr, $commentStart) === true) {
2✔
52
                return;
2✔
53
            }
54
        }
55

56
        foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
2✔
57
            if ($tokens[$tag]['content'] === '@return') {
2✔
58
                if ($return !== null) {
2✔
59
                    $error = 'Only 1 @return tag is allowed in a function comment';
2✔
60
                    $phpcsFile->addError($error, $tag, 'DuplicateReturn');
2✔
61
                    return;
2✔
62
                }
63

64
                $return = $tag;
2✔
65
            }
66
        }
67

68
        // Skip constructor and destructor.
69
        $methodName      = $phpcsFile->getDeclarationName($stackPtr);
2✔
70
        $isSpecialMethod = in_array($methodName,  $this->specialMethods, true);
2✔
71

72
        if ($return !== null) {
2✔
73
            $content = $tokens[($return + 2)]['content'];
2✔
74
            if (empty($content) === true || $tokens[($return + 2)]['code'] !== T_DOC_COMMENT_STRING) {
2✔
75
                $error = 'Return type missing for @return tag in function comment';
2✔
76
                $phpcsFile->addError($error, $return, 'MissingReturnType');
2✔
77
            } else {
78
                // Support both a return type and a description.
79
                preg_match('`^((?:\|?(?:array\([^\)]*\)|[\\\\a-z0-9_\[\]]+))*)( .*)?`i', $content, $returnParts);
2✔
80
                if (isset($returnParts[1]) === false) {
2✔
81
                    return;
×
82
                }
83

84
                $returnType = $returnParts[1];
2✔
85

86
                // Check return type (can be multiple, separated by '|').
87
                $typeNames      = explode('|', $returnType);
2✔
88
                $suggestedNames = [];
2✔
89
                foreach ($typeNames as $typeName) {
2✔
90
                    $suggestedName = Common::suggestType($typeName);
2✔
91
                    if (in_array($suggestedName, $suggestedNames, true) === false) {
2✔
92
                        $suggestedNames[] = $suggestedName;
2✔
93
                    }
94
                }
95

96
                $suggestedType = implode('|', $suggestedNames);
2✔
97
                if ($returnType !== $suggestedType) {
2✔
98
                    $error = 'Expected "%s" but found "%s" for function return type';
2✔
99
                    $data  = [
2✔
100
                        $suggestedType,
2✔
101
                        $returnType,
2✔
102
                    ];
2✔
103
                    $fix   = $phpcsFile->addFixableError($error, $return, 'InvalidReturn', $data);
2✔
104
                    if ($fix === true) {
2✔
105
                        $replacement = $suggestedType;
2✔
106
                        if (empty($returnParts[2]) === false) {
2✔
107
                            $replacement .= $returnParts[2];
2✔
108
                        }
109

110
                        $phpcsFile->fixer->replaceToken(($return + 2), $replacement);
2✔
111
                        unset($replacement);
2✔
112
                    }
113
                }
114

115
                // If the return type is void, make sure there is
116
                // no return statement in the function.
117
                if ($returnType === 'void') {
2✔
118
                    if (isset($tokens[$stackPtr]['scope_closer']) === true) {
2✔
119
                        $endToken = $tokens[$stackPtr]['scope_closer'];
2✔
120
                        for ($returnToken = $stackPtr; $returnToken < $endToken; $returnToken++) {
2✔
121
                            if ($tokens[$returnToken]['code'] === T_CLOSURE
2✔
122
                                || $tokens[$returnToken]['code'] === T_ANON_CLASS
2✔
123
                            ) {
124
                                $returnToken = $tokens[$returnToken]['scope_closer'];
2✔
125
                                continue;
2✔
126
                            }
127

128
                            if ($tokens[$returnToken]['code'] === T_RETURN
2✔
129
                                || $tokens[$returnToken]['code'] === T_YIELD
2✔
130
                                || $tokens[$returnToken]['code'] === T_YIELD_FROM
2✔
131
                            ) {
132
                                break;
2✔
133
                            }
134
                        }
135

136
                        if ($returnToken !== $endToken) {
2✔
137
                            // If the function is not returning anything, just
138
                            // exiting, then there is no problem.
139
                            $semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true);
2✔
140
                            if ($tokens[$semicolon]['code'] !== T_SEMICOLON) {
2✔
141
                                $error = 'Function return type is void, but function contains return statement';
2✔
142
                                $phpcsFile->addError($error, $return, 'InvalidReturnVoid');
2✔
143
                            }
144
                        }
145
                    }//end if
146
                } else if ($returnType !== 'mixed'
2✔
147
                    && $returnType !== 'never'
2✔
148
                    && in_array('void', $typeNames, true) === false
2✔
149
                ) {
150
                    // If return type is not void, never, or mixed, there needs to be a
151
                    // return statement somewhere in the function that returns something.
152
                    if (isset($tokens[$stackPtr]['scope_closer']) === true) {
2✔
153
                        $endToken = $tokens[$stackPtr]['scope_closer'];
2✔
154
                        for ($returnToken = $stackPtr; $returnToken < $endToken; $returnToken++) {
2✔
155
                            if ($tokens[$returnToken]['code'] === T_CLOSURE
2✔
156
                                || $tokens[$returnToken]['code'] === T_ANON_CLASS
2✔
157
                            ) {
158
                                $returnToken = $tokens[$returnToken]['scope_closer'];
2✔
159
                                continue;
2✔
160
                            }
161

162
                            if ($tokens[$returnToken]['code'] === T_RETURN
2✔
163
                                || $tokens[$returnToken]['code'] === T_YIELD
2✔
164
                                || $tokens[$returnToken]['code'] === T_YIELD_FROM
2✔
165
                            ) {
166
                                break;
2✔
167
                            }
168
                        }
169

170
                        if ($returnToken === $endToken) {
2✔
171
                            $error = 'Function return type is not void, but function has no return statement';
2✔
172
                            $phpcsFile->addError($error, $return, 'InvalidNoReturn');
2✔
173
                        } else {
174
                            $semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true);
2✔
175
                            if ($tokens[$semicolon]['code'] === T_SEMICOLON) {
2✔
176
                                $error = 'Function return type is not void, but function is returning void here';
2✔
177
                                $phpcsFile->addError($error, $returnToken, 'InvalidReturnNotVoid');
2✔
178
                            }
179
                        }
180
                    }//end if
181
                }//end if
182
            }//end if
183
        } else {
184
            if ($isSpecialMethod === true) {
2✔
185
                return;
2✔
186
            }
187

188
            $error = 'Missing @return tag in function comment';
2✔
189
            $phpcsFile->addError($error, $tokens[$commentStart]['comment_closer'], 'MissingReturn');
2✔
190
        }//end if
191

192
    }//end processReturn()
193

194

195
    /**
196
     * Process any throw tags that this function comment has.
197
     *
198
     * @param \PHP_CodeSniffer\Files\File $phpcsFile    The file being scanned.
199
     * @param int                         $stackPtr     The position of the current token
200
     *                                                  in the stack passed in $tokens.
201
     * @param int                         $commentStart The position in the stack where the comment started.
202
     *
203
     * @return void
204
     */
205
    protected function processThrows(File $phpcsFile, $stackPtr, $commentStart)
2✔
206
    {
207
        $tokens = $phpcsFile->getTokens();
2✔
208

209
        if ($this->skipIfInheritdoc === true) {
2✔
210
            if ($this->checkInheritdoc($phpcsFile, $stackPtr, $commentStart) === true) {
2✔
211
                return;
2✔
212
            }
213
        }
214

215
        foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) {
2✔
216
            if ($tokens[$tag]['content'] !== '@throws') {
2✔
217
                continue;
2✔
218
            }
219

220
            $exception = null;
2✔
221
            $comment   = null;
2✔
222
            if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) {
2✔
223
                $matches = [];
2✔
224
                preg_match('/([^\s]+)(?:\s+(.*))?/', $tokens[($tag + 2)]['content'], $matches);
2✔
225
                $exception = $matches[1];
2✔
226
                if (isset($matches[2]) === true && trim($matches[2]) !== '') {
2✔
227
                    $comment = $matches[2];
2✔
228
                }
229
            }
230

231
            if ($exception === null) {
2✔
232
                $error = 'Exception type and comment missing for @throws tag in function comment';
2✔
233
                $phpcsFile->addError($error, $tag, 'InvalidThrows');
2✔
234
            } else if ($comment === null) {
2✔
235
                $error = 'Comment missing for @throws tag in function comment';
2✔
236
                $phpcsFile->addError($error, $tag, 'EmptyThrows');
2✔
237
            } else {
238
                // Any strings until the next tag belong to this comment.
239
                if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) {
2✔
240
                    $end = $tokens[$commentStart]['comment_tags'][($pos + 1)];
×
241
                } else {
242
                    $end = $tokens[$commentStart]['comment_closer'];
2✔
243
                }
244

245
                for ($i = ($tag + 3); $i < $end; $i++) {
2✔
246
                    if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
2✔
247
                        $comment .= ' '.$tokens[$i]['content'];
2✔
248
                    }
249
                }
250

251
                $comment = trim($comment);
2✔
252

253
                // Starts with a capital letter and ends with a fullstop.
254
                $firstChar = $comment[0];
2✔
255
                if (strtoupper($firstChar) !== $firstChar) {
2✔
256
                    $error = '@throws tag comment must start with a capital letter';
2✔
257
                    $phpcsFile->addError($error, ($tag + 2), 'ThrowsNotCapital');
2✔
258
                }
259

260
                $lastChar = substr($comment, -1);
2✔
261
                if ($lastChar !== '.') {
2✔
262
                    $error = '@throws tag comment must end with a full stop';
2✔
263
                    $phpcsFile->addError($error, ($tag + 2), 'ThrowsNoFullStop');
2✔
264
                }
265
            }//end if
266
        }//end foreach
267

268
    }//end processThrows()
269

270

271
    /**
272
     * Process the function parameter comments.
273
     *
274
     * @param \PHP_CodeSniffer\Files\File $phpcsFile    The file being scanned.
275
     * @param int                         $stackPtr     The position of the current token
276
     *                                                  in the stack passed in $tokens.
277
     * @param int                         $commentStart The position in the stack where the comment started.
278
     *
279
     * @return void
280
     */
281
    protected function processParams(File $phpcsFile, $stackPtr, $commentStart)
2✔
282
    {
283
        if ($this->phpVersion === null) {
2✔
284
            $this->phpVersion = Config::getConfigData('php_version');
2✔
285
            if ($this->phpVersion === null) {
2✔
286
                $this->phpVersion = PHP_VERSION_ID;
2✔
287
            }
288
        }
289

290
        $tokens = $phpcsFile->getTokens();
2✔
291

292
        if ($this->skipIfInheritdoc === true) {
2✔
293
            if ($this->checkInheritdoc($phpcsFile, $stackPtr, $commentStart) === true) {
2✔
294
                return;
2✔
295
            }
296
        }
297

298
        $params  = [];
2✔
299
        $maxType = 0;
2✔
300
        $maxVar  = 0;
2✔
301
        foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) {
2✔
302
            if ($tokens[$tag]['content'] !== '@param') {
2✔
303
                continue;
2✔
304
            }
305

306
            $type         = '';
2✔
307
            $typeSpace    = 0;
2✔
308
            $var          = '';
2✔
309
            $varSpace     = 0;
2✔
310
            $comment      = '';
2✔
311
            $commentLines = [];
2✔
312
            if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) {
2✔
313
                $matches = [];
2✔
314
                preg_match('/((?:(?![$.]|&(?=\$)).)*)(?:((?:\.\.\.)?(?:\$|&)[^\s]+)(?:(\s+)(.*))?)?/', $tokens[($tag + 2)]['content'], $matches);
2✔
315

316
                if (empty($matches) === false) {
2✔
317
                    $typeLen   = strlen($matches[1]);
2✔
318
                    $type      = trim($matches[1]);
2✔
319
                    $typeSpace = ($typeLen - strlen($type));
2✔
320
                    $typeLen   = strlen($type);
2✔
321
                    if ($typeLen > $maxType) {
2✔
322
                        $maxType = $typeLen;
2✔
323
                    }
324
                }
325

326
                if ($tokens[($tag + 2)]['content'][0] === '$') {
2✔
327
                    $error = 'Missing parameter type';
2✔
328
                    $phpcsFile->addError($error, $tag, 'MissingParamType');
2✔
329
                } else if (isset($matches[2]) === true) {
2✔
330
                    $var    = $matches[2];
2✔
331
                    $varLen = strlen($var);
2✔
332
                    if ($varLen > $maxVar) {
2✔
333
                        $maxVar = $varLen;
2✔
334
                    }
335

336
                    if (isset($matches[4]) === true) {
2✔
337
                        $varSpace       = strlen($matches[3]);
2✔
338
                        $comment        = $matches[4];
2✔
339
                        $commentLines[] = [
2✔
340
                            'comment' => $comment,
2✔
341
                            'token'   => ($tag + 2),
2✔
342
                            'indent'  => $varSpace,
2✔
343
                        ];
2✔
344

345
                        // Any strings until the next tag belong to this comment.
346
                        if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) {
2✔
347
                            $end = $tokens[$commentStart]['comment_tags'][($pos + 1)];
2✔
348
                        } else {
349
                            $end = $tokens[$commentStart]['comment_closer'];
×
350
                        }
351

352
                        for ($i = ($tag + 3); $i < $end; $i++) {
2✔
353
                            if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
2✔
354
                                $indent = 0;
2✔
355
                                if ($tokens[($i - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) {
2✔
356
                                    $indent = $tokens[($i - 1)]['length'];
2✔
357
                                }
358

359
                                $comment       .= ' '.$tokens[$i]['content'];
2✔
360
                                $commentLines[] = [
2✔
361
                                    'comment' => $tokens[$i]['content'],
2✔
362
                                    'token'   => $i,
2✔
363
                                    'indent'  => $indent,
2✔
364
                                ];
2✔
365
                            }
366
                        }
367
                    } else {
368
                        $error = 'Missing parameter comment';
2✔
369
                        $phpcsFile->addError($error, $tag, 'MissingParamComment');
2✔
370
                        $commentLines[] = ['comment' => ''];
2✔
371
                    }//end if
372
                } else {
373
                    $error = 'Missing parameter name';
2✔
374
                    $phpcsFile->addError($error, $tag, 'MissingParamName');
2✔
375
                }//end if
376
            } else {
377
                $error = 'Missing parameter type';
2✔
378
                $phpcsFile->addError($error, $tag, 'MissingParamType');
2✔
379
            }//end if
380

381
            $params[] = [
2✔
382
                'tag'          => $tag,
2✔
383
                'type'         => $type,
2✔
384
                'var'          => $var,
2✔
385
                'comment'      => $comment,
2✔
386
                'commentLines' => $commentLines,
2✔
387
                'type_space'   => $typeSpace,
2✔
388
                'var_space'    => $varSpace,
2✔
389
            ];
2✔
390
        }//end foreach
391

392
        $realParams  = $phpcsFile->getMethodParameters($stackPtr);
2✔
393
        $foundParams = [];
2✔
394

395
        // We want to use ... for all variable length arguments, so added
396
        // this prefix to the variable name so comparisons are easier.
397
        foreach ($realParams as $pos => $param) {
2✔
398
            if ($param['variable_length'] === true) {
2✔
399
                $realParams[$pos]['name'] = '...'.$realParams[$pos]['name'];
2✔
400
            }
401
        }
402

403
        foreach ($params as $pos => $param) {
2✔
404
            // If the type is empty, the whole line is empty.
405
            if ($param['type'] === '') {
2✔
406
                continue;
2✔
407
            }
408

409
            // Check the param type value.
410
            $typeNames          = explode('|', $param['type']);
2✔
411
            $suggestedTypeNames = [];
2✔
412

413
            foreach ($typeNames as $typeName) {
2✔
414
                if ($typeName === '') {
2✔
415
                    continue;
2✔
416
                }
417

418
                // Strip nullable operator.
419
                if ($typeName[0] === '?') {
2✔
420
                    $typeName = substr($typeName, 1);
2✔
421
                }
422

423
                $suggestedName        = Common::suggestType($typeName);
2✔
424
                $suggestedTypeNames[] = $suggestedName;
2✔
425

426
                if (count($typeNames) > 1) {
2✔
427
                    continue;
2✔
428
                }
429

430
                // Check type hint for array and custom type.
431
                $suggestedTypeHint = '';
2✔
432
                if (strpos($suggestedName, 'array') !== false || substr($suggestedName, -2) === '[]') {
2✔
433
                    $suggestedTypeHint = 'array';
2✔
434
                } else if (strpos($suggestedName, 'callable') !== false) {
2✔
435
                    $suggestedTypeHint = 'callable';
2✔
436
                } else if (strpos($suggestedName, 'callback') !== false) {
2✔
437
                    $suggestedTypeHint = 'callable';
2✔
438
                } else if (isset(Common::ALLOWED_TYPES[$suggestedName]) === false) {
2✔
439
                    $suggestedTypeHint = $suggestedName;
2✔
440
                }
441

442
                if ($this->phpVersion >= 70000) {
2✔
443
                    if ($suggestedName === 'string') {
2✔
444
                        $suggestedTypeHint = 'string';
2✔
445
                    } else if ($suggestedName === 'int' || $suggestedName === 'integer') {
2✔
446
                        $suggestedTypeHint = 'int';
2✔
447
                    } else if ($suggestedName === 'float') {
2✔
448
                        $suggestedTypeHint = 'float';
2✔
449
                    } else if ($suggestedName === 'bool' || $suggestedName === 'boolean') {
2✔
450
                        $suggestedTypeHint = 'bool';
2✔
451
                    }
452
                }
453

454
                if ($this->phpVersion >= 70200) {
2✔
455
                    if ($suggestedName === 'object') {
2✔
456
                        $suggestedTypeHint = 'object';
2✔
457
                    }
458
                }
459

460
                if ($this->phpVersion >= 80000) {
2✔
461
                    if ($suggestedName === 'mixed') {
2✔
462
                        $suggestedTypeHint = 'mixed';
2✔
463
                    }
464
                }
465

466
                if ($suggestedTypeHint !== '' && isset($realParams[$pos]) === true && $param['var'] !== '') {
2✔
467
                    $typeHint = $realParams[$pos]['type_hint'];
2✔
468

469
                    // Remove namespace prefixes when comparing.
470
                    $compareTypeHint = substr($suggestedTypeHint, (strlen($typeHint) * -1));
2✔
471

472
                    if ($typeHint === '') {
2✔
473
                        $error = 'Type hint "%s" missing for %s';
2✔
474
                        $data  = [
2✔
475
                            $suggestedTypeHint,
2✔
476
                            $param['var'],
2✔
477
                        ];
2✔
478

479
                        $errorCode = 'TypeHintMissing';
2✔
480
                        if ($suggestedTypeHint === 'string'
2✔
481
                            || $suggestedTypeHint === 'int'
2✔
482
                            || $suggestedTypeHint === 'float'
2✔
483
                            || $suggestedTypeHint === 'bool'
2✔
484
                        ) {
485
                            $errorCode = 'Scalar'.$errorCode;
2✔
486
                        }
487

488
                        $phpcsFile->addError($error, $stackPtr, $errorCode, $data);
2✔
489
                    } else if ($typeHint !== $compareTypeHint && $typeHint !== '?'.$compareTypeHint) {
2✔
490
                        $error = 'Expected type hint "%s"; found "%s" for %s';
2✔
491
                        $data  = [
2✔
492
                            $suggestedTypeHint,
2✔
493
                            $typeHint,
2✔
494
                            $param['var'],
2✔
495
                        ];
2✔
496
                        $phpcsFile->addError($error, $stackPtr, 'IncorrectTypeHint', $data);
2✔
497
                    }//end if
498
                } else if ($suggestedTypeHint === '' && isset($realParams[$pos]) === true) {
2✔
499
                    $typeHint = $realParams[$pos]['type_hint'];
2✔
500
                    if ($typeHint !== '') {
2✔
501
                        $error = 'Unknown type hint "%s" found for %s';
×
502
                        $data  = [
×
503
                            $typeHint,
×
504
                            $param['var'],
×
505
                        ];
×
506
                        $phpcsFile->addError($error, $stackPtr, 'InvalidTypeHint', $data);
×
507
                    }
508
                }//end if
509
            }//end foreach
510

511
            $suggestedType = implode('|', $suggestedTypeNames);
2✔
512
            if ($param['type'] !== $suggestedType) {
2✔
513
                $error = 'Expected "%s" but found "%s" for parameter type';
2✔
514
                $data  = [
2✔
515
                    $suggestedType,
2✔
516
                    $param['type'],
2✔
517
                ];
2✔
518

519
                $fix = $phpcsFile->addFixableError($error, $param['tag'], 'IncorrectParamVarName', $data);
2✔
520
                if ($fix === true) {
2✔
521
                    $phpcsFile->fixer->beginChangeset();
2✔
522

523
                    $content  = $suggestedType;
2✔
524
                    $content .= str_repeat(' ', $param['type_space']);
2✔
525
                    $content .= $param['var'];
2✔
526
                    $content .= str_repeat(' ', $param['var_space']);
2✔
527
                    if (isset($param['commentLines'][0]) === true) {
2✔
528
                        $content .= $param['commentLines'][0]['comment'];
2✔
529
                    }
530

531
                    $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content);
2✔
532

533
                    // Fix up the indent of additional comment lines.
534
                    foreach ($param['commentLines'] as $lineNum => $line) {
2✔
535
                        if ($lineNum === 0
2✔
536
                            || $param['commentLines'][$lineNum]['indent'] === 0
2✔
537
                        ) {
538
                            continue;
2✔
539
                        }
540

541
                        $diff      = (strlen($param['type']) - strlen($suggestedType));
2✔
542
                        $newIndent = ($param['commentLines'][$lineNum]['indent'] - $diff);
2✔
543
                        $phpcsFile->fixer->replaceToken(
2✔
544
                            ($param['commentLines'][$lineNum]['token'] - 1),
2✔
545
                            str_repeat(' ', $newIndent)
2✔
546
                        );
2✔
547
                    }
548

549
                    $phpcsFile->fixer->endChangeset();
2✔
550
                }//end if
551
            }//end if
552

553
            if ($param['var'] === '') {
2✔
554
                continue;
2✔
555
            }
556

557
            $foundParams[] = $param['var'];
2✔
558

559
            // Check number of spaces after the type.
560
            $this->checkSpacingAfterParamType($phpcsFile, $param, $maxType);
2✔
561

562
            // Make sure the param name is correct.
563
            if (isset($realParams[$pos]) === true) {
2✔
564
                $realName     = $realParams[$pos]['name'];
2✔
565
                $paramVarName = $param['var'];
2✔
566

567
                if ($param['var'][0] === '&') {
2✔
568
                    // Even when passed by reference, the variable name in $realParams does not have
569
                    // a leading '&'. This sniff will accept both '&$var' and '$var' in these cases.
570
                    $paramVarName = substr($param['var'], 1);
2✔
571

572
                    // This makes sure that the 'MissingParamTag' check won't throw a false positive.
573
                    $foundParams[(count($foundParams) - 1)] = $paramVarName;
2✔
574

575
                    if ($realParams[$pos]['pass_by_reference'] !== true && $realName === $paramVarName) {
2✔
576
                        // Don't complain about this unless the param name is otherwise correct.
577
                        $error = 'Doc comment for parameter %s is prefixed with "&" but parameter is not passed by reference';
2✔
578
                        $code  = 'ParamNameUnexpectedAmpersandPrefix';
2✔
579
                        $data  = [$paramVarName];
2✔
580

581
                        // We're not offering an auto-fix here because we can't tell if the docblock
582
                        // is wrong, or the parameter should be passed by reference.
583
                        $phpcsFile->addError($error, $param['tag'], $code, $data);
2✔
584
                    }
585
                }
586

587
                if ($realName !== $paramVarName) {
2✔
588
                    $code = 'ParamNameNoMatch';
2✔
589
                    $data = [
2✔
590
                        $paramVarName,
2✔
591
                        $realName,
2✔
592
                    ];
2✔
593

594
                    $error = 'Doc comment for parameter %s does not match ';
2✔
595
                    if (strtolower($paramVarName) === strtolower($realName)) {
2✔
596
                        $error .= 'case of ';
2✔
597
                        $code   = 'ParamNameNoCaseMatch';
2✔
598
                    }
599

600
                    $error .= 'actual variable name %s';
2✔
601

602
                    $phpcsFile->addError($error, $param['tag'], $code, $data);
2✔
603
                }//end if
604
            } else if (substr($param['var'], -4) !== ',...') {
2✔
605
                // We must have an extra parameter comment.
606
                $error = 'Superfluous parameter comment';
2✔
607
                $phpcsFile->addError($error, $param['tag'], 'ExtraParamComment');
2✔
608
            }//end if
609

610
            if ($param['comment'] === '') {
2✔
611
                continue;
2✔
612
            }
613

614
            // Check number of spaces after the var name.
615
            $this->checkSpacingAfterParamName($phpcsFile, $param, $maxVar);
2✔
616

617
            // Param comments must start with a capital letter and end with a full stop.
618
            if (preg_match('/^(\p{Ll}|\P{L})/u', $param['comment']) === 1) {
2✔
619
                $error = 'Parameter comment must start with a capital letter';
2✔
620
                $phpcsFile->addError($error, $param['tag'], 'ParamCommentNotCapital');
2✔
621
            }
622

623
            $lastChar = substr($param['comment'], -1);
2✔
624
            if ($lastChar !== '.') {
2✔
625
                $error = 'Parameter comment must end with a full stop';
2✔
626
                $phpcsFile->addError($error, $param['tag'], 'ParamCommentFullStop');
2✔
627
            }
628
        }//end foreach
629

630
        $realNames = [];
2✔
631
        foreach ($realParams as $realParam) {
2✔
632
            $realNames[] = $realParam['name'];
2✔
633
        }
634

635
        // Report missing comments.
636
        $diff = array_diff($realNames, $foundParams);
2✔
637
        foreach ($diff as $neededParam) {
2✔
638
            $error = 'Doc comment for parameter "%s" missing';
2✔
639
            $data  = [$neededParam];
2✔
640
            $phpcsFile->addError($error, $commentStart, 'MissingParamTag', $data);
2✔
641
        }
642

643
    }//end processParams()
644

645

646
    /**
647
     * Check the spacing after the type of a parameter.
648
     *
649
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
650
     * @param array                       $param     The parameter to be checked.
651
     * @param int                         $maxType   The maxlength of the longest parameter type.
652
     * @param int                         $spacing   The number of spaces to add after the type.
653
     *
654
     * @return void
655
     */
656
    protected function checkSpacingAfterParamType(File $phpcsFile, $param, $maxType, $spacing=1)
2✔
657
    {
658
        // Check number of spaces after the type.
659
        $spaces = ($maxType - strlen($param['type']) + $spacing);
2✔
660
        if ($param['type_space'] !== $spaces) {
2✔
661
            $error = 'Expected %s spaces after parameter type; %s found';
2✔
662
            $data  = [
2✔
663
                $spaces,
2✔
664
                $param['type_space'],
2✔
665
            ];
2✔
666

667
            $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamType', $data);
2✔
668
            if ($fix === true) {
2✔
669
                $phpcsFile->fixer->beginChangeset();
2✔
670

671
                $content  = $param['type'];
2✔
672
                $content .= str_repeat(' ', $spaces);
2✔
673
                $content .= $param['var'];
2✔
674
                $content .= str_repeat(' ', $param['var_space']);
2✔
675
                $content .= $param['commentLines'][0]['comment'];
2✔
676
                $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content);
2✔
677

678
                // Fix up the indent of additional comment lines.
679
                $diff = ($param['type_space'] - $spaces);
2✔
680
                foreach ($param['commentLines'] as $lineNum => $line) {
2✔
681
                    if ($lineNum === 0
2✔
682
                        || $param['commentLines'][$lineNum]['indent'] === 0
2✔
683
                    ) {
684
                        continue;
2✔
685
                    }
686

687
                    $newIndent = ($param['commentLines'][$lineNum]['indent'] - $diff);
2✔
688
                    if ($newIndent <= 0) {
2✔
689
                        continue;
2✔
690
                    }
691

692
                    $phpcsFile->fixer->replaceToken(
2✔
693
                        ($param['commentLines'][$lineNum]['token'] - 1),
2✔
694
                        str_repeat(' ', $newIndent)
2✔
695
                    );
2✔
696
                }
697

698
                $phpcsFile->fixer->endChangeset();
2✔
699
            }//end if
700
        }//end if
701

702
    }//end checkSpacingAfterParamType()
703

704

705
    /**
706
     * Check the spacing after the name of a parameter.
707
     *
708
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
709
     * @param array                       $param     The parameter to be checked.
710
     * @param int                         $maxVar    The maxlength of the longest parameter name.
711
     * @param int                         $spacing   The number of spaces to add after the type.
712
     *
713
     * @return void
714
     */
715
    protected function checkSpacingAfterParamName(File $phpcsFile, $param, $maxVar, $spacing=1)
2✔
716
    {
717
        // Check number of spaces after the var name.
718
        $spaces = ($maxVar - strlen($param['var']) + $spacing);
2✔
719
        if ($param['var_space'] !== $spaces) {
2✔
720
            $error = 'Expected %s spaces after parameter name; %s found';
2✔
721
            $data  = [
2✔
722
                $spaces,
2✔
723
                $param['var_space'],
2✔
724
            ];
2✔
725

726
            $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamName', $data);
2✔
727
            if ($fix === true) {
2✔
728
                $phpcsFile->fixer->beginChangeset();
2✔
729

730
                $content  = $param['type'];
2✔
731
                $content .= str_repeat(' ', $param['type_space']);
2✔
732
                $content .= $param['var'];
2✔
733
                $content .= str_repeat(' ', $spaces);
2✔
734
                $content .= $param['commentLines'][0]['comment'];
2✔
735
                $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content);
2✔
736

737
                // Fix up the indent of additional comment lines.
738
                foreach ($param['commentLines'] as $lineNum => $line) {
2✔
739
                    if ($lineNum === 0
2✔
740
                        || $param['commentLines'][$lineNum]['indent'] === 0
2✔
741
                    ) {
742
                        continue;
2✔
743
                    }
744

745
                    $diff      = ($param['var_space'] - $spaces);
2✔
746
                    $newIndent = ($param['commentLines'][$lineNum]['indent'] - $diff);
2✔
747
                    if ($newIndent <= 0) {
2✔
748
                        continue;
2✔
749
                    }
750

751
                    $phpcsFile->fixer->replaceToken(
2✔
752
                        ($param['commentLines'][$lineNum]['token'] - 1),
2✔
753
                        str_repeat(' ', $newIndent)
2✔
754
                    );
2✔
755
                }
756

757
                $phpcsFile->fixer->endChangeset();
2✔
758
            }//end if
759
        }//end if
760

761
    }//end checkSpacingAfterParamName()
762

763

764
    /**
765
     * Determines whether the whole comment is an inheritdoc comment.
766
     *
767
     * @param \PHP_CodeSniffer\Files\File $phpcsFile    The file being scanned.
768
     * @param int                         $stackPtr     The position of the current token
769
     *                                                  in the stack passed in $tokens.
770
     * @param int                         $commentStart The position in the stack where the comment started.
771
     *
772
     * @return boolean TRUE if the docblock contains only {@inheritdoc} (case-insensitive).
773
     */
774
    protected function checkInheritdoc(File $phpcsFile, $stackPtr, $commentStart)
2✔
775
    {
776
        $tokens = $phpcsFile->getTokens();
2✔
777

778
        $allowedTokens = [
2✔
779
            T_DOC_COMMENT_OPEN_TAG,
2✔
780
            T_DOC_COMMENT_WHITESPACE,
2✔
781
            T_DOC_COMMENT_STAR,
2✔
782
        ];
2✔
783
        for ($i = $commentStart; $i <= $tokens[$commentStart]['comment_closer']; $i++) {
2✔
784
            if (in_array($tokens[$i]['code'], $allowedTokens) === false) {
2✔
785
                $trimmedContent = strtolower(trim($tokens[$i]['content']));
2✔
786

787
                if ($trimmedContent === '{@inheritdoc}') {
2✔
788
                    return true;
2✔
789
                } else {
790
                    return false;
2✔
791
                }
792
            }
793
        }
794

795
        return false;
×
796

797
    }//end checkInheritdoc()
798

799

800
}//end class
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc