1<?php
2/**
3 * Squiz_Sniffs_WhiteSpace_ControlStructureSpacingSniff.
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_ControlStructureSpacingSniff.
18 *
19 * Checks that control structures have the correct spacing around brackets.
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_ControlStructureSpacingSniff implements PHP_CodeSniffer_Sniff
31{
32
33    /**
34     * A list of tokenizers this sniff supports.
35     *
36     * @var array
37     */
38    public $supportedTokenizers = array(
39                                   'PHP',
40                                   'JS',
41                                  );
42
43
44    /**
45     * Returns an array of tokens this test wants to listen for.
46     *
47     * @return array
48     */
49    public function register()
50    {
51        return array(
52                T_IF,
53                T_WHILE,
54                T_FOREACH,
55                T_FOR,
56                T_SWITCH,
57                T_DO,
58                T_ELSE,
59                T_ELSEIF,
60                T_TRY,
61                T_CATCH,
62               );
63
64    }//end register()
65
66
67    /**
68     * Processes this test, when one of its tokens is encountered.
69     *
70     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
71     * @param int                  $stackPtr  The position of the current token
72     *                                        in the stack passed in $tokens.
73     *
74     * @return void
75     */
76    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
77    {
78        $tokens = $phpcsFile->getTokens();
79
80        if (isset($tokens[$stackPtr]['parenthesis_opener']) === true
81            && isset($tokens[$stackPtr]['parenthesis_closer']) === true
82        ) {
83            $parenOpener = $tokens[$stackPtr]['parenthesis_opener'];
84            $parenCloser = $tokens[$stackPtr]['parenthesis_closer'];
85            if ($tokens[($parenOpener + 1)]['code'] === T_WHITESPACE) {
86                $gap = $tokens[($parenOpener + 1)]['length'];
87
88                if ($gap === 0) {
89                    $phpcsFile->recordMetric($stackPtr, 'Spaces after control structure open parenthesis', 'newline');
90                    $gap = 'newline';
91                } else {
92                    $phpcsFile->recordMetric($stackPtr, 'Spaces after control structure open parenthesis', $gap);
93                }
94
95                $error = 'Expected 0 spaces after opening bracket; %s found';
96                $data  = array($gap);
97                $fix   = $phpcsFile->addFixableError($error, ($parenOpener + 1), 'SpacingAfterOpenBrace', $data);
98                if ($fix === true) {
99                    $phpcsFile->fixer->replaceToken(($parenOpener + 1), '');
100                }
101            } else {
102                $phpcsFile->recordMetric($stackPtr, 'Spaces after control structure open parenthesis', 0);
103            }
104
105            if ($tokens[$parenOpener]['line'] === $tokens[$parenCloser]['line']
106                && $tokens[($parenCloser - 1)]['code'] === T_WHITESPACE
107            ) {
108                $gap   = $tokens[($parenCloser - 1)]['length'];
109                $error = 'Expected 0 spaces before closing bracket; %s found';
110                $data  = array($gap);
111                $fix   = $phpcsFile->addFixableError($error, ($parenCloser - 1), 'SpaceBeforeCloseBrace', $data);
112                if ($fix === true) {
113                    $phpcsFile->fixer->replaceToken(($parenCloser - 1), '');
114                }
115
116                if ($gap === 0) {
117                    $phpcsFile->recordMetric($stackPtr, 'Spaces before control structure close parenthesis', 'newline');
118                } else {
119                    $phpcsFile->recordMetric($stackPtr, 'Spaces before control structure close parenthesis', $gap);
120                }
121            } else {
122                $phpcsFile->recordMetric($stackPtr, 'Spaces before control structure close parenthesis', 0);
123            }
124        }//end if
125
126        if (isset($tokens[$stackPtr]['scope_closer']) === false) {
127            return;
128        }
129
130        $scopeOpener = $tokens[$stackPtr]['scope_opener'];
131        $scopeCloser = $tokens[$stackPtr]['scope_closer'];
132
133        for ($firstContent = ($scopeOpener + 1); $firstContent < $phpcsFile->numTokens; $firstContent++) {
134            if ($tokens[$firstContent]['code'] !== T_WHITESPACE) {
135                break;
136            }
137        }
138
139        // We ignore spacing for some structures that tend to have their own rules.
140        $ignore = array(
141                   T_FUNCTION             => true,
142                   T_CLASS                => true,
143                   T_INTERFACE            => true,
144                   T_TRAIT                => true,
145                   T_DOC_COMMENT_OPEN_TAG => true,
146                  );
147
148        if (isset($ignore[$tokens[$firstContent]['code']]) === false
149            && $tokens[$firstContent]['line'] >= ($tokens[$scopeOpener]['line'] + 2)
150        ) {
151            $error = 'Blank line found at start of control structure';
152            $fix   = $phpcsFile->addFixableError($error, $scopeOpener, 'SpacingAfterOpen');
153
154            if ($fix === true) {
155                $phpcsFile->fixer->beginChangeset();
156                $i = ($scopeOpener + 1);
157                while ($tokens[$i]['line'] !== $tokens[$firstContent]['line']) {
158                    $phpcsFile->fixer->replaceToken($i, '');
159                    $i++;
160                }
161
162                $phpcsFile->fixer->addNewline($scopeOpener);
163                $phpcsFile->fixer->endChangeset();
164            }
165        }
166
167        if ($firstContent !== $scopeCloser) {
168            $lastContent = $phpcsFile->findPrevious(
169                T_WHITESPACE,
170                ($scopeCloser - 1),
171                null,
172                true
173            );
174
175            $lastNonEmptyContent = $phpcsFile->findPrevious(
176                PHP_CodeSniffer_Tokens::$emptyTokens,
177                ($scopeCloser - 1),
178                null,
179                true
180            );
181
182            $checkToken = $lastContent;
183            if (isset($tokens[$lastNonEmptyContent]['scope_condition']) === true) {
184                $checkToken = $tokens[$lastNonEmptyContent]['scope_condition'];
185            }
186
187            if (isset($ignore[$tokens[$checkToken]['code']]) === false
188                && $tokens[$lastContent]['line'] <= ($tokens[$scopeCloser]['line'] - 2)
189            ) {
190                $errorToken = $scopeCloser;
191                for ($i = ($scopeCloser - 1); $i > $lastContent; $i--) {
192                    if ($tokens[$i]['line'] < $tokens[$scopeCloser]['line']) {
193                        $errorToken = $i;
194                        break;
195                    }
196                }
197
198                $error = 'Blank line found at end of control structure';
199                $fix   = $phpcsFile->addFixableError($error, $errorToken, 'SpacingBeforeClose');
200
201                if ($fix === true) {
202                    $phpcsFile->fixer->beginChangeset();
203                    $i = ($scopeCloser - 1);
204                    for ($i = ($scopeCloser - 1); $i > $lastContent; $i--) {
205                        if ($tokens[$i]['line'] === $tokens[$scopeCloser]['line']) {
206                            continue;
207                        }
208
209                        if ($tokens[$i]['line'] === $tokens[$lastContent]['line']) {
210                            break;
211                        }
212
213                        $phpcsFile->fixer->replaceToken($i, '');
214                    }
215
216                    $phpcsFile->fixer->endChangeset();
217                }
218            }//end if
219        }//end if
220
221        $trailingContent = $phpcsFile->findNext(
222            T_WHITESPACE,
223            ($scopeCloser + 1),
224            null,
225            true
226        );
227
228        if ($tokens[$trailingContent]['code'] === T_COMMENT) {
229            // Special exception for code where the comment about
230            // an ELSE or ELSEIF is written between the control structures.
231            $nextCode = $phpcsFile->findNext(
232                PHP_CodeSniffer_Tokens::$emptyTokens,
233                ($scopeCloser + 1),
234                null,
235                true
236            );
237
238            if ($tokens[$nextCode]['code'] === T_ELSE
239                || $tokens[$nextCode]['code'] === T_ELSEIF
240            ) {
241                $trailingContent = $nextCode;
242            }
243        }//end if
244
245        if ($tokens[$trailingContent]['code'] === T_ELSE) {
246            if ($tokens[$stackPtr]['code'] === T_IF) {
247                // IF with ELSE.
248                return;
249            }
250        }
251
252        if ($tokens[$trailingContent]['code'] === T_WHILE
253            && $tokens[$stackPtr]['code'] === T_DO
254        ) {
255            // DO with WHILE.
256            return;
257        }
258
259        if ($tokens[$trailingContent]['code'] === T_CLOSE_TAG) {
260            // At the end of the script or embedded code.
261            return;
262        }
263
264        if (isset($tokens[$trailingContent]['scope_condition']) === true
265            && $tokens[$trailingContent]['scope_condition'] !== $trailingContent
266            && isset($tokens[$trailingContent]['scope_opener']) === true
267            && $tokens[$trailingContent]['scope_opener'] !== $trailingContent
268        ) {
269            // Another control structure's closing brace.
270            $owner = $tokens[$trailingContent]['scope_condition'];
271            if ($tokens[$owner]['code'] === T_FUNCTION) {
272                // The next content is the closing brace of a function
273                // so normal function rules apply and we can ignore it.
274                return;
275            }
276
277            if ($tokens[$owner]['code'] === T_CLOSURE
278                && ($phpcsFile->hasCondition($stackPtr, T_FUNCTION) === true
279                || $phpcsFile->hasCondition($stackPtr, T_CLOSURE) === true
280                || isset($tokens[$stackPtr]['nested_parenthesis']) === true)
281            ) {
282                return;
283            }
284
285            if ($tokens[$trailingContent]['line'] !== ($tokens[$scopeCloser]['line'] + 1)) {
286                $error = 'Blank line found after control structure';
287                $fix   = $phpcsFile->addFixableError($error, $scopeCloser, 'LineAfterClose');
288
289                if ($fix === true) {
290                    $phpcsFile->fixer->beginChangeset();
291                    $i = ($scopeCloser + 1);
292                    while ($tokens[$i]['line'] !== $tokens[$trailingContent]['line']) {
293                        $phpcsFile->fixer->replaceToken($i, '');
294                        $i++;
295                    }
296
297                    $phpcsFile->fixer->addNewline($scopeCloser);
298                    $phpcsFile->fixer->endChangeset();
299                }
300            }
301        } else if ($tokens[$trailingContent]['code'] !== T_ELSE
302            && $tokens[$trailingContent]['code'] !== T_ELSEIF
303            && $tokens[$trailingContent]['code'] !== T_CATCH
304            && $tokens[$trailingContent]['line'] === ($tokens[$scopeCloser]['line'] + 1)
305        ) {
306            $error = 'No blank line found after control structure';
307            $fix   = $phpcsFile->addFixableError($error, $scopeCloser, 'NoLineAfterClose');
308            if ($fix === true) {
309                $phpcsFile->fixer->addNewline($scopeCloser);
310            }
311        }//end if
312
313    }//end process()
314
315
316}//end class
317