Completed
Push — master ( d91fed...fd66aa )
by Josh
17:36
created

removeBracesInCurrentContext()   B

Complexity

Conditions 10
Paths 24

Size

Total Lines 56

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 56
rs 7.0933
c 0
b 0
f 0
cc 10
nc 24
nop 0

How to fix   Long Method    Complexity   

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
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 2010-2018 The s9e Authors
6
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
7
*/
8
namespace s9e\TextFormatter\Configurator\RendererGenerators\PHP;
9
10
/**
11
* Optimize the control structures of a script
12
*
13
* Removes brackets in control structures wherever possible. Prevents the generation of EXT_STMT
14
* opcodes where they're not strictly required.
15
*/
16
class ControlStructuresOptimizer extends AbstractOptimizer
17
{
18
	/**
19
	* @var integer Number of braces encountered in current source
20
	*/
21
	protected $braces;
22
23
	/**
24
	* @var array Current context
25
	*/
26
	protected $context;
27
28
	/**
29
	* Test whether current block ends with an if or elseif control structure
30
	*
31
	* @return bool
32
	*/
33
	protected function blockEndsWithIf()
34
	{
35
		return in_array($this->context['lastBlock'], [T_IF, T_ELSEIF], true);
36
	}
37
38
	/**
39
	* Test whether the token at current index is a control structure
40
	*
41
	* @return bool
42
	*/
43
	protected function isControlStructure()
44
	{
45
		return in_array(
46
			$this->tokens[$this->i][0],
47
			[T_ELSE, T_ELSEIF, T_FOR, T_FOREACH, T_IF, T_WHILE],
48
			true
49
		);
50
	}
51
52
	/**
53
	* Test whether current block is followed by an elseif/else structure
54
	*
55
	* @return bool
56
	*/
57
	protected function isFollowedByElse()
58
	{
59
		if ($this->i > $this->cnt - 4)
60
		{
61
			// It doesn't have room for another block
62
			return false;
63
		}
64
65
		// Compute the index of the next non-whitespace token
66
		$k = $this->i + 1;
67
68
		if ($this->tokens[$k][0] === T_WHITESPACE)
69
		{
70
			++$k;
71
		}
72
73
		return in_array($this->tokens[$k][0], [T_ELSEIF, T_ELSE], true);
74
	}
75
76
	/**
77
	* Test whether braces must be preserved in current context
78
	*
79
	* @return bool
80
	*/
81
	protected function mustPreserveBraces()
82
	{
83
		// If current block ends with if/elseif and is followed by elseif/else, we must preserve
84
		// its braces to prevent it from merging with the outer elseif/else. IOW, we must preserve
85
		// the braces if "if{if{}}else" would become "if{if else}"
86
		return ($this->blockEndsWithIf() && $this->isFollowedByElse());
87
	}
88
89
	/**
90
	* Optimize control structures in stored tokens
91
	*
92
	* @return void
93
	*/
94
	protected function optimizeTokens()
95
	{
96
		while (++$this->i < $this->cnt)
97
		{
98
			if ($this->tokens[$this->i] === ';')
99
			{
100
				++$this->context['statements'];
101
			}
102
			elseif ($this->tokens[$this->i] === '{')
103
			{
104
				++$this->braces;
105
			}
106
			elseif ($this->tokens[$this->i] === '}')
107
			{
108
				if ($this->context['braces'] === $this->braces)
109
				{
110
					$this->processEndOfBlock();
111
				}
112
113
				--$this->braces;
114
			}
115
			elseif ($this->isControlStructure())
116
			{
117
				$this->processControlStructure();
118
			}
119
		}
120
	}
121
122
	/**
123
	* Process the control structure starting at current index
124
	*
125
	* @return void
126
	*/
127
	protected function processControlStructure()
128
	{
129
		// Save the index so we can rewind back to it in case of failure
130
		$savedIndex = $this->i;
131
132
		// Count this control structure in this context's statements unless it's an elseif/else
133
		// in which case it's already been counted as part of the if
134
		if (!in_array($this->tokens[$this->i][0], [T_ELSE, T_ELSEIF], true))
135
		{
136
			++$this->context['statements'];
137
		}
138
139
		// If the control structure is anything but an "else", skip its condition to reach the first
140
		// brace or statement
141
		if ($this->tokens[$this->i][0] !== T_ELSE)
142
		{
143
			$this->skipCondition();
144
		}
145
146
		$this->skipWhitespace();
147
148
		// Abort if this control structure does not use braces
149
		if ($this->tokens[$this->i] !== '{')
150
		{
151
			// Rewind all the way to the original token
152
			$this->i = $savedIndex;
153
154
			return;
155
		}
156
157
		++$this->braces;
158
159
		// Replacement for the first brace
160
		$replacement = [T_WHITESPACE, ''];
161
162
		// Add a space after "else" if the brace is removed and it's not followed by whitespace or a
163
		// variable
164
		if ($this->tokens[$savedIndex][0]  === T_ELSE
165
		 && $this->tokens[$this->i + 1][0] !== T_VARIABLE
166
		 && $this->tokens[$this->i + 1][0] !== T_WHITESPACE)
167
		{
168
			$replacement = [T_WHITESPACE, ' '];
169
		}
170
171
		// Record the token of the control structure (T_IF, T_WHILE, etc...) in the current context
172
		$this->context['lastBlock'] = $this->tokens[$savedIndex][0];
173
174
		// Create a new context
175
		$this->context = [
176
			'braces'      => $this->braces,
177
			'index'       => $this->i,
178
			'lastBlock'   => null,
179
			'parent'      => $this->context,
180
			'replacement' => $replacement,
181
			'savedIndex'  => $savedIndex,
182
			'statements'  => 0
183
		];
184
	}
185
186
	/**
187
	* Process the block ending at current index
188
	*
189
	* @return void
190
	*/
191
	protected function processEndOfBlock()
192
	{
193
		if ($this->context['statements'] < 2 && !$this->mustPreserveBraces())
194
		{
195
			$this->removeBracesInCurrentContext();
196
		}
197
198
		$this->context = $this->context['parent'];
199
200
		// Propagate the "lastBlock" property upwards to handle multiple nested if statements
201
		$this->context['parent']['lastBlock'] = $this->context['lastBlock'];
202
	}
203
204
	/**
205
	* Remove the braces surrounding current context
206
	*
207
	* @return void
208
	*/
209
	protected function removeBracesInCurrentContext()
210
	{
211
		// Replace the first brace with the saved replacement
212
		$this->tokens[$this->context['index']] = $this->context['replacement'];
213
214
		// Remove the second brace or replace it with a semicolon if there are no statements in this
215
		// block
216
		$this->tokens[$this->i] = ($this->context['statements']) ? [T_WHITESPACE, ''] : ';';
217
218
		// Remove the whitespace before braces. This is mainly cosmetic
219
		foreach ([$this->context['index'] - 1, $this->i - 1] as $tokenIndex)
220
		{
221
			if ($this->tokens[$tokenIndex][0] === T_WHITESPACE)
222
			{
223
				$this->tokens[$tokenIndex][1] = '';
224
			}
225
		}
226
227
		// Test whether the current block followed an else statement then test whether this
228
		// else was followed by an if
229
		if ($this->tokens[$this->context['savedIndex']][0] === T_ELSE)
230
		{
231
			$j = 1 + $this->context['savedIndex'];
232
233
			while ($this->tokens[$j][0] === T_WHITESPACE
234
			    || $this->tokens[$j][0] === T_COMMENT
235
			    || $this->tokens[$j][0] === T_DOC_COMMENT)
236
			{
237
				++$j;
238
			}
239
240
			if ($this->tokens[$j][0] === T_IF)
241
			{
242
				// Replace if with elseif
243
				$this->tokens[$j] = [T_ELSEIF, 'elseif'];
244
245
				// Remove the original else
246
				$j = $this->context['savedIndex'];
247
				$this->tokens[$j] = [T_WHITESPACE, ''];
248
249
				// Remove any whitespace before the original else
250
				if ($this->tokens[$j - 1][0] === T_WHITESPACE)
251
				{
252
					$this->tokens[$j - 1][1] = '';
253
				}
254
255
				// Unindent what was the else's content
256
				$this->unindentBlock($j, $this->i - 1);
257
258
				// Ensure that the brace after the now-removed "else" was not replaced with a space
259
				$this->tokens[$this->context['index']] = [T_WHITESPACE, ''];
260
			}
261
		}
262
263
		$this->changed = true;
264
	}
265
266
	/**
267
	* {@inheritdoc}
268
	*/
269
	protected function reset($php)
270
	{
271
		parent::reset($php);
272
273
		$this->braces  = 0;
274
		$this->context = [
275
			'braces'      => 0,
276
			'index'       => -1,
277
			'parent'      => [],
278
			'preventElse' => false,
279
			'savedIndex'  => 0,
280
			'statements'  => 0
281
		];
282
	}
283
284
	/**
285
	* Skip the condition of a control structure
286
	*
287
	* @return void
288
	*/
289
	protected function skipCondition()
290
	{
291
		// Reach the opening parenthesis
292
		$this->skipToString('(');
293
294
		// Iterate through tokens until we have a match for every left parenthesis
295
		$parens = 0;
296
		while (++$this->i < $this->cnt)
297
		{
298
			if ($this->tokens[$this->i] === ')')
299
			{
300
				if ($parens)
301
				{
302
					--$parens;
303
				}
304
				else
305
				{
306
					break;
307
				}
308
			}
309
			elseif ($this->tokens[$this->i] === '(')
310
			{
311
				++$parens;
312
			}
313
		}
314
	}
315
}