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

FunctionCallSignatureSniff::processSingleLineCall()   F

Complexity

Conditions 25
Paths 1079

Size

Total Lines 110

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 110
rs 0
cc 25
nc 1079
nop 4

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
 * Ensures function calls are formatted correctly.
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\Functions;
11
12
use PHP_CodeSniffer\Sniffs\Sniff;
13
use PHP_CodeSniffer\Files\File;
14
use PHP_CodeSniffer\Util\Tokens;
15
16
class FunctionCallSignatureSniff implements Sniff
17
{
18
19
    /**
20
     * A list of tokenizers this sniff supports.
21
     *
22
     * @var array
23
     */
24
    public $supportedTokenizers = [
25
        'PHP',
26
        'JS',
27
    ];
28
29
    /**
30
     * The number of spaces code should be indented.
31
     *
32
     * @var integer
33
     */
34
    public $indent = 4;
35
36
    /**
37
     * If TRUE, multiple arguments can be defined per line in a multi-line call.
38
     *
39
     * @var boolean
40
     */
41
    public $allowMultipleArguments = true;
42
43
    /**
44
     * How many spaces should follow the opening bracket.
45
     *
46
     * @var integer
47
     */
48
    public $requiredSpacesAfterOpen = 0;
49
50
    /**
51
     * How many spaces should precede the closing bracket.
52
     *
53
     * @var integer
54
     */
55
    public $requiredSpacesBeforeClose = 0;
56
57
58
    /**
59
     * Returns an array of tokens this test wants to listen for.
60
     *
61
     * @return array
62
     */
63
    public function register()
64
    {
65
        $tokens = Tokens::$functionNameTokens;
66
67
        $tokens[] = T_VARIABLE;
68
        $tokens[] = T_CLOSE_CURLY_BRACKET;
69
        $tokens[] = T_CLOSE_PARENTHESIS;
70
71
        return $tokens;
72
73
    }//end register()
74
75
76
    /**
77
     * Processes this test, when one of its tokens is encountered.
78
     *
79
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
80
     * @param int                         $stackPtr  The position of the current token
81
     *                                               in the stack passed in $tokens.
82
     *
83
     * @return void
84
     */
85
    public function process(File $phpcsFile, $stackPtr)
86
    {
87
        $this->requiredSpacesAfterOpen   = (int) $this->requiredSpacesAfterOpen;
88
        $this->requiredSpacesBeforeClose = (int) $this->requiredSpacesBeforeClose;
89
        $tokens = $phpcsFile->getTokens();
90
91
        if ($tokens[$stackPtr]['code'] === T_CLOSE_CURLY_BRACKET
92
            && isset($tokens[$stackPtr]['scope_condition']) === true
93
        ) {
94
            // Not a function call.
95
            return;
96
        }
97
98
        // Find the next non-empty token.
99
        $openBracket = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
100
101
        if ($tokens[$openBracket]['code'] !== T_OPEN_PARENTHESIS) {
102
            // Not a function call.
103
            return;
104
        }
105
106
        if (isset($tokens[$openBracket]['parenthesis_closer']) === false) {
107
            // Not a function call.
108
            return;
109
        }
110
111
        // Find the previous non-empty token.
112
        $search   = Tokens::$emptyTokens;
113
        $search[] = T_BITWISE_AND;
114
        $previous = $phpcsFile->findPrevious($search, ($stackPtr - 1), null, true);
115
        if ($tokens[$previous]['code'] === T_FUNCTION) {
116
            // It's a function definition, not a function call.
117
            return;
118
        }
119
120
        $closeBracket = $tokens[$openBracket]['parenthesis_closer'];
121
122
        if (($stackPtr + 1) !== $openBracket) {
123
            // Checking this: $value = my_function[*](...).
124
            $error = 'Space before opening parenthesis of function call prohibited';
125
            $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceBeforeOpenBracket');
126
            if ($fix === true) {
127
                $phpcsFile->fixer->beginChangeset();
128
                for ($i = ($stackPtr + 1); $i < $openBracket; $i++) {
129
                    $phpcsFile->fixer->replaceToken($i, '');
130
                }
131
132
                // Modify the bracket as well to ensure a conflict if the bracket
133
                // has been changed in some way by another sniff.
134
                $phpcsFile->fixer->replaceToken($openBracket, '(');
135
                $phpcsFile->fixer->endChangeset();
136
            }
137
        }
138
139
        $next = $phpcsFile->findNext(T_WHITESPACE, ($closeBracket + 1), null, true);
140
        if ($tokens[$next]['code'] === T_SEMICOLON) {
141
            if (isset(Tokens::$emptyTokens[$tokens[($closeBracket + 1)]['code']]) === true) {
142
                $error = 'Space after closing parenthesis of function call prohibited';
143
                $fix   = $phpcsFile->addFixableError($error, $closeBracket, 'SpaceAfterCloseBracket');
144
                if ($fix === true) {
145
                    $phpcsFile->fixer->beginChangeset();
146
                    for ($i = ($closeBracket + 1); $i < $next; $i++) {
147
                        $phpcsFile->fixer->replaceToken($i, '');
148
                    }
149
150
                    // Modify the bracket as well to ensure a conflict if the bracket
151
                    // has been changed in some way by another sniff.
152
                    $phpcsFile->fixer->replaceToken($closeBracket, ')');
153
                    $phpcsFile->fixer->endChangeset();
154
                }
155
            }
156
        }
157
158
        // Check if this is a single line or multi-line function call.
159
        if ($this->isMultiLineCall($phpcsFile, $stackPtr, $openBracket, $tokens) === true) {
160
            $this->processMultiLineCall($phpcsFile, $stackPtr, $openBracket, $tokens);
161
        } else {
162
            $this->processSingleLineCall($phpcsFile, $stackPtr, $openBracket, $tokens);
163
        }
164
165
    }//end process()
166
167
168
    /**
169
     * Determine if this is a multi-line function call.
170
     *
171
     * @param \PHP_CodeSniffer\Files\File $phpcsFile   The file being scanned.
172
     * @param int                         $stackPtr    The position of the current token
173
     *                                                 in the stack passed in $tokens.
174
     * @param int                         $openBracket The position of the opening bracket
175
     *                                                 in the stack passed in $tokens.
176
     * @param array                       $tokens      The stack of tokens that make up
177
     *                                                 the file.
178
     *
179
     * @return void
180
     */
181
    public function isMultiLineCall(File $phpcsFile, $stackPtr, $openBracket, $tokens)
182
    {
183
        $closeBracket = $tokens[$openBracket]['parenthesis_closer'];
184
        if ($tokens[$openBracket]['line'] !== $tokens[$closeBracket]['line']) {
185
            return true;
186
        }
187
188
        return false;
189
190
    }//end isMultiLineCall()
191
192
193
    /**
194
     * Processes single-line calls.
195
     *
196
     * @param \PHP_CodeSniffer\Files\File $phpcsFile   The file being scanned.
197
     * @param int                         $stackPtr    The position of the current token
198
     *                                                 in the stack passed in $tokens.
199
     * @param int                         $openBracket The position of the opening bracket
200
     *                                                 in the stack passed in $tokens.
201
     * @param array                       $tokens      The stack of tokens that make up
202
     *                                                 the file.
203
     *
204
     * @return void
205
     */
206
    public function processSingleLineCall(File $phpcsFile, $stackPtr, $openBracket, $tokens)
207
    {
208
        // If the function call has no arguments or comments, enforce 0 spaces.
209
        $closer = $tokens[$openBracket]['parenthesis_closer'];
210
        if ($openBracket === ($closer - 1)) {
211
            return;
212
        }
213
214
        $next = $phpcsFile->findNext(T_WHITESPACE, ($openBracket + 1), $closer, true);
215
        if ($next === false) {
216
            $requiredSpacesAfterOpen   = 0;
217
            $requiredSpacesBeforeClose = 0;
218
        } else {
219
            $requiredSpacesAfterOpen   = $this->requiredSpacesAfterOpen;
220
            $requiredSpacesBeforeClose = $this->requiredSpacesBeforeClose;
221
        }
222
223
        if ($requiredSpacesAfterOpen === 0 && $tokens[($openBracket + 1)]['code'] === T_WHITESPACE) {
224
            // Checking this: $value = my_function([*]...).
225
            $error = 'Space after opening parenthesis of function call prohibited';
226
            $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceAfterOpenBracket');
227
            if ($fix === true) {
228
                $phpcsFile->fixer->replaceToken(($openBracket + 1), '');
229
            }
230
        } else if ($requiredSpacesAfterOpen > 0) {
231
            $spaceAfterOpen = 0;
232
            if ($tokens[($openBracket + 1)]['code'] === T_WHITESPACE) {
233
                $spaceAfterOpen = strlen($tokens[($openBracket + 1)]['content']);
234
            }
235
236
            if ($spaceAfterOpen !== $requiredSpacesAfterOpen) {
237
                $error = 'Expected %s spaces after opening bracket; %s found';
238
                $data  = [
239
                    $requiredSpacesAfterOpen,
240
                    $spaceAfterOpen,
241
                ];
242
                $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceAfterOpenBracket', $data);
243
                if ($fix === true) {
244
                    $padding = str_repeat(' ', $requiredSpacesAfterOpen);
245
                    if ($spaceAfterOpen === 0) {
246
                        $phpcsFile->fixer->addContent($openBracket, $padding);
247
                    } else {
248
                        $phpcsFile->fixer->replaceToken(($openBracket + 1), $padding);
249
                    }
250
                }
251
            }
252
        }//end if
253
254
        // Checking this: $value = my_function(...[*]).
255
        $spaceBeforeClose = 0;
256
        $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($closer - 1), $openBracket, true);
257
        if ($tokens[$prev]['code'] === T_END_HEREDOC || $tokens[$prev]['code'] === T_END_NOWDOC) {
258
            // Need a newline after these tokens, so ignore this rule.
259
            return;
260
        }
261
262
        if ($tokens[$prev]['line'] !== $tokens[$closer]['line']) {
263
            $spaceBeforeClose = 'newline';
264
        } else if ($tokens[($closer - 1)]['code'] === T_WHITESPACE) {
265
            $spaceBeforeClose = strlen($tokens[($closer - 1)]['content']);
266
        }
267
268
        if ($spaceBeforeClose !== $requiredSpacesBeforeClose) {
269
            $error = 'Expected %s spaces before closing bracket; %s found';
270
            $data  = [
271
                $requiredSpacesBeforeClose,
272
                $spaceBeforeClose,
273
            ];
274
            $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceBeforeCloseBracket', $data);
275
            if ($fix === true) {
276
                $padding = str_repeat(' ', $requiredSpacesBeforeClose);
277
278
                if ($spaceBeforeClose === 0) {
279
                    $phpcsFile->fixer->addContentBefore($closer, $padding);
280
                } else if ($spaceBeforeClose === 'newline') {
281
                    $phpcsFile->fixer->beginChangeset();
282
283
                    $closingContent = ')';
284
285
                    $next = $phpcsFile->findNext(T_WHITESPACE, ($closer + 1), null, true);
286
                    if ($tokens[$next]['code'] === T_SEMICOLON) {
287
                        $closingContent .= ';';
288
                        for ($i = ($closer + 1); $i <= $next; $i++) {
289
                            $phpcsFile->fixer->replaceToken($i, '');
290
                        }
291
                    }
292
293
                    // We want to jump over any whitespace or inline comment and
294
                    // move the closing parenthesis after any other token.
295
                    $prev = ($closer - 1);
296
                    while (isset(Tokens::$emptyTokens[$tokens[$prev]['code']]) === true) {
297
                        if (($tokens[$prev]['code'] === T_COMMENT)
298
                            && (strpos($tokens[$prev]['content'], '*/') !== false)
299
                        ) {
300
                            break;
301
                        }
302
303
                        $prev--;
304
                    }
305
306
                    $phpcsFile->fixer->addContent($prev, $padding.$closingContent);
307
308
                    $prevNonWhitespace = $phpcsFile->findPrevious(T_WHITESPACE, ($closer - 1), null, true);
309
                    for ($i = ($prevNonWhitespace + 1); $i <= $closer; $i++) {
310
                        $phpcsFile->fixer->replaceToken($i, '');
311
                    }
312
313
                    $phpcsFile->fixer->endChangeset();
314
                } else {
315
                    $phpcsFile->fixer->replaceToken(($closer - 1), $padding);
316
                }//end if
317
            }//end if
318
        }//end if
319
320
    }//end processSingleLineCall()
