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

Issue:
`statement_indentation` is too aggressive since 7384 PR
## Feature request

#7384 introduced support for `comment for continuous control statement`. As not everyone up for it, we can introduce rule configuration to opt in/out

----------

Original content: 

## Bug report

introduced in #7384

the PR removed/updated several important tests like:

![image](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/assets/2228672/6db3f3b4-40fb-4627-acf7-13584878a6fd)

In short, it cannot be assumed:

```
if ($v) {
    // todo
}
```

comment only in `if` body is fine but

```diff
-perfectly fine code
+output after 7384 PR
 if ($v) {
     $u = 5;
-    // todo
+// todo
 } else {}
```

with comment at the end it is not. Comments below code can be still contextually valid and relevant to the previous code.

Pull request description:
feat: StatementIndentationFixer - introduce stick_comment_to_next_continuous_control_statement config
closes #7497 

Patch:
diff --git a/doc/ruleSets/Symfony.rst b/doc/ruleSets/Symfony.rst
index 94bea51aa..8e7471ff2 100644
--- a/doc/ruleSets/Symfony.rst
+++ b/doc/ruleSets/Symfony.rst
@@ -165,6 +165,10 @@ Rules
 
 - `standardize_increment <./../rules/operator/standardize_increment.rst>`_
 - `standardize_not_equals <./../rules/operator/standardize_not_equals.rst>`_
+- `statement_indentation <./../rules/whitespace/statement_indentation.rst>`_ with config:
+
+  ``['stick_comment_to_next_continuous_control_statement' => true]``
+
 - `switch_continue_to_break <./../rules/control_structure/switch_continue_to_break.rst>`_
 - `trailing_comma_in_multiline <./../rules/control_structure/trailing_comma_in_multiline.rst>`_
 - `trim_array_spaces <./../rules/array_notation/trim_array_spaces.rst>`_
diff --git a/doc/rules/whitespace/statement_indentation.rst b/doc/rules/whitespace/statement_indentation.rst
index 2d1cb36d0..68b037050 100644
--- a/doc/rules/whitespace/statement_indentation.rst
+++ b/doc/rules/whitespace/statement_indentation.rst
@@ -4,12 +4,26 @@ Rule ``statement_indentation``
 
 Each statement must be indented.
 
+Configuration
+-------------
+
+``stick_comment_to_next_continuous_control_statement``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Last comment of code block counts as comment for next block.
+
+Allowed types: ``bool``
+
+Default value: ``false``
+
 Examples
 --------
 
 Example #1
 ~~~~~~~~~~
 
+*Default* configuration.
+
 .. code-block:: diff
 
    --- Original
@@ -24,6 +38,53 @@ Example #1
    +    echo "bar";
     }
 
+Example #2
+~~~~~~~~~~
+
+With configuration: ``['stick_comment_to_next_continuous_control_statement' => false]``.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+   -        // foo
+   +// foo
+    if ($foo) {
+        echo "foo";
+   -        // this is treated as comment of `if` block, as `stick_comment_to_next_continuous_control_statement` is disabled
+   +    // this is treated as comment of `if` block, as `stick_comment_to_next_continuous_control_statement` is disabled
+    } else {
+        $aaa = 1;
+    }
+
+Example #3
+~~~~~~~~~~
+
+With configuration: ``['stick_comment_to_next_continuous_control_statement' => true]``.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+   -        // foo
+   +// foo
+    if ($foo) {
+        echo "foo";
+   -        // this is treated as comment of `elseif(1)` block, as `stick_comment_to_next_continuous_control_statement` is enabled
+   +// this is treated as comment of `elseif(1)` block, as `stick_comment_to_next_continuous_control_statement` is enabled
+    } elseif(1) {
+        echo "bar";
+    } elseif(2) {
+   -        // this is treated as comment of `elseif(2)` block, as the only content of that block
+   +    // this is treated as comment of `elseif(2)` block, as the only content of that block
+    } elseif(3) {
+        $aaa = 1;
+   -        // this is treated as comment of `elseif(3)` block, as it is a comment in the final block
+   +    // this is treated as comment of `elseif(3)` block, as it is a comment in the final block
+    }
+
 Rule sets
 ---------
 
