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

Issue:
return_assignment should not be formatted when the variable is used in catch and finally
## Bug report

<!--
  Before reporting a bug, make sure PHP CS Fixer is up-to-date.

  Check that the issue is NOT regarded to an applied rule by using the `-vvv` option (will list applied rules) on a minimal code snippet.

  Please describe the problem and provide technical details such as:
   * PHP CS Fixer version and PHP runtime: php-cs-fixer -V
   * the command used to run PHP CS Fixer (run with `-vvv`)
   * the configuration (file) you used
-->

```shell
# ./vendor/bin/php-cs-fixer -V
PHP CS Fixer 3.13.0 Oliva by Fabien Potencier and Dariusz Ruminski.
PHP runtime: 8.0.23
```

**config:**

```php
<?php

declare(strict_types=1);

return (new PhpCsFixer\Config())
    ->setRules([
        '@PHP74Migration'            => true,
        '@PHP74Migration:risky'      => true,
        '@Symfony'                   => true,
        '@Symfony:risky'             => true,
        '@DoctrineAnnotation'        => true,
        'return_assignment'          => true,
    ])
    ->setRiskyAllowed(true)
;
```

### Code snippet that reproduces the problem

**code:**

```php
<?php

declare(strict_types=1);

function add($a, $b): mixed
{
    try {
        $result = $a + $b;

        return $result;
    } catch (\Throwable $th) {
        throw $th;
    } finally {
        echo 'result:', $result, \PHP_EOL;
    }
}

add(1, 2);

```

**result:**

```shell
# ./vendor/bin/php-cs-fixer fix --dry-run --diff test2.php 
Loaded config default from "/mnt/d/projects/test/.php-cs-fixer.php".
Using cache file ".php-cs-fixer.cache".
   1) test2.php
      ---------- begin diff ----------
--- /mnt/d/projects/test/test2.php
+++ /mnt/d/projects/test/test2.php
@@ -5,9 +5,7 @@
 function add($a, $b): mixed
 {
     try {
-        $result = $a + $b;
-
-        return $result;
+        return $a + $b;
     } catch (\Throwable $th) {
         throw $th;
     } finally {

      ----------- end diff -----------


Checked all files in 0.017 seconds, 14.000 MB memory used
```

<!--
  Provide a minimal code snippet before running PHP CS Fixer. If relevant,
  also provide the expected output, the actual output and/or the error that
  occurred, if any.

  Please use markdown syntax for each code snippet, e.g.:

  ```php
  <?php
  $var = 'foo';
  ```
-->



Discussion:
Thank you for your report, it is interesting case 🙂. Do you mind providing a PR with a fix?

FYI: Reproduced on `3.16-DEV` (current master).
OK, I've tried to fix the problem, please correct me if there are any problems




Patch:
diff --git a/src/Fixer/ReturnNotation/ReturnAssignmentFixer.php b/src/Fixer/ReturnNotation/ReturnAssignmentFixer.php
index fe17b30246a..291f9705d4d 100644
--- a/src/Fixer/ReturnNotation/ReturnAssignmentFixer.php
+++ b/src/Fixer/ReturnNotation/ReturnAssignmentFixer.php
@@ -84,10 +84,11 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
                     $functionCloseIndex
                 );
 
+                $functionCloseIndex += $tokensAdded;
                 $totalTokensAdded += $tokensAdded;
             } while ($tokensAdded > 0);
 
-            $index = $functionCloseIndex + $totalTokensAdded;
+            $index = $functionCloseIndex;
             $tokenCount += $totalTokensAdded;
         }
     }
@@ -275,6 +276,11 @@ private function fixFunction(Tokens $tokens, int $functionIndex, int $functionOp
                 continue;
             }
 
+            // Check if there is a `catch` or `finally` block between the assignment and the return
+            if ($this->isUsedInCatchOrFinally($tokens, $returnVarIndex, $functionOpenIndex, $functionCloseIndex)) {
+                continue;
+            }
+
             // Note: here we are @ "[;{}] $a = [^;{<? ? >] ; return $a;"
             $inserted += $this->simplifyReturnStatement(
                 $tokens,
@@ -463,4 +469,59 @@ private function isOpenBraceOfMatch(Tokens $tokens, int $index): ?int
 
         return $tokens[$index]->isGivenKind(T_MATCH) ? $index : null;
     }