321
322
323
    /**
324
     * Processes multi-line calls.
325
     *
326
     * @param \PHP_CodeSniffer\Files\File $phpcsFile   The file being scanned.
327
     * @param int                         $stackPtr    The position of the current token
328
     *                                                 in the stack passed in $tokens.
329
     * @param int                         $openBracket The position of the opening bracket
330
     *                                                 in the stack passed in $tokens.
331
     * @param array                       $tokens      The stack of tokens that make up
332
     *                                                 the file.
333
     *
334
     * @return void
335
     */
336
    public function processMultiLineCall(File $phpcsFile, $stackPtr, $openBracket, $tokens)
337
    {
338
        // We need to work out how far indented the function
339
        // call itself is, so we can work out how far to
340
        // indent the arguments.
341
        $first = $phpcsFile->findFirstOnLine(T_WHITESPACE, $stackPtr, true);
342
        if ($tokens[$first]['code'] === T_CONSTANT_ENCAPSED_STRING
343
            && $tokens[($first - 1)]['code'] === T_CONSTANT_ENCAPSED_STRING
344
        ) {
345
            // We are in a multi-line string, so find the start and use
346
            // the indent from there.
347
            $prev  = $phpcsFile->findPrevious(T_CONSTANT_ENCAPSED_STRING, ($first - 2), null, true);
348
            $first = $phpcsFile->findFirstOnLine(Tokens::$emptyTokens, $prev, true);
349
        }
350
351
        $foundIndent = 0;
352
        if ($first !== false) {
353
            if ($tokens[$first]['code'] === T_INLINE_HTML) {
354
                $trimmed = ltrim($tokens[$first]['content']);
355
                if ($trimmed === '') {
356
                    $foundIndent = strlen($tokens[$first]['content']);
357
                } else {
358
                    $foundIndent = (strlen($tokens[$first]['content']) - strlen($trimmed));
359
                }
360
            } else {
361
                $foundIndent = ($tokens[$first]['column'] - 1);
362
            }
363
        }
364
365
        // Make sure the function indent is divisible by the indent size.
366
        // We round down here because this accounts for times when the
367
        // surrounding code is indented a little too far in, and not correctly
368
        // at a tab stop. Without this, the function will be indented a further
369
        // $indent spaces to the right.
370
        $functionIndent = (int) (floor($foundIndent / $this->indent) * $this->indent);
371
        if ($foundIndent !== $functionIndent) {
372
            $error = 'Opening statement of multi-line function call not indented correctly; expected %s spaces but found %s';
373
            $data  = [
374
                $functionIndent,
375
                $foundIndent,
376
            ];
377
378
            $fix = $phpcsFile->addFixableError($error, $first, 'OpeningIndent', $data);
379
            if ($fix === true) {
380
                $padding = str_repeat(' ', $functionIndent);
381
                if ($foundIndent === 0) {
382
                    $phpcsFile->fixer->addContentBefore($first, $padding);
383
                } else {
384
                    $phpcsFile->fixer->replaceToken(($first - 1), $padding);
385
                }
386
            }
387
        }
388
389
        $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($openBracket + 1), null, true);
