Completed
Pull Request — master (#41)
by Tomáš
10:18 queued 07:10
created

ensureClosingBraceAlignment()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.2559

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 7
ccs 3
cts 5
cp 0.6
rs 9.4285
cc 2
eloc 4
nc 2
nop 1
crap 2.2559
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(PHP_CodeSniffer_File $file, int $position, array $tokens, string $type, int $caseAlignment)
110
	{
111 1
		if ($tokens[$position]['column'] !== $caseAlignment) {
112 1
			$error = strtoupper($type) . ' keyword must be indented ' . $this->indent . ' spaces from SWITCH keyword';
113 1
			$file->addError($error, $position, $type . 'Indent');
114
		}
115 1
	}
116
117
118 1
	private function checkBreak(int $nextCase, int $nextBreak, string $type)
119
	{
120 1
		if ($type === 'Case') {
121
			// Ensure empty CASE statements are not allowed.
122
			// They must have some code content in them. A comment is not enough.
123
			// But count RETURN statements as valid content if they also
124
			// happen to close the CASE statement.
125 1
			$foundContent = FALSE;
126 1
			for ($i = ($this->tokens[$nextCase]['scope_opener'] + 1); $i < $nextBreak; $i++) {
127 1
				if ($this->tokens[$i]['code'] === T_CASE) {
128
					$i = $this->tokens[$i]['scope_opener'];
129
					continue;
130
				}
131
132 1
				$tokenCode = $this->tokens[$i]['code'];
133 1
				$emptyTokens = PHP_CodeSniffer_Tokens::$emptyTokens;
134 1
				if (in_array($tokenCode, $emptyTokens) === FALSE) {
135 1
					$foundContent = TRUE;
136 1
					break;
137
				}
138
			}
139 1
			if ($foundContent === FALSE) {
140 1
				$error = 'Empty CASE statements are not allowed';
141 1
				$this->file->addError($error, $nextCase, 'EmptyCase');
142
			}
143
144
		} else {
145
			// Ensure empty DEFAULT statements are not allowed.
146
			// They must (at least) have a comment describing why
147
			// the default case is being ignored.
148 1
			$foundContent = FALSE;
149 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...
150 1
				if ($this->tokens[$i]['type'] !== 'T_WHITESPACE') {
151 1
					$foundContent = TRUE;
152 1
					break;
153
				}
154
			}
155 1
			if ($foundContent === FALSE) {
156
				$error = 'Comment required for empty DEFAULT case';
157
				$this->file->addError($error, $nextCase, 'EmptyDefault');
158
			}
159
		}
160 1
	}
161
162
163 1
	private function areSwitchStartAndEndKnown() : bool
164
	{
165 1
		if ( ! isset($this->tokens[$this->position]['scope_opener'])) {
166
			return FALSE;
167
		}
168
169 1
		if ( ! isset($this->tokens[$this->position]['scope_closer'])) {
170
			return FALSE;
171
		}
172
173 1
		return TRUE;
174
	}
175
176
177 1
	private function processSwitchStructureToken(int $nextBreak, int $nextCase, int $caseAlignment, string $type, int $opener)
178
	{
179 1
		if ($this->tokens[$nextBreak]['scope_condition'] === $nextCase) {
180 1
			$this->ensureCaseIndention($nextBreak, $caseAlignment);
181
182 1
			$this->ensureNoBlankLinesBeforeBreak($nextBreak);
183
184 1
			$breakLine = $this->tokens[$nextBreak]['line'];
185 1
			$nextLine = $this->getNextLineFromNextBreak($nextBreak);
186 1
			if ($type !== 'Case') {
187 1
				$this->ensureBreakIsNotFollowedByBlankLine($nextLine, $breakLine, $nextBreak);
188
			}
189
190 1
			$this->ensureNoBlankLinesAfterStatement($nextCase, $nextBreak, $type, $opener);
191
		}
192
193 1
		if ($this->tokens[$nextBreak]['code'] === T_BREAK) {
194 1
			$this->checkBreak($nextCase, $nextBreak, $type);
195
		}
196 1
	}
197
198
199 1
	private function ensureBreakIsNotFollowedByBlankLine(int $nextLine, int $breakLine, int $nextBreak)
200
	{
201 1
		if ($nextLine !== ($breakLine + 1)) {
202
			$error = 'Blank lines are not allowed after the DEFAULT case\'s breaking statement';
203
			$this->file->addError($error, $nextBreak, 'SpacingAfterDefaultBreak');
204
		}
205 1
	}
