SwitchDeclarationSniff::process()   B
last analyzed

Complexity

Conditions 7
Paths 9

Size

Total Lines 56
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 7.2269

Importance

Changes 0
Metric Value
dl 0
loc 56
ccs 30
cts 36
cp 0.8333
rs 7.7489
c 0
b 0
f 0
cc 7
eloc 36
nc 9
nop 2
crap 7.2269

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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