1<?php
2/**
3 * PEAR_Sniffs_Functions_FunctionDeclarationSniff.
4 *
5 * PHP version 5
6 *
7 * @category  PHP
8 * @package   PHP_CodeSniffer
9 * @author    Greg Sherwood <gsherwood@squiz.net>
10 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
11 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
12 * @link      http://pear.php.net/package/PHP_CodeSniffer
13 */
14
15/**
16 * PEAR_Sniffs_Functions_FunctionDeclarationSniff.
17 *
18 * Ensure single and multi-line function declarations are defined correctly.
19 *
20 * @category  PHP
21 * @package   PHP_CodeSniffer
22 * @author    Greg Sherwood <gsherwood@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_FunctionDeclarationSniff implements PHP_CodeSniffer_Sniff
29{
30
31    /**
32     * The number of spaces code should be indented.
33     *
34     * @var int
35     */
36    public $indent = 4;
37
38
39    /**
40     * Returns an array of tokens this test wants to listen for.
41     *
42     * @return array
43     */
44    public function register()
45    {
46        return array(
47                T_FUNCTION,
48                T_CLOSURE,
49               );
50
51    }//end register()
52
53
54    /**
55     * Processes this test, when one of its tokens is encountered.
56     *
57     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
58     * @param int                  $stackPtr  The position of the current token
59     *                                        in the stack passed in $tokens.
60     *
61     * @return void
62     */
63    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
64    {
65        $tokens = $phpcsFile->getTokens();
66
67        if (isset($tokens[$stackPtr]['parenthesis_opener']) === false
68            || isset($tokens[$stackPtr]['parenthesis_closer']) === false
69            || $tokens[$stackPtr]['parenthesis_opener'] === null
70            || $tokens[$stackPtr]['parenthesis_closer'] === null
71        ) {
72            return;
73        }
74
75        $openBracket  = $tokens[$stackPtr]['parenthesis_opener'];
76        $closeBracket = $tokens[$stackPtr]['parenthesis_closer'];
77
78        // Must be one space after the FUNCTION keyword.
79        if ($tokens[($stackPtr + 1)]['content'] === $phpcsFile->eolChar) {
80            $spaces = 'newline';
81        } else if ($tokens[($stackPtr + 1)]['code'] === T_WHITESPACE) {
82            $spaces = strlen($tokens[($stackPtr + 1)]['content']);
83        } else {
84            $spaces = 0;
85        }
86
87        if ($spaces !== 1) {
88            $error = 'Expected 1 space after FUNCTION keyword; %s found';
89            $data  = array($spaces);
90            $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceAfterFunction', $data);
91            if ($fix === true) {
92                if ($spaces === 0) {
93                    $phpcsFile->fixer->addContent($stackPtr, ' ');
94                } else {
95                    $phpcsFile->fixer->replaceToken(($stackPtr + 1), ' ');
96                }
97            }
98        }
99
100        // Must be one space before the opening parenthesis. For closures, this is
101        // enforced by the first check because there is no content between the keywords
102        // and the opening parenthesis.
103        if ($tokens[$stackPtr]['code'] === T_FUNCTION) {
104            if ($tokens[($openBracket - 1)]['content'] === $phpcsFile->eolChar) {
105                $spaces = 'newline';
106            } else if ($tokens[($openBracket - 1)]['code'] === T_WHITESPACE) {
107                $spaces = strlen($tokens[($openBracket - 1)]['content']);
108            } else {
109                $spaces = 0;
110            }
111
112            if ($spaces !== 0) {
113                $error = 'Expected 0 spaces before opening parenthesis; %s found';
114                $data  = array($spaces);
115                $fix   = $phpcsFile->addFixableError($error, $openBracket, 'SpaceBeforeOpenParen', $data);
116                if ($fix === true) {
117                    $phpcsFile->fixer->replaceToken(($openBracket - 1), '');
118                }
119            }
120        }//end if
121
122        // Must be one space before and after USE keyword for closures.
123        if ($tokens[$stackPtr]['code'] === T_CLOSURE) {
124            $use = $phpcsFile->findNext(T_USE, ($closeBracket + 1), $tokens[$stackPtr]['scope_opener']);
125            if ($use !== false) {
126                if ($tokens[($use + 1)]['code'] !== T_WHITESPACE) {
127                    $length = 0;
128                } else if ($tokens[($use + 1)]['content'] === "\t") {
129                    $length = '\t';
130                } else {
131                    $length = strlen($tokens[($use + 1)]['content']);
132                }
133
134                if ($length !== 1) {
135                    $error = 'Expected 1 space after USE keyword; found %s';
136                    $data  = array($length);
137                    $fix   = $phpcsFile->addFixableError($error, $use, 'SpaceAfterUse', $data);
138                    if ($fix === true) {
139                        if ($length === 0) {
140                            $phpcsFile->fixer->addContent($use, ' ');
141                        } else {
142                            $phpcsFile->fixer->replaceToken(($use + 1), ' ');
143                        }
144                    }
145                }
146
147                if ($tokens[($use - 1)]['code'] !== T_WHITESPACE) {
148                    $length = 0;
149                } else if ($tokens[($use - 1)]['content'] === "\t") {
150                    $length = '\t';
151                } else {
152                    $length = strlen($tokens[($use - 1)]['content']);
153                }
154
155                if ($length !== 1) {
156                    $error = 'Expected 1 space before USE keyword; found %s';
157                    $data  = array($length);
158                    $fix   = $phpcsFile->addFixableError($error, $use, 'SpaceBeforeUse', $data);
159                    if ($fix === true) {
160                        if ($length === 0) {
161                            $phpcsFile->fixer->addContentBefore($use, ' ');
162                        } else {
163                            $phpcsFile->fixer->replaceToken(($use - 1), ' ');
164                        }
165                    }
166                }
167            }//end if
168        }//end if
169
170        if ($this->isMultiLineDeclaration($phpcsFile, $stackPtr, $openBracket, $tokens) === true) {
171            $this->processMultiLineDeclaration($phpcsFile, $stackPtr, $tokens);
172        } else {
173            $this->processSingleLineDeclaration($phpcsFile, $stackPtr, $tokens);
174        }
175
176    }//end process()
177
178
179    /**
180     * Determine if this is a multi-line function declaration.
181     *
182     * @param PHP_CodeSniffer_File $phpcsFile   The file being scanned.
183     * @param int                  $stackPtr    The position of the current token
184     *                                          in the stack passed in $tokens.
185     * @param int                  $openBracket The position of the opening bracket
186     *                                          in the stack passed in $tokens.
187     * @param array                $tokens      The stack of tokens that make up
188     *                                          the file.
189     *
190     * @return void
191     */
192    public function isMultiLineDeclaration(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $openBracket, $tokens)
193    {
194        $closeBracket = $tokens[$openBracket]['parenthesis_closer'];
195        if ($tokens[$openBracket]['line'] !== $tokens[$closeBracket]['line']) {
196            return true;
197        }
198
199        // Closures may use the USE keyword and so be multi-line in this way.
200        if ($tokens[$stackPtr]['code'] === T_CLOSURE) {
201            $use = $phpcsFile->findNext(T_USE, ($closeBracket + 1), $tokens[$stackPtr]['scope_opener']);
202            if ($use !== false) {
203                // If the opening and closing parenthesis of the use statement
204                // are also on the same line, this is a single line declaration.
205                $open  = $phpcsFile->findNext(T_OPEN_PARENTHESIS, ($use + 1));
206                $close = $tokens[$open]['parenthesis_closer'];
207                if ($tokens[$open]['line'] !== $tokens[$close]['line']) {
208                    return true;
209                }
210            }
211        }
212
213        return false;
214
215    }//end isMultiLineDeclaration()
216
217
218    /**
219     * Processes single-line declarations.
220     *
221     * Just uses the Generic BSD-Allman brace sniff.
222     *
223     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
224     * @param int                  $stackPtr  The position of the current token
225     *                                        in the stack passed in $tokens.
226     * @param array                $tokens    The stack of tokens that make up
227     *                                        the file.
228     *
229     * @return void
230     */
231    public function processSingleLineDeclaration(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $tokens)
232    {
233        if ($tokens[$stackPtr]['code'] === T_CLOSURE) {
234            if (class_exists('Generic_Sniffs_Functions_OpeningFunctionBraceKernighanRitchieSniff', true) === false) {
235                throw new PHP_CodeSniffer_Exception('Class Generic_Sniffs_Functions_OpeningFunctionBraceKernighanRitchieSniff not found');
236            }
237
238            $sniff = new Generic_Sniffs_Functions_OpeningFunctionBraceKernighanRitchieSniff();
239        } else {
240            if (class_exists('Generic_Sniffs_Functions_OpeningFunctionBraceBsdAllmanSniff', true) === false) {
241                throw new PHP_CodeSniffer_Exception('Class Generic_Sniffs_Functions_OpeningFunctionBraceBsdAllmanSniff not found');
242            }
243
244            $sniff = new Generic_Sniffs_Functions_OpeningFunctionBraceBsdAllmanSniff();
245        }
246
247        $sniff->checkClosures = true;
248        $sniff->process($phpcsFile, $stackPtr);
249
250    }//end processSingleLineDeclaration()
251
252
253    /**
254     * Processes multi-line declarations.
255     *
256     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
257     * @param int                  $stackPtr  The position of the current token
258     *                                        in the stack passed in $tokens.
259     * @param array                $tokens    The stack of tokens that make up
260     *                                        the file.
261     *
262     * @return void
263     */
264    public function processMultiLineDeclaration(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $tokens)
265    {
266        // We need to work out how far indented the function
267        // declaration itself is, so we can work out how far to
268        // indent parameters.
269        $functionIndent = 0;
270        for ($i = ($stackPtr - 1); $i >= 0; $i--) {
271            if ($tokens[$i]['line'] !== $tokens[$stackPtr]['line']) {
272                $i++;
273                break;
274            }
275        }
276
277        if ($tokens[$i]['code'] === T_WHITESPACE) {
278            $functionIndent = strlen($tokens[$i]['content']);
279        }
280
281        // The closing parenthesis must be on a new line, even
282        // when checking abstract function definitions.
283        $closeBracket = $tokens[$stackPtr]['parenthesis_closer'];
284        $prev         = $phpcsFile->findPrevious(
285            T_WHITESPACE,
286            ($closeBracket - 1),
287            null,
288            true
289        );
290
291        if ($tokens[$closeBracket]['line'] !== $tokens[$tokens[$closeBracket]['parenthesis_opener']]['line']) {
292            if ($tokens[$prev]['line'] === $tokens[$closeBracket]['line']) {
293                $error = 'The closing parenthesis of a multi-line function declaration must be on a new line';
294                $fix   = $phpcsFile->addFixableError($error, $closeBracket, 'CloseBracketLine');
295                if ($fix === true) {
296                    $phpcsFile->fixer->addNewlineBefore($closeBracket);
297                }
298            }
299        }
300
301        // If this is a closure and is using a USE statement, the closing
302        // parenthesis we need to look at from now on is the closing parenthesis
303        // of the USE statement.
304        if ($tokens[$stackPtr]['code'] === T_CLOSURE) {
305            $use = $phpcsFile->findNext(T_USE, ($closeBracket + 1), $tokens[$stackPtr]['scope_opener']);
306            if ($use !== false) {
307                $open         = $phpcsFile->findNext(T_OPEN_PARENTHESIS, ($use + 1));
308                $closeBracket = $tokens[$open]['parenthesis_closer'];
309
310                $prev = $phpcsFile->findPrevious(
311                    T_WHITESPACE,
312                    ($closeBracket - 1),
313                    null,
314                    true
315                );
316
317                if ($tokens[$closeBracket]['line'] !== $tokens[$tokens[$closeBracket]['parenthesis_opener']]['line']) {
318                    if ($tokens[$prev]['line'] === $tokens[$closeBracket]['line']) {
319                        $error = 'The closing parenthesis of a multi-line use declaration must be on a new line';
320                        $fix   = $phpcsFile->addFixableError($error, $closeBracket, 'UseCloseBracketLine');
321                        if ($fix === true) {
322                            $phpcsFile->fixer->addNewlineBefore($closeBracket);
323                        }
324                    }
325                }
326            }//end if
327        }//end if
328
329        // Each line between the parenthesis should be indented 4 spaces.
330        $openBracket = $tokens[$stackPtr]['parenthesis_opener'];
331        $lastLine    = $tokens[$openBracket]['line'];
332        for ($i = ($openBracket + 1); $i < $closeBracket; $i++) {
333            if ($tokens[$i]['line'] !== $lastLine) {
334                if ($i === $tokens[$stackPtr]['parenthesis_closer']
335                    || ($tokens[$i]['code'] === T_WHITESPACE
336                    && (($i + 1) === $closeBracket
337                    || ($i + 1) === $tokens[$stackPtr]['parenthesis_closer']))
338                ) {
339                    // Closing braces need to be indented to the same level
340                    // as the function.
341                    $expectedIndent = $functionIndent;
342                } else {
343                    $expectedIndent = ($functionIndent + $this->indent);
344                }
345
346                // We changed lines, so this should be a whitespace indent token.
347                if ($tokens[$i]['code'] !== T_WHITESPACE) {
348                    $foundIndent = 0;
349                } else if ($tokens[$i]['line'] !== $tokens[($i + 1)]['line']) {
350                    // This is an empty line, so don't check the indent.
351                    $foundIndent = $expectedIndent;
352
353                    $error = 'Blank lines are not allowed in a multi-line function declaration';
354                    $fix   = $phpcsFile->addFixableError($error, $i, 'EmptyLine');
355                    if ($fix === true) {
356                        $phpcsFile->fixer->replaceToken($i, '');
357                    }
358                } else {
359                    $foundIndent = strlen($tokens[$i]['content']);
360                }
361
362                if ($expectedIndent !== $foundIndent) {
363                    $error = 'Multi-line function declaration not indented correctly; expected %s spaces but found %s';
364                    $data  = array(
365                              $expectedIndent,
366                              $foundIndent,
367                             );
368
369                    $fix = $phpcsFile->addFixableError($error, $i, 'Indent', $data);
370                    if ($fix === true) {
371                        $spaces = str_repeat(' ', $expectedIndent);
372                        if ($foundIndent === 0) {
373                            $phpcsFile->fixer->addContentBefore($i, $spaces);
374                        } else {
375                            $phpcsFile->fixer->replaceToken($i, $spaces);
376                        }
377                    }
378                }
379
380                $lastLine = $tokens[$i]['line'];
381            }//end if
382
383            if ($tokens[$i]['code'] === T_ARRAY || $tokens[$i]['code'] === T_OPEN_SHORT_ARRAY) {
384                // Skip arrays as they have their own indentation rules.
385                if ($tokens[$i]['code'] === T_OPEN_SHORT_ARRAY) {
386                    $i = $tokens[$i]['bracket_closer'];
387                } else {
388                    $i = $tokens[$i]['parenthesis_closer'];
389                }
390
391                $lastLine = $tokens[$i]['line'];
392                continue;
393            }
394        }//end for
395
396        if (isset($tokens[$stackPtr]['scope_opener']) === false) {
397            return;
398        }
399
400        // The opening brace needs to be one space away from the closing parenthesis.
401        $opener = $tokens[$stackPtr]['scope_opener'];
402        if ($tokens[$opener]['line'] !== $tokens[$closeBracket]['line']) {
403            $error = 'The closing parenthesis and the opening brace of a multi-line function declaration must be on the same line';
404            $fix   = $phpcsFile->addFixableError($error, $opener, 'NewlineBeforeOpenBrace');
405            if ($fix === true) {
406                $prev = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($opener - 1), $closeBracket, true);
407                $phpcsFile->fixer->beginChangeset();
408                $phpcsFile->fixer->addContent($prev, ' {');
409                $phpcsFile->fixer->replaceToken($opener, '');
410                $phpcsFile->fixer->endChangeset();
411            }
412        } else {
413            $prev = $tokens[($opener - 1)];
414            if ($prev['code'] !== T_WHITESPACE) {
415                $length = 0;
416            } else {
417                $length = strlen($prev['content']);
418            }
419
420            if ($length !== 1) {
421                $error = 'There must be a single space between the closing parenthesis and the opening brace of a multi-line function declaration; found %s spaces';
422                $fix   = $phpcsFile->addFixableError($error, ($opener - 1), 'SpaceBeforeOpenBrace', array($length));
423                if ($fix === true) {
424                    if ($length === 0) {
425                        $phpcsFile->fixer->addContentBefore($opener, ' ');
426                    } else {
427                        $phpcsFile->fixer->replaceToken(($opener - 1), ' ');
428                    }
429                }
430
431                return;
432            }//end if
433        }//end if
434
435    }//end processMultiLineDeclaration()
436
437
438}//end class
439