SwitchDeclarationSniff   B
last analyzed

Complexity

Total Complexity 48

Size/Duplication

Total Lines 297
Duplicated Lines 4.04 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 82.48%

Importance

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

14 Methods

Rating   Name   Duplication   Size   Complexity  
B process() 0 56 7
A checkIfKeywordIsIndented() 0 12 2
D checkBreak() 6 43 9
A areSwitchStartAndEndKnown() 0 12 3
B processSwitchStructureToken() 0 25 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
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++) {
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...
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++) {
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...
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