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