Completed
Push — master ( 9b18dc...e19264 )
by Tomáš
18s
created

ensureNoBlankLinesAfterStatement()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 15
Code Lines 10

Duplication

Lines 6
Ratio 40 %

Code Coverage

Tests 9
CRAP Score 4.0961

Importance

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