390
        if ($tokens[$next]['line'] === $tokens[$openBracket]['line']) {
391
            $error = 'Opening parenthesis of a multi-line function call must be the last content on the line';
392
            $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'ContentAfterOpenBracket');
393
            if ($fix === true) {
394
                $phpcsFile->fixer->addContent(
395
                    $openBracket,
396
                    $phpcsFile->eolChar.str_repeat(' ', ($functionIndent + $this->indent))
397
                );
398
            }
399
        }
400
401
        $closeBracket = $tokens[$openBracket]['parenthesis_closer'];
402
        $prev         = $phpcsFile->findPrevious(T_WHITESPACE, ($closeBracket - 1), null, true);
403
        if ($tokens[$prev]['line'] === $tokens[$closeBracket]['line']) {
404
            $error = 'Closing parenthesis of a multi-line function call must be on a line by itself';
405
            $fix   = $phpcsFile->addFixableError($error, $closeBracket, 'CloseBracketLine');
406
            if ($fix === true) {
407
                $phpcsFile->fixer->addContentBefore(
408
                    $closeBracket,
409
                    $phpcsFile->eolChar.str_repeat(' ', ($functionIndent + $this->indent))
410
                );
411
            }
412
        }
413
414
        // Each line between the parenthesis should be indented n spaces.
