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

Issue:
MethodChainingIndentationFixer - Recognize chained arrow functions
## Feature request

Currently `method_chaining_indentation` will pull every line back to a single level of indentation when using chained arrow functions.  This produces extremely confusing output.

```php
// desired:
Connection::query()
    ->whereExists(fn ($query) => $query
        ->from('table1')
        ->join('table2', fn ($joinClause) => $joinClause
            ->on('table2.id', 'table1.id'))
        ->where('a', 'b'))
    ->where('c', 'd');

// current output:
Connection::query()
    ->whereExists(fn ($query) => $query
    ->from('table1')
    ->join('table2', fn ($joinClause) => $joinClause
    ->on('table2.id', 'table1.id'))
    ->where('a', 'b'))
    ->where('c', 'd');
```

Currently the best I can do is much less compact than I would prefer, and in my opinion less readable:
```php
Connection::query()
    ->whereExists(
        fn ($query) => $query
            ->from('table1')
            ->join(
                'table2',
                fn ($joinClause) => $joinClause
                    ->on('table2.id', 'table1.id'),
            )
            ->where('a', 'b')
    )
    ->where('c', 'd');
```
Also, if I use traditional closures, it already does what I would expect:
```php
Connection::query()
    ->whereExists(function ($query) {
        return $query
            ->from('table1')
            ->join('table2', function ($joinClause) {
                return $joinClause
                    ->on('table2.id', 'table1.id');
            })
            ->where('a', 'b');
    })
    ->where('c', 'd');
```

Pull request description:
bug: Fix MethodChainingIndentationFixer with arrow functions and class instantiation
Fixes #5021 

Patch:
diff --git a/src/Fixer/Whitespace/MethodChainingIndentationFixer.php b/src/Fixer/Whitespace/MethodChainingIndentationFixer.php
index 01bf9a807..1aa773613 100644
--- a/src/Fixer/Whitespace/MethodChainingIndentationFixer.php
+++ b/src/Fixer/Whitespace/MethodChainingIndentationFixer.php
@@ -20,7 +20,6 @@
 use PhpCsFixer\FixerDefinition\FixerDefinition;
 use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
 use PhpCsFixer\Preg;
-use PhpCsFixer\Tokenizer\CT;
 use PhpCsFixer\Tokenizer\Token;
 use PhpCsFixer\Tokenizer\Tokens;
 
@@ -197,12 +196,15 @@ private function getIndentContentAt(Tokens $tokens, int $index): string
      */
     private function currentLineRequiresExtraIndentLevel(Tokens $tokens, int $start, int $end): bool
     {
-        if ($tokens[$start + 1]->isObjectOperator()) {
-            return false;
-        }
+        $firstMeaningful = $tokens->getNextMeaningfulToken($start);
+
+        if ($tokens[$firstMeaningful]->isObjectOperator()) {
+            $thirdMeaningful = $tokens->getNextMeaningfulToken($tokens->getNextMeaningfulToken($firstMeaningful));
 
-        if ($tokens[$end]->isGivenKind(CT::T_BRACE_CLASS_INSTANTIATION_CLOSE)) {
-            return true;
+            return
+                $tokens[$thirdMeaningful]->equals('(')
+                && $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $thirdMeaningful) > $end
+            ;
         }
 
         return
diff --git a/src/Tokenizer/Analyzer/CommentsAnalyzer.php b/src/Tokenizer/Analyzer/CommentsAnalyzer.php
index cfda768a9..0666a21da 100644
--- a/src/Tokenizer/Analyzer/CommentsAnalyzer.php
+++ b/src/Tokenizer/Analyzer/CommentsAnalyzer.php
@@ -106,6 +106,10 @@ public function isBeforeStructuralElement(Tokens $tokens, int $index): bool
             return true;
         }
 
+        if ($tokens[$nextIndex]->isGivenKind(CT::T_USE_TRAIT)) {
+            return true;
+        }
+
         return false;
     }
 


Test patch:
diff --git a/tests/Fixer/Comment/CommentToPhpdocFixerTest.php b/tests/Fixer/Comment/CommentToPhpdocFixerTest.php
index 6ad3eb190..ccd93f459 100644
--- a/tests/Fixer/Comment/CommentToPhpdocFixerTest.php
+++ b/tests/Fixer/Comment/CommentToPhpdocFixerTest.php
@@ -334,6 +334,10 @@ public function provideTestCases(): array
                 namespace Foo\Bar;
 ',
             ],
+            [
+                '<?php /* header comment */ $foo = true; class Foo { /** @phpstan-use Bar<Baz> $bar */ use Bar; }',
+                '<?php /* header comment */ $foo = true; class Foo { /* @phpstan-use Bar<Baz> $bar */ use Bar; }',
+            ],
         ];
     }
 }
