Completed
Branch development (b1b115)
by Johannes
10:28
created

FunctionCommentSniff::processParams()   F

Complexity

Conditions 35
Paths > 20000

Size

Total Lines 260

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 260
rs 0
cc 35
nc 121536
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Parses and verifies the doc comments for functions.
4
 *
5
 * @author    Greg Sherwood <[email protected]>
6
 * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
7
 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
8
 */
9
10
namespace PHP_CodeSniffer\Standards\PEAR\Sniffs\Commenting;
11
12
use PHP_CodeSniffer\Sniffs\Sniff;
13
use PHP_CodeSniffer\Files\File;
14
use PHP_CodeSniffer\Util\Tokens;
15
16
class FunctionCommentSniff implements Sniff
17
{
18
19
20
    /**
21
     * Returns an array of tokens this test wants to listen for.
22
     *
23
     * @return array
24
     */
25
    public function register()
26
    {
27
        return [T_FUNCTION];
28
29
    }//end register()
30
31
32
    /**
33
     * Processes this test, when one of its tokens is encountered.
34
     *
35
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
36
     * @param int                         $stackPtr  The position of the current token
37
     *                                               in the stack passed in $tokens.
38
     *
39
     * @return void
40
     */
41
    public function process(File $phpcsFile, $stackPtr)
42
    {
43
        $tokens = $phpcsFile->getTokens();
44
        $find   = Tokens::$methodPrefixes;
45
        $find[] = T_WHITESPACE;
46
47
        $commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1), null, true);
48
        if ($tokens[$commentEnd]['code'] === T_COMMENT) {
49
            // Inline comments might just be closing comments for
50
            // control structures or functions instead of function comments
51
            // using the wrong comment type. If there is other code on the line,
52
            // assume they relate to that code.
53
            $prev = $phpcsFile->findPrevious($find, ($commentEnd - 1), null, true);
54
            if ($prev !== false && $tokens[$prev]['line'] === $tokens[$commentEnd]['line']) {
55
                $commentEnd = $prev;
56
            }
57
        }
58
59
        if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG
60
            && $tokens[$commentEnd]['code'] !== T_COMMENT
61
        ) {
62
            $phpcsFile->addError('Missing function doc comment', $stackPtr, 'Missing');
63
            $phpcsFile->recordMetric($stackPtr, 'Function has doc comment', 'no');
64
            return;
65
        } else {
66
            $phpcsFile->recordMetric($stackPtr, 'Function has doc comment', 'yes');
67
        }
68
69
        if ($tokens[$commentEnd]['code'] === T_COMMENT) {
70
            $phpcsFile->addError('You must use "/**" style comments for a function comment', $stackPtr, 'WrongStyle');
71
            return;
72
        }
73
74
        if ($tokens[$commentEnd]['line'] !== ($tokens[$stackPtr]['line'] - 1)) {
75
            $error = 'There must be no blank lines after the function comment';
76
            $phpcsFile->addError($error, $commentEnd, 'SpacingAfter');
77
        }
78
79
        $commentStart = $tokens[$commentEnd]['comment_opener'];
80
        foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
81
            if ($tokens[$tag]['content'] === '@see') {
82
                // Make sure the tag isn't empty.
83
                $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd);
84
                if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) {
85
                    $error = 'Content missing for @see tag in function comment';
86
                    $phpcsFile->addError($error, $tag, 'EmptySees');
87
                }
88
            }
89
        }
90
91
        $this->processReturn($phpcsFile, $stackPtr, $commentStart);
92
        $this->processThrows($phpcsFile, $stackPtr, $commentStart);
93
        $this->processParams($phpcsFile, $stackPtr, $commentStart);
94
95
    }//end process()
96
97
98
    /**
99
     * Process the return comment of this function comment.
100
     *
101
     * @param \PHP_CodeSniffer\Files\File $phpcsFile    The file being scanned.
102
     * @param int                         $stackPtr     The position of the current token
103
     *                                                  in the stack passed in $tokens.
104
     * @param int                         $commentStart The position in the stack where the comment started.
105
     *
106
     * @return void
107
     */
108
    protected function processReturn(File $phpcsFile, $stackPtr, $commentStart)
