1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* @package s9e\TextFormatter |
5
|
|
|
* @copyright Copyright (c) 2010-2019 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); |
|
|
|
|
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 void |
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
|
|
|
} |
This check looks for function or method calls that always return null and whose return value is used.
The method
getObject()
can return nothing but null, so it makes no sense to use the return value.The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.