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  */
31 class 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