Repository: PHP-CS-Fixer/PHP-CS-Fixer (php)

Issue:
`binary_operator_spaces` problem with several shifts in single line
## Bug report

<!--
  Before reporting a bug, make sure PHP CS Fixer is up-to-date.

  Please describe the problem and provide technical details such as:
   * PHP version: php -v
   * PHP CS Fixer version: php-cs-fixer -V
   * the command used to run PHP CS Fixer (run with `-vvv`)
   * the configuration (file) you used
-->

### Code snippet that reproduces the problem

file.php:
```php
<?php

$planPercent = $periodFilled['plan']/$maxPlan * 100;
$factPercent = $periodFilled['actual']/$maxPlan * 100;

```
.php_cs:
```php
<?php

$finder = PhpCsFixer\Finder::create()
    ->in(__DIR__)
;

return PhpCsFixer\Config::create()
    ->setFinder($finder)
    ->setRules([
        'binary_operator_spaces' => ['default' => 'align_single_space_minimal']
    ])
    ->setLineEnding("\n")
;

```
### Fixing
```
 php-cs-fixer fix -vvv file.php
PHP CS Fixer 2.18.5 Remote Void by Fabien Potencier and Dariusz Ruminski (e0f6d05)
Runtime: PHP 7.4.16
Loaded config default from "<masked>/.php_cs".
Using cache file ".php_cs.cache".
Paths from configuration file have been overridden by paths provided as command arguments.
F
Legend: ?-unknown, I-invalid file syntax (file ignored), S-skipped (cached or empty file), .-no changes, F-fixed, E-error
   1) file.php (binary_operator_spaces)

Fixed all files in 0.011 seconds, 12.000 MB memory used
```
#### Actual result
```
 cat file.php                  
<?php

$planPercent = $periodFilled['plan']   / $maxPlan   * 100;
$factPercent = $periodFilled['actual'] / $maxPlan * 100;
```

#### Expected result
```
 cat file.php                  
<?php

$planPercent = $periodFilled['plan']   / $maxPlan * 100;
$factPercent = $periodFilled['actual'] / $maxPlan * 100;
```

Pull request description:
fix: `BinaryOperatorSpacesFixer` - align correctly when multiple shifts occurs in single line
Fixes #5605 

Hope it helps.

Prev PR #7365 

Patch:
diff --git a/src/Fixer/Operator/BinaryOperatorSpacesFixer.php b/src/Fixer/Operator/BinaryOperatorSpacesFixer.php
index 3f95b1895..4fe108a56 100644
--- a/src/Fixer/Operator/BinaryOperatorSpacesFixer.php
+++ b/src/Fixer/Operator/BinaryOperatorSpacesFixer.php
@@ -552,19 +552,19 @@ private function fixAlignment(Tokens $tokens, array $toAlign): void
         $this->deepestLevel = 0;
         $this->currentLevel = 0;
 