+
+    private function isUsedInCatchOrFinally(Tokens $tokens, int $returnVarIndex, int $functionOpenIndex, int $functionCloseIndex): bool
+    {
+        // Find try
+        $tryIndex = $tokens->getPrevTokenOfKind($returnVarIndex, [[T_TRY]]);
+        if (null === $tryIndex || $tryIndex <= $functionOpenIndex) {
+            return false;
+        }
+        $tryOpenIndex = $tokens->getNextTokenOfKind($tryIndex, ['{']);
+        $tryCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $tryOpenIndex);
+
+        // Find catch or finally
+        $nextIndex = $tokens->getNextMeaningfulToken($tryCloseIndex);
+        if (null === $nextIndex) {
+            return false;
+        }
+
+        // Find catches
+        while ($tokens[$nextIndex]->isGivenKind(T_CATCH)) {
+            $catchOpenIndex = $tokens->getNextTokenOfKind($nextIndex, ['{']);
+            $catchCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $catchOpenIndex);
+
+            if ($catchCloseIndex >= $functionCloseIndex) {
+                return false;
+            }
+            $varIndex = $tokens->getNextTokenOfKind($catchOpenIndex, [$tokens[$returnVarIndex]]);
+            // Check if the variable is used in the finally block
+            if (null !== $varIndex && $varIndex < $catchCloseIndex) {
+                return true;
+            }
+
+            $nextIndex = $tokens->getNextMeaningfulToken($catchCloseIndex);
+            if (null === $nextIndex) {
+                return false;
+            }
+        }
+
+        if (!$tokens[$nextIndex]->isGivenKind(T_FINALLY)) {
+            return false;
+        }
+
+        $finallyIndex = $nextIndex;
+        if ($finallyIndex >= $functionCloseIndex) {
+            return false;
+        }
+        $finallyOpenIndex = $tokens->getNextTokenOfKind($finallyIndex, ['{']);
+        $finallyCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $finallyOpenIndex);
+        $varIndex = $tokens->getNextTokenOfKind($finallyOpenIndex, [$tokens[$returnVarIndex]]);
+        // Check if the variable is used in the finally block
+        if (null !== $varIndex && $varIndex < $finallyCloseIndex) {
+            return true;
+        }
+
+        return false;
+    }
 }


Test patch:
diff --git a/tests/Fixer/ReturnNotation/ReturnAssignmentFixerTest.php b/tests/Fixer/ReturnNotation/ReturnAssignmentFixerTest.php
index f43e7b5943d..6fb05bddd67 100644
--- a/tests/Fixer/ReturnNotation/ReturnAssignmentFixerTest.php
+++ b/tests/Fixer/ReturnNotation/ReturnAssignmentFixerTest.php
@@ -620,6 +620,178 @@ function Foo() {
                     }
                 ',
             ],
