1<?php
2/**
3 * Squiz_Sniffs_Formatting_FunctionSpacingSniff.
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_WhiteSpace_FunctionSpacingSniff.
18 *
19 * Checks the separation between methods in a class or interface.
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_WhiteSpace_FunctionSpacingSniff implements PHP_CodeSniffer_Sniff
31{
32
33    /**
34     * The number of blank lines between functions.
35     *
36     * @var int
37     */
38    public $spacing = 2;
39
40
41    /**
42     * Returns an array of tokens this test wants to listen for.
43     *
44     * @return array
45     */
46    public function register()
47    {
48        return array(T_FUNCTION);
49
50    }//end register()
51
52
53    /**
54     * Processes this sniff when one of its tokens is encountered.
55     *
56     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
57     * @param int                  $stackPtr  The position of the current token
58     *                                        in the stack passed in $tokens.
59     *
60     * @return void
61     */
62    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
63    {
64        $tokens        = $phpcsFile->getTokens();
65        $this->spacing = (int) $this->spacing;
66
67        /*
68            Check the number of blank lines
69            after the function.
70        */
71
72        if (isset($tokens[$stackPtr]['scope_closer']) === false) {
73            // Must be an interface method, so the closer is the semicolon.
74            $closer = $phpcsFile->findNext(T_SEMICOLON, $stackPtr);
75        } else {
76            $closer = $tokens[$stackPtr]['scope_closer'];
77        }
78
79        // Allow for comments on the same line as the closer.
80        for ($nextLineToken = ($closer + 1); $nextLineToken < $phpcsFile->numTokens; $nextLineToken++) {
81            if ($tokens[$nextLineToken]['line'] !== $tokens[$closer]['line']) {
82                break;
83            }
84        }
85
86        $foundLines = 0;
87        if ($nextLineToken === ($phpcsFile->numTokens - 1)) {
88            // We are at the end of the file.
89            // Don't check spacing after the function because this
90            // should be done by an EOF sniff.
91            $foundLines = $this->spacing;
92        } else {
93            $nextContent = $phpcsFile->findNext(T_WHITESPACE, $nextLineToken, null, true);
94            if ($nextContent === false) {
95                // We are at the end of the file.
96                // Don't check spacing after the function because this
97                // should be done by an EOF sniff.
98                $foundLines = $this->spacing;
99            } else {
100                $foundLines += ($tokens[$nextContent]['line'] - $tokens[$nextLineToken]['line']);
101            }
102        }
103
104        if ($foundLines !== $this->spacing) {
105            $error = 'Expected %s blank line';
106            if ($this->spacing !== 1) {
107                $error .= 's';
108            }
109
110            $error .= ' after function; %s found';
111            $data   = array(
112                       $this->spacing,
113                       $foundLines,
114                      );
115
116            $fix = $phpcsFile->addFixableError($error, $closer, 'After', $data);
117            if ($fix === true) {
118                $phpcsFile->fixer->beginChangeset();
119                for ($i = $nextLineToken; $i <= $nextContent; $i++) {
120                    if ($tokens[$i]['line'] === $tokens[$nextContent]['line']) {
121                        $phpcsFile->fixer->addContentBefore($i, str_repeat($phpcsFile->eolChar, $this->spacing));
122                        break;
123                    }
124
125                    $phpcsFile->fixer->replaceToken($i, '');
126                }
127
128                $phpcsFile->fixer->endChangeset();
129            }//end if
130        }//end if
131
132        /*
133            Check the number of blank lines
134            before the function.
135        */
136
137        $prevLineToken = null;
138        for ($i = $stackPtr; $i > 0; $i--) {
139            if (strpos($tokens[$i]['content'], $phpcsFile->eolChar) === false) {
140                continue;
141            } else {
142                $prevLineToken = $i;
143                break;
144            }
145        }
146
147        if (is_null($prevLineToken) === true) {
148            // Never found the previous line, which means
149            // there are 0 blank lines before the function.
150            $foundLines  = 0;
151            $prevContent = 0;
152        } else {
153            $currentLine = $tokens[$stackPtr]['line'];
154
155            $prevContent = $phpcsFile->findPrevious(T_WHITESPACE, $prevLineToken, null, true);
156            if ($tokens[$prevContent]['code'] === T_COMMENT) {
157                // Ignore comments as they can have different spacing rules, and this
158                // isn't a proper function comment anyway.
159                return;
160            }
161
162            if ($tokens[$prevContent]['code'] === T_DOC_COMMENT_CLOSE_TAG
163                && $tokens[$prevContent]['line'] === ($currentLine - 1)
164            ) {
165                // Account for function comments.
166                $prevContent = $phpcsFile->findPrevious(T_WHITESPACE, ($tokens[$prevContent]['comment_opener'] - 1), null, true);
167            }
168
169            // Before we throw an error, check that we are not throwing an error
170            // for another function. We don't want to error for no blank lines after
171            // the previous function and no blank lines before this one as well.
172            $prevLine   = ($tokens[$prevContent]['line'] - 1);
173            $i          = ($stackPtr - 1);
174            $foundLines = 0;
175            while ($currentLine !== $prevLine && $currentLine > 1 && $i > 0) {
176                if (isset($tokens[$i]['scope_condition']) === true) {
177                    $scopeCondition = $tokens[$i]['scope_condition'];
178                    if ($tokens[$scopeCondition]['code'] === T_FUNCTION) {
179                        // Found a previous function.
180                        return;
181                    }
182                } else if ($tokens[$i]['code'] === T_FUNCTION) {
183                    // Found another interface function.
184                    return;
185                }
186
187                $currentLine = $tokens[$i]['line'];
188                if ($currentLine === $prevLine) {
189                    break;
190                }
191
192                if ($tokens[($i - 1)]['line'] < $currentLine && $tokens[($i + 1)]['line'] > $currentLine) {
193                    // This token is on a line by itself. If it is whitespace, the line is empty.
194                    if ($tokens[$i]['code'] === T_WHITESPACE) {
195                        $foundLines++;
196                    }
197                }
198
199                $i--;
200            }//end while
201        }//end if
202
203        if ($foundLines !== $this->spacing) {
204            $error = 'Expected %s blank line';
205            if ($this->spacing !== 1) {
206                $error .= 's';
207            }
208
209            $error .= ' before function; %s found';
210            $data   = array(
211                       $this->spacing,
212                       $foundLines,
213                      );
214
215            $fix = $phpcsFile->addFixableError($error, $stackPtr, 'Before', $data);
216            if ($fix === true) {
217                if ($prevContent === 0) {
218                    $nextSpace = 0;
219                } else {
220                    $nextSpace = $phpcsFile->findNext(T_WHITESPACE, ($prevContent + 1), $stackPtr);
221                    if ($nextSpace === false) {
222                        $nextSpace = ($stackPtr - 1);
223                    }
224                }
225
226                if ($foundLines < $this->spacing) {
227                    $padding = str_repeat($phpcsFile->eolChar, ($this->spacing - $foundLines));
228                    $phpcsFile->fixer->addContent($nextSpace, $padding);
229                } else {
230                    $nextContent = $phpcsFile->findNext(T_WHITESPACE, ($nextSpace + 1), null, true);
231                    $phpcsFile->fixer->beginChangeset();
232                    for ($i = $nextSpace; $i < ($nextContent - 1); $i++) {
233                        $phpcsFile->fixer->replaceToken($i, '');
234                    }
235
236                    $phpcsFile->fixer->replaceToken($i, str_repeat($phpcsFile->eolChar, $this->spacing));
237                    $phpcsFile->fixer->endChangeset();
238                }
239            }//end if
240        }//end if
241
242    }//end process()
243
244
245}//end class
246