206
207
208 1
	private function ensureNoBlankLinesBeforeBreak(int $nextBreak)
209
	{
210 1
		$prev = $this->file->findPrevious(T_WHITESPACE, ($nextBreak - 1), $this->position, TRUE);
211 1
		if ($this->tokens[$prev]['line'] !== ($this->tokens[$nextBreak]['line'] - 1)) {
212
			$error = 'Blank lines are not allowed before case breaking statements';
213
			$this->file->addError($error, $nextBreak, 'SpacingBeforeBreak');
214
		}
215 1
	}
216
217
218 1
	private function ensureNoBlankLinesAfterStatement(int $nextCase, int $nextBreak, string $type, int $opener)
219
	{
220 1
		$caseLine = $this->tokens[$nextCase]['line'];
221 1
		$nextLine = $this->tokens[$nextBreak]['line'];
222 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...
223 1
			if ($this->tokens[$i]['type'] !== 'T_WHITESPACE') {
224 1
				$nextLine = $this->tokens[$i]['line'];
225 1
				break;
226
			}
227
		}
228 1
		if ($nextLine !== ($caseLine + 1)) {
229
			$error = 'Blank lines are not allowed after ' . strtoupper($type) . ' statements';
230
			$this->file->addError($error, $nextCase, 'SpacingAfter' . $type);
231
		}
232 1
	}
233
234
235 1
	private function getNextLineFromNextBreak(int $nextBreak) : int
236
	{
237 1
		$semicolon = $this->file->findNext(T_SEMICOLON, $nextBreak);
238 1
		for ($i = ($semicolon + 1); $i < $this->tokens[$this->position]['scope_closer']; $i++) {
239 1
			if ($this->tokens[$i]['type'] !== 'T_WHITESPACE') {
240 1
				return $this->tokens[$i]['line'];
241
			}
242
		}
243
244 1
		return $this->tokens[$this->tokens[$this->position]['scope_closer']]['line'];
245
	}
246
247
248 1
	private function ensureCaseIndention(int $nextBreak, int $caseAlignment)
249
	{
250
		// Only need to check a couple of things once, even if the
251
		// break is shared between multiple case statements, or even
252
		// the default case.
253 1
		if (($this->tokens[$nextBreak]['column'] - 1) !== $caseAlignment) {
254 1
			$error = 'Case breaking statement must be indented ' . ($this->indent + 1) . ' tabs from SWITCH keyword';
255 1
			$this->file->addError($error, $nextBreak, 'BreakIndent');
256
		}
257 1
	}
258
259
260 1
	private function ensureDefaultIsPresent(bool $foundDefault)
261
	{
262 1
		if ($foundDefault === FALSE) {
263 1
			$error = 'All SWITCH statements must contain a DEFAULT case';
264 1
			$this->file->addError($error, $this->position, 'MissingDefault');
265
		}
266 1
	}
267
268
269 1
	private function ensureClosingBraceAlignment(array $switch)
270
	{
271 1
		if ($this->tokens[$switch['scope_closer']]['column'] !== $switch['column']) {
272
			$error = 'Closing brace of SWITCH statement must be aligned with SWITCH keyword';
273
			$this->file->addError($error, $switch['scope_closer'], 'CloseBraceAlign');
274
		}
275 1
	}
276
277
278 1
	private function ensureNoSpaceBeforeColon(int $opener, int $nextCase, string $type)
279
	{
280 1
		if ($this->tokens[($opener - 1)]['type'] === 'T_WHITESPACE') {
281
			$error = 'There must be no space before the colon in a ' . strtoupper($type) . ' statement';
282
			$this->file->addError($error, $nextCase, 'SpaceBeforeColon' . $type);
283
		}
284 1
	}
285
286
287 1
	private function checkSpaceAfterKeyword(int $nextCase, string $type)
288
	{
289 1
		if ($type === 'Case' && ($this->tokens[($nextCase + 1)]['type'] !== 'T_WHITESPACE'
290 1
			|| $this->tokens[($nextCase + 1)]['content'] !== ' ')
291
		) {
292
			$error = 'CASE keyword must be followed by a single space';
293
			$this->file->addError($error, $nextCase, 'SpacingAfterCase');
294
		}
295 1
	}
296
297
}
298