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

Issue:
`class_definition` is reformatting phpdoc for anonymous class
## Bug report

Code like:
```
$model = new
/**
 * @property string $x
 */
class() extends AtkModel {
    public int $p;
};
```

is currently fixed to:
```
$model = new /**
 * @property string $x
 */ class() extends AtkModel {
    public int $p;
};
```

I would expect no change and all wrongly formatted code fixed to the 1st example.

related with https://github.com/phpstan/phpstan/issues/7345#issuecomment-1848966265

Pull request:
fix: ClassDefinitionFixer for anonymous class with phpdoc/attribute on separate line
fix #7541

Patch:
diff --git a/src/Fixer/ClassNotation/ClassDefinitionFixer.php b/src/Fixer/ClassNotation/ClassDefinitionFixer.php
index ce8fb1826..e9a8b00af 100644
--- a/src/Fixer/ClassNotation/ClassDefinitionFixer.php
+++ b/src/Fixer/ClassNotation/ClassDefinitionFixer.php
@@ -23,6 +23,7 @@
 use PhpCsFixer\FixerDefinition\CodeSample;
 use PhpCsFixer\FixerDefinition\FixerDefinition;
 use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
+use PhpCsFixer\Tokenizer\CT;
 use PhpCsFixer\Tokenizer\Token;
 use PhpCsFixer\Tokenizer\Tokens;
 use PhpCsFixer\Tokenizer\TokensAnalyzer;
@@ -362,15 +363,37 @@ private function makeClassyDefinitionSingleLine(Tokens $tokens, int $startIndex,
     {
         for ($i = $endIndex; $i >= $startIndex; --$i) {
             if ($tokens[$i]->isWhitespace()) {
-                if ($tokens[$i - 1]->isComment() || $tokens[$i + 1]->isComment()) {
+                if (str_contains($tokens[$i]->getContent(), "\n")) {
+                    if (\defined('T_ATTRIBUTE')) { // @TODO: drop condition and else when PHP 8.0+ is required
+                        if ($tokens[$i - 1]->isGivenKind(CT::T_ATTRIBUTE_CLOSE) || $tokens[$i + 1]->isGivenKind(T_ATTRIBUTE)) {
+                            continue;
+                        }
+                    } else {
+                        if (($tokens[$i - 1]->isComment() && str_ends_with($tokens[$i - 1]->getContent(), ']'))
+                            || ($tokens[$i + 1]->isComment() && str_starts_with($tokens[$i + 1]->getContent(), '#['))
+                        ) {
+                            continue;
+                        }
+                    }
+
+                    if ($tokens[$i - 1]->isGivenKind(T_DOC_COMMENT) || $tokens[$i + 1]->isGivenKind(T_DOC_COMMENT)) {
+                        continue;
+                    }
+                }
+
+                if ($tokens[$i - 1]->isComment()) {
                     $content = $tokens[$i - 1]->getContent();
+                    if (!str_starts_with($content, '//') && !str_starts_with($content, '#')) {
+                        $tokens[$i] = new Token([T_WHITESPACE, ' ']);
+                    }
 
-                    if (!('#' === $content || str_starts_with($content, '//'))) {
-                        $content = $tokens[$i + 1]->getContent();
+                    continue;
+                }
 
-                        if (!('#' === $content || str_starts_with($content, '//'))) {
-                            $tokens[$i] = new Token([T_WHITESPACE, ' ']);
-                        }
+                if ($tokens[$i + 1]->isComment()) {
+                    $content = $tokens[$i + 1]->getContent();
+                    if (!str_starts_with($content, '//')) {
+                        $tokens[$i] = new Token([T_WHITESPACE, ' ']);
                     }
 
                     continue;
diff --git a/src/Fixer/LanguageConstruct/SingleSpaceAroundConstructFixer.php b/src/Fixer/LanguageConstruct/SingleSpaceAroundConstructFixer.php
index 29d395f99..6e8cb79d4 100644
--- a/src/Fixer/LanguageConstruct/SingleSpaceAroundConstructFixer.php
+++ b/src/Fixer/LanguageConstruct/SingleSpaceAroundConstructFixer.php
@@ -355,6 +355,23 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
                 }
             }
 
+            if ($tokens[$whitespaceTokenIndex]->isWhitespace() && str_contains($tokens[$whitespaceTokenIndex]->getContent(), "\n")) {
+                $nextNextToken = $tokens[$whitespaceTokenIndex + 1];
+                if (\defined('T_ATTRIBUTE')) { // @TODO: drop condition and else when PHP 8.0+ is required
+                    if ($nextNextToken->isGivenKind(T_ATTRIBUTE)) {
+                        continue;
+                    }
+                } else {
+                    if ($nextNextToken->isComment() && str_starts_with($nextNextToken->getContent(), '#[')) {
+                        continue;
+                    }
+                }
+
+                if ($nextNextToken->isGivenKind(T_DOC_COMMENT)) {
+                    continue;
+                }
+            }
+
             $tokens->ensureWhitespaceAtIndex($whitespaceTokenIndex, 0, ' ');
         }
     }


Test patch:
diff --git a/tests/Fixer/ClassNotation/ClassDefinitionFixerTest.php b/tests/Fixer/ClassNotation/ClassDefinitionFixerTest.php
index 7bc2bc835..32089e2e7 100644
--- a/tests/Fixer/ClassNotation/ClassDefinitionFixerTest.php
+++ b/tests/Fixer/ClassNotation/ClassDefinitionFixerTest.php
@@ -79,7 +79,7 @@ public static function provideInvalidConfigurationCases(): iterable
      *
      * @dataProvider provideFixCases
      */
-    public function testFix(string $expected, string $input, array $configuration = []): void
+    public function testFix(string $expected, string $input = null, array $configuration = []): void
     {
         $this->fixer->configure($configuration);
         $this->doTest($expected, $input);
@@ -305,6 +305,67 @@ class#
             ['space_before_parenthesis' => true, 'inline_constructor_arguments' => false],
         ];
 
+        yield 'single attribute on separate line' => [
+            <<<'EOF'
+                <?php
+                $a = new
+                #[FOO]
+                class() {};
+                EOF,
+        ];
+
+        yield 'multiple attributes on separate line' => [
+            <<<'EOF'
+                <?php
+                $a = new
+                #[FOO]
+                #[\Ns\Bar]
+                class() {};
+                EOF,
+        ];
+
+        yield 'single line phpdoc on separate line' => [
+            <<<'EOF'
+                <?php
+                $a = new
+                /** @property string $x */
+                class() {};
+                EOF,
+        ];
+
+        yield 'multi line phpdoc on separate line' => [
+            <<<'EOF'
+                <?php
+                $a = new
+                /**
+                 @property string $x
+                 */
+                class() {};
+                EOF,
+        ];
+
+        yield 'phpdoc and single attribute on separate line' => [
+            <<<'EOF'
+                <?php
+                $a = new
+                /**
+                 @property string $x
+                 */
+                #[FOO]
+                class() {};
+                EOF,
+        ];
+
+        yield 'phpdoc and multiple attributes on separate line' => [
+            <<<'EOF'
+                <?php
+                $a = new
+                /** @property string $x */
+                #[FOO] #[\Ns\Bar]
+                class() {};
+                EOF,
+        ];
+
         yield from self::provideClassyCases('class');
 
         yield from self::provideClassyExtendingCases('class');
diff --git a/tests/Fixer/LanguageConstruct/SingleSpaceAfterConstructFixerTest.php b/tests/Fixer/LanguageConstruct/SingleSpaceAfterConstructFixerTest.php
index 8a5eb6ffd..d39c6cbd1 100644
--- a/tests/Fixer/LanguageConstruct/SingleSpaceAfterConstructFixerTest.php
+++ b/tests/Fixer/LanguageConstruct/SingleSpaceAfterConstructFixerTest.php
@@ -1878,6 +1878,26 @@ public static function provideFixWithNewCases(): iterable
             '<?php new /* foo */Bar();',
             '<?php new/* foo */Bar();',
         ];
+
+        yield 'attribute on separate line' => [
+            <<<'EOF'
+                <?php
+                $a = new
+                #[FOO]
+                class() {};
+                EOF,
+        ];
+
+        yield 'phpdoc on separate line' => [
+            <<<'EOF'
+                <?php
+                $a = new
+                /**
+                 @property string $x
+                 */
+                class() {};
+                EOF,
+        ];
     }
 
     /**
diff --git a/tests/Fixer/LanguageConstruct/SingleSpaceAroundConstructFixerTest.php b/tests/Fixer/LanguageConstruct/SingleSpaceAroundConstructFixerTest.php
index 436885a17..2c11922b9 100644
--- a/tests/Fixer/LanguageConstruct/SingleSpaceAroundConstructFixerTest.php
+++ b/tests/Fixer/LanguageConstruct/SingleSpaceAroundConstructFixerTest.php
@@ -2122,6 +2122,26 @@ public static function provideFixWithNewCases(): iterable
             '<?php new /* foo */Bar();',
             '<?php new/* foo */Bar();',
         ];