-        foreach ($toAlign as $tokenContent => $alignStrategy) {
-            // This fixer works partially on Tokens and partially on string representation of code.
-            // During the process of fixing internal state of single Token may be affected by injecting ALIGN_PLACEHOLDER to its content.
-            // The placeholder will be resolved by `replacePlaceholders` method by removing placeholder or changing it into spaces.
-            // That way of fixing the code causes disturbances in marking Token as changed - if code is perfectly valid then placeholder
-            // still be injected and removed, which will cause the `changed` flag to be set.
-            // To handle that unwanted behavior we work on clone of Tokens collection and then override original collection with fixed collection.
-            $tokensClone = clone $tokens;
+        // This fixer works partially on Tokens and partially on string representation of code.
+        // During the process of fixing internal state of single Token may be affected by injecting ALIGN_PLACEHOLDER to its content.
+        // The placeholder will be resolved by `replacePlaceholders` method by removing placeholder or changing it into spaces.
+        // That way of fixing the code causes disturbances in marking Token as changed - if code is perfectly valid then placeholder
+        // still be injected and removed, which will cause the `changed` flag to be set.
+        // To handle that unwanted behavior we work on clone of Tokens collection and then override original collection with fixed collection.
+        $tokensClone = clone $tokens;
 
+        foreach ($toAlign as $tokenContent => $alignStrategy) {
             if ('=>' === $tokenContent) {
-                $this->injectAlignmentPlaceholdersForArrow($tokensClone, 0, \count($tokens));
+                $this->injectAlignmentPlaceholdersForArrow($tokensClone, 0, \count($tokensClone));
             } else {
-                $this->injectAlignmentPlaceholdersDefault($tokensClone, 0, \count($tokens), $tokenContent);
+                $this->injectAlignmentPlaceholdersDefault($tokensClone, 0, \count($tokensClone), $tokenContent);
             }
 
             // for all tokens that should be aligned but do not have anything to align with, fix spacing if needed
@@ -575,20 +575,20 @@ private function fixAlignment(Tokens $tokens, array $toAlign): void
                 || self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE === $alignStrategy
             ) {
                 if ('=>' === $tokenContent) {
-                    for ($index = $tokens->count() - 2; $index > 0; --$index) {
-                        if ($tokens[$index]->isGivenKind(T_DOUBLE_ARROW)) { // always binary operator, never part of declare statement
+                    for ($index = $tokensClone->count() - 2; $index > 0; --$index) {
+                        if ($tokensClone[$index]->isGivenKind(T_DOUBLE_ARROW)) { // always binary operator, never part of declare statement
                             $this->fixWhiteSpaceBeforeOperator($tokensClone, $index, $alignStrategy);
                         }
                     }
                 } elseif ('=' === $tokenContent) {
-                    for ($index = $tokens->count() - 2; $index > 0; --$index) {
-                        if ('=' === $tokens[$index]->getContent() && !$this->isEqualPartOfDeclareStatement($tokens, $index) && $this->tokensAnalyzer->isBinaryOperator($index)) {
+                    for ($index = $tokensClone->count() - 2; $index > 0; --$index) {
+                        if ('=' === $tokensClone[$index]->getContent() && !$this->isEqualPartOfDeclareStatement($tokensClone, $index) && $this->tokensAnalyzer->isBinaryOperator($index)) {
                             $this->fixWhiteSpaceBeforeOperator($tokensClone, $index, $alignStrategy);
                         }
                     }
                 } else {
-                    for ($index = $tokens->count() - 2; $index > 0; --$index) {
-                        $content = $tokens[$index]->getContent();
+                    for ($index = $tokensClone->count() - 2; $index > 0; --$index) {
+                        $content = $tokensClone[$index]->getContent();
                         if (strtolower($content) === $tokenContent && $this->tokensAnalyzer->isBinaryOperator($index)) { // never part of declare statement
                             $this->fixWhiteSpaceBeforeOperator($tokensClone, $index, $alignStrategy);
                         }
@@ -596,7 +596,11 @@ private function fixAlignment(Tokens $tokens, array $toAlign): void
                 }
             }
 
-            $tokens->setCode($this->replacePlaceholders($tokensClone, $alignStrategy, $tokenContent));
+            $tokensClone->setCode($this->replacePlaceholders($tokensClone, $alignStrategy, $tokenContent));
+        }
+
+        if ($tokensClone->generateCode() !== $tokens->generateCode()) {
+            $tokens->setCode($tokensClone->generateCode());
         }
     }
 
@@ -925,7 +929,7 @@ private function replacePlaceholders(Tokens $tokens, string $alignStrategy, stri
 
                     if ($delta > 0) {
                         $line = str_replace($placeholder, str_repeat(' ', $delta).$placeholder, $line);
-                        $lines[$index] = $line;
+                        $lines[$index] = $this->getReAlignedLineWithMultipleShiftsInSingleLine($line, $alignStrategy, $placeholder, $tokenContent, $delta);
                     }
                 }
             }
@@ -935,4 +939,58 @@ private function replacePlaceholders(Tokens $tokens, string $alignStrategy, stri
 
         return $tmpCode;
     }
+
+    private function getReAlignedLineWithMultipleShiftsInSingleLine(
+        string $line,
+        string $alignStrategy,
+        string $placeholder,
+        string $tokenContent,
+        int $delta
+    ): string {
+        if (
+            self::ALIGN === $alignStrategy
+            || self::ALIGN_BY_SCOPE === $alignStrategy
+        ) {
+            return $line;
+        }
+
+        $strAlreadyAligned = str_replace(
+            $placeholder.$tokenContent,
+            '',
+            mb_strstr($line, $placeholder.$tokenContent)
+        );
+
+        $quotedOperators = [];
+        foreach (array_keys($this->alignOperatorTokens) as $alignOperatorToken) {
+            $quotedOperators[] = preg_quote($alignOperatorToken, '/');
+        }
+
+        $count = 0;
+        $replacedStr = Preg::replaceCallback(
+            '/\s*?('.implode('|', $quotedOperators).')/',
+            static function ($matches) use (&$count, $delta) {
+                $tokenWithEmptyStr = $matches[0];
+                $token = $matches[1];
+                $emptyStrCount = mb_substr_count($tokenWithEmptyStr, ' ');
+                $reAlignedStr = $tokenWithEmptyStr;
+                if (0 === $count) {
+                    if ($emptyStrCount - $delta > 0) {
+                        $reAlignedStr = str_repeat(' ', $emptyStrCount - $delta).$token;
+                    }
+                } else {
+                    $reAlignedStr = str_repeat(' ', $delta).$tokenWithEmptyStr;
+                }
+                ++$count;
+
+                return $reAlignedStr;
+            },
+            $strAlreadyAligned,
+        );
+
+        return mb_strstr(
+            $line,
+            $placeholder.$tokenContent,
+            true
+        ).$placeholder.$tokenContent.$replacedStr;
+    }
 }


Test patch:
diff --git a/tests/Fixer/Operator/BinaryOperatorSpacesFixerTest.php b/tests/Fixer/Operator/BinaryOperatorSpacesFixerTest.php
index 824e64463..5a6d567ae 100644
--- a/tests/Fixer/Operator/BinaryOperatorSpacesFixerTest.php
+++ b/tests/Fixer/Operator/BinaryOperatorSpacesFixerTest.php
@@ -556,6 +556,24 @@ function A(){}[$a] = $a[$c];
             null,
             ['operators' => ['|' => BinaryOperatorSpacesFixer::NO_SPACE]],
         ];
+
+        yield 'multiple shifts in single line' => [
+            '<?php
+             $testA = $testB["abc"]    / $testC * 100;
+             $testD = $testE["abcdef"] / $testF * 100;
+
+             $testA = $testB["abc"]    / $testC * 10000000    * 100;
+             $testD = $testE["abcdef"] / $testF * 10000000000 * 100;
+            ',
+            '<?php
+             $testA = $testB["abc"]/$testC * 100;
+             $testD = $testE["abcdef"]/$testF * 100;
+
+             $testA = $testB["abc"]/$testC * 10000000 * 100;
+             $testD = $testE["abcdef"]/$testF * 10000000000 * 100;
+            ',
+            ['default' => BinaryOperatorSpacesFixer::ALIGN_SINGLE_SPACE_MINIMAL],
+        ];
     }
 
     /**