@@ -35,8 +96,14 @@ The rule is part of the following rule sets:
 - `@PER-CS2.0 <./../../ruleSets/PER-CS2.0.rst>`_
 - `@PSR2 <./../../ruleSets/PSR2.rst>`_
 - `@PSR12 <./../../ruleSets/PSR12.rst>`_
-- `@PhpCsFixer <./../../ruleSets/PhpCsFixer.rst>`_
-- `@Symfony <./../../ruleSets/Symfony.rst>`_
+- `@PhpCsFixer <./../../ruleSets/PhpCsFixer.rst>`_ with config:
+
+  ``['stick_comment_to_next_continuous_control_statement' => true]``
+
+- `@Symfony <./../../ruleSets/Symfony.rst>`_ with config:
+
+  ``['stick_comment_to_next_continuous_control_statement' => true]``
+
 
 Source class
 ------------
diff --git a/phpstan.dist.neon b/phpstan.dist.neon
index c729245b6..8c0b644f3 100644
--- a/phpstan.dist.neon
+++ b/phpstan.dist.neon
@@ -21,7 +21,7 @@ parameters:
         -
             message: '#^Method PhpCsFixer\\Tests\\.+::provide.+Cases\(\) return type has no value type specified in iterable type iterable\.$#'
             path: tests
-            count: 1039
+            count: 1035
 
         -
             message: '#Call to static method .+ with .+ will always evaluate to true.$#'
diff --git a/src/Fixer/Whitespace/StatementIndentationFixer.php b/src/Fixer/Whitespace/StatementIndentationFixer.php
index d15f56c8a..59fed8f1c 100644
--- a/src/Fixer/Whitespace/StatementIndentationFixer.php
+++ b/src/Fixer/Whitespace/StatementIndentationFixer.php
@@ -15,8 +15,12 @@
 namespace PhpCsFixer\Fixer\Whitespace;
 
 use PhpCsFixer\AbstractFixer;
+use PhpCsFixer\Fixer\ConfigurableFixerInterface;
 use PhpCsFixer\Fixer\Indentation;
 use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
+use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
+use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
+use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
 use PhpCsFixer\FixerDefinition\CodeSample;
 use PhpCsFixer\FixerDefinition\FixerDefinition;
 use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
@@ -26,7 +30,7 @@
 use PhpCsFixer\Tokenizer\Token;
 use PhpCsFixer\Tokenizer\Tokens;
 
-final class StatementIndentationFixer extends AbstractFixer implements WhitespacesAwareFixerInterface
+final class StatementIndentationFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
 {
     use Indentation;
 
@@ -56,6 +60,35 @@ public function getDefinition(): FixerDefinitionInterface
 }
 '
                 ),
+                new CodeSample(
+                    '<?php
+        // foo
+if ($foo) {
+    echo "foo";
+        // this is treated as comment of `if` block, as `stick_comment_to_next_continuous_control_statement` is disabled
+} else {
+    $aaa = 1;
+}
+',
+                    ['stick_comment_to_next_continuous_control_statement' => false]
+                ),
+                new CodeSample(
+                    '<?php
+        // foo
+if ($foo) {
+    echo "foo";
+        // this is treated as comment of `elseif(1)` block, as `stick_comment_to_next_continuous_control_statement` is enabled
+} elseif(1) {
+    echo "bar";
+} elseif(2) {
+        // this is treated as comment of `elseif(2)` block, as the only content of that block
+} elseif(3) {
+    $aaa = 1;
+        // this is treated as comment of `elseif(3)` block, as it is a comment in the final block
+}
+',
+                    ['stick_comment_to_next_continuous_control_statement' => true]
+                ),
             ]
         );
     }
@@ -76,6 +109,16 @@ public function isCandidate(Tokens $tokens): bool
         return true;
     }
 
