1<?php
2/**
3 * Squiz_Sniffs_ControlStructures_SwitchDeclarationSniff.
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_ControlStructures_SwitchDeclarationSniff.
18 *
19 * Ensures all the breaks and cases are aligned correctly according to their
20 * parent switch's alignment and enforces other switch formatting.
21 *
22 * @category  PHP
23 * @package   PHP_CodeSniffer
24 * @author    Greg Sherwood <gsherwood@squiz.net>
25 * @author    Marc McIntyre <mmcintyre@squiz.net>
26 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
27 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
28 * @version   Release: @package_version@
29 * @link      http://pear.php.net/package/PHP_CodeSniffer
30 */
31class Squiz_Sniffs_ControlStructures_SwitchDeclarationSniff implements PHP_CodeSniffer_Sniff
32{
33
34    /**
35     * A list of tokenizers this sniff supports.
36     *
37     * @var array
38     */
39    public $supportedTokenizers = array(
40                                   'PHP',
41                                   'JS',
42                                  );
43
44    /**
45     * The number of spaces code should be indented.
46     *
47     * @var int
48     */
49    public $indent = 4;
50
51
52    /**
53     * Returns an array of tokens this test wants to listen for.
54     *
55     * @return array
56     */
57    public function register()
58    {
59        return array(T_SWITCH);
60
61    }//end register()
62
63
64    /**
65     * Processes this test, when one of its tokens is encountered.
66     *
67     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
68     * @param int                  $stackPtr  The position of the current token in the
69     *                                        stack passed in $tokens.
70     *
71     * @return void
72     */
73    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
74    {
75        $tokens = $phpcsFile->getTokens();
76
77        // We can't process SWITCH statements unless we know where they start and end.
78        if (isset($tokens[$stackPtr]['scope_opener']) === false
79            || isset($tokens[$stackPtr]['scope_closer']) === false
80        ) {
81            return;
82        }
83
84        $switch        = $tokens[$stackPtr];
85        $nextCase      = $stackPtr;
86        $caseAlignment = ($switch['column'] + $this->indent);
87        $caseCount     = 0;
88        $foundDefault  = false;
89
90        while (($nextCase = $phpcsFile->findNext(array(T_CASE, T_DEFAULT, T_SWITCH), ($nextCase + 1), $switch['scope_closer'])) !== false) {
91            // Skip nested SWITCH statements; they are handled on their own.
92            if ($tokens[$nextCase]['code'] === T_SWITCH) {
93                $nextCase = $tokens[$nextCase]['scope_closer'];
94                continue;
95            }
96
97            if ($tokens[$nextCase]['code'] === T_DEFAULT) {
98                $type         = 'Default';
99                $foundDefault = true;
100            } else {
101                $type = 'Case';
102                $caseCount++;
103            }
104
105            if ($tokens[$nextCase]['content'] !== strtolower($tokens[$nextCase]['content'])) {
106                $expected = strtolower($tokens[$nextCase]['content']);
107                $error    = strtoupper($type).' keyword must be lowercase; expected "%s" but found "%s"';
108                $data     = array(
109                             $expected,
110                             $tokens[$nextCase]['content'],
111                            );
112
113                $fix = $phpcsFile->addFixableError($error, $nextCase, $type.'NotLower', $data);
114                if ($fix === true) {
115                    $phpcsFile->fixer->replaceToken($nextCase, $expected);
116                }
117            }
118
119            if ($tokens[$nextCase]['column'] !== $caseAlignment) {
120                $error = strtoupper($type).' keyword must be indented '.$this->indent.' spaces from SWITCH keyword';
121                $fix   = $phpcsFile->addFixableError($error, $nextCase, $type.'Indent');
122
123                if ($fix === true) {
124                    $padding = str_repeat(' ', ($caseAlignment - 1));
125                    if ($tokens[$nextCase]['column'] === 1
126                        || $tokens[($nextCase - 1)]['code'] !== T_WHITESPACE
127                    ) {
128                        $phpcsFile->fixer->addContentBefore($nextCase, $padding);
129                    } else {
130                        $phpcsFile->fixer->replaceToken(($nextCase - 1), $padding);
131                    }
132                }
133            }
134
135            if ($type === 'Case'
136                && ($tokens[($nextCase + 1)]['type'] !== 'T_WHITESPACE'
137                || $tokens[($nextCase + 1)]['content'] !== ' ')
138            ) {
139                $error = 'CASE keyword must be followed by a single space';
140                $fix   = $phpcsFile->addFixableError($error, $nextCase, 'SpacingAfterCase');
141                if ($fix === true) {
142                    if ($tokens[($nextCase + 1)]['type'] !== 'T_WHITESPACE') {
143                        $phpcsFile->fixer->addContent($nextCase, ' ');
144                    } else {
145                        $phpcsFile->fixer->replaceToken(($nextCase + 1), ' ');
146                    }
147                }
148            }
149
150            if (isset($tokens[$nextCase]['scope_opener']) === false) {
151                $error = 'Possible parse error: CASE missing opening colon';
152                $phpcsFile->addWarning($error, $nextCase, 'MissingColon');
153                continue;
154            }
155
156            $opener = $tokens[$nextCase]['scope_opener'];
157            if ($tokens[($opener - 1)]['type'] === 'T_WHITESPACE') {
158                $error = 'There must be no space before the colon in a '.strtoupper($type).' statement';
159                $fix   = $phpcsFile->addFixableError($error, $nextCase, 'SpaceBeforeColon'.$type);
160                if ($fix === true) {
161                    $phpcsFile->fixer->replaceToken(($opener - 1), '');
162                }
163            }
164
165            $nextBreak = $tokens[$nextCase]['scope_closer'];
166            if ($tokens[$nextBreak]['code'] === T_BREAK
167                || $tokens[$nextBreak]['code'] === T_RETURN
168                || $tokens[$nextBreak]['code'] === T_CONTINUE
169                || $tokens[$nextBreak]['code'] === T_THROW
170                || $tokens[$nextBreak]['code'] === T_EXIT
171            ) {
172                if ($tokens[$nextBreak]['scope_condition'] === $nextCase) {
173                    // Only need to check a couple of things once, even if the
174                    // break is shared between multiple case statements, or even
175                    // the default case.
176                    if ($tokens[$nextBreak]['column'] !== $caseAlignment) {
177                        $error = 'Case breaking statement must be indented '.$this->indent.' spaces from SWITCH keyword';
178                        $fix   = $phpcsFile->addFixableError($error, $nextBreak, 'BreakIndent');
179
180                        if ($fix === true) {
181                            $padding = str_repeat(' ', ($caseAlignment - 1));
182                            if ($tokens[$nextBreak]['column'] === 1
183                                || $tokens[($nextBreak - 1)]['code'] !== T_WHITESPACE
184                            ) {
185                                $phpcsFile->fixer->addContentBefore($nextBreak, $padding);
186                            } else {
187                                $phpcsFile->fixer->replaceToken(($nextBreak - 1), $padding);
188                            }
189                        }
190                    }
191
192                    $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($nextBreak - 1), $stackPtr, true);
193                    if ($tokens[$prev]['line'] !== ($tokens[$nextBreak]['line'] - 1)) {
194                        $error = 'Blank lines are not allowed before case breaking statements';
195                        $phpcsFile->addError($error, $nextBreak, 'SpacingBeforeBreak');
196                    }
197
198                    $nextLine  = $tokens[$tokens[$stackPtr]['scope_closer']]['line'];
199                    $semicolon = $phpcsFile->findEndOfStatement($nextBreak);
200                    for ($i = ($semicolon + 1); $i < $tokens[$stackPtr]['scope_closer']; $i++) {
201                        if ($tokens[$i]['type'] !== 'T_WHITESPACE') {
202                            $nextLine = $tokens[$i]['line'];
203                            break;
204                        }
205                    }
206
207                    if ($type === 'Case') {
208                        // Ensure the BREAK statement is followed by
209                        // a single blank line, or the end switch brace.
210                        if ($nextLine !== ($tokens[$semicolon]['line'] + 2) && $i !== $tokens[$stackPtr]['scope_closer']) {
211                            $error = 'Case breaking statements must be followed by a single blank line';
212                            $fix   = $phpcsFile->addFixableError($error, $nextBreak, 'SpacingAfterBreak');
213                            if ($fix === true) {
214                                $phpcsFile->fixer->beginChangeset();
215                                for ($i = ($semicolon + 1); $i <= $tokens[$stackPtr]['scope_closer']; $i++) {
216                                    if ($tokens[$i]['line'] === $nextLine) {
217                                        $phpcsFile->fixer->addNewlineBefore($i);
218                                        break;
219                                    }
220
221                                    if ($tokens[$i]['line'] === $tokens[$semicolon]['line']) {
222                                        continue;
223                                    }
224
225                                    $phpcsFile->fixer->replaceToken($i, '');
226                                }
227
228                                $phpcsFile->fixer->endChangeset();
229                            }
230                        }//end if
231                    } else {
232                        // Ensure the BREAK statement is not followed by a blank line.
233                        if ($nextLine !== ($tokens[$semicolon]['line'] + 1)) {
234                            $error = 'Blank lines are not allowed after the DEFAULT case\'s breaking statement';
235                            $phpcsFile->addError($error, $nextBreak, 'SpacingAfterDefaultBreak');
236                        }
237                    }//end if
238
239                    $caseLine = $tokens[$nextCase]['line'];
240                    $nextLine = $tokens[$nextBreak]['line'];
241                    for ($i = ($opener + 1); $i < $nextBreak; $i++) {
242                        if ($tokens[$i]['type'] !== 'T_WHITESPACE') {
243                            $nextLine = $tokens[$i]['line'];
244                            break;
245                        }
246                    }
247
248                    if ($nextLine !== ($caseLine + 1)) {
249                        $error = 'Blank lines are not allowed after '.strtoupper($type).' statements';
250                        $phpcsFile->addError($error, $nextCase, 'SpacingAfter'.$type);
251                    }
252                }//end if
253
254                if ($tokens[$nextBreak]['code'] === T_BREAK) {
255                    if ($type === 'Case') {
256                        // Ensure empty CASE statements are not allowed.
257                        // They must have some code content in them. A comment is not enough.
258                        // But count RETURN statements as valid content if they also
259                        // happen to close the CASE statement.
260                        $foundContent = false;
261                        for ($i = ($tokens[$nextCase]['scope_opener'] + 1); $i < $nextBreak; $i++) {
262                            if ($tokens[$i]['code'] === T_CASE) {
263                                $i = $tokens[$i]['scope_opener'];
264                                continue;
265                            }
266
267                            if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$tokens[$i]['code']]) === false) {
268                                $foundContent = true;
269                                break;
270                            }
271                        }
272
273                        if ($foundContent === false) {
274                            $error = 'Empty CASE statements are not allowed';
275                            $phpcsFile->addError($error, $nextCase, 'EmptyCase');
276                        }
277                    } else {
278                        // Ensure empty DEFAULT statements are not allowed.
279                        // They must (at least) have a comment describing why
280                        // the default case is being ignored.
281                        $foundContent = false;
282                        for ($i = ($tokens[$nextCase]['scope_opener'] + 1); $i < $nextBreak; $i++) {
283                            if ($tokens[$i]['type'] !== 'T_WHITESPACE') {
284                                $foundContent = true;
285                                break;
286                            }
287                        }
288
289                        if ($foundContent === false) {
290                            $error = 'Comment required for empty DEFAULT case';
291                            $phpcsFile->addError($error, $nextCase, 'EmptyDefault');
292                        }
293                    }//end if
294                }//end if
295            } else if ($type === 'Default') {
296                $error = 'DEFAULT case must have a breaking statement';
297                $phpcsFile->addError($error, $nextCase, 'DefaultNoBreak');
298            }//end if
299        }//end while
300
301        if ($foundDefault === false) {
302            $error = 'All SWITCH statements must contain a DEFAULT case';
303            $phpcsFile->addError($error, $stackPtr, 'MissingDefault');
304        }
305
306        if ($tokens[$switch['scope_closer']]['column'] !== $switch['column']) {
307            $error = 'Closing brace of SWITCH statement must be aligned with SWITCH keyword';
308            $phpcsFile->addError($error, $switch['scope_closer'], 'CloseBraceAlign');
309        }
310
311        if ($caseCount === 0) {
312            $error = 'SWITCH statements must contain at least one CASE statement';
313            $phpcsFile->addError($error, $stackPtr, 'MissingCase');
314        }
315
316    }//end process()
317
318
319}//end class
320