+
+        yield 'attribute on separate line' => [
+            <<<'EOF'
+                <?php
+                $a = new
+                #[FOO]
+                class() {};
+                EOF,
+        ];
+
+        yield 'phpdoc on separate line' => [
+            <<<'EOF'
+                <?php
+                $a = new
+                /**
+                 @property string $x
+                 */
+                class() {};
+                EOF,
+        ];
     }
 
     /**
diff --git a/tests/Fixtures/Integration/misc/anonymous_class_with_phpdoc.test b/tests/Fixtures/Integration/misc/anonymous_class_with_phpdoc.test
new file mode 100644
index 000000000..a1faa8cb5
--- /dev/null
+++ b/tests/Fixtures/Integration/misc/anonymous_class_with_phpdoc.test
@@ -0,0 +1,50 @@
+--TEST--
+Integration of fixers: Anonymous class /w PHPDoc and attributes on separate line.
+--RULESET--
+{"@PhpCsFixer": true}
+--EXPECT--
+<?php
+
+$a = new
+/** @property string $x */
+class() {};
+
+$b = new
+#[X]
+class() {};
+
+$c = new
+/** @property string $x */
+#[X] #[Y\Z]
+class() {};
+
+class Z {}
+
+$d = new
+#[X] // comment
+class() {};
+
+--INPUT--
+<?php
+
+$a = new
+ /** @property string $x */
+
+class() {};
+
+
+$b = new
+#[X]
+class() {};
+
+
+$c = new
+/** @property string $x */
+#[X] #[Y\Z]
+class() {};
+
+class Z {}
+
+$d = new
+#[X] // comment
+class() {};