+    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
+    {
+        return new FixerConfigurationResolver([
+            (new FixerOptionBuilder('stick_comment_to_next_continuous_control_statement', 'Last comment of code block counts as comment for next block.'))
+                ->setAllowedTypes(['bool'])
+                ->setDefault(false)
+                ->getOption(),
+        ]);
+    }
+
     protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
     {
         $this->alternativeSyntaxAnalyzer = new AlternativeSyntaxAnalyzer();
@@ -383,7 +426,7 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
                                     $nextNextIndex = $tokens->getNextMeaningfulToken($nextIndex);
 
                                     if (null !== $nextNextIndex && $tokens[$nextNextIndex]->isGivenKind([T_ELSE, T_ELSEIF])) {
-                                        $indent = false;
+                                        $indent = true !== $this->configuration['stick_comment_to_next_continuous_control_statement'];
                                     } else {
                                         $indent = true;
                                     }
diff --git a/src/RuleSet/Sets/SymfonySet.php b/src/RuleSet/Sets/SymfonySet.php
index d2b1f1773..6e75abcea 100644
--- a/src/RuleSet/Sets/SymfonySet.php
+++ b/src/RuleSet/Sets/SymfonySet.php
@@ -206,6 +206,9 @@ public function getRules(): array
             ],
             'standardize_increment' => true,
             'standardize_not_equals' => true,
+            'statement_indentation' => [
+                'stick_comment_to_next_continuous_control_statement' => true,
+            ],
             'switch_continue_to_break' => true,
             'trailing_comma_in_multiline' => true,
             'trim_array_spaces' => true,


Test patch:
diff --git a/tests/Fixer/Basic/BracesFixerTest.php b/tests/Fixer/Basic/BracesFixerTest.php
index f4ae52451..9e1cac831 100644
--- a/tests/Fixer/Basic/BracesFixerTest.php
+++ b/tests/Fixer/Basic/BracesFixerTest.php
@@ -1136,7 +1136,7 @@ public static function provideFixMissingBracesAndIndentCases(): iterable
         $a = "a";
     } elseif (3) {
         $b = "b";
-    // comment
+        // comment
     } else {
         $c = "c";
     }
@@ -1898,7 +1898,7 @@ public function __construct(
         $a = "a";
     } elseif (3) {
         $b = "b";
-    // comment
+        // comment
     } else {
         $c = "c";
     }
@@ -1908,29 +1908,6 @@ public function __construct(
             self::CONFIGURATION_OOP_POSITION_SAME_LINE,
         ];
 
