Completed
Pull Request — master (#41)
by Tomáš
07:50 queued 04:44
created

SwitchDeclarationSniff   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 337
Duplicated Lines 3.56 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 82.48%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 48
c 1
b 0
f 0
lcom 1
cbo 3
dl 12
loc 337
ccs 113
cts 137
cp 0.8248
rs 8.4864

14 Methods

Rating   Name   Duplication   Size   Complexity  
B process() 0 56 7
A checkIfKeywordIsIndented() 0 7 2
D checkBreak() 6 43 9
A areSwitchStartAndEndKnown() 0 12 3
A processSwitchStructureToken() 0 20 4
A ensureBreakIsNotFollowedByBlankLine() 0 7 2
A ensureNoBlankLinesBeforeBreak() 0 8 2
A ensureNoBlankLinesAfterStatement() 6 15 4
A getNextLineFromNextBreak() 0 11 3
A ensureCaseIndention() 0 10 2
A ensureDefaultIsPresent() 0 7 2
A ensureClosingBraceAlignment() 0 7 2
A ensureNoSpaceBeforeColon() 0 7 2
A checkSpaceAfterKeyword() 0 9 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like SwitchDeclarationSniff often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SwitchDeclarationSniff, and based on these observations, apply Extract Interface, too.

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++) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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++) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
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