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