ControlStructuresOptimizer::reset()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 12
ccs 4
cts 4
cp 1
rs 9.9666
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 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 26
	protected function blockEndsWithIf()
34
	{
35 26
		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 28
	protected function isControlStructure()
44
	{
45 28
		return in_array(
46 28
			$this->tokens[$this->i][0],
47 28
			[T_ELSE, T_ELSEIF, T_FOR, T_FOREACH, T_IF, T_WHILE],
48 28
			true
49
		);
50
	}
51
52
	/**
53
	* Test whether current block is followed by an elseif/else structure
54
	*
55
	* @return bool
56
	*/
57 6
	protected function isFollowedByElse()
58
	{
59 6
		if ($this->i > $this->cnt - 4)
60
		{
61
			// It doesn't have room for another block
62 3
			return false;
63
		}
64
65
		// Compute the index of the next non-whitespace token
66 3
		$k = $this->i + 1;
67
68 3
		if ($this->tokens[$k][0] === T_WHITESPACE)
69
		{
70 1
			++$k;
71
		}
72
73 3
		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 26
	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 26
		return ($this->blockEndsWithIf() && $this->isFollowedByElse());
87
	}
88
89
	/**
90
	* Optimize control structures in stored tokens
91
	*
92
	* @return void
93
	*/
94 28
	protected function optimizeTokens()
95
	{
96 28
		while (++$this->i < $this->cnt)
97
		{
98 28
			if ($this->tokens[$this->i] === ';')
99
			{
100 22
				++$this->context['statements'];
101
			}
102 28
			elseif ($this->tokens[$this->i] === '{')
103
			{
104 1
				++$this->braces;
105
			}
106 28
			elseif ($this->tokens[$this->i] === '}')
107
			{
108 28
				if ($this->context['braces'] === $this->braces)
109
				{
110 27
					$this->processEndOfBlock();
111
				}
112
113 28
				--$this->braces;
114
			}
115 28
			elseif ($this->isControlStructure())
116
			{
117 28
				$this->processControlStructure();
118
			}
119
		}
120
	}
121
122
	/**
123
	* Process the control structure starting at current index
124
	*
125
	* @return void
126
	*/
127 28
	protected function processControlStructure()
128
	{
129
		// Save the index so we can rewind back to it in case of failure
130 28
		$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 28
		if (!in_array($this->tokens[$this->i][0], [T_ELSE, T_ELSEIF], true))
135
		{
136 28
			++$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 28
		if ($this->tokens[$this->i][0] !== T_ELSE)
142
		{
143 28
			$this->skipCondition();
144
		}
145
146 28
		$this->skipWhitespace();
147
148
		// Abort if this control structure does not use braces
149 28
		if ($this->tokens[$this->i] !== '{')
150
		{
151
			// Rewind all the way to the original token
152 1
			$this->i = $savedIndex;
153
154 1
			return;
155
		}
156
157 27
		++$this->braces;
158
159
		// Replacement for the first brace
160 27
		$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 27
		if ($this->tokens[$savedIndex][0]  === T_ELSE
165 27
		 && $this->tokens[$this->i + 1][0] !== T_VARIABLE
166 27
		 && $this->tokens[$this->i + 1][0] !== T_WHITESPACE)
167
		{
168 9
			$replacement = [T_WHITESPACE, ' '];
169
		}
170
171
		// Record the token of the control structure (T_IF, T_WHILE, etc...) in the current context
172 27
		$this->context['lastBlock'] = $this->tokens[$savedIndex][0];
173
174
		// Create a new context
175 27
		$this->context = [
176 27
			'braces'      => $this->braces,
177 27
			'index'       => $this->i,
178
			'lastBlock'   => null,
179 27
			'parent'      => $this->context,
180 27
			'replacement' => $replacement,
181 27
			'savedIndex'  => $savedIndex,
182 27
			'statements'  => 0
183
		];
184
	}
185
186
	/**
187
	* Process the block ending at current index
188
	*
189
	* @return void
190
	*/
191 27
	protected function processEndOfBlock()
192
	{
193 27
		if ($this->context['statements'] < 2 && !$this->mustPreserveBraces())
194
		{
195 26
			$this->removeBracesInCurrentContext();
196
		}
197
198 27
		$this->context = $this->context['parent'];
199
200
		// Propagate the "lastBlock" property upwards to handle multiple nested if statements
201 27
		$this->context['parent']['lastBlock'] = $this->context['lastBlock'];
202
	}
203
204
	/**
205
	* Remove the braces surrounding current context
206
	*
207
	* @return void
208
	*/
209 26
	protected function removeBracesInCurrentContext()
210
	{
211
		// Replace the first brace with the saved replacement
212 26
		$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 26
		$this->tokens[$this->i] = ($this->context['statements']) ? [T_WHITESPACE, ''] : ';';
217
218
		// Remove the whitespace before braces. This is mainly cosmetic
219 26
		foreach ([$this->context['index'] - 1, $this->i - 1] as $tokenIndex)
220
		{
221 26
			if ($this->tokens[$tokenIndex][0] === T_WHITESPACE)
222
			{
223 22
				$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 26
		if ($this->tokens[$this->context['savedIndex']][0] === T_ELSE)
230
		{
231 16
			$j = 1 + $this->context['savedIndex'];
232
233 16
			while ($this->tokens[$j][0] === T_WHITESPACE
234 16
			    || $this->tokens[$j][0] === T_COMMENT
235 16
			    || $this->tokens[$j][0] === T_DOC_COMMENT)
236
			{
237 16
				++$j;
238
			}
239
240 16
			if ($this->tokens[$j][0] === T_IF)
241
			{
242
				// Replace if with elseif
243 4
				$this->tokens[$j] = [T_ELSEIF, 'elseif'];
244
245
				// Remove the original else
246 4
				$j = $this->context['savedIndex'];
247 4
				$this->tokens[$j] = [T_WHITESPACE, ''];
248
249
				// Remove any whitespace before the original else
250 4
				if ($this->tokens[$j - 1][0] === T_WHITESPACE)
251
				{
252 3
					$this->tokens[$j - 1][1] = '';
253
				}
254
255
				// Unindent what was the else's content
256 4
				$this->unindentBlock($j, $this->i - 1);
257
258
				// Ensure that the brace after the now-removed "else" was not replaced with a space
259 4
				$this->tokens[$this->context['index']] = [T_WHITESPACE, ''];
260
			}
261
		}
262
263 26
		$this->changed = true;
264
	}
265
266
	/**
267
	* {@inheritdoc}
268
	*/
269 28
	protected function reset($php)
270
	{
271 28
		parent::reset($php);
272
273 28
		$this->braces  = 0;
274 28
		$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 28
	protected function skipCondition()
290
	{
291
		// Reach the opening parenthesis
292 28
		$this->skipToString('(');
293
294
		// Iterate through tokens until we have a match for every left parenthesis
295 28
		$parens = 0;
296 28
		while (++$this->i < $this->cnt)
297
		{
298 28
			if ($this->tokens[$this->i] === ')')
299
			{
300 28
				if ($parens)
301
				{
302 1
					--$parens;
303
				}
304
				else
305
				{
306 28
					break;
307
				}
308
			}
309 28
			elseif ($this->tokens[$this->i] === '(')
310
			{
311 1
				++$parens;
312
			}
313
		}
314
	}
315
}