diff --git a/tests/Fixer/Phpdoc/PhpdocToCommentFixerTest.php b/tests/Fixer/Phpdoc/PhpdocToCommentFixerTest.php
index 34da69d77..4e22c5080 100644
--- a/tests/Fixer/Phpdoc/PhpdocToCommentFixerTest.php
+++ b/tests/Fixer/Phpdoc/PhpdocToCommentFixerTest.php
@@ -55,6 +55,11 @@ public function provideDocblocksCases(): array
  */
 class DocBlocks
 {
+    /**
+     * Do not convert this
+     */
+    use TestTrait;
+
     /**
      * Do not convert this
      */
diff --git a/tests/Fixer/Whitespace/MethodChainingIndentationFixerTest.php b/tests/Fixer/Whitespace/MethodChainingIndentationFixerTest.php
index e04de2199..85dba2207 100644
--- a/tests/Fixer/Whitespace/MethodChainingIndentationFixerTest.php
+++ b/tests/Fixer/Whitespace/MethodChainingIndentationFixerTest.php
@@ -250,6 +250,154 @@ public function provideFixCases(): array
      ->setPassword("233434" ,)
         ->setEmailConfirmed(false , )
 ->setEmailConfirmationCode("123456",    );
+',
+            ],
+            [
+                '<?php
+
+                $obj = (new Foo)
+                    ->setBar((new Bar)
+                        ->baz());
+',
+                '<?php
+
+                $obj = (new Foo)
+        ->setBar((new Bar)
+                            ->baz());
+',
+            ],
+            [
+                '<?php
+
+                $obj
+                    ->foo("bar", function ($baz) {
+                                    return $baz
+                                        ->on("table1", "table2");
+                                })
+                    ->where("a", "b");
+',
+                '<?php
+
+                $obj
+        ->foo("bar", function ($baz) {
+                        return $baz
+                                    ->on("table1", "table2");
+                    })
+                ->where("a", "b");
+',
+            ],
+            [
+                '<?php
+
+                $obj
+                    ->foo("baz", fn ($bar) => $bar
+                        ->baz("foobar"))
+                    ->baz();
+',
+                '<?php
+
+                $obj
+                                        ->foo("baz", fn ($bar) => $bar
+        ->baz("foobar"))
+                                ->baz();
+',
+            ],
+            [
+                '<?php
+
+                $obj
+                    ->foo("baz", fn (string $bar) => otherFunc($bar)
+                        ->baz("foobar"))
+                    ->baz();
+',
+                '<?php
+
+                $obj
+                                        ->foo("baz", fn (string $bar) => otherFunc($bar)
+                            ->baz("foobar"))
+                                ->baz();
+',
+            ],
+            [
+                '<?php
+
+                $obj
+                    ->foo("baz", fn (SomeClass $bar) => $bar
+                        ->baz("foobar"))
+                    ->baz();
+',
+                '<?php
+
+                $obj
+                                        ->foo("baz", fn (SomeClass $bar) => $bar
+        ->baz("foobar"))
+                                ->baz();
+',
+            ],
+            [
+                '<?php
+
+                $obj
+                    ->foo("baz", fn (?AnotherClass $bar) => $bar
+                        ->baz("foobar"))
+                    ->baz();
+',
+                '<?php
+
+                $obj
+                                        ->foo("baz", fn (?AnotherClass $bar) => $bar
+        ->baz("foobar"))
+                                ->baz();
+',
+            ],
+            [
+                '<?php
+
+                $obj
+        /*buahaha*/
+                    ->foo("baz", fn ($bar) => $bar
+                        ->baz/*buahaha*/("foobar"))
+                    ->/**buahaha*/baz();
+',
+                '<?php
+
+                $obj
+        /*buahaha*/                                ->foo("baz", fn ($bar) => $bar
+        ->baz/*buahaha*/("foobar"))
+                                ->/**buahaha*/baz();
+',
+            ],
+            [
+                '<?php
+
+                $obj
+                    ->      foo("baz", fn ($bar) => $bar
+                        ->baz              ("foobar"))
+                    ->       baz  ();
+',
+                '<?php
+
+                $obj
+                                        ->      foo("baz", fn ($bar) => $bar
+        ->baz              ("foobar"))
+                                ->       baz  ();
+',
+            ],
+            [
+                '<?php
+
+    $user->setEmail("voff.web@gmail.com", )
+        ->setPassword("233434" ,)
+        ->setEmailConfirmed(false , )
+        ->setEmailConfirmationCode("123456",    );
+',
+                '<?php
+
+    $user->setEmail("voff.web@gmail.com", )
+
+     ->setPassword("233434" ,)
+        ->setEmailConfirmed(false , )
+->setEmailConfirmationCode("123456",    );
 ',
             ],
         ];
diff --git a/tests/Tokenizer/Analyzer/CommentsAnalyzerTest.php b/tests/Tokenizer/Analyzer/CommentsAnalyzerTest.php
index 9e63e33a0..db5f96a18 100644
--- a/tests/Tokenizer/Analyzer/CommentsAnalyzerTest.php
+++ b/tests/Tokenizer/Analyzer/CommentsAnalyzerTest.php
@@ -238,6 +238,7 @@ public function providePhpdocCandidateCases(): array
             ['<?php class Foo { /* Before property */ public $bar; }'],
             ['<?php class Foo { /* Before property */ var $bar; }'],
             ['<?php class Foo { /* Before function */ function bar() {} }'],
+            ['<?php class Foo { /* Before use */ use Bar; }'],
             ['<?php class Foo { /* Before function */ final function bar() {} }'],
             ['<?php class Foo { /* Before function */ private function bar() {} }'],
             ['<?php class Foo { /* Before function */ protected function bar() {} }'],