109
    {
110
        $tokens = $phpcsFile->getTokens();
111
112
        // Skip constructor and destructor.
113
        $methodName      = $phpcsFile->getDeclarationName($stackPtr);
114
        $isSpecialMethod = ($methodName === '__construct' || $methodName === '__destruct');
115
116
        $return = null;
117
        foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
118
            if ($tokens[$tag]['content'] === '@return') {
119
                if ($return !== null) {
120
                    $error = 'Only 1 @return tag is allowed in a function comment';
121
                    $phpcsFile->addError($error, $tag, 'DuplicateReturn');
122
                    return;
123
                }
124
125
                $return = $tag;
126
            }
127
        }
128
129
        if ($isSpecialMethod === true) {
130
            return;
131
        }
132
133
        if ($return !== null) {
134
            $content = $tokens[($return + 2)]['content'];
135
            if (empty($content) === true || $tokens[($return + 2)]['code'] !== T_DOC_COMMENT_STRING) {
136
                $error = 'Return type missing for @return tag in function comment';
137
                $phpcsFile->addError($error, $return, 'MissingReturnType');
138
            }
139
        } else {
140
            $error = 'Missing @return tag in function comment';
141
            $phpcsFile->addError($error, $tokens[$commentStart]['comment_closer'], 'MissingReturn');
142
        }//end if
143
144
    }//end processReturn()
145
146
147
    /**
148
     * Process any throw tags that this function comment has.
149
     *
150
     * @param \PHP_CodeSniffer\Files\File $phpcsFile    The file being scanned.
151
     * @param int                         $stackPtr     The position of the current token
152
     *                                                  in the stack passed in $tokens.
153
     * @param int                         $commentStart The position in the stack where the comment started.
154
     *
155
     * @return void
156
     */
157
    protected function processThrows(File $phpcsFile, $stackPtr, $commentStart)
158
    {
159
        $tokens = $phpcsFile->getTokens();
160
161
        foreach ($tokens[$commentStart]['comment_tags'] as $tag) {
162
            if ($tokens[$tag]['content'] !== '@throws') {
163
                continue;
164
            }
165
166
            $exception = null;
167
            if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) {
168
                $matches = [];
169
                preg_match('/([^\s]+)(?:\s+(.*))?/', $tokens[($tag + 2)]['content'], $matches);
170
                $exception = $matches[1];
171
            }
172
173
            if ($exception === null) {
174
                $error = 'Exception type missing for @throws tag in function comment';
175
                $phpcsFile->addError($error, $tag, 'InvalidThrows');
176
            }
177
        }//end foreach
178
179
    }//end processThrows()
180
181
182
    /**
183
     * Process the function parameter comments.
184
     *
185
     * @param \PHP_CodeSniffer\Files\File $phpcsFile    The file being scanned.
186
     * @param int                         $stackPtr     The position of the current token
187
     *                                                  in the stack passed in $tokens.
188
     * @param int                         $commentStart The position in the stack where the comment started.
189
     *
190
     * @return void
191
     */
192
    protected function processParams(File $phpcsFile, $stackPtr, $commentStart)
193
    {
194
        $tokens = $phpcsFile->getTokens();
195
196
        $params  = [];
197
        $maxType = 0;
198
        $maxVar  = 0;
199
        foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) {
200
            if ($tokens[$tag]['content'] !== '@param') {
201
                continue;
202
            }
203
204
            $type          = '';
205
            $typeSpace     = 0;
206
            $var           = '';
207
            $varSpace      = 0;
208
            $comment       = '';
209
            $commentEnd    = 0;
210
            $commentTokens = [];
211
212
            if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) {
213
                $matches = [];
214
                preg_match('/([^$&.]+)(?:((?:\.\.\.)?(?:\$|&)[^\s]+)(?:(\s+)(.*))?)?/', $tokens[($tag + 2)]['content'], $matches);
215
216
                if (empty($matches) === false) {
217
                    $typeLen   = strlen($matches[1]);
218
                    $type      = trim($matches[1]);
219
                    $typeSpace = ($typeLen - strlen($type));
220
                    $typeLen   = strlen($type);
221
                    if ($typeLen > $maxType) {
222
                        $maxType = $typeLen;
223
                    }
224
                }
225
226
                if (isset($matches[2]) === true) {
227
                    $var    = $matches[2];
228
                    $varLen = strlen($var);
229
                    if ($varLen > $maxVar) {
230
                        $maxVar = $varLen;
231
                    }
232
233
                    if (isset($matches[4]) === true) {
234
                        $varSpace = strlen($matches[3]);
235
                        $comment  = $matches[4];
236
237
                        // Any strings until the next tag belong to this comment.
238
                        if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) {
239
                            $end = $tokens[$commentStart]['comment_tags'][($pos + 1)];
240
                        } else {
241
                            $end = $tokens[$commentStart]['comment_closer'];
242
                        }
243
244
                        for ($i = ($tag + 3); $i < $end; $i++) {
245
                            if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) {
246
                                $comment        .= ' '.$tokens[$i]['content'];
247
                                $commentEnd      = $i;
248
                                $commentTokens[] = $i;
249
                            }
250
                        }
251
                    } else {
252
                        $error = 'Missing parameter comment';
253
                        $phpcsFile->addError($error, $tag, 'MissingParamComment');
254
                    }//end if
