1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
/* |
6
|
|
|
* This file is part of PHP CS Fixer. |
7
|
|
|
* |
8
|
|
|
* (c) Fabien Potencier <[email protected]> |
9
|
|
|
* Dariusz Rumiński <[email protected]> |
10
|
|
|
* |
11
|
|
|
* This source file is subject to the MIT license that is bundled |
12
|
|
|
* with this source code in the file LICENSE. |
13
|
|
|
*/ |
14
|
|
|
|
15
|
|
|
namespace PhpCsFixer\Fixer\Operator; |
16
|
|
|
|
17
|
|
|
use PhpCsFixer\AbstractFixer; |
18
|
|
|
use PhpCsFixer\Fixer\ConfigurableFixerInterface; |
19
|
|
|
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver; |
20
|
|
|
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface; |
21
|
|
|
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder; |
22
|
|
|
use PhpCsFixer\FixerDefinition\CodeSample; |
23
|
|
|
use PhpCsFixer\FixerDefinition\FixerDefinition; |
24
|
|
|
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface; |
25
|
|
|
use PhpCsFixer\Preg; |
26
|
|
|
use PhpCsFixer\Tokenizer\CT; |
27
|
|
|
use PhpCsFixer\Tokenizer\Token; |
28
|
|
|
use PhpCsFixer\Tokenizer\Tokens; |
29
|
|
|
use PhpCsFixer\Tokenizer\TokensAnalyzer; |
30
|
|
|
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* @author Dariusz Rumiński <[email protected]> |
34
|
|
|
* @author SpacePossum |
35
|
|
|
*/ |
36
|
|
|
final class BinaryOperatorSpacesFixer extends AbstractFixer implements ConfigurableFixerInterface |
37
|
|
|
{ |
38
|
|
|
/** |
39
|
|
|
* @internal |
40
|
|
|
*/ |
41
|
|
|
public const SINGLE_SPACE = 'single_space'; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @internal |
45
|
|
|
*/ |
46
|
|
|
public const NO_SPACE = 'no_space'; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @internal |
50
|
|
|
*/ |
51
|
|
|
public const ALIGN = 'align'; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @internal |
55
|
|
|
*/ |
56
|
|
|
public const ALIGN_SINGLE_SPACE = 'align_single_space'; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @internal |
60
|
|
|
*/ |
61
|
|
|
public const ALIGN_SINGLE_SPACE_MINIMAL = 'align_single_space_minimal'; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* @internal |
65
|
|
|
* @const Placeholder used as anchor for right alignment. |
66
|
|
|
*/ |
67
|
|
|
public const ALIGN_PLACEHOLDER = "\x2 ALIGNABLE%d \x3"; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* @var string[] |
71
|
|
|
*/ |
72
|
|
|
private const SUPPORTED_OPERATORS = [ |
73
|
|
|
'=', |
74
|
|
|
'*', |
75
|
|
|
'/', |
76
|
|
|
'%', |
77
|
|
|
'<', |
78
|
|
|
'>', |
79
|
|
|
'|', |
80
|
|
|
'^', |
81
|
|
|
'+', |
82
|
|
|
'-', |
83
|
|
|
'&', |
84
|
|
|
'&=', |
85
|
|
|
'&&', |
86
|
|
|
'||', |
87
|
|
|
'.=', |
88
|
|
|
'/=', |
89
|
|
|
'=>', |
90
|
|
|
'==', |
91
|
|
|
'>=', |
92
|
|
|
'===', |
93
|
|
|
'!=', |
94
|
|
|
'<>', |
95
|
|
|
'!==', |
96
|
|
|
'<=', |
97
|
|
|
'and', |
98
|
|
|
'or', |
99
|
|
|
'xor', |
100
|
|
|
'-=', |
101
|
|
|
'%=', |
102
|
|
|
'*=', |
103
|
|
|
'|=', |
104
|
|
|
'+=', |
105
|
|
|
'<<', |
106
|
|
|
'<<=', |
107
|
|
|
'>>', |
108
|
|
|
'>>=', |
109
|
|
|
'^=', |
110
|
|
|
'**', |
111
|
|
|
'**=', |
112
|
|
|
'<=>', |
113
|
|
|
'??', |
114
|
|
|
'??=', |
115
|
|
|
]; |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* Keep track of the deepest level ever achieved while |
119
|
|
|
* parsing the code. Used later to replace alignment |
120
|
|
|
* placeholders with spaces. |
121
|
|
|
* |
122
|
|
|
* @var int |
123
|
|
|
*/ |
124
|
|
|
private $deepestLevel; |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* Level counter of the current nest level. |
128
|
|
|
* So one level alignments are not mixed with |
129
|
|
|
* other level ones. |
130
|
|
|
* |
131
|
|
|
* @var int |
132
|
|
|
*/ |
133
|
|
|
private $currentLevel; |
134
|
|
|
|
135
|
|
|
private static $allowedValues = [ |
136
|
|
|
self::ALIGN, |
137
|
|
|
self::ALIGN_SINGLE_SPACE, |
138
|
|
|
self::ALIGN_SINGLE_SPACE_MINIMAL, |
139
|
|
|
self::SINGLE_SPACE, |
140
|
|
|
self::NO_SPACE, |
141
|
|
|
null, |
142
|
|
|
]; |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* @var TokensAnalyzer |
146
|
|
|
*/ |
147
|
|
|
private $tokensAnalyzer; |
148
|
|
|
|
149
|
|
|
/** |
150
|
|
|
* @var array<string, string> |
151
|
|
|
*/ |
152
|
|
|
private $alignOperatorTokens = []; |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* @var array<string, string> |
156
|
|
|
*/ |
157
|
|
|
private $operators = []; |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* {@inheritdoc} |
161
|
|
|
*/ |
162
|
|
|
public function configure(array $configuration): void |
163
|
|
|
{ |
164
|
|
|
parent::configure($configuration); |
165
|
|
|
|
166
|
|
|
$this->operators = $this->resolveOperatorsFromConfig(); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* {@inheritdoc} |
171
|
|
|
*/ |
172
|
|
|
public function getDefinition(): FixerDefinitionInterface |
173
|
|
|
{ |
174
|
|
|
return new FixerDefinition( |
175
|
|
|
'Binary operators should be surrounded by space as configured.', |
176
|
|
|
[ |
177
|
|
|
new CodeSample( |
178
|
|
|
"<?php\n\$a= 1 + \$b^ \$d !== \$e or \$f;\n" |
179
|
|
|
), |
180
|
|
|
new CodeSample( |
181
|
|
|
'<?php |
182
|
|
|
$aa= 1; |
183
|
|
|
$b=2; |
184
|
|
|
|
185
|
|
|
$c = $d xor $e; |
186
|
|
|
$f -= 1; |
187
|
|
|
', |
188
|
|
|
['operators' => ['=' => 'align', 'xor' => null]] |
189
|
|
|
), |
190
|
|
|
new CodeSample( |
191
|
|
|
'<?php |
192
|
|
|
$a = $b +=$c; |
193
|
|
|
$d = $ee+=$f; |
194
|
|
|
|
195
|
|
|
$g = $b +=$c; |
196
|
|
|
$h = $ee+=$f; |
197
|
|
|
', |
198
|
|
|
['operators' => ['+=' => 'align_single_space']] |
199
|
|
|
), |
200
|
|
|
new CodeSample( |
201
|
|
|
'<?php |
202
|
|
|
$a = $b===$c; |
203
|
|
|
$d = $f === $g; |
204
|
|
|
$h = $i=== $j; |
205
|
|
|
', |
206
|
|
|
['operators' => ['===' => 'align_single_space_minimal']] |
207
|
|
|
), |
208
|
|
|
new CodeSample( |
209
|
|
|
'<?php |
210
|
|
|
$foo = \json_encode($bar, JSON_PRESERVE_ZERO_FRACTION | JSON_PRETTY_PRINT); |
211
|
|
|
', |
212
|
|
|
['operators' => ['|' => 'no_space']] |
213
|
|
|
), |
214
|
|
|
new CodeSample( |
215
|
|
|
'<?php |
216
|
|
|
$array = [ |
217
|
|
|
"foo" => 1, |
218
|
|
|
"baaaaaaaaaaar" => 11, |
219
|
|
|
]; |
220
|
|
|
', |
221
|
|
|
['operators' => ['=>' => 'single_space']] |
222
|
|
|
), |
223
|
|
|
new CodeSample( |
224
|
|
|
'<?php |
225
|
|
|
$array = [ |
226
|
|
|
"foo" => 12, |
227
|
|
|
"baaaaaaaaaaar" => 13, |
228
|
|
|
]; |
229
|
|
|
', |
230
|
|
|
['operators' => ['=>' => 'align']] |
231
|
|
|
), |
232
|
|
|
new CodeSample( |
233
|
|
|
'<?php |
234
|
|
|
$array = [ |
235
|
|
|
"foo" => 12, |
236
|
|
|
"baaaaaaaaaaar" => 13, |
237
|
|
|
]; |
238
|
|
|
', |
239
|
|
|
['operators' => ['=>' => 'align_single_space']] |
240
|
|
|
), |
241
|
|
|
new CodeSample( |
242
|
|
|
'<?php |
243
|
|
|
$array = [ |
244
|
|
|
"foo" => 12, |
245
|
|
|
"baaaaaaaaaaar" => 13, |
246
|
|
|
]; |
247
|
|
|
', |
248
|
|
|
['operators' => ['=>' => 'align_single_space_minimal']] |
249
|
|
|
), |
250
|
|
|
] |
251
|
|
|
); |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
/** |
255
|
|
|
* {@inheritdoc} |
256
|
|
|
* |
257
|
|
|
* Must run after ArrayIndentationFixer, ArraySyntaxFixer, ListSyntaxFixer, NoMultilineWhitespaceAroundDoubleArrowFixer, NoUnsetCastFixer, PowToExponentiationFixer, StandardizeNotEqualsFixer, StrictComparisonFixer. |
258
|
|
|
*/ |
259
|
|
|
public function getPriority(): int |
260
|
|
|
{ |
261
|
|
|
return -32; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* {@inheritdoc} |
266
|
|
|
*/ |
267
|
|
|
public function isCandidate(Tokens $tokens): bool |
268
|
|
|
{ |
269
|
|
|
return true; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* {@inheritdoc} |
274
|
|
|
*/ |
275
|
|
|
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void |
276
|
|
|
{ |
277
|
|
|
$this->tokensAnalyzer = new TokensAnalyzer($tokens); |
278
|
|
|
|
279
|
|
|
// last and first tokens cannot be an operator |
280
|
|
|
for ($index = $tokens->count() - 2; $index > 0; --$index) { |
281
|
|
|
if (!$this->tokensAnalyzer->isBinaryOperator($index)) { |
282
|
|
|
continue; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
if ('=' === $tokens[$index]->getContent()) { |
286
|
|
|
$isDeclare = $this->isEqualPartOfDeclareStatement($tokens, $index); |
287
|
|
|
if (false === $isDeclare) { |
288
|
|
|
$this->fixWhiteSpaceAroundOperator($tokens, $index); |
289
|
|
|
} else { |
290
|
|
|
$index = $isDeclare; // skip `declare(foo ==bar)`, see `declare_equal_normalize` |
291
|
|
|
} |
292
|
|
|
} else { |
293
|
|
|
$this->fixWhiteSpaceAroundOperator($tokens, $index); |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
// previous of binary operator is now never an operator / previous of declare statement cannot be an operator |
297
|
|
|
--$index; |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
if (\count($this->alignOperatorTokens)) { |
301
|
|
|
$this->fixAlignment($tokens, $this->alignOperatorTokens); |
302
|
|
|
} |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
/** |
306
|
|
|
* {@inheritdoc} |
307
|
|
|
*/ |
308
|
|
|
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface |
309
|
|
|
{ |
310
|
|
|
return new FixerConfigurationResolver([ |
311
|
|
|
(new FixerOptionBuilder('default', 'Default fix strategy.')) |
312
|
|
|
->setDefault(self::SINGLE_SPACE) |
313
|
|
|
->setAllowedValues(self::$allowedValues) |
314
|
|
|
->getOption(), |
315
|
|
|
(new FixerOptionBuilder('operators', 'Dictionary of `binary operator` => `fix strategy` values that differ from the default strategy.')) |
316
|
|
|
->setAllowedTypes(['array']) |
317
|
|
|
->setAllowedValues([static function (array $option) { |
318
|
|
|
foreach ($option as $operator => $value) { |
319
|
|
|
if (!\in_array($operator, self::SUPPORTED_OPERATORS, true)) { |
320
|
|
|
throw new InvalidOptionsException( |
321
|
|
|
sprintf( |
322
|
|
|
'Unexpected "operators" key, expected any of "%s", got "%s".', |
323
|
|
|
implode('", "', self::SUPPORTED_OPERATORS), |
324
|
|
|
\gettype($operator).'#'.$operator |
325
|
|
|
) |
326
|
|
|
); |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
if (!\in_array($value, self::$allowedValues, true)) { |
330
|
|
|
throw new InvalidOptionsException( |
331
|
|
|
sprintf( |
332
|
|
|
'Unexpected value for operator "%s", expected any of "%s", got "%s".', |
333
|
|
|
$operator, |
334
|
|
|
implode('", "', self::$allowedValues), |
335
|
|
|
\is_object($value) ? \get_class($value) : (null === $value ? 'null' : \gettype($value).'#'.$value) |
336
|
|
|
) |
337
|
|
|
); |
338
|
|
|
} |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
return true; |
342
|
|
|
}]) |
343
|
|
|
->setDefault([]) |
344
|
|
|
->getOption(), |
345
|
|
|
]); |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
private function fixWhiteSpaceAroundOperator(Tokens $tokens, int $index): void |
349
|
|
|
{ |
350
|
|
|
$tokenContent = strtolower($tokens[$index]->getContent()); |
351
|
|
|
|
352
|
|
|
if (!\array_key_exists($tokenContent, $this->operators)) { |
353
|
|
|
return; // not configured to be changed |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
if (self::SINGLE_SPACE === $this->operators[$tokenContent]) { |
357
|
|
|
$this->fixWhiteSpaceAroundOperatorToSingleSpace($tokens, $index); |
358
|
|
|
|
359
|
|
|
return; |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
if (self::NO_SPACE === $this->operators[$tokenContent]) { |
363
|
|
|
$this->fixWhiteSpaceAroundOperatorToNoSpace($tokens, $index); |
364
|
|
|
|
365
|
|
|
return; |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
// schedule for alignment |
369
|
|
|
$this->alignOperatorTokens[$tokenContent] = $this->operators[$tokenContent]; |
370
|
|
|
|
371
|
|
|
if (self::ALIGN === $this->operators[$tokenContent]) { |
372
|
|
|
return; |
373
|
|
|
} |
374
|
|
|
|
375
|
|
|
// fix white space after operator |
376
|
|
|
if ($tokens[$index + 1]->isWhitespace()) { |
377
|
|
|
if (self::ALIGN_SINGLE_SPACE_MINIMAL === $this->operators[$tokenContent]) { |
378
|
|
|
$tokens[$index + 1] = new Token([T_WHITESPACE, ' ']); |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
return; |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
$tokens->insertAt($index + 1, new Token([T_WHITESPACE, ' '])); |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
private function fixWhiteSpaceAroundOperatorToSingleSpace(Tokens $tokens, int $index): void |
388
|
|
|
{ |
389
|
|
|
// fix white space after operator |
390
|
|
|
if ($tokens[$index + 1]->isWhitespace()) { |
391
|
|
|
$content = $tokens[$index + 1]->getContent(); |
392
|
|
|
if (' ' !== $content && false === strpos($content, "\n") && !$tokens[$tokens->getNextNonWhitespace($index + 1)]->isComment()) { |
393
|
|
|
$tokens[$index + 1] = new Token([T_WHITESPACE, ' ']); |
394
|
|
|
} |
395
|
|
|
} else { |
396
|
|
|
$tokens->insertAt($index + 1, new Token([T_WHITESPACE, ' '])); |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
// fix white space before operator |
400
|
|
|
if ($tokens[$index - 1]->isWhitespace()) { |
401
|
|
|
$content = $tokens[$index - 1]->getContent(); |
402
|
|
|
if (' ' !== $content && false === strpos($content, "\n") && !$tokens[$tokens->getPrevNonWhitespace($index - 1)]->isComment()) { |
403
|
|
|
$tokens[$index - 1] = new Token([T_WHITESPACE, ' ']); |
404
|
|
|
} |
405
|
|
|
} else { |
406
|
|
|
$tokens->insertAt($index, new Token([T_WHITESPACE, ' '])); |
407
|
|
|
} |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
private function fixWhiteSpaceAroundOperatorToNoSpace(Tokens $tokens, int $index): void |
411
|
|
|
{ |
412
|
|
|
// fix white space after operator |
413
|
|
|
if ($tokens[$index + 1]->isWhitespace()) { |
414
|
|
|
$content = $tokens[$index + 1]->getContent(); |
415
|
|
|
if (false === strpos($content, "\n") && !$tokens[$tokens->getNextNonWhitespace($index + 1)]->isComment()) { |
416
|
|
|
$tokens->clearAt($index + 1); |
417
|
|
|
} |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
// fix white space before operator |
421
|
|
|
if ($tokens[$index - 1]->isWhitespace()) { |
422
|
|
|
$content = $tokens[$index - 1]->getContent(); |
423
|
|
|
if (false === strpos($content, "\n") && !$tokens[$tokens->getPrevNonWhitespace($index - 1)]->isComment()) { |
424
|
|
|
$tokens->clearAt($index - 1); |
425
|
|
|
} |
426
|
|
|
} |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
/** |
430
|
|
|
* @return false|int index of T_DECLARE where the `=` belongs to or `false` |
431
|
|
|
*/ |
432
|
|
|
private function isEqualPartOfDeclareStatement(Tokens $tokens, int $index) |
433
|
|
|
{ |
434
|
|
|
$prevMeaningfulIndex = $tokens->getPrevMeaningfulToken($index); |
435
|
|
|
if ($tokens[$prevMeaningfulIndex]->isGivenKind(T_STRING)) { |
436
|
|
|
$prevMeaningfulIndex = $tokens->getPrevMeaningfulToken($prevMeaningfulIndex); |
|
|
|
|
437
|
|
|
if ($tokens[$prevMeaningfulIndex]->equals('(')) { |
438
|
|
|
$prevMeaningfulIndex = $tokens->getPrevMeaningfulToken($prevMeaningfulIndex); |
439
|
|
|
if ($tokens[$prevMeaningfulIndex]->isGivenKind(T_DECLARE)) { |
440
|
|
|
return $prevMeaningfulIndex; |
441
|
|
|
} |
442
|
|
|
} |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
return false; |
446
|
|
|
} |
447
|
|
|
|
448
|
|
|
/** |
449
|
|
|
* @return array<string, string> |
450
|
|
|
*/ |
451
|
|
|
private function resolveOperatorsFromConfig(): array |
452
|
|
|
{ |
453
|
|
|
$operators = []; |
454
|
|
|
|
455
|
|
|
if (null !== $this->configuration['default']) { |
456
|
|
|
foreach (self::SUPPORTED_OPERATORS as $operator) { |
457
|
|
|
$operators[$operator] = $this->configuration['default']; |
458
|
|
|
} |
459
|
|
|
} |
460
|
|
|
|
461
|
|
|
foreach ($this->configuration['operators'] as $operator => $value) { |
462
|
|
|
if (null === $value) { |
463
|
|
|
unset($operators[$operator]); |
464
|
|
|
} else { |
465
|
|
|
$operators[$operator] = $value; |
466
|
|
|
} |
467
|
|
|
} |
468
|
|
|
|
469
|
|
|
// @TODO: drop condition when PHP 7.4+ is required |
470
|
|
|
if (!\defined('T_COALESCE_EQUAL')) { |
471
|
|
|
unset($operators['??=']); |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
return $operators; |
475
|
|
|
} |
476
|
|
|
|
477
|
|
|
// Alignment logic related methods |
478
|
|
|
|
479
|
|
|
/** |
480
|
|
|
* @param array<string, string> $toAlign |
481
|
|
|
*/ |
482
|
|
|
private function fixAlignment(Tokens $tokens, array $toAlign): void |
483
|
|
|
{ |
484
|
|
|
$this->deepestLevel = 0; |
485
|
|
|
$this->currentLevel = 0; |
486
|
|
|
|
487
|
|
|
foreach ($toAlign as $tokenContent => $alignStrategy) { |
488
|
|
|
// This fixer works partially on Tokens and partially on string representation of code. |
489
|
|
|
// During the process of fixing internal state of single Token may be affected by injecting ALIGN_PLACEHOLDER to its content. |
490
|
|
|
// The placeholder will be resolved by `replacePlaceholders` method by removing placeholder or changing it into spaces. |
491
|
|
|
// That way of fixing the code causes disturbances in marking Token as changed - if code is perfectly valid then placeholder |
492
|
|
|
// still be injected and removed, which will cause the `changed` flag to be set. |
493
|
|
|
// To handle that unwanted behavior we work on clone of Tokens collection and then override original collection with fixed collection. |
494
|
|
|
$tokensClone = clone $tokens; |
495
|
|
|
|
496
|
|
|
if ('=>' === $tokenContent) { |
497
|
|
|
$this->injectAlignmentPlaceholdersForArrow($tokensClone, 0, \count($tokens)); |
498
|
|
|
} else { |
499
|
|
|
$this->injectAlignmentPlaceholders($tokensClone, 0, \count($tokens), $tokenContent); |
500
|
|
|
} |
501
|
|
|
|
502
|
|
|
// for all tokens that should be aligned but do not have anything to align with, fix spacing if needed |
503
|
|
|
if (self::ALIGN_SINGLE_SPACE === $alignStrategy || self::ALIGN_SINGLE_SPACE_MINIMAL === $alignStrategy) { |
504
|
|
|
if ('=>' === $tokenContent) { |
505
|
|
|
for ($index = $tokens->count() - 2; $index > 0; --$index) { |
506
|
|
|
if ($tokens[$index]->isGivenKind(T_DOUBLE_ARROW)) { // always binary operator, never part of declare statement |
507
|
|
|
$this->fixWhiteSpaceBeforeOperator($tokensClone, $index, $alignStrategy); |
508
|
|
|
} |
509
|
|
|
} |
510
|
|
|
} elseif ('=' === $tokenContent) { |
511
|
|
|
for ($index = $tokens->count() - 2; $index > 0; --$index) { |
512
|
|
|
if ('=' === $tokens[$index]->getContent() && !$this->isEqualPartOfDeclareStatement($tokens, $index) && $this->tokensAnalyzer->isBinaryOperator($index)) { |
|
|
|
|
513
|
|
|
$this->fixWhiteSpaceBeforeOperator($tokensClone, $index, $alignStrategy); |
514
|
|
|
} |
515
|
|
|
} |
516
|
|
|
} else { |
517
|
|
|
for ($index = $tokens->count() - 2; $index > 0; --$index) { |
518
|
|
|
$content = $tokens[$index]->getContent(); |
519
|
|
|
if (strtolower($content) === $tokenContent && $this->tokensAnalyzer->isBinaryOperator($index)) { // never part of declare statement |
520
|
|
|
$this->fixWhiteSpaceBeforeOperator($tokensClone, $index, $alignStrategy); |
521
|
|
|
} |
522
|
|
|
} |
523
|
|
|
} |
524
|
|
|
} |
525
|
|
|
|
526
|
|
|
$tokens->setCode($this->replacePlaceholders($tokensClone, $alignStrategy)); |
527
|
|
|
} |
528
|
|
|
} |
529
|
|
|
|
530
|
|
|
private function injectAlignmentPlaceholders(Tokens $tokens, int $startAt, int $endAt, string $tokenContent): void |
531
|
|
|
{ |
532
|
|
|
for ($index = $startAt; $index < $endAt; ++$index) { |
533
|
|
|
$token = $tokens[$index]; |
534
|
|
|
|
535
|
|
|
$content = $token->getContent(); |
536
|
|
|
if ( |
537
|
|
|
strtolower($content) === $tokenContent |
538
|
|
|
&& $this->tokensAnalyzer->isBinaryOperator($index) |
539
|
|
|
&& ('=' !== $content || !$this->isEqualPartOfDeclareStatement($tokens, $index)) |
|
|
|
|
540
|
|
|
) { |
541
|
|
|
$tokens[$index] = new Token(sprintf(self::ALIGN_PLACEHOLDER, $this->deepestLevel).$content); |
542
|
|
|
|
543
|
|
|
continue; |
544
|
|
|
} |
545
|
|
|
|
546
|
|
|
if ($token->isGivenKind(T_FUNCTION)) { |
547
|
|
|
++$this->deepestLevel; |
548
|
|
|
|
549
|
|
|
continue; |
550
|
|
|
} |
551
|
|
|
|
552
|
|
|
if ($token->equals('(')) { |
553
|
|
|
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index); |
554
|
|
|
|
555
|
|
|
continue; |
556
|
|
|
} |
557
|
|
|
|
558
|
|
|
if ($token->equals('[')) { |
559
|
|
|
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_INDEX_SQUARE_BRACE, $index); |
560
|
|
|
|
561
|
|
|
continue; |
562
|
|
|
} |
563
|
|
|
|
564
|
|
|
if ($token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN)) { |
565
|
|
|
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $index); |
566
|
|
|
|
567
|
|
|
continue; |
568
|
|
|
} |
569
|
|
|
} |
570
|
|
|
} |
571
|
|
|
|
572
|
|
|
private function injectAlignmentPlaceholdersForArrow(Tokens $tokens, int $startAt, int $endAt): void |
573
|
|
|
{ |
574
|
|
|
for ($index = $startAt; $index < $endAt; ++$index) { |
575
|
|
|
$token = $tokens[$index]; |
576
|
|
|
|
577
|
|
|
if ($token->isGivenKind([T_FOREACH, T_FOR, T_WHILE, T_IF, T_SWITCH])) { |
578
|
|
|
$index = $tokens->getNextMeaningfulToken($index); |
579
|
|
|
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index); |
|
|
|
|
580
|
|
|
|
581
|
|
|
continue; |
582
|
|
|
} |
583
|
|
|
|
584
|
|
|
if ($token->isGivenKind(T_ARRAY)) { // don't use "$tokens->isArray()" here, short arrays are handled in the next case |
585
|
|
|
$from = $tokens->getNextMeaningfulToken($index); |
586
|
|
|
$until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $from); |
587
|
|
|
$index = $until; |
588
|
|
|
|
589
|
|
|
$this->injectArrayAlignmentPlaceholders($tokens, $from + 1, $until - 1); |
590
|
|
|
|
591
|
|
|
continue; |
592
|
|
|
} |
593
|
|
|
|
594
|
|
|
if ($token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN)) { |
595
|
|
|
$from = $index; |
596
|
|
|
$until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $from); |
597
|
|
|
$index = $until; |
598
|
|
|
|
599
|
|
|
$this->injectArrayAlignmentPlaceholders($tokens, $from + 1, $until - 1); |
600
|
|
|
|
601
|
|
|
continue; |
602
|
|
|
} |
603
|
|
|
|
604
|
|
|
if ($token->isGivenKind(T_DOUBLE_ARROW)) { // no need to analyze for `isBinaryOperator` (always true), nor if part of declare statement (not valid PHP) |
605
|
|
|
$tokenContent = sprintf(self::ALIGN_PLACEHOLDER, $this->currentLevel).$token->getContent(); |
606
|
|
|
|
607
|
|
|
$nextToken = $tokens[$index + 1]; |
608
|
|
|
if (!$nextToken->isWhitespace()) { |
609
|
|
|
$tokenContent .= ' '; |
610
|
|
|
} elseif ($nextToken->isWhitespace(" \t")) { |
611
|
|
|
$tokens[$index + 1] = new Token([T_WHITESPACE, ' ']); |
612
|
|
|
} |
613
|
|
|
|
614
|
|
|
$tokens[$index] = new Token([T_DOUBLE_ARROW, $tokenContent]); |
615
|
|
|
|
616
|
|
|
continue; |
617
|
|
|
} |
618
|
|
|
|
619
|
|
|
if ($token->equals(';')) { |
620
|
|
|
++$this->deepestLevel; |
621
|
|
|
++$this->currentLevel; |
622
|
|
|
|
623
|
|
|
continue; |
624
|
|
|
} |
625
|
|
|
|
626
|
|
|
if ($token->equals(',')) { |
627
|
|
|
for ($i = $index; $i < $endAt - 1; ++$i) { |
628
|
|
|
if (false !== strpos($tokens[$i - 1]->getContent(), "\n")) { |
629
|
|
|
break; |
630
|
|
|
} |
631
|
|
|
|
632
|
|
|
if ($tokens[$i + 1]->isGivenKind([T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN])) { |
633
|
|
|
$arrayStartIndex = $tokens[$i + 1]->isGivenKind(T_ARRAY) |
634
|
|
|
? $tokens->getNextMeaningfulToken($i + 1) |
635
|
|
|
: $i + 1 |
636
|
|
|
; |
637
|
|
|
$blockType = Tokens::detectBlockType($tokens[$arrayStartIndex]); |
638
|
|
|
$arrayEndIndex = $tokens->findBlockEnd($blockType['type'], $arrayStartIndex); |
639
|
|
|
|
640
|
|
|
if ($tokens->isPartialCodeMultiline($arrayStartIndex, $arrayEndIndex)) { |
|
|
|
|
641
|
|
|
break; |
642
|
|
|
} |
643
|
|
|
} |
644
|
|
|
|
645
|
|
|
++$index; |
646
|
|
|
} |
647
|
|
|
} |
648
|
|
|
} |
649
|
|
|
} |
650
|
|
|
|
651
|
|
|
private function injectArrayAlignmentPlaceholders(Tokens $tokens, int $from, int $until): void |
652
|
|
|
{ |
653
|
|
|
// Only inject placeholders for multi-line arrays |
654
|
|
|
if ($tokens->isPartialCodeMultiline($from, $until)) { |
655
|
|
|
++$this->deepestLevel; |
656
|
|
|
++$this->currentLevel; |
657
|
|
|
$this->injectAlignmentPlaceholdersForArrow($tokens, $from, $until); |
658
|
|
|
--$this->currentLevel; |
659
|
|
|
} |
660
|
|
|
} |
661
|
|
|
|
662
|
|
|
private function fixWhiteSpaceBeforeOperator(Tokens $tokens, int $index, string $alignStrategy): void |
663
|
|
|
{ |
664
|
|
|
// fix white space after operator is not needed as BinaryOperatorSpacesFixer took care of this (if strategy is _not_ ALIGN) |
665
|
|
|
if (!$tokens[$index - 1]->isWhitespace()) { |
666
|
|
|
$tokens->insertAt($index, new Token([T_WHITESPACE, ' '])); |
667
|
|
|
|
668
|
|
|
return; |
669
|
|
|
} |
670
|
|
|
|
671
|
|
|
if (self::ALIGN_SINGLE_SPACE_MINIMAL !== $alignStrategy || $tokens[$tokens->getPrevNonWhitespace($index - 1)]->isComment()) { |
672
|
|
|
return; |
673
|
|
|
} |
674
|
|
|
|
675
|
|
|
$content = $tokens[$index - 1]->getContent(); |
676
|
|
|
if (' ' !== $content && false === strpos($content, "\n")) { |
677
|
|
|
$tokens[$index - 1] = new Token([T_WHITESPACE, ' ']); |
678
|
|
|
} |
679
|
|
|
} |
680
|
|
|
|
681
|
|
|
/** |
682
|
|
|
* Look for group of placeholders and provide vertical alignment. |
683
|
|
|
*/ |
684
|
|
|
private function replacePlaceholders(Tokens $tokens, string $alignStrategy): string |
685
|
|
|
{ |
686
|
|
|
$tmpCode = $tokens->generateCode(); |
687
|
|
|
|
688
|
|
|
for ($j = 0; $j <= $this->deepestLevel; ++$j) { |
689
|
|
|
$placeholder = sprintf(self::ALIGN_PLACEHOLDER, $j); |
690
|
|
|
|
691
|
|
|
if (false === strpos($tmpCode, $placeholder)) { |
692
|
|
|
continue; |
693
|
|
|
} |
694
|
|
|
|
695
|
|
|
$lines = explode("\n", $tmpCode); |
696
|
|
|
$groups = []; |
697
|
|
|
$groupIndex = 0; |
698
|
|
|
$groups[$groupIndex] = []; |
699
|
|
|
|
700
|
|
|
foreach ($lines as $index => $line) { |
701
|
|
|
if (substr_count($line, $placeholder) > 0) { |
702
|
|
|
$groups[$groupIndex][] = $index; |
703
|
|
|
} else { |
704
|
|
|
++$groupIndex; |
705
|
|
|
$groups[$groupIndex] = []; |
706
|
|
|
} |
707
|
|
|
} |
708
|
|
|
|
709
|
|
|
foreach ($groups as $group) { |
710
|
|
|
if (\count($group) < 1) { |
711
|
|
|
continue; |
712
|
|
|
} |
713
|
|
|
|
714
|
|
|
if (self::ALIGN !== $alignStrategy) { |
715
|
|
|
// move place holders to match strategy |
716
|
|
|
foreach ($group as $index) { |
717
|
|
|
$currentPosition = strpos($lines[$index], $placeholder); |
718
|
|
|
$before = substr($lines[$index], 0, $currentPosition); |
719
|
|
|
|
720
|
|
|
if (self::ALIGN_SINGLE_SPACE === $alignStrategy) { |
721
|
|
|
if (1 > \strlen($before) || ' ' !== substr($before, -1)) { // if last char of before-content is not ' '; add it |
722
|
|
|
$before .= ' '; |
723
|
|
|
} |
724
|
|
|
} elseif (self::ALIGN_SINGLE_SPACE_MINIMAL === $alignStrategy) { |
725
|
|
|
if (1 !== Preg::match('/^\h+$/', $before)) { // if indent; do not move, leave to other fixer |
726
|
|
|
$before = rtrim($before).' '; |
727
|
|
|
} |
728
|
|
|
} |
729
|
|
|
|
730
|
|
|
$lines[$index] = $before.substr($lines[$index], $currentPosition); |
731
|
|
|
} |
732
|
|
|
} |
733
|
|
|
|
734
|
|
|
$rightmostSymbol = 0; |
735
|
|
|
foreach ($group as $index) { |
736
|
|
|
$rightmostSymbol = max($rightmostSymbol, strpos(utf8_decode($lines[$index]), $placeholder)); |
737
|
|
|
} |
738
|
|
|
|
739
|
|
|
foreach ($group as $index) { |
740
|
|
|
$line = $lines[$index]; |
741
|
|
|
$currentSymbol = strpos(utf8_decode($line), $placeholder); |
742
|
|
|
$delta = abs($rightmostSymbol - $currentSymbol); |
743
|
|
|
|
744
|
|
|
if ($delta > 0) { |
745
|
|
|
$line = str_replace($placeholder, str_repeat(' ', $delta).$placeholder, $line); |
|
|
|
|
746
|
|
|
$lines[$index] = $line; |
747
|
|
|
} |
748
|
|
|
} |
749
|
|
|
} |
750
|
|
|
|
751
|
|
|
$tmpCode = str_replace($placeholder, '', implode("\n", $lines)); |
752
|
|
|
} |
753
|
|
|
|
754
|
|
|
return $tmpCode; |
755
|
|
|
} |
756
|
|
|
} |
757
|
|
|
|