1<?php
2/**
3 * PSR2_Sniffs_ControlStructures_SwitchDeclarationSniff.
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 * PSR2_Sniffs_ControlStructures_SwitchDeclarationSniff.
17 *
18 * Ensures all switch statements 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 PSR2_Sniffs_ControlStructures_SwitchDeclarationSniff 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(T_SWITCH);
47
48    }//end register()
49
50
51    /**
52     * Processes this test, when one of its tokens is encountered.
53     *
54     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
55     * @param int                  $stackPtr  The position of the current token in the
56     *                                        stack passed in $tokens.
57     *
58     * @return void
59     */
60    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
61    {
62        $tokens = $phpcsFile->getTokens();
63
64        // We can't process SWITCH statements unless we know where they start and end.
65        if (isset($tokens[$stackPtr]['scope_opener']) === false
66            || isset($tokens[$stackPtr]['scope_closer']) === false
67        ) {
68            return;
69        }
70
71        $switch        = $tokens[$stackPtr];
72        $nextCase      = $stackPtr;
73        $caseAlignment = ($switch['column'] + $this->indent);
74        $caseCount     = 0;
75        $foundDefault  = false;
76
77        while (($nextCase = $this->_findNextCase($phpcsFile, ($nextCase + 1), $switch['scope_closer'])) !== false) {
78            if ($tokens[$nextCase]['code'] === T_DEFAULT) {
79                $type         = 'default';
80                $foundDefault = true;
81            } else {
82                $type = 'case';
83                $caseCount++;
84            }
85
86            if ($tokens[$nextCase]['content'] !== strtolower($tokens[$nextCase]['content'])) {
87                $expected = strtolower($tokens[$nextCase]['content']);
88                $error    = strtoupper($type).' keyword must be lowercase; expected "%s" but found "%s"';
89                $data     = array(
90                             $expected,
91                             $tokens[$nextCase]['content'],
92                            );
93
94                $fix = $phpcsFile->addFixableError($error, $nextCase, $type.'NotLower', $data);
95                if ($fix === true) {
96                    $phpcsFile->fixer->replaceToken($nextCase, $expected);
97                }
98            }
99
100            if ($type === 'case'
101                && ($tokens[($nextCase + 1)]['code'] !== T_WHITESPACE
102                || $tokens[($nextCase + 1)]['content'] !== ' ')
103            ) {
104                $error = 'CASE keyword must be followed by a single space';
105                $fix   = $phpcsFile->addFixableError($error, $nextCase, 'SpacingAfterCase');
106                if ($fix === true) {
107                    if ($tokens[($nextCase + 1)]['code'] !== T_WHITESPACE) {
108                        $phpcsFile->fixer->addContent($nextCase, ' ');
109                    } else {
110                        $phpcsFile->fixer->replaceToken(($nextCase + 1), ' ');
111                    }
112                }
113            }
114
115            $opener     = $tokens[$nextCase]['scope_opener'];
116            $nextCloser = $tokens[$nextCase]['scope_closer'];
117            if ($tokens[$opener]['code'] === T_COLON) {
118                if ($tokens[($opener - 1)]['code'] === T_WHITESPACE) {
119                    $error = 'There must be no space before the colon in a '.strtoupper($type).' statement';
120                    $fix   = $phpcsFile->addFixableError($error, $nextCase, 'SpaceBeforeColon'.strtoupper($type));
121                    if ($fix === true) {
122                        $phpcsFile->fixer->replaceToken(($opener - 1), '');
123                    }
124                }
125
126                $next = $phpcsFile->findNext(T_WHITESPACE, ($opener + 1), null, true);
127                if ($tokens[$next]['line'] === $tokens[$opener]['line']
128                    && $tokens[$next]['code'] === T_COMMENT
129                ) {
130                    // Skip comments on the same line.
131                    $next = $phpcsFile->findNext(T_WHITESPACE, ($next + 1), null, true);
132                }
133
134                if ($tokens[$next]['line'] !== ($tokens[$opener]['line'] + 1)) {
135                    $error = 'The '.strtoupper($type).' body must start on the line following the statement';
136                    $fix   = $phpcsFile->addFixableError($error, $nextCase, 'BodyOnNextLine'.strtoupper($type));
137                    if ($fix === true) {
138                        if ($tokens[$next]['line'] === $tokens[$opener]['line']) {
139                            $padding = str_repeat(' ', ($caseAlignment + $this->indent - 1));
140                            $phpcsFile->fixer->addContentBefore($next, $phpcsFile->eolChar.$padding);
141                        } else {
142                            $phpcsFile->fixer->beginChangeset();
143                            for ($i = ($opener + 1); $i < $next; $i++) {
144                                if ($tokens[$i]['line'] === $tokens[$next]['line']) {
145                                    break;
146                                }
147
148                                $phpcsFile->fixer->replaceToken($i, '');
149                            }
150
151                            $phpcsFile->fixer->addNewLineBefore($i);
152                            $phpcsFile->fixer->endChangeset();
153                        }
154                    }
155                }//end if
156
157                if ($tokens[$nextCloser]['scope_condition'] === $nextCase) {
158                    // Only need to check some things once, even if the
159                    // closer is shared between multiple case statements, or even
160                    // the default case.
161                    $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($nextCloser - 1), $nextCase, true);
162                    if ($tokens[$prev]['line'] === $tokens[$nextCloser]['line']) {
163                        $error = 'Terminating statement must be on a line by itself';
164                        $fix   = $phpcsFile->addFixableError($error, $nextCloser, 'BreakNotNewLine');
165                        if ($fix === true) {
166                            $phpcsFile->fixer->addNewLine($prev);
167                            $phpcsFile->fixer->replaceToken($nextCloser, trim($tokens[$nextCloser]['content']));
168                        }
169                    } else {
170                        $diff = ($caseAlignment + $this->indent - $tokens[$nextCloser]['column']);
171                        if ($diff !== 0) {
172                            $error = 'Terminating statement must be indented to the same level as the CASE body';
173                            $fix   = $phpcsFile->addFixableError($error, $nextCloser, 'BreakIndent');
174                            if ($fix === true) {
175                                if ($diff > 0) {
176                                    $phpcsFile->fixer->addContentBefore($nextCloser, str_repeat(' ', $diff));
177                                } else {
178                                    $phpcsFile->fixer->substrToken(($nextCloser - 1), 0, $diff);
179                                }
180                            }
181                        }
182                    }//end if
183                }//end if
184            } else {
185                $error = strtoupper($type).' statements must be defined using a colon';
186                $phpcsFile->addError($error, $nextCase, 'WrongOpener'.$type);
187            }//end if
188
189            // We only want cases from here on in.
190            if ($type !== 'case') {
191                continue;
192            }
193
194            $nextCode = $phpcsFile->findNext(
195                T_WHITESPACE,
196                ($tokens[$nextCase]['scope_opener'] + 1),
197                $nextCloser,
198                true
199            );
200
201            if ($tokens[$nextCode]['code'] !== T_CASE && $tokens[$nextCode]['code'] !== T_DEFAULT) {
202                // This case statement has content. If the next case or default comes
203                // before the closer, it means we dont have a terminating statement
204                // and instead need a comment.
205                $nextCode = $this->_findNextCase($phpcsFile, ($tokens[$nextCase]['scope_opener'] + 1), $nextCloser);
206                if ($nextCode !== false) {
207                    $prevCode = $phpcsFile->findPrevious(T_WHITESPACE, ($nextCode - 1), $nextCase, true);
208                    if ($tokens[$prevCode]['code'] !== T_COMMENT) {
209                        $error = 'There must be a comment when fall-through is intentional in a non-empty case body';
210                        $phpcsFile->addError($error, $nextCase, 'TerminatingComment');
211                    }
212                }
213            }
214        }//end while
215
216    }//end process()
217
218
219    /**
220     * Find the next CASE or DEFAULT statement from a point in the file.
221     *
222     * Note that nested switches are ignored.
223     *
224     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
225     * @param int                  $stackPtr  The position to start looking at.
226     * @param int                  $end       The position to stop looking at.
227     *
228     * @return int | bool
229     */
230    private function _findNextCase(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $end)
231    {
232        $tokens = $phpcsFile->getTokens();
233        while (($stackPtr = $phpcsFile->findNext(array(T_CASE, T_DEFAULT, T_SWITCH), $stackPtr, $end)) !== false) {
234            // Skip nested SWITCH statements; they are handled on their own.
235            if ($tokens[$stackPtr]['code'] === T_SWITCH) {
236                $stackPtr = $tokens[$stackPtr]['scope_closer'];
237                continue;
238            }
239
240            break;
241        }
242
243        return $stackPtr;
244
245    }//end _findNextCase()
246
247
248}//end class
249