255
                } else {
256
                    $error = 'Missing parameter name';
257
                    $phpcsFile->addError($error, $tag, 'MissingParamName');
258
                }//end if
259
            } else {
260
                $error = 'Missing parameter type';
261
                $phpcsFile->addError($error, $tag, 'MissingParamType');
262
            }//end if
263
264
            $params[] = [
265
                'tag'            => $tag,
266
                'type'           => $type,
267
                'var'            => $var,
268
                'comment'        => $comment,
269
                'comment_end'    => $commentEnd,
270
                'comment_tokens' => $commentTokens,
271
                'type_space'     => $typeSpace,
272
                'var_space'      => $varSpace,
273
            ];
274
        }//end foreach
275
276
        $realParams  = $phpcsFile->getMethodParameters($stackPtr);
277
        $foundParams = [];
278
279
        // We want to use ... for all variable length arguments, so add
280
        // this prefix to the variable name so comparisons are easier.
281
        foreach ($realParams as $pos => $param) {
282
            if ($param['variable_length'] === true) {
283
                $realParams[$pos]['name'] = '...'.$realParams[$pos]['name'];
284
            }
285
        }
286
287
        foreach ($params as $pos => $param) {
288
            if ($param['var'] === '') {
289
                continue;
290
            }
291
292
            $foundParams[] = $param['var'];
293
294
            // Check number of spaces after the type.
295
            $spaces = ($maxType - strlen($param['type']) + 1);
296
            if ($param['type_space'] !== $spaces) {
297
                $error = 'Expected %s spaces after parameter type; %s found';
298
                $data  = [
299
                    $spaces,
300
                    $param['type_space'],
301
                ];
302
303
                $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamType', $data);
304
                if ($fix === true) {
305
                    $commentToken = ($param['tag'] + 2);
306
307
                    $content  = $param['type'];
308
                    $content .= str_repeat(' ', $spaces);
309
                    $content .= $param['var'];
310
                    $content .= str_repeat(' ', $param['var_space']);
311
312
                    $wrapLength = ($tokens[$commentToken]['length'] - $param['type_space'] - $param['var_space'] - strlen($param['type']) - strlen($param['var']));
313
314
                    $star        = $phpcsFile->findPrevious(T_DOC_COMMENT_STAR, $param['tag']);
315
                    $spaceLength = (strlen($content) + $tokens[($commentToken - 1)]['length'] + $tokens[($commentToken - 2)]['length']);
316
317
                    $padding  = str_repeat(' ', ($tokens[$star]['column'] - 1));
318
                    $padding .= '* ';
319
                    $padding .= str_repeat(' ', $spaceLength);
320
321
                    $content .= wordwrap(
322
                        $param['comment'],
323
                        $wrapLength,
324
                        $phpcsFile->eolChar.$padding
325
                    );
326
327
                    $phpcsFile->fixer->replaceToken($commentToken, $content);
328
                    for ($i = ($commentToken + 1); $i <= $param['comment_end']; $i++) {
329
                        $phpcsFile->fixer->replaceToken($i, '');
330
                    }
331
                }//end if
332
            }//end if
333
334
            // Make sure the param name is correct.
335
            if (isset($realParams[$pos]) === true) {
336
                $realName = $realParams[$pos]['name'];
337
                if ($realName !== $param['var']) {
338
                    $code = 'ParamNameNoMatch';
339
                    $data = [
340
                        $param['var'],
341
                        $realName,
342
                    ];
343
344
                    $error = 'Doc comment for parameter %s does not match ';
345
                    if (strtolower($param['var']) === strtolower($realName)) {
346
                        $error .= 'case of ';
347
                        $code   = 'ParamNameNoCaseMatch';
348
                    }
349
350
                    $error .= 'actual variable name %s';
351
352
                    $phpcsFile->addError($error, $param['tag'], $code, $data);
353
                }
354
            } else if (substr($param['var'], -4) !== ',...') {
355
                // We must have an extra parameter comment.
356
                $error = 'Superfluous parameter comment';
357
                $phpcsFile->addError($error, $param['tag'], 'ExtraParamComment');
358
            }//end if
359
360
            if ($param['comment'] === '') {
361
                continue;
362
            }
363
364
            // Check number of spaces after the param name.
365
            $spaces = ($maxVar - strlen($param['var']) + 1);
366
            if ($param['var_space'] !== $spaces) {
367
                $error = 'Expected %s spaces after parameter name; %s found';
368
                $data  = [
369
                    $spaces,
370
                    $param['var_space'],
371
                ];
372
373
                $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamName', $data);
374
                if ($fix === true) {
375
                    $commentToken = ($param['tag'] + 2);
376
377
                    $content  = $param['type'];
378
                    $content .= str_repeat(' ', $param['type_space']);
379
                    $content .= $param['var'];
380
                    $content .= str_repeat(' ', $spaces);
381
382
                    $wrapLength = ($tokens[$commentToken]['length'] - $param['type_space'] - $param['var_space'] - strlen($param['type']) - strlen($param['var']));
383
384
                    $star        = $phpcsFile->findPrevious(T_DOC_COMMENT_STAR, $param['tag']);
385
                    $spaceLength = (strlen($content) + $tokens[($commentToken - 1)]['length'] + $tokens[($commentToken - 2)]['length']);
386
387
                    $padding  = str_repeat(' ', ($tokens[$star]['column'] - 1));
388
                    $padding .= '* ';
389
                    $padding .= str_repeat(' ', $spaceLength);
390
391
                    $content .= wordwrap(
392
                        $param['comment'],
393
                        $wrapLength,
394
                        $phpcsFile->eolChar.$padding
395
                    );
396
397
                    $phpcsFile->fixer->replaceToken($commentToken, $content);
398
                    for ($i = ($commentToken + 1); $i <= $param['comment_end']; $i++) {
399
                        $phpcsFile->fixer->replaceToken($i, '');
400
                    }
401
                }//end if
402
            }//end if
403
404
            // Check the alignment of multi-line param comments.
405
            if ($param['tag'] !== $param['comment_end']) {
406
                $wrapLength = ($tokens[($param['tag'] + 2)]['length'] - $param['type_space'] - $param['var_space'] - strlen($param['type']) - strlen($param['var']));
407
408
                $startColumn = ($tokens[($param['tag'] + 2)]['column'] + $tokens[($param['tag'] + 2)]['length'] - $wrapLength);
409
410
                $star     = $phpcsFile->findPrevious(T_DOC_COMMENT_STAR, $param['tag']);
411
                $expected = ($startColumn - $tokens[$star]['column'] - 1);
412
413
                foreach ($param['comment_tokens'] as $commentToken) {
414
                    if ($tokens[$commentToken]['column'] === $startColumn) {
415
                        continue;
416
                    }
417
418
                    $found = 0;
419
                    if ($tokens[($commentToken - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) {
420
                        $found = $tokens[($commentToken - 1)]['length'];
421
                    }
422
423
                    $error = 'Parameter comment not aligned correctly; expected %s spaces but found %s';
424
                    $data  = [
425
                        $expected,
426
                        $found,
427
                    ];
428
                    $fix   = $phpcsFile->addFixableError($error, $commentToken, 'ParamCommentAlignment', $data);
429
                    if ($fix === true) {
430
                        $padding = str_repeat(' ', $expected);
431
                        if ($tokens[($commentToken - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) {
432
                            $phpcsFile->fixer->replaceToken(($commentToken - 1), $padding);
433
                        } else {
434
                            $phpcsFile->fixer->addContentBefore($commentToken, $padding);
435
                        }
436
                    }
437
                }//end foreach
438
            }//end if
439
        }//end foreach
440
441
        $realNames = [];
442
        foreach ($realParams as $realParam) {
443
            $realNames[] = $realParam['name'];
444
        }
445
446
        // Report missing comments.
447
        $diff = array_diff($realNames, $foundParams);
448
        foreach ($diff as $neededParam) {
449
            $error = 'Doc comment for parameter "%s" missing';
450
            $data  = [$neededParam];
451
            $phpcsFile->addError($error, $commentStart, 'MissingParamTag', $data);
452
        }
453
454
    }//end processParams()
455
456
457
}//end class
458