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

PHPCSStandards / PHP_CodeSniffer / 15036337869

15 May 2025 04:03AM UTC coverage: 78.375% (-0.2%) from 78.556%
15036337869

Pull #856

github

web-flow
Merge 93f570b46 into f5e7943d0
Pull Request #856: [Doc] Cover all errors of PEAR ClassDeclaration

25112 of 32041 relevant lines covered (78.37%)

69.4 hits per line

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

99.22
/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)
3✔
46
    {
47
        $tokens = $phpcsFile->getTokens();
3✔
48
        $return = null;
3✔
49

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

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

64
                $return = $tag;
3✔
65
            }
1✔
66
        }
1✔
67

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

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

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

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

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

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

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

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

136
                        if ($returnToken !== $endToken) {
3✔
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);
3✔
140
                            if ($tokens[$semicolon]['code'] !== T_SEMICOLON) {
3✔
141
                                $error = 'Function return type is void, but function contains return statement';
3✔
142
                                $phpcsFile->addError($error, $return, 'InvalidReturnVoid');
3✔
143
                            }
1✔
144
                        }
1✔
145
                    }//end if
1✔
146
                } else if ($returnType !== 'mixed'
3✔
147
                    && $returnType !== 'never'
3✔
148
                    && in_array('void', $typeNames, true) === false
3✔
149
                ) {
1✔
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) {
3✔
153
                        $endToken = $tokens[$stackPtr]['scope_closer'];
3✔
154
                        for ($returnToken = $stackPtr; $returnToken < $endToken; $returnToken++) {
3✔
155
                            if ($tokens[$returnToken]['code'] === T_CLOSURE
3✔
156
                                || $tokens[$returnToken]['code'] === T_ANON_CLASS
3✔
157
                            ) {
1✔
158
                                $returnToken = $tokens[$returnToken]['scope_closer'];
3✔
159
                                continue;
3✔
160
                            }
161

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

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

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

192
    }//end processReturn()
2✔
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)
3✔
206
    {
207
        $tokens = $phpcsFile->getTokens();
3✔
208

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

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

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

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

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

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

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

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

268
    }//end processThrows()
2✔
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)
3✔
282
    {
283
        if ($this->phpVersion === null) {
3✔
284
            $this->phpVersion = Config::getConfigData('php_version');
3✔
285
            if ($this->phpVersion === null) {
3✔
286
                $this->phpVersion = PHP_VERSION_ID;
3✔
287
            }
1✔
288
        }
1✔
289

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

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

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

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

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

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

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

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

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

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

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

392
        $realParams  = $phpcsFile->getMethodParameters($stackPtr);
3✔
393
        $foundParams = [];
3✔
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) {
3✔
398
            if ($param['variable_length'] === true) {
3✔
399
                $realParams[$pos]['name'] = '...'.$realParams[$pos]['name'];
3✔
400
            }
1✔
401
        }
1✔
402

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

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

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

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

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

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

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

442
                if ($this->phpVersion >= 70000) {
3✔
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) {
3✔
455
                    if ($suggestedName === 'object') {
2✔
456
                        $suggestedTypeHint = 'object';
2✔
457
                    }
458
                }
459

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

567
                if ($param['var'][0] === '&') {
3✔
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);
3✔
571

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

575
                    if ($realParams[$pos]['pass_by_reference'] !== true && $realName === $paramVarName) {
3✔
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';
3✔
578
                        $code  = 'ParamNameUnexpectedAmpersandPrefix';
3✔
579
                        $data  = [$paramVarName];
3✔
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);
3✔
584
                    }
1✔
585
                }
1✔
586

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

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

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

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

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

614
            // Check number of spaces after the var name.
615
            $this->checkSpacingAfterParamName($phpcsFile, $param, $maxVar);
3✔
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) {
3✔
619
                $error = 'Parameter comment must start with a capital letter';
3✔
620
                $phpcsFile->addError($error, $param['tag'], 'ParamCommentNotCapital');
3✔
621
            }
1✔
622

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

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

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

643
    }//end processParams()
2✔
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)
3✔
657
    {
658
        // Check number of spaces after the type.
659
        $spaces = ($maxType - strlen($param['type']) + $spacing);
3✔
660
        if ($param['type_space'] !== $spaces) {
3✔
661
            $error = 'Expected %s spaces after parameter type; %s found';
3✔
662
            $data  = [
1✔
663
                $spaces,
3✔
664
                $param['type_space'],
3✔
665
            ];
2✔
666

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

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

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

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

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

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

702
    }//end checkSpacingAfterParamType()
2✔
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)
3✔
716
    {
717
        // Check number of spaces after the var name.
718
        $spaces = ($maxVar - strlen($param['var']) + $spacing);
3✔
719
        if ($param['var_space'] !== $spaces) {
3✔
720
            $error = 'Expected %s spaces after parameter name; %s found';
3✔
721
            $data  = [
1✔
722
                $spaces,
3✔
723
                $param['var_space'],
3✔
724
            ];
2✔
725

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

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

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

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

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

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

761
    }//end checkSpacingAfterParamName()
2✔
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)
3✔
775
    {
776
        $tokens = $phpcsFile->getTokens();
3✔
777

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

787
                if ($trimmedContent === '{@inheritdoc}') {
3✔
788
                    return true;
3✔
789
                } else {
790
                    return false;
3✔
791
                }
792
            }
793
        }
1✔
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