BranchOutputOptimizer   F
last analyzed

Complexity

Total Complexity 63

Size/Duplication

Total Lines 456
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 63
eloc 141
dl 0
loc 456
ccs 143
cts 143
cp 1
rs 3.36
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A serializeToken() 0 3 2
B skipOutputAssignment() 0 16 7
A mergeOutput() 0 23 5
A captureStructure() 0 13 2
A optimizeBranchesOutput() 0 23 6
B captureOutputExpression() 0 28 7
A captureOutput() 0 13 3
A serializeIfBlock() 0 3 1
A optimizeBranchesTail() 0 3 1
A parseIfBlock() 0 13 3
B parseBranch() 0 62 8
A optimizeBranchesHead() 0 19 4
A serializeBranch() 0 12 5
A isBranchToken() 0 3 1
A optimize() 0 23 3
A serializeOutput() 0 8 2
A mergeIfBranches() 0 25 3

How to fix   Complexity   

Complex Class

Complex classes like BranchOutputOptimizer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BranchOutputOptimizer, and based on these observations, apply Extract Interface, too.

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
class BranchOutputOptimizer
11
{
12
	/**
13
	* @var integer Number of tokens
14
	*/
15
	protected $cnt;
16
17
	/**
18
	* @var integer Current token index
19
	*/
20
	protected $i;
21
22
	/**
23
	* @var array Tokens from current source
24
	*/
25
	protected $tokens;
26
27
	/**
28
	* Optimize the code used to output content
29
	*
30
	* This method will go through the array of tokens, identify if/elseif/else blocks that contain
31
	* identical code at the beginning or the end and move the common code outside of the block
32
	*
33
	* @param  array $tokens Array of tokens from token_get_all()
34
	* @return string        Optimized code
35
	*/
36 12
	public function optimize(array $tokens)
37
	{
38 12
		$this->tokens = $tokens;
39 12
		$this->i      = 0;
40 12
		$this->cnt    = count($this->tokens);
41
42 12
		$php = '';
43 12
		while (++$this->i < $this->cnt)
44
		{
45 12
			if ($this->tokens[$this->i][0] === T_IF)
46
			{
47 12
				$php .= $this->serializeIfBlock($this->parseIfBlock());
48
			}
49
			else
50
			{
51 2
				$php .= $this->serializeToken($this->tokens[$this->i]);
52
			}
53
		}
54
55
		// Free the memory taken up by the tokens
56 12
		unset($this->tokens);
57
58 12
		return $php;
59
	}
60
61
	/**
62
	* Capture the expressions used in any number of consecutive output statements
63
	*
64
	* Starts looking at current index. Ends at the first token that's not part of an output
65
	* statement
66
	*
67
	* @return string[]
68
	*/
69 12
	protected function captureOutput()
70
	{
71 12
		$expressions = [];
72 12
		while ($this->skipOutputAssignment())
73
		{
74
			do
75
			{
76 12
				$expressions[] = $this->captureOutputExpression();
77
			}
78 12
			while ($this->tokens[$this->i++] === '.');
79
		}
80
81 12
		return $expressions;
82
	}
83
84
	/**
85
	* Capture an expression used in output at current index
86
	*
87
	* Ends on "." or ";"
88
	*
89
	* @return string
90
	*/
91 12
	protected function captureOutputExpression()
92
	{
93 12
		$parens = 0;
94 12
		$php = '';
95
		do
96
		{
97 12
			if ($this->tokens[$this->i] === ';')
98
			{
99 12
				break;
100
			}
101 12
			elseif ($this->tokens[$this->i] === '.' && !$parens)
102
			{
103 7
				break;
104
			}
105 12
			elseif ($this->tokens[$this->i] === '(')
106
			{
107 1
				++$parens;
108
			}
109 12
			elseif ($this->tokens[$this->i] === ')')
110
			{
111 1
				--$parens;
112
			}
113
114 12
			$php .= $this->serializeToken($this->tokens[$this->i]);
115
		}
116 12
		while (++$this->i < $this->cnt);
117
118 12
		return $php;
119
	}
120
121
	/**
122
	* Capture the source of a control structure from its keyword to its opening brace
123
	*
124
	* Ends after the brace, but the brace itself is not returned
125
	*
126
	* @return string
127
	*/
128 12
	protected function captureStructure()
129
	{
130 12
		$php = '';
131
		do
132
		{
133 12
			$php .= $this->serializeToken($this->tokens[$this->i]);
134
		}
135 12
		while ($this->tokens[++$this->i] !== '{');
136
137
		// Move past the {
138 12
		++$this->i;
139
140 12
		return $php;
141
	}
142
143
	/**
144
	* Test whether the token at current index is an if/elseif/else token
145
	*
146
	* @return bool
147
	*/
148 12
	protected function isBranchToken()
149
	{
150 12
		return in_array($this->tokens[$this->i][0], [T_ELSE, T_ELSEIF, T_IF], true);
151
	}
152
153
	/**
154
	* Merge the branches of an if/elseif/else block
155
	*
156
	* Returns an array that contains the following:
157
	*
158
	*  - before: array of PHP expressions to be output before the block
159
	*  - source: PHP code for the if block
160
	*  - after:  array of PHP expressions to be output after the block
161
	*
162
	* @param  array $branches
163
	* @return array
164
	*/
165 12
	protected function mergeIfBranches(array $branches)
166
	{
167
		// Test whether the branches cover all code paths. Without a "else" branch at the end, we
168
		// cannot optimize
169 12
		$lastBranch = end($branches);
170 12
		if ($lastBranch['structure'] === 'else')
171
		{
172 11
			$before = $this->optimizeBranchesHead($branches);
173 11
			$after  = $this->optimizeBranchesTail($branches);
174
		}
175
		else
176
		{
177 1
			$before = $after = [];
178
		}
179
180 12
		$source = '';
181 12
		foreach ($branches as $branch)
182
		{
183 12
			$source .= $this->serializeBranch($branch);
184
		}
185
186
		return [
187 12
			'before' => $before,
188 12
			'source' => $source,
189 12
			'after'  => $after
190
		];
191
	}
192
193
	/**
194
	* Merge two consecutive series of consecutive output expressions together
195
	*
196
	* @param  array $left  First series
197
	* @param  array $right Second series
198
	* @return array        Merged series
199
	*/
200 12
	protected function mergeOutput(array $left, array $right)
201
	{
202 12
		if (empty($left))
203
		{
204 12
			return $right;
205
		}
206
207 2
		if (empty($right))
208
		{
209 1
			return $left;
210
		}
211
212
		// Test whether we can merge the last expression on the left with the first expression on
213
		// the right
214 1
		$k = count($left) - 1;
215
216 1
		if (substr($left[$k], -1) === "'" && $right[0][0] === "'")
217
		{
218 1
			$right[0] = substr($left[$k], 0, -1) . substr($right[0], 1);
0 ignored issues
show
Bug introduced by
$right[0] of type array is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

218
			$right[0] = substr($left[$k], 0, -1) . substr(/** @scrutinizer ignore-type */ $right[0], 1);
Loading history...
219 1
			unset($left[$k]);
220
		}
221
222 1
		return array_merge($left, $right);
223
	}
224
225
	/**
226
	* Optimize the "head" part of a series of branches in-place
227
	*
228
	* @param  array    &$branches Array of branches, modified in-place
229
	* @return string[]            PHP expressions removed from the "head" part of the branches
230
	*/
231 11
	protected function optimizeBranchesHead(array &$branches)
232
	{
233
		// Capture common output
234 11
		$before = $this->optimizeBranchesOutput($branches, 'head');
235
236
		// Move the branch output to the tail for branches that have no body
237 11
		foreach ($branches as &$branch)
238
		{
239 11
			if ($branch['body'] !== '' || !empty($branch['tail']))
240
			{
241 7
				continue;
242
			}
243
244 8
			$branch['tail'] = array_reverse($branch['head']);
245 8
			$branch['head'] = [];
246
		}
247 11
		unset($branch);
248
249 11
		return $before;
250
	}
251
252
	/**
253
	* Optimize the output of given branches
254
	*
255
	* @param  array    &$branches Array of branches
256
	* @param  string    $which    Which end to optimize ("head" or "tail")
257
	* @return string[]            PHP expressions removed from the given part of the branches
258
	*/
259 11
	protected function optimizeBranchesOutput(array &$branches, $which)
260
	{
261 11
		$expressions = [];
262 11
		while (isset($branches[0][$which][0]))
263
		{
264 11
			$expr = $branches[0][$which][0];
265 11
			foreach ($branches as $branch)
266
			{
267 11
				if (!isset($branch[$which][0]) || $branch[$which][0] !== $expr)
268
				{
269 8
					break 2;
270
				}
271
			}
272
273 11
			$expressions[] = $expr;
274 11
			foreach ($branches as &$branch)
275
			{
276 11
				array_shift($branch[$which]);
277
			}
278 11
			unset($branch);
279
		}
280
281 11
		return $expressions;
282
	}
283
284
	/**
285
	* Optimize the "tail" part of a series of branches in-place
286
	*
287
	* @param  array    &$branches Array of branches, modified in-place
288
	* @return string[]            PHP expressions removed from the "tail" part of the branches
289
	*/
290 11
	protected function optimizeBranchesTail(array &$branches)
291
	{
292 11
		return $this->optimizeBranchesOutput($branches, 'tail');
293
	}
294
295
	/**
296
	* Parse the if, elseif or else branch starting at current index
297
	*
298
	* Ends at the last }
299
	*
300
	* @return array Branch's data ("structure", "head", "body", "tail")
301
	*/
302 12
	protected function parseBranch()
303
	{
304
		// Record the control structure
305 12
		$structure = $this->captureStructure();
306
307
		// Record the output expressions at the start of this branch
308 12
		$head = $this->captureOutput();
309 12
		$body = '';
310 12
		$tail = [];
311
312 12
		$braces = 0;
313
		do
314
		{
315 12
			$tail = $this->mergeOutput($tail, array_reverse($this->captureOutput()));
316 12
			if ($this->tokens[$this->i] === '}' && !$braces)
317
			{
318 12
				break;
319
			}
320
321 8
			$body .= $this->serializeOutput(array_reverse($tail));
322 8
			$tail  = [];
323
324 8
			if ($this->tokens[$this->i][0] === T_IF)
325
			{
326 4
				$child = $this->parseIfBlock();
327
328
				// If this is the start of current branch, what's been optimized away and moved
329
				// outside, before the child branch is the head of this one. Otherwise it's just
330
				// part of its body
331 4
				if ($body === '')
332
				{
333 3
					$head = $this->mergeOutput($head, $child['before']);
334
				}
335
				else
336
				{
337 1
					$body .= $this->serializeOutput($child['before']);
338
				}
339
340 4
				$body .= $child['source'];
341 4
				$tail  = $child['after'];
342
			}
343
			else
344
			{
345 5
				$body .= $this->serializeToken($this->tokens[$this->i]);
346
347 5
				if ($this->tokens[$this->i] === '{')
348
				{
349 1
					++$braces;
350
				}
351 5
				elseif ($this->tokens[$this->i] === '}')
352
				{
353 1
					--$braces;
354
				}
355
			}
356
		}
357 8
		while (++$this->i < $this->cnt);
358
359
		return [
360 12
			'structure' => $structure,
361 12
			'head'      => $head,
362 12
			'body'      => $body,
363 12
			'tail'      => $tail
364
		];
365
	}
366
367
	/**
368
	* Parse the if block (including elseif/else branches) starting at current index
369
	*
370
	* @return array
371
	*/
372 12
	protected function parseIfBlock()
373
	{
374 12
		$branches = [];
375
		do
376
		{
377 12
			$branches[] = $this->parseBranch();
378
		}
379 12
		while (++$this->i < $this->cnt && $this->isBranchToken());
380
381
		// Move the index back to the last token used
382 12
		--$this->i;
383
384 12
		return $this->mergeIfBranches($branches);
385
	}
386
387
	/**
388
	* Serialize a recorded branch back to PHP
389
	*
390
	* @param  array  $branch
391
	* @return string
392
	*/
393 12
	protected function serializeBranch(array $branch)
394
	{
395
		// Optimize away "else{}" completely
396 12
		if ($branch['structure'] === 'else'
397 12
		 && $branch['body']      === ''
398 12
		 && empty($branch['head'])
399 12
		 && empty($branch['tail']))
400
		{
401 3
			return '';
402
		}
403
404 12
		return $branch['structure'] . '{' . $this->serializeOutput($branch['head']) . $branch['body'] . $this->serializeOutput(array_reverse($branch['tail'])) . '}';
405
	}
406
407
	/**
408
	* Serialize a series of recorded branch back to PHP
409
	*
410
	* @param  array  $block
411
	* @return string
412
	*/
413 12
	protected function serializeIfBlock(array $block)
414
	{
415 12
		return $this->serializeOutput($block['before']) . $block['source'] . $this->serializeOutput(array_reverse($block['after']));
416
	}
417
418
	/**
419
	* Serialize a series of output expressions
420
	*
421
	* @param  string[] $expressions Array of PHP expressions
422
	* @return string                PHP code used to append given expressions to the output
423
	*/
424 12
	protected function serializeOutput(array $expressions)
425
	{
426 12
		if (empty($expressions))
427
		{
428 12
			return '';
429
		}
430
431 12
		return '$this->out.=' . implode('.', $expressions) . ';';
432
	}
433
434
	/**
435
	* Serialize a token back to PHP
436
	*
437
	* @param  array|string $token Token from token_get_all()
438
	* @return string              PHP code
439
	*/
440 12
	protected function serializeToken($token)
441
	{
442 12
		return (is_array($token)) ? $token[1] : $token;
443
	}
444
445
	/**
446
	* Attempt to move past output assignment at current index
447
	*
448
	* @return bool Whether if an output assignment was skipped
449
	*/
450 12
	protected function skipOutputAssignment()
451
	{
452 12
		if ($this->tokens[$this->i    ][0] !== T_VARIABLE
453 12
		 || $this->tokens[$this->i    ][1] !== '$this'
454 12
		 || $this->tokens[$this->i + 1][0] !== T_OBJECT_OPERATOR
455 12
		 || $this->tokens[$this->i + 2][0] !== T_STRING
456 12
		 || $this->tokens[$this->i + 2][1] !== 'out'
457 12
		 || $this->tokens[$this->i + 3][0] !== T_CONCAT_EQUAL)
458
		{
459 12
			 return false;
460
		}
461
462
		// Move past the concat assignment
463 12
		$this->i += 4;
464
465 12
		return true;
466
	}
467
}