415
        $lastLine = ($tokens[$openBracket]['line'] - 1);
416
        $argStart = null;
417
        $argEnd   = null;
418
419
        // Start processing at the first argument.
420
        $i = $phpcsFile->findNext(T_WHITESPACE, ($openBracket + 1), null, true);
421
        if ($tokens[($i - 1)]['code'] === T_WHITESPACE
422
            && $tokens[($i - 1)]['line'] === $tokens[$i]['line']
423
        ) {
424
            // Make sure we check the indent.
425
            $i--;
426
        }
427
428
        for ($i; $i < $closeBracket; $i++) {
429
            if ($i > $argStart && $i < $argEnd) {
430
                $inArg = true;
431
            } else {
432
                $inArg = false;
433
            }
434
435
            if ($tokens[$i]['line'] !== $lastLine) {
436
                $lastLine = $tokens[$i]['line'];
437
438
                // Ignore heredoc indentation.
439
                if (isset(Tokens::$heredocTokens[$tokens[$i]['code']]) === true) {
440
                    continue;
441
                }
442
443
                // Ignore multi-line string indentation.
444
                if (isset(Tokens::$stringTokens[$tokens[$i]['code']]) === true
445
                    && $tokens[$i]['code'] === $tokens[($i - 1)]['code']
446
                ) {
447
                    continue;
448
                }
449
450
                // Ignore inline HTML.
451
                if ($tokens[$i]['code'] === T_INLINE_HTML) {
452
                    continue;
453
                }
454
455
                if ($tokens[$i]['line'] !== $tokens[$openBracket]['line']) {
456
                    // We changed lines, so this should be a whitespace indent token, but first make
457
                    // sure it isn't a blank line because we don't need to check indent unless there
458
                    // is actually some code to indent.
459
                    if ($tokens[$i]['code'] === T_WHITESPACE) {
460
                        $nextCode = $phpcsFile->findNext(T_WHITESPACE, ($i + 1), ($closeBracket + 1), true);
461
                        if ($tokens[$nextCode]['line'] !== $lastLine) {
462
                            if ($inArg === false) {
463
                                $error = 'Empty lines are not allowed in multi-line function calls';
464
                                $fix   = $phpcsFile->addFixableError($error, $i, 'EmptyLine');
465
                                if ($fix === true) {
466
                                    $phpcsFile->fixer->replaceToken($i, '');
467
                                }
468
                            }
469
470
                            continue;
471
                        }
472
                    } else {
473
                        $nextCode = $i;
474
                    }
475
476
                    if ($tokens[$nextCode]['line'] === $tokens[$closeBracket]['line']) {
477
                        // Closing brace needs to be indented to the same level
478
                        // as the function call.
479
                        $inArg          = false;
480
                        $expectedIndent = $functionIndent;
481
                    } else {
482
                        $expectedIndent = ($functionIndent + $this->indent);
483
                    }
484
485
                    if ($tokens[$i]['code'] !== T_WHITESPACE
486
                        && $tokens[$i]['code'] !== T_DOC_COMMENT_WHITESPACE
487
                    ) {
488
                        // Just check if it is a multi-line block comment. If so, we can
489
                        // calculate the indent from the whitespace before the content.
490
                        if ($tokens[$i]['code'] === T_COMMENT
491
                            && $tokens[($i - 1)]['code'] === T_COMMENT
492
                        ) {
493
                            $trimmedLength = strlen(ltrim($tokens[$i]['content']));
494
                            if ($trimmedLength === 0) {
495
                                // This is a blank comment line, so indenting it is
496
                                // pointless.
497
                                continue;
498
                            }
499
500
                            $foundIndent = (strlen($tokens[$i]['content']) - $trimmedLength);
501
                        } else {
502
                            $foundIndent = 0;
503
                        }
504
                    } else {
505
                        $foundIndent = strlen($tokens[$i]['content']);
506
                    }
507
508
                    if ($foundIndent < $expectedIndent
509
                        || ($inArg === false
510
                        && $expectedIndent !== $foundIndent)
511
                    ) {
512
                        $error = 'Multi-line function call not indented correctly; expected %s spaces but found %s';
513
                        $data  = [
514
                            $expectedIndent,
515
                            $foundIndent,
516
                        ];
517
518
                        $fix = $phpcsFile->addFixableError($error, $i, 'Indent', $data);
519
                        if ($fix === true) {
520
                            $padding = str_repeat(' ', $expectedIndent);
521
                            if ($foundIndent === 0) {
522
                                $phpcsFile->fixer->addContentBefore($i, $padding);
523
                            } else {
524
                                if ($tokens[$i]['code'] === T_COMMENT) {
525
                                    $comment = $padding.ltrim($tokens[$i]['content']);
526
                                    $phpcsFile->fixer->replaceToken($i, $comment);
527
                                } else {
528
                                    $phpcsFile->fixer->replaceToken($i, $padding);
529
                                }
530
                            }
531
                        }
532
                    }//end if
533
                } else {
534
                    $nextCode = $i;
535
                }//end if
536
537
                if ($inArg === false) {
538
                    $argStart = $nextCode;
539
                    $argEnd   = $phpcsFile->findEndOfStatement($nextCode);
540
                }
541
            }//end if
542
543
            // If we are within an argument we should be ignoring commas
544
            // as these are not signaling the end of an argument.
545
            if ($inArg === false && $tokens[$i]['code'] === T_COMMA) {
546
                $next = $phpcsFile->findNext([T_WHITESPACE, T_COMMENT], ($i + 1), $closeBracket, true);
547
                if ($next === false) {
548
                    continue;
549
                }
550
551
                if ($this->allowMultipleArguments === false) {
552
                    // Comma has to be the last token on the line.
553
                    if ($tokens[$i]['line'] === $tokens[$next]['line']) {
554
                        $error = 'Only one argument is allowed per line in a multi-line function call';
555
                        $fix   = $phpcsFile->addFixableError($error, $next, 'MultipleArguments');
556
                        if ($fix === true) {
557
                            $phpcsFile->fixer->beginChangeset();
558
                            for ($x = ($next - 1); $x > $i; $x--) {
559
                                if ($tokens[$x]['code'] !== T_WHITESPACE) {
560
                                    break;
561
                                }
562
563
                                $phpcsFile->fixer->replaceToken($x, '');
564
                            }
565
566
                            $phpcsFile->fixer->addContentBefore(
567
                                $next,
568
                                $phpcsFile->eolChar.str_repeat(' ', ($functionIndent + $this->indent))
569
                            );
570
                            $phpcsFile->fixer->endChangeset();
571
                        }
572
                    }
573
                }//end if
574
575
                $argStart = $next;
576
                $argEnd   = $phpcsFile->findEndOfStatement($next);
577
            }//end if
578
        }//end for
579
580
    }//end processMultiLineCall()
581
582
583
}//end class
584