+            'try catch' => [
+                '<?php
+                function foo()
+                {
+                    if (isSomeCondition()) {
+                        return getSomeResult();
+                    }
+
+                    try {
+                        $result = getResult();
+
+                        return $result;
+                    } catch (\Throwable $exception) {
+                        baz($result ?? null);
+                    }
+                }
+                ',
+                '<?php
+                function foo()
+                {
+                    if (isSomeCondition()) {
+                        $result = getSomeResult();
+
+                        return $result;
+                    }
+
+                    try {
+                        $result = getResult();
+
+                        return $result;
+                    } catch (\Throwable $exception) {
+                        baz($result ?? null);
+                    }
+                }
+                ',
+            ],
+            'multiple try/catch blocks separated with conditional return' => [
+                '<?php
+                function foo()
+                {
+                    try {
+                        return getResult();
+                    } catch (\Throwable $exception) {
+                        error_log($exception->getMessage());
+                    }
+
+                    if (isSomeCondition()) {
+                        return getSomeResult();
+                    }
+
+                    try {
+                        $result = $a + $b;
+                        return $result;
+                    } catch (\Throwable $th) {
+                        var_dump($result ?? null);
+                    }
+                }
+                ',
+                '<?php
+                function foo()
+                {
+                    try {
+                        $result = getResult();
+                        return $result;
+                    } catch (\Throwable $exception) {
+                        error_log($exception->getMessage());
+                    }
+
+                    if (isSomeCondition()) {
+                        $result = getSomeResult();
+                        return $result;
+                    }
+
+                    try {
+                        $result = $a + $b;
+                        return $result;
+                    } catch (\Throwable $th) {
+                        var_dump($result ?? null);
+                    }
+                }
+                ',
+            ],
+            'try/catch/finally' => [
+                '<?php
+                function foo()
+                {
+                    if (isSomeCondition()) {
+                        return getSomeResult();
+                    }
+
+                    try {
+                        $result = getResult();
+
+                        return $result;
+                    } catch (\Throwable $exception) {
+                        error_log($exception->getMessage());
+                    } finally {
+                        baz($result);
+                    }
+                }
+                ',
+                '<?php
+                function foo()
+                {
+                    if (isSomeCondition()) {
+                        $result = getSomeResult();
+
+                        return $result;
+                    }
+
+                    try {
+                        $result = getResult();
+
+                        return $result;
+                    } catch (\Throwable $exception) {
+                        error_log($exception->getMessage());
+                    } finally {
+                        baz($result);
+                    }
+                }
+                ',
+            ],
+            'multiple try/catch separated with conditional return, with finally block' => [
+                '<?php
+                function foo()
+                {
+                    try {
+                        return getResult();
+                    } catch (\Throwable $exception) {
+                        error_log($exception->getMessage());
+                    }
+
+                    if (isSomeCondition()) {
+                        return getSomeResult();
+                    }
+
+                    try {
+                        $result = $a + $b;
+                        return $result;
+                    } catch (\Throwable $th) {
+                        throw $th;
+                    } finally {
+                        echo "result:", $result, \PHP_EOL;
+                    }
+                }
+                ',
+                '<?php
+                function foo()
+                {
+                    try {
+                        $result = getResult();
+                        return $result;
+                    } catch (\Throwable $exception) {
+                        error_log($exception->getMessage());
+                    }
+
+                    if (isSomeCondition()) {
+                        $result = getSomeResult();
+                        return $result;
+                    }
+
+                    try {
+                        $result = $a + $b;
+                        return $result;
+                    } catch (\Throwable $th) {
+                        throw $th;
+                    } finally {
+                        echo "result:", $result, \PHP_EOL;
+                    }
+                }
+                ',
+            ],
         ];
     }
 
@@ -977,6 +1149,90 @@ function F() {
                 }
                 ',
             ],
+            'try/catch/finally' => [
+                '<?php
+                function add($a, $b): mixed
+                {
+                    try {
+                        $result = $a + $b;
+
+                        return $result;
+                    } catch (\Throwable $th) {
+                        throw $th;
+                    } finally {
+                        echo \'result:\', $result, \PHP_EOL;
+                    }
+                }
+                ',
+            ],
+            'try with multiple catch blocks' => [
+                '<?php
+                function foo() {
+                    try {
+                        $bar = bar();
+
+                        return $bar;
+                    } catch (\LogicException $e) {
+                        echo "catch ... ";
+                    } catch (\RuntimeException $e) {
+                        echo $bar;
+                    }
+                }',
+            ],
+            'try/catch/finally with some comments' => [
+                '<?php
+                function add($a, $b): mixed
+                {
+                    try {
+                        $result = $a + $b;
+
+                        return $result;
+                    } /* foo */ catch /** bar */ (\LogicException $th) {
+                        throw $th;
+                    }
+                    // Or maybe this....
+                    catch (\RuntimeException $th) {
+                        throw $th;
+                    }
+                    # Print the result anyway
+                    finally {
+                        echo \'result:\', $result, \PHP_EOL;
+                    }
+                }
+                ',
+            ],
+        ];
+    }
+
+    /**
+     * @dataProvider provideDoNotFix80Cases
+     *
+     * @requires PHP 8.0
+     */
+    public function testDoNotFix80(string $expected): void
+    {
+        $this->doTest($expected);
+    }
+
+    public static function provideDoNotFix80Cases(): iterable
+    {
+        yield 'try with non-capturing catch block' => [
+            '<?php
+                function add($a, $b): mixed
+                {
+                    try {
+                        $result = $a + $b;
+
+                        return $result;
+                    }
+                    catch (\Throwable) {
+                        noop();
+                    }
+                    finally {
+                        echo \'result:\', $result, \PHP_EOL;
+                    }
+                }
+                ',
         ];
     }