1<?php
2/**
3 * Squiz_Sniffs_Functions_FunctionDeclarationArgumentSpacingSniff.
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 * Squiz_Sniffs_Functions_FunctionDeclarationArgumentSpacingSniff.
18 *
19 * Checks that arguments in function declarations are spaced correctly.
20 *
21 * @category  PHP
22 * @package   PHP_CodeSniffer
23 * @author    Greg Sherwood <gsherwood@squiz.net>
24 * @author    Marc McIntyre <mmcintyre@squiz.net>
25 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
26 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
27 * @version   Release: @package_version@
28 * @link      http://pear.php.net/package/PHP_CodeSniffer
29 */
30class Squiz_Sniffs_Functions_FunctionDeclarationArgumentSpacingSniff implements PHP_CodeSniffer_Sniff
31{
32
33    /**
34     * How many spaces should surround the equals signs.
35     *
36     * @var int
37     */
38    public $equalsSpacing = 0;
39
40    /**
41     * How many spaces should follow the opening bracket.
42     *
43     * @var int
44     */
45    public $requiredSpacesAfterOpen = 0;
46
47    /**
48     * How many spaces should precede the closing bracket.
49     *
50     * @var int
51     */
52    public $requiredSpacesBeforeClose = 0;
53
54
55    /**
56     * Returns an array of tokens this test wants to listen for.
57     *
58     * @return array
59     */
60    public function register()
61    {
62        return array(
63                T_FUNCTION,
64                T_CLOSURE,
65               );
66
67    }//end register()
68
69
70    /**
71     * Processes this test, when one of its tokens is encountered.
72     *
73     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
74     * @param int                  $stackPtr  The position of the current token in the
75     *                                        stack passed in $tokens.
76     *
77     * @return void
78     */
79    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
80    {
81        $tokens = $phpcsFile->getTokens();
82
83        if (isset($tokens[$stackPtr]['parenthesis_opener']) === false
84            || isset($tokens[$stackPtr]['parenthesis_closer']) === false
85            || $tokens[$stackPtr]['parenthesis_opener'] === null
86            || $tokens[$stackPtr]['parenthesis_closer'] === null
87        ) {
88            return;
89        }
90
91        $this->equalsSpacing           = (int) $this->equalsSpacing;
92        $this->requiredSpacesAfterOpen = (int) $this->requiredSpacesAfterOpen;
93        $this->requiredSpacesBeforeClose = (int) $this->requiredSpacesBeforeClose;
94
95        $openBracket = $tokens[$stackPtr]['parenthesis_opener'];
96        $this->processBracket($phpcsFile, $openBracket);
97
98        if ($tokens[$stackPtr]['code'] === T_CLOSURE) {
99            $use = $phpcsFile->findNext(T_USE, ($tokens[$openBracket]['parenthesis_closer'] + 1), $tokens[$stackPtr]['scope_opener']);
100            if ($use !== false) {
101                $openBracket = $phpcsFile->findNext(T_OPEN_PARENTHESIS, ($use + 1), null);
102                $this->processBracket($phpcsFile, $openBracket);
103            }
104        }
105
106    }//end process()
107
108
109    /**
110     * Processes the contents of a single set of brackets.
111     *
112     * @param PHP_CodeSniffer_File $phpcsFile   The file being scanned.
113     * @param int                  $openBracket The position of the open bracket
114     *                                          in the stack passed in $tokens.
115     *
116     * @return void
117     */
118    public function processBracket(PHP_CodeSniffer_File $phpcsFile, $openBracket)
119    {
120        $tokens       = $phpcsFile->getTokens();
121        $closeBracket = $tokens[$openBracket]['parenthesis_closer'];
122        $multiLine    = ($tokens[$openBracket]['line'] !== $tokens[$closeBracket]['line']);
123
124        $nextParam = $openBracket;
125        $params    = array();
126        while (($nextParam = $phpcsFile->findNext(T_VARIABLE, ($nextParam + 1), $closeBracket)) !== false) {
127            $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($nextParam + 1), ($closeBracket + 1), true);
128            if ($nextToken === false) {
129                break;
130            }
131
132            $nextCode = $tokens[$nextToken]['code'];
133
134            if ($nextCode === T_EQUAL) {
135                // Check parameter default spacing.
136                $spacesBefore = 0;
137                if (($nextToken - $nextParam) > 1) {
138                    $spacesBefore = strlen($tokens[($nextParam + 1)]['content']);
139                }
140
141                if ($spacesBefore !== $this->equalsSpacing) {
142                    $error = 'Incorrect spacing between argument "%s" and equals sign; expected '.$this->equalsSpacing.' but found %s';
143                    $data  = array(
144                              $tokens[$nextParam]['content'],
145                              $spacesBefore,
146                             );
147
148                    $fix = $phpcsFile->addFixableError($error, $nextToken, 'SpaceBeforeEquals', $data);
149                    if ($fix === true) {
150                        $padding = str_repeat(' ', $this->equalsSpacing);
151                        if ($spacesBefore === 0) {
152                            $phpcsFile->fixer->addContentBefore($nextToken, $padding);
153                        } else {
154                            $phpcsFile->fixer->replaceToken(($nextToken - 1), $padding);
155                        }
156                    }
157                }//end if
158
159                $spacesAfter = 0;
160                if ($tokens[($nextToken + 1)]['code'] === T_WHITESPACE) {
161                    $spacesAfter = strlen($tokens[($nextToken + 1)]['content']);
162                }
163
164                if ($spacesAfter !== $this->equalsSpacing) {
165                    $error = 'Incorrect spacing between default value and equals sign for argument "%s"; expected '.$this->equalsSpacing.' but found %s';
166                    $data  = array(
167                              $tokens[$nextParam]['content'],
168                              $spacesAfter,
169                             );
170
171                    $fix = $phpcsFile->addFixableError($error, $nextToken, 'SpaceAfterDefault', $data);
172                    if ($fix === true) {
173                        $padding = str_repeat(' ', $this->equalsSpacing);
174                        if ($spacesAfter === 0) {
175                            $phpcsFile->fixer->addContent($nextToken, $padding);
176                        } else {
177                            $phpcsFile->fixer->replaceToken(($nextToken + 1), $padding);
178                        }
179                    }
180                }//end if
181            }//end if
182
183            // Find and check the comma (if there is one).
184            $nextComma = $phpcsFile->findNext(T_COMMA, ($nextParam + 1), $closeBracket);
185            if ($nextComma !== false) {
186                // Comma found.
187                if ($tokens[($nextComma - 1)]['code'] === T_WHITESPACE) {
188                    $error = 'Expected 0 spaces between argument "%s" and comma; %s found';
189                    $data  = array(
190                              $tokens[$nextParam]['content'],
191                              strlen($tokens[($nextComma - 1)]['content']),
192                             );
193
194                    $fix = $phpcsFile->addFixableError($error, $nextToken, 'SpaceBeforeComma', $data);
195                    if ($fix === true) {
196                        $phpcsFile->fixer->replaceToken(($nextComma - 1), '');
197                    }
198                }
199            }
200
201            $checkToken = ($nextParam - 1);
202            $prev       = $phpcsFile->findPrevious(T_WHITESPACE, $checkToken, null, true);
203            if ($tokens[$prev]['code'] === T_ELLIPSIS) {
204                $checkToken = ($prev - 1);
205            }
206
207            // Take references into account when expecting the
208            // location of whitespace.
209            if ($phpcsFile->isReference($checkToken) === true) {
210                $whitespace = ($checkToken - 1);
211            } else {
212                $whitespace = $checkToken;
213            }
214
215            if (empty($params) === false) {
216                // This is not the first argument in the function declaration.
217                $arg = $tokens[$nextParam]['content'];
218
219                // Before we throw an error, make sure there is no type hint.
220                $comma     = $phpcsFile->findPrevious(T_COMMA, ($nextParam - 1));
221                $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($comma + 1), null, true);
222                if ($phpcsFile->isReference($nextToken) === true) {
223                    $nextToken++;
224                }
225
226                $gap = 0;
227                if ($tokens[$whitespace]['code'] === T_WHITESPACE) {
228                    $gap = strlen($tokens[$whitespace]['content']);
229                }
230
231                if ($nextToken !== $nextParam) {
232                    // There was a type hint, so check the spacing between
233                    // the hint and the variable as well.
234                    $hint = $tokens[$nextToken]['content'];
235
236                    if ($gap !== 1) {
237                        $error = 'Expected 1 space between type hint and argument "%s"; %s found';
238                        $data  = array(
239                                  $arg,
240                                  $gap,
241                                 );
242                        $fix   = $phpcsFile->addFixableError($error, $nextToken, 'SpacingAfterHint', $data);
243                        if ($fix === true) {
244                            if ($gap === 0) {
245                                $phpcsFile->fixer->addContent($whitespace, ' ');
246                            } else {
247                                $phpcsFile->fixer->replaceToken($whitespace, ' ');
248                            }
249                        }
250                    }
251
252                    if ($multiLine === false) {
253                        if ($tokens[($comma + 1)]['code'] !== T_WHITESPACE) {
254                            $error = 'Expected 1 space between comma and type hint "%s"; 0 found';
255                            $data  = array($hint);
256                            $fix   = $phpcsFile->addFixableError($error, $nextToken, 'NoSpaceBeforeHint', $data);
257                            if ($fix === true) {
258                                $phpcsFile->fixer->addContent($comma, ' ');
259                            }
260                        } else {
261                            $gap = strlen($tokens[($comma + 1)]['content']);
262                            if ($gap !== 1) {
263                                $error = 'Expected 1 space between comma and type hint "%s"; %s found';
264                                $data  = array(
265                                          $hint,
266                                          $gap,
267                                         );
268                                $fix   = $phpcsFile->addFixableError($error, $nextToken, 'SpacingBeforeHint', $data);
269                                if ($fix === true) {
270                                    $phpcsFile->fixer->replaceToken(($comma + 1), ' ');
271                                }
272                            }
273                        }//end if
274                    }//end if
275                } else {
276                    // No type hint.
277                    if ($gap === 0) {
278                        $error = 'Expected 1 space between comma and argument "%s"; 0 found';
279                        $data  = array($arg);
280                        $fix   = $phpcsFile->addFixableError($error, $nextToken, 'NoSpaceBeforeArg', $data);
281                        if ($fix === true) {
282                            $phpcsFile->fixer->addContent($whitespace, ' ');
283                        }
284                    } else if ($gap !== 1) {
285                        // Just make sure this is not actually an indent.
286                        if ($tokens[$whitespace]['line'] === $tokens[($whitespace - 1)]['line']) {
287                            $error = 'Expected 1 space between comma and argument "%s"; %s found';
288                            $data  = array(
289                                      $arg,
290                                      $gap,
291                                     );
292
293                            $fix = $phpcsFile->addFixableError($error, $nextToken, 'SpacingBeforeArg', $data);
294                            if ($fix === true) {
295                                $phpcsFile->fixer->replaceToken($whitespace, ' ');
296                            }
297                        }
298                    }//end if
299                }//end if
300            } else {
301                $gap = 0;
302                if ($tokens[$whitespace]['code'] === T_WHITESPACE) {
303                    $gap = strlen($tokens[$whitespace]['content']);
304                }
305
306                $arg = $tokens[$nextParam]['content'];
307
308                // Before we throw an error, make sure there is no type hint.
309                $bracket   = $phpcsFile->findPrevious(T_OPEN_PARENTHESIS, ($nextParam - 1));
310                $nextToken = $phpcsFile->findNext(T_WHITESPACE, ($bracket + 1), null, true);
311                if ($phpcsFile->isReference($nextToken) === true) {
312                    $nextToken++;
313                }
314
315                if ($tokens[$nextToken]['code'] !== T_ELLIPSIS && $nextToken !== $nextParam) {
316                    // There was a type hint, so check the spacing between
317                    // the hint and the variable as well.
318                    $hint = $tokens[$nextToken]['content'];
319
320                    if ($gap !== 1) {
321                        $error = 'Expected 1 space between type hint and argument "%s"; %s found';
322                        $data  = array(
323                                  $arg,
324                                  $gap,
325                                 );
326                        $fix   = $phpcsFile->addFixableError($error, $nextToken, 'SpacingAfterHint', $data);
327                        if ($fix === true) {
328                            if ($gap === 0) {
329                                $phpcsFile->fixer->addContent($nextToken, ' ');
330                            } else {
331                                $phpcsFile->fixer->replaceToken(($nextToken + 1), ' ');
332                            }
333                        }
334                    }
335
336                    $spaceAfterOpen = 0;
337                    if ($tokens[($bracket + 1)]['code'] === T_WHITESPACE) {
338                        $spaceAfterOpen = strlen($tokens[($bracket + 1)]['content']);
339                    }
340
341                    if ($multiLine === false && $spaceAfterOpen !== $this->requiredSpacesAfterOpen) {
342                        $error = 'Expected %s spaces between opening bracket and type hint "%s"; %s found';
343                        $data  = array(
344                                  $this->requiredSpacesAfterOpen,
345                                  $hint,
346                                  $spaceAfterOpen,
347                                 );
348                        $fix   = $phpcsFile->addFixableError($error, $nextToken, 'SpacingAfterOpenHint', $data);
349                        if ($fix === true) {
350                            $padding = str_repeat(' ', $this->requiredSpacesAfterOpen);
351                            if ($spaceAfterOpen === 0) {
352                                $phpcsFile->fixer->addContent($openBracket, $padding);
353                            } else {
354                                $phpcsFile->fixer->replaceToken(($openBracket + 1), $padding);
355                            }
356                        }
357                    }
358                } else if ($multiLine === false && $gap !== $this->requiredSpacesAfterOpen) {
359                    $error = 'Expected %s spaces between opening bracket and argument "%s"; %s found';
360                    $data  = array(
361                              $this->requiredSpacesAfterOpen,
362                              $arg,
363                              $gap,
364                             );
365                    $fix   = $phpcsFile->addFixableError($error, $nextToken, 'SpacingAfterOpen', $data);
366                    if ($fix === true) {
367                        $padding = str_repeat(' ', $this->requiredSpacesAfterOpen);
368                        if ($gap === 0) {
369                            $phpcsFile->fixer->addContent($openBracket, $padding);
370                        } else {
371                            $phpcsFile->fixer->replaceToken(($openBracket + 1), $padding);
372                        }
373                    }
374                }//end if
375            }//end if
376
377            $params[] = $nextParam;
378        }//end while
379
380        $gap = 0;
381        if ($tokens[($closeBracket - 1)]['code'] === T_WHITESPACE) {
382            $gap = strlen($tokens[($closeBracket - 1)]['content']);
383        }
384
385        if (empty($params) === true) {
386            // There are no parameters for this function.
387            if (($closeBracket - $openBracket) !== 1) {
388                $error = 'Expected 0 spaces between brackets of function declaration; %s found';
389                $data  = array($gap);
390                $fix   = $phpcsFile->addFixableError($error, $openBracket, 'SpacingBetween', $data);
391                if ($fix === true) {
392                    $phpcsFile->fixer->replaceToken(($openBracket + 1), '');
393                }
394            }
395        } else if ($multiLine === false && $gap !== $this->requiredSpacesBeforeClose) {
396            $lastParam = array_pop($params);
397            $error     = 'Expected %s spaces between argument "%s" and closing bracket; %s found';
398            $data      = array(
399                          $this->requiredSpacesBeforeClose,
400                          $tokens[$lastParam]['content'],
401                          $gap,
402                         );
403            $fix       = $phpcsFile->addFixableError($error, $closeBracket, 'SpacingBeforeClose', $data);
404            if ($fix === true) {
405                $padding = str_repeat(' ', $this->requiredSpacesBeforeClose);
406                if ($gap === 0) {
407                    $phpcsFile->fixer->addContentBefore($closeBracket, $padding);
408                } else {
409                    $phpcsFile->fixer->replaceToken(($closeBracket - 1), $padding);
410                }
411            }
412        }//end if
413
414    }//end processBracket()
415
416
417}//end class
418