1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of Zenify |
5
|
|
|
* Copyright (c) 2012 Tomas Votruba (http://tomasvotruba.cz) |
6
|
|
|
*/ |
7
|
|
|
|
8
|
|
|
namespace ZenifyCodingStandard\Sniffs\ControlStructures; |
9
|
|
|
|
10
|
|
|
use PHP_CodeSniffer_File; |
11
|
|
|
use PHP_CodeSniffer_Tokens; |
12
|
|
|
use Squiz_Sniffs_ControlStructures_SwitchDeclarationSniff; |
13
|
|
|
|
14
|
|
|
|
15
|
|
|
class SwitchDeclarationSniff extends Squiz_Sniffs_ControlStructures_SwitchDeclarationSniff |
16
|
|
|
{ |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* The number of spaces code should be indented. |
20
|
|
|
* |
21
|
|
|
* @var int |
22
|
|
|
*/ |
23
|
|
|
public $indent = 1; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* @var array |
27
|
|
|
*/ |
28
|
|
|
private $token; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var array[] |
32
|
|
|
*/ |
33
|
|
|
private $tokens; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @var int |
37
|
|
|
*/ |
38
|
|
|
private $position; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* @var PHP_CodeSniffer_File |
42
|
|
|
*/ |
43
|
|
|
private $file; |
44
|
|
|
|
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* {@inheritdoc} |
48
|
|
|
*/ |
49
|
1 |
|
public function process(PHP_CodeSniffer_File $file, $position) |
50
|
|
|
{ |
51
|
1 |
|
$this->file = $file; |
52
|
1 |
|
$this->position = $position; |
53
|
|
|
|
54
|
1 |
|
$this->tokens = $tokens = $file->getTokens(); |
55
|
1 |
|
$this->token = $tokens[$position]; |
56
|
|
|
|
57
|
1 |
|
if ($this->areSwitchStartAndEndKnown() === FALSE) { |
58
|
|
|
return; |
59
|
|
|
} |
60
|
|
|
|
61
|
1 |
|
$switch = $tokens[$position]; |
62
|
1 |
|
$nextCase = $position; |
63
|
1 |
|
$caseAlignment = ($switch['column'] + $this->indent); |
64
|
1 |
|
$caseCount = 0; |
65
|
1 |
|
$foundDefault = FALSE; |
66
|
|
|
|
67
|
1 |
|
$lookFor = [T_CASE, T_DEFAULT, T_SWITCH]; |
68
|
1 |
|
while (($nextCase = $file->findNext($lookFor, ($nextCase + 1), $switch['scope_closer'])) !== FALSE) { |
69
|
|
|
// Skip nested SWITCH statements; they are handled on their own. |
70
|
1 |
|
if ($tokens[$nextCase]['code'] === T_SWITCH) { |
71
|
|
|
$nextCase = $tokens[$nextCase]['scope_closer']; |
72
|
|
|
continue; |
73
|
|
|
} |
74
|
1 |
|
if ($tokens[$nextCase]['code'] === T_DEFAULT) { |
75
|
1 |
|
$type = 'Default'; |
76
|
1 |
|
$foundDefault = TRUE; |
77
|
|
|
|
78
|
|
|
} else { |
79
|
1 |
|
$type = 'Case'; |
80
|
1 |
|
$caseCount++; |
81
|
|
|
} |
82
|
|
|
|
83
|
1 |
|
$this->checkIfKeywordIsIndented($file, $nextCase, $tokens, $type, $caseAlignment); |
84
|
1 |
|
$this->checkSpaceAfterKeyword($nextCase, $type); |
85
|
|
|
|
86
|
1 |
|
$opener = $tokens[$nextCase]['scope_opener']; |
87
|
|
|
|
88
|
1 |
|
$this->ensureNoSpaceBeforeColon($opener, $nextCase, $type); |
89
|
|
|
|
90
|
1 |
|
$nextBreak = $tokens[$nextCase]['scope_closer']; |
91
|
|
|
|
92
|
1 |
|
$allowedTokens = [T_BREAK, T_RETURN, T_CONTINUE, T_THROW, T_EXIT]; |
93
|
1 |
|
if (in_array($tokens[$nextBreak]['code'], $allowedTokens)) { |
94
|
1 |
|
$this->processSwitchStructureToken($nextBreak, $nextCase, $caseAlignment, $type, $opener); |
95
|
|
|
|
96
|
|
|
} elseif ($type === 'Default') { |
97
|
|
|
$error = 'DEFAULT case must have a breaking statement'; |
98
|
|
|
$file->addError($error, $nextCase, 'DefaultNoBreak'); |
99
|
|
|
} |
100
|
|
|
} |
101
|
|
|
|
102
|
1 |
|
$this->ensureDefaultIsPresent($foundDefault); |
103
|
1 |
|
$this->ensureClosingBraceAlignment($switch); |
104
|
1 |
|
} |
105
|
|
|
|
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* @param PHP_CodeSniffer_File $file |
109
|
|
|
* @param int $position |
110
|
|
|
* @param array $tokens |
111
|
|
|
* @param string $type |
112
|
|
|
* @param int $caseAlignment |
113
|
|
|
*/ |
114
|
1 |
|
private function checkIfKeywordIsIndented(PHP_CodeSniffer_File $file, $position, $tokens, $type, $caseAlignment) |
115
|
|
|
{ |
116
|
1 |
|
if ($tokens[$position]['column'] !== $caseAlignment) { |
117
|
1 |
|
$error = strtoupper($type) . ' keyword must be indented ' . $this->indent . ' spaces from SWITCH keyword'; |
118
|
1 |
|
$file->addError($error, $position, $type . 'Indent'); |
119
|
|
|
} |
120
|
1 |
|
} |
121
|
|
|
|
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* @param int $nextCase |
125
|
|
|
* @param int $nextBreak |
126
|
|
|
* @param string $type |
127
|
|
|
*/ |
128
|
1 |
|
private function checkBreak($nextCase, $nextBreak, $type) |
129
|
|
|
{ |
130
|
1 |
|
if ($type === 'Case') { |
131
|
|
|
// Ensure empty CASE statements are not allowed. |
132
|
|
|
// They must have some code content in them. A comment is not enough. |
133
|
|
|
// But count RETURN statements as valid content if they also |
134
|
|
|
// happen to close the CASE statement. |
135
|
1 |
|
$foundContent = FALSE; |
136
|
1 |
|
for ($i = ($this->tokens[$nextCase]['scope_opener'] + 1); $i < $nextBreak; $i++) { |
137
|
1 |
|
if ($this->tokens[$i]['code'] === T_CASE) { |
138
|
|
|
$i = $this->tokens[$i]['scope_opener']; |
139
|
|
|
continue; |
140
|
|
|
} |
141
|
|
|
|
142
|
1 |
|
$tokenCode = $this->tokens[$i]['code']; |
143
|
1 |
|
$emptyTokens = PHP_CodeSniffer_Tokens::$emptyTokens; |
144
|
1 |
|
if (in_array($tokenCode, $emptyTokens) === FALSE) { |
145
|
1 |
|
$foundContent = TRUE; |
146
|
1 |
|
break; |
147
|
|
|
} |
148
|
|
|
} |
149
|
1 |
|
if ($foundContent === FALSE) { |
150
|
1 |
|
$error = 'Empty CASE statements are not allowed'; |
151
|
1 |
|
$this->file->addError($error, $nextCase, 'EmptyCase'); |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
} else { |
155
|
|
|
// Ensure empty DEFAULT statements are not allowed. |
156
|
|
|
// They must (at least) have a comment describing why |
157
|
|
|
// the default case is being ignored. |
158
|
1 |
|
$foundContent = FALSE; |
159
|
1 |
View Code Duplication |
for ($i = ($this->tokens[$nextCase]['scope_opener'] + 1); $i < $nextBreak; $i++) { |
|
|
|
|
160
|
1 |
|
if ($this->tokens[$i]['type'] !== 'T_WHITESPACE') { |
161
|
1 |
|
$foundContent = TRUE; |
162
|
1 |
|
break; |
163
|
|
|
} |
164
|
|
|
} |
165
|
1 |
|
if ($foundContent === FALSE) { |
166
|
|
|
$error = 'Comment required for empty DEFAULT case'; |
167
|
|
|
$this->file->addError($error, $nextCase, 'EmptyDefault'); |
168
|
|
|
} |
169
|
|
|
} |
170
|
1 |
|
} |
171
|
|
|
|
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* @return bool |
175
|
|
|
*/ |
176
|
1 |
|
private function areSwitchStartAndEndKnown() |
177
|
|
|
{ |
178
|
1 |
|
if ( ! isset($this->tokens[$this->position]['scope_opener'])) { |
179
|
|
|
return FALSE; |
180
|
|
|
} |
181
|
|
|
|
182
|
1 |
|
if ( ! isset($this->tokens[$this->position]['scope_closer'])) { |
183
|
|
|
return FALSE; |
184
|
|
|
} |
185
|
|
|
|
186
|
1 |
|
return TRUE; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* @param int $nextBreak |
192
|
|
|
* @param int $nextCase |
193
|
|
|
* @param int $caseAlignment |
194
|
|
|
* @param string $type |
195
|
|
|
* @param int $opener |
196
|
|
|
*/ |
197
|
1 |
|
private function processSwitchStructureToken($nextBreak, $nextCase, $caseAlignment, $type, $opener) |
198
|
|
|
{ |
199
|
1 |
|
if ($this->tokens[$nextBreak]['scope_condition'] === $nextCase) { |
200
|
1 |
|
$this->ensureCaseIndention($nextBreak, $caseAlignment); |
201
|
|
|
|
202
|
1 |
|
$this->ensureNoBlankLinesBeforeBreak($nextBreak); |
203
|
|
|
|
204
|
1 |
|
$breakLine = $this->tokens[$nextBreak]['line']; |
205
|
1 |
|
$nextLine = $this->getNextLineFromNextBreak($nextBreak); |
206
|
1 |
|
if ($type !== 'Case') { |
207
|
1 |
|
$this->ensureBreakIsNotFollowedByBlankLine($nextLine, $breakLine, $nextBreak); |
208
|
|
|
} |
209
|
|
|
|
210
|
1 |
|
$this->ensureNoBlankLinesAfterStatement($nextCase, $nextBreak, $type, $opener); |
211
|
|
|
} |
212
|
|
|
|
213
|
1 |
|
if ($this->tokens[$nextBreak]['code'] === T_BREAK) { |
214
|
1 |
|
$this->checkBreak($nextCase, $nextBreak, $type); |
215
|
|
|
} |
216
|
1 |
|
} |
217
|
|
|
|
218
|
|
|
|
219
|
|
|
/** |
220
|
|
|
* @param int $nextLine |
221
|
|
|
* @param int $breakLine |
222
|
|
|
* @param int $nextBreak |
223
|
|
|
*/ |
224
|
1 |
|
private function ensureBreakIsNotFollowedByBlankLine($nextLine, $breakLine, $nextBreak) |
225
|
|
|
{ |
226
|
1 |
|
if ($nextLine !== ($breakLine + 1)) { |
227
|
|
|
$error = 'Blank lines are not allowed after the DEFAULT case\'s breaking statement'; |
228
|
|
|
$this->file->addError($error, $nextBreak, 'SpacingAfterDefaultBreak'); |
229
|
|
|
} |
230
|
1 |
|
} |
231
|
|
|
|
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* @param int $nextBreak |
235
|
|
|
*/ |
236
|
1 |
|
private function ensureNoBlankLinesBeforeBreak($nextBreak) |
237
|
|
|
{ |
238
|
1 |
|
$prev = $this->file->findPrevious(T_WHITESPACE, ($nextBreak - 1), $this->position, TRUE); |
239
|
1 |
|
if ($this->tokens[$prev]['line'] !== ($this->tokens[$nextBreak]['line'] - 1)) { |
240
|
|
|
$error = 'Blank lines are not allowed before case breaking statements'; |
241
|
|
|
$this->file->addError($error, $nextBreak, 'SpacingBeforeBreak'); |
242
|
|
|
} |
243
|
1 |
|
} |
244
|
|
|
|
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* @param int $nextCase |
248
|
|
|
* @param int $nextBreak |
249
|
|
|
* @param string $type |
250
|
|
|
* @param int $opener |
251
|
|
|
*/ |
252
|
1 |
|
private function ensureNoBlankLinesAfterStatement($nextCase, $nextBreak, $type, $opener) |
253
|
|
|
{ |
254
|
1 |
|
$caseLine = $this->tokens[$nextCase]['line']; |
255
|
1 |
|
$nextLine = $this->tokens[$nextBreak]['line']; |
256
|
1 |
View Code Duplication |
for ($i = ($opener + 1); $i < $nextBreak; $i++) { |
|
|
|
|
257
|
1 |
|
if ($this->tokens[$i]['type'] !== 'T_WHITESPACE') { |
258
|
1 |
|
$nextLine = $this->tokens[$i]['line']; |
259
|
1 |
|
break; |
260
|
|
|
} |
261
|
|
|
} |
262
|
1 |
|
if ($nextLine !== ($caseLine + 1)) { |
263
|
|
|
$error = 'Blank lines are not allowed after ' . strtoupper($type) . ' statements'; |
264
|
|
|
$this->file->addError($error, $nextCase, 'SpacingAfter' . $type); |
265
|
|
|
} |
266
|
1 |
|
} |
267
|
|
|
|
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* @param int $nextBreak |
271
|
|
|
* @return int |
272
|
|
|
*/ |
273
|
1 |
|
private function getNextLineFromNextBreak($nextBreak) |
274
|
|
|
{ |
275
|
1 |
|
$semicolon = $this->file->findNext(T_SEMICOLON, $nextBreak); |
276
|
1 |
|
for ($i = ($semicolon + 1); $i < $this->tokens[$this->position]['scope_closer']; $i++) { |
277
|
1 |
|
if ($this->tokens[$i]['type'] !== 'T_WHITESPACE') { |
278
|
1 |
|
return $this->tokens[$i]['line']; |
279
|
|
|
} |
280
|
|
|
} |
281
|
|
|
|
282
|
1 |
|
return $this->tokens[$this->tokens[$this->position]['scope_closer']]['line']; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
|
286
|
|
|
/** |
287
|
|
|
* @param int $nextBreak |
288
|
|
|
* @param int $caseAlignment |
289
|
|
|
*/ |
290
|
1 |
|
private function ensureCaseIndention($nextBreak, $caseAlignment) |
291
|
|
|
{ |
292
|
|
|
// Only need to check a couple of things once, even if the |
293
|
|
|
// break is shared between multiple case statements, or even |
294
|
|
|
// the default case. |
295
|
1 |
|
if (($this->tokens[$nextBreak]['column'] - 1) !== $caseAlignment) { |
296
|
1 |
|
$error = 'Case breaking statement must be indented ' . ($this->indent + 1) . ' tabs from SWITCH keyword'; |
297
|
1 |
|
$this->file->addError($error, $nextBreak, 'BreakIndent'); |
298
|
|
|
} |
299
|
1 |
|
} |
300
|
|
|
|
301
|
|
|
|
302
|
|
|
/** |
303
|
|
|
* @param bool $foundDefault |
304
|
|
|
*/ |
305
|
1 |
|
private function ensureDefaultIsPresent($foundDefault) |
306
|
|
|
{ |
307
|
1 |
|
if ($foundDefault === FALSE) { |
308
|
1 |
|
$error = 'All SWITCH statements must contain a DEFAULT case'; |
309
|
1 |
|
$this->file->addError($error, $this->position, 'MissingDefault'); |
310
|
|
|
} |
311
|
1 |
|
} |
312
|
|
|
|
313
|
|
|
|
314
|
1 |
|
private function ensureClosingBraceAlignment(array $switch) |
315
|
|
|
{ |
316
|
1 |
|
if ($this->tokens[$switch['scope_closer']]['column'] !== $switch['column']) { |
317
|
|
|
$error = 'Closing brace of SWITCH statement must be aligned with SWITCH keyword'; |
318
|
|
|
$this->file->addError($error, $switch['scope_closer'], 'CloseBraceAlign'); |
319
|
|
|
} |
320
|
1 |
|
} |
321
|
|
|
|
322
|
|
|
|
323
|
|
|
/** |
324
|
|
|
* @param string $opener |
325
|
|
|
* @param int $nextCase |
326
|
|
|
* @param string $type |
327
|
|
|
*/ |
328
|
1 |
|
private function ensureNoSpaceBeforeColon($opener, $nextCase, $type) |
329
|
|
|
{ |
330
|
1 |
|
if ($this->tokens[($opener - 1)]['type'] === 'T_WHITESPACE') { |
331
|
|
|
$error = 'There must be no space before the colon in a ' . strtoupper($type) . ' statement'; |
332
|
|
|
$this->file->addError($error, $nextCase, 'SpaceBeforeColon' . $type); |
333
|
|
|
} |
334
|
1 |
|
} |
335
|
|
|
|
336
|
|
|
|
337
|
|
|
/** |
338
|
|
|
* @param int $nextCase |
339
|
|
|
* @param string $type |
340
|
|
|
*/ |
341
|
1 |
|
private function checkSpaceAfterKeyword($nextCase, $type) |
342
|
|
|
{ |
343
|
1 |
|
if ($type === 'Case' && ($this->tokens[($nextCase + 1)]['type'] !== 'T_WHITESPACE' |
344
|
1 |
|
|| $this->tokens[($nextCase + 1)]['content'] !== ' ') |
345
|
|
|
) { |
346
|
|
|
$error = 'CASE keyword must be followed by a single space'; |
347
|
|
|
$this->file->addError($error, $nextCase, 'SpacingAfterCase'); |
348
|
|
|
} |
349
|
1 |
|
} |
350
|
|
|
|
351
|
|
|
} |
352
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.