RemoveBraces::removeBraces()   B
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 26
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 26
ccs 12
cts 12
cp 1
rs 8.8571
c 0
b 0
f 0
cc 3
eloc 12
nc 3
nop 1
crap 3
1
<?php
2
3
/**
4
* @package   s9e\SourceOptimizer
5
* @copyright Copyright (c) 2014-2018 The s9e Authors
6
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
7
*/
8
namespace s9e\SourceOptimizer\Passes;
9
10
/**
11
* Optimizes the control structures of a script.
12
*
13
* Removes braces in control structures wherever possible. Prevents the generation of EXT_STMT
14
* opcodes where they're not strictly required.
15
*/
16
class RemoveBraces extends AbstractPass
17
{
18
	/**
19
	* @var array Offsets of braces that need to be preserved (offsets used as keys)
20
	*/
21
	protected $preservedBraces;
22
23
	/**
24
	* {@inheritdoc}
25
	*/
26 39
	protected function optimizeStream()
27
	{
28 39
		$this->preservedBraces = [];
29 39
		$structures = [];
30 39
		while ($this->stream->valid())
31
		{
32 39
			if ($this->isControlStructure())
33
			{
34 39
				$structure = $this->parseControlStructure();
35 39
				if ($structure)
36
				{
37 35
					$structures[] = $structure;
38
				}
39
			}
40 39
			$this->stream->next();
41
		}
42
43 39
		$this->optimizeStructures($structures);
44 39
	}
45
46
	/**
47
	* Test whether the given structure's braces can be removed
48
	*
49
	* @param  array $structure
50
	* @return bool
51
	*/
52 35
	protected function canRemoveBracesFrom(array $structure)
53
	{
54 35
		return ($structure['statements'] <= 1 && !isset($this->preservedBraces[$structure['offsetRightBrace']]));
55
	}
56
57
	/**
58
	* Test whether the token at current offset is a control structure
59
	*
60
	* NOTE: we ignore T_DO since the braces are not optional
61
	*
62
	* @return bool
63
	*/
64 39
	protected function isControlStructure()
65
	{
66 39
		return $this->stream->isAny([T_ELSE, T_ELSEIF, T_FOR, T_FOREACH, T_IF, T_WHILE]);
67
	}
68
69
	/**
70
	* Test whether the token at current offset is an open curly brace
71
	*
72
	* @return bool
73
	*/
74 35
	protected function isCurlyOpen()
75
	{
76 35
		return ($this->stream->current() === '{' || $this->stream->isAny([T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES]));
77
	}
78
79
	/**
80
	* Test whether current token is followed by a function or class-related declaration
81
	*
82
	* @return bool
83
	*/
84 37
	protected function isFollowedByDeclaraction()
85
	{
86 37
		$keywords = [T_ABSTRACT, T_CLASS, T_FINAL, T_FUNCTION, T_INTERFACE, T_TRAIT];
87
88 37
		$offset = $this->stream->key();
89 37
		$this->stream->next();
90 37
		$this->stream->skipNoise();
91 37
		$isFollowedByFunction = ($this->stream->valid() && $this->stream->isAny($keywords));
92 37
		$this->stream->seek($offset);
93
94 37
		return $isFollowedByFunction;
95
	}
96
97
	/**
98
	* Optimize given T_ELSE structure
99
	*
100
	* @param  array $structure
101
	* @return void
102
	*/
103 21
	protected function optimizeElse(array $structure)
104
	{
105 21
		$this->stream->seek($structure['offsetLeftBrace']);
106 21
		$this->stream->next();
107 21
		$this->stream->skipNoise();
108 21
		if (!$this->stream->is(T_IF))
109
		{
110 17
			return;
111
		}
112 7
		$this->stream->replace([T_ELSEIF, 'elseif']);
113 7
		$this->unindentBlock($structure['offsetLeftBrace'] + 1, $structure['offsetRightBrace'] - 1);
114 7
		$this->stream->seek($structure['offsetConstruct']);
115 7
		$this->stream->remove();
116 7
		$this->removeWhitespaceBefore($this->stream->key());
117 7
	}
118
119
	/**
120
	* Optimize given structure and the other structures it contains
121
	*
122
	* @param  array $structure
123
	* @return void
124
	*/
125 35
	protected function optimizeStructure(array $structure)
126
	{
127 35
		$this->optimizeStructures($structure['structures']);
128
129 35
		if ($structure['isIf'] || $structure['isElseif'])
130
		{
131 31
			$this->markPreservedBraces($structure['offsetRightBrace']);
132
		}
133
134 35
		if ($this->canRemoveBracesFrom($structure))
135
		{
136 31
			if ($structure['isElse'])
137
			{
138 21
				$this->optimizeElse($structure);
139
			}
140 31
			$this->removeBraces($structure);
141
		}
142 35
	}
143
144
	/**
145
	* Optimize a list of parsed structures
146
	*
147
	* @param  array[] $structures
148
	* @return void
149
	*/
150 39
	protected function optimizeStructures(array $structures)
151
	{
152 39
		foreach ($structures as $structure)
153
		{
154 35
			$this->optimizeStructure($structure);
155
		}
156 39
	}
157
158
	/**
159
	* Parse the control structure starting at current offset
160
	*
161
	* @return array|false
162
	*/
163 39
	protected function parseControlStructure()
164
	{
165
		$structure = [
166 39
			'isElse'           => $this->stream->is(T_ELSE),
167 39
			'isElseif'         => $this->stream->is(T_ELSEIF),
168 39
			'isIf'             => $this->stream->is(T_IF),
169 39
			'offsetConstruct'  => $this->stream->key(),
170
			'offsetLeftBrace'  => null,
171
			'offsetRightBrace' => null,
172 39
			'statements'       => 0,
173
			'structures'       => []
174
		];
175
176 39
		if ($structure['isElse'])
177
		{
178 24
			$this->stream->next();
179
		}
180
		else
181
		{
182 39
			$this->skipParenthesizedExpression();
183
		}
184 39
		$this->stream->skipNoise();
185
186 39
		if ($this->stream->current() !== '{' || $this->isFollowedByDeclaraction())
187
		{
188 4
			return false;
189
		}
190
191 35
		$braces = 0;
192 35
		$structure['offsetLeftBrace'] = $this->stream->key();
193 35
		while ($this->stream->valid())
194
		{
195 35
			$token = $this->stream->current();
196 35
			if ($token === ';')
197
			{
198 29
				++$structure['statements'];
199
			}
200 35
			elseif ($token === '}')
201
			{
202 35
				--$braces;
203 35
				if (!$braces)
204
				{
205 35
					break;
206
				}
207
			}
208 35
			elseif ($this->isCurlyOpen())
209
			{
210 35
				++$braces;
211
			}
212 33
			elseif ($this->isControlStructure())
213
			{
214 18 View Code Duplication
				if (!$this->stream->isAny([T_ELSE, T_ELSEIF]))
215
				{
216 18
					++$structure['statements'];
217
				}
218
219 18
				$innerStructure = $this->parseControlStructure();
220 18
				if ($innerStructure)
221
				{
222 18
					$structure['structures'][] = $innerStructure;
223
				}
224
			}
225
226 35
			$this->stream->next();
227
		}
228 35
		$structure['offsetRightBrace'] = $this->stream->key();
229
230 35
		return $structure;
231
	}
232
233
	/**
234
	* Mark the offset of right braces that must be preserved
235
	*
236
	* Works by counting the number of consecutive braces between the starting point and the next
237
	* non-brace token. If the next token is a T_ELSE or T_ELSEIF and it does not immediately follow
238
	* current brace then its branch belongs to another conditional, which means the brace must be
239
	* preserved
240
	*
241
	* @param  integer $offset Offset of the first closing brace
242
	* @return void
243
	*/
244 31
	protected function markPreservedBraces($offset)
245
	{
246 31
		$braces = 0;
247 31
		$braceOffset = $offset;
248 31
		$isFollowedByElse = false;
249
250 31
		$this->stream->seek($offset);
251 31
		while ($this->stream->valid() && $this->stream->current() === '}')
252
		{
253 31
			++$braces;
254 31
			$braceOffset = $this->stream->key();
255
256 31
			$this->stream->next();
257 31
			$this->stream->skipNoise();
258 31 View Code Duplication
			if ($this->stream->valid())
259
			{
260 23
				$isFollowedByElse = $this->stream->isAny([T_ELSE, T_ELSEIF]);
261
			}
262
		}
263
264 31
		if ($isFollowedByElse && $braces > 1)
265
		{
266 5
			$this->preservedBraces[$braceOffset] = 1;
267
		}
268 31
	}
269
270
	/**
271
	* Remove braces from given structure
272
	*
273
	* @param  array $structure
274
	* @return void
275
	*/
276 31
	protected function removeBraces(array $structure)
277
	{
278
		// Replace the opening brace with a semicolon if the control structure is empty, remove the
279
		// brace if possible or replace it with whitespace otherwise (e.g. in "else foreach")
280 31
		$this->stream->seek($structure['offsetLeftBrace']);
281 31
		if (!$structure['statements'])
282
		{
283 15
			$this->stream->replace(';');
284
		}
285 25
		elseif ($this->stream->canRemoveCurrentToken())
286
		{
287 23
			$this->stream->remove();
288
		}
289
		else
290
		{
291 7
			$this->stream->replace([T_WHITESPACE, ' ']);
292
		}
293
294
		// Remove the closing brace
295 31
		$this->stream->seek($structure['offsetRightBrace']);
296 31
		$this->stream->remove();
297
298
		// Remove the whitespace before the braces if possible. This is purely cosmetic
299 31
		$this->removeWhitespaceBefore($structure['offsetLeftBrace']);
300 31
		$this->removeWhitespaceBefore($structure['offsetRightBrace']);
301 31
	}
302
303
	/**
304
	* Remove the whitespace before given offset, if possible
305
	*
306
	* @return void
307
	*/
308 31
	protected function removeWhitespaceBefore($offset)
309
	{
310 31
		if (!isset($this->stream[$offset - 1]))
311
		{
312 5
			return;
313
		}
314
315 31
		$this->stream->seek($offset - 1);
316 31
		if ($this->stream->is(T_WHITESPACE) && $this->stream->canRemoveCurrentToken())
317
		{
318 17
			$this->stream->remove();
319
		}
320 31
	}
321
322
	/**
323
	* Skip the condition of a control structure
324
	*
325
	* @return void
326
	*/
327 39
	protected function skipParenthesizedExpression()
328
	{
329
		// Reach the opening parenthesis
330 39
		$this->stream->skipToToken('(');
331
332
		// Iterate through tokens until we have a match for every left parenthesis
333 39
		$parens = 0;
334 39
		$this->stream->next();
335 39
		while ($this->stream->valid())
336
		{
337 39
			$token = $this->stream->current();
338 39
			if ($token === ')')
339
			{
340 39
				if ($parens)
341
				{
342 1
					--$parens;
343
				}
344
				else
345
				{
346
					// Skip the last parenthesis
347 39
					$this->stream->next();
348 39
					break;
349
				}
350
			}
351 39
			elseif ($token === '(')
352
			{
353 1
				++$parens;
354
			}
355 39
			$this->stream->next();
356
		}
357 39
	}
358
359
	/**
360
	* Remove one tab's worth of indentation off a range of PHP tokens
361
	*
362
	* @param  integer $start  Offset of the first token to unindent
363
	* @param  integer $end    Offset of the last token to unindent
364
	* @return void
365
	*/
366 7
	protected function unindentBlock($start, $end)
367
	{
368 7
		$this->stream->seek($start);
369 7
		$anchor = "\n";
370 7
		while ($this->stream->key() <= $end)
371
		{
372 7
			$token = $this->stream->current();
373 7
			if ($this->stream->isNoise())
374
			{
375 6
				$token[1] = preg_replace('((' . $anchor . ")(?:    |\t))", '$1', $token[1]);
376 6
				$this->stream->replace($token);
377
			}
378 7
			$anchor = ($token[0] === T_COMMENT) ? "^|\n" : "\n";
379 7
			$this->stream->next();
380
		}
381
	}
382
}