Total Complexity | 115 |
Total Lines | 719 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 0 | Features | 0 |
Complex classes like BinaryOperatorSpacesFixer 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 BinaryOperatorSpacesFixer, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
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 |
||
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 |
||
646 | } |
||
647 | } |
||
648 | } |
||
649 | } |
||
650 | |||
651 | private function injectArrayAlignmentPlaceholders(Tokens $tokens, int $from, int $until): void |
||
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 |
||
755 | } |
||
756 | } |
||
757 |