-        yield 'multiline comment in block' => [
-            '<?php
-if (1) {
-    $b = "a";
-// multiline comment line 1
-// multiline comment line 2
-} elseif (2) {
-    // empty
-} else {
-    $c = "b";
-}',
-            '<?php
-if (1) {
-    $b = "a";
-        // multiline comment line 1
-        // multiline comment line 2
-} elseif (2) {
-// empty
-} else {
-    $c = "b";
-}',
-        ];
-
         yield [
             '<?php
 if (1) {
@@ -1938,10 +1915,10 @@ public function __construct(
         $a = "a";
     } elseif (3) {
         $b = "b";
-    // comment line 1
-    // comment line 2
-    // comment line 3
-    // comment line 4
+        // comment line 1
+        // comment line 2
+        // comment line 3
+        // comment line 4
     } else {
         $c = "c";
     }
@@ -3245,10 +3222,10 @@ public static function provideFixCommentBeforeBraceCases(): iterable
     // 2.5+ API
     if (isNewApi()) {
         echo "new API";
-    // 2.4- API
+        // 2.4- API
     } elseif (isOldApi()) {
         echo "old API";
-    // other API
+        // 2.4- API
     } else {
         echo "unknown API";
         // sth
diff --git a/tests/Fixer/Whitespace/StatementIndentationFixerTest.php b/tests/Fixer/Whitespace/StatementIndentationFixerTest.php
index cf1e300eb..1338963ae 100644
--- a/tests/Fixer/Whitespace/StatementIndentationFixerTest.php
+++ b/tests/Fixer/Whitespace/StatementIndentationFixerTest.php
@@ -25,13 +25,19 @@
 final class StatementIndentationFixerTest extends AbstractFixerTestCase
 {
     /**
+     * @param array<string, mixed> $configuration
+     *
      * @dataProvider provideFixCases
      */
-    public function testFix(string $expected, ?string $input = null): void
+    public function testFix(string $expected, ?string $input = null, array $configuration = []): void
     {
+        $this->fixer->configure($configuration);
         $this->doTest($expected, $input);
     }
 
+    /**
+     * @return iterable<array{0: string, 1?: ?string, 2?: array<string, mixed>}>
+     */
     public static function provideFixCases(): iterable
     {
         yield 'no brace block' => [
@@ -930,7 +936,27 @@ public function foo() {
 }',
         ];
 
-        yield 'comment before else blocks' => [
+        yield 'comment before else blocks WITHOUT stick_comment_to_next_continuous_control_statement' => [
+            '<?php
+// foo
+if ($foo) {
+    echo "foo";
+    // bar
+} else {
+    $aaa = 1;
+}',
+            '<?php
+        // foo
+if ($foo) {
+    echo "foo";
+        // bar
+} else {
+    $aaa = 1;
+}',
+            ['stick_comment_to_next_continuous_control_statement' => false],
+        ];
+
+        yield 'comment before else blocks WITH stick_comment_to_next_continuous_control_statement' => [
             '<?php
 // foo
 if ($foo) {
@@ -940,13 +966,14 @@ public function foo() {
     $aaa = 1;
 }',
             '<?php
-    // foo
+        // foo
 if ($foo) {
     echo "foo";
-    // bar
+        // bar
 } else {
     $aaa = 1;
 }',
+            ['stick_comment_to_next_continuous_control_statement' => true],
         ];
 
         yield 'multiline comment in block - describing next block' => [
@@ -968,6 +995,7 @@ public function foo() {
 } else {
     $c = "b";
 }',
+            ['stick_comment_to_next_continuous_control_statement' => true],
         ];
 
         yield 'multiline comment in block - the only content in block' => [
@@ -1016,6 +1044,7 @@ public function foo() {
     $aaa = 1;
     // end comment in final block
 }',
+            ['stick_comment_to_next_continuous_control_statement' => true],
         ];
 
         yield 'comments at the end of if/elseif/else blocks' => [
@@ -1041,18 +1070,25 @@ public function foo() {
     echo "baz";
     // baz
 }',
+            ['stick_comment_to_next_continuous_control_statement' => true],
         ];
     }
 
     /**
+     * @param array<string, mixed> $configuration
+     *
      * @dataProvider provideFixWithTabsCases
      */
-    public function testFixWithTabs(string $expected, ?string $input = null): void
+    public function testFixWithTabs(string $expected, ?string $input = null, array $configuration = []): void
     {
+        $this->fixer->configure($configuration);
         $this->fixer->setWhitespacesConfig(new WhitespacesFixerConfig("\t"));
         $this->doTest($expected, $input);
     }
 
+    /**
+     * @return iterable<array{0: string, 1?: ?string, 2?: array<string, mixed>}>
+     */
     public static function provideFixWithTabsCases(): iterable
     {
         yield 'simple' => [
@@ -1070,15 +1106,21 @@ public static function provideFixWithTabsCases(): iterable
     }
 
     /**
+     * @param array<string, mixed> $configuration
+     *
      * @dataProvider provideFixPhp80Cases
      *
      * @requires PHP 8.0
      */
-    public function testFixPhp80(string $expected, ?string $input = null): void
+    public function testFixPhp80(string $expected, ?string $input = null, array $configuration = []): void
     {
+        $this->fixer->configure($configuration);
         $this->doTest($expected, $input);
     }
 
+    /**
+     * @return iterable<array{0: string, 1?: ?string, 2?: array<string, mixed>}>
+     */
     public static function provideFixPhp80Cases(): iterable
     {
         yield 'match expression' => [
@@ -1137,15 +1179,21 @@ public function bar()
     }
 
     /**
+     * @param array<string, mixed> $configuration
+     *
      * @dataProvider provideFixPhp81Cases
      *
      * @requires PHP 8.1
      */
-    public function testFixPhp81(string $expected, ?string $input = null): void
+    public function testFixPhp81(string $expected, ?string $input = null, array $configuration = []): void
     {
+        $this->fixer->configure($configuration);
         $this->doTest($expected, $input);
     }
 
+    /**
+     * @return iterable<array{0: string, 1?: ?string, 2?: array<string, mixed>}>
+     */
     public static function provideFixPhp81Cases(): iterable
     {
         yield 'simple enum' => [