Browse Source

feature: add SingleLineEmptyBodyFixer (#6933)

Replaces https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/pull/5757 with new approach.
Kuba Werłos 1 year ago
parent
commit
aee6ebf2ec

+ 5 - 0
doc/list.rst

@@ -2845,6 +2845,11 @@ List of Available Rules
    Part of rule sets `@PhpCsFixer <./ruleSets/PhpCsFixer.rst>`_ `@Symfony <./ruleSets/Symfony.rst>`_
 
    `Source PhpCsFixer\\Fixer\\Comment\\SingleLineCommentStyleFixer <./../src/Fixer/Comment/SingleLineCommentStyleFixer.php>`_
+-  `single_line_empty_body <./rules/basic/single_line_empty_body.rst>`_
+
+   Empty body of class or function must be abbreviated as ``{}`` and placed on the same line as the previous symbol, separated by a space.
+
+   `Source PhpCsFixer\\Fixer\\Basic\\SingleLineEmptyBodyFixer <./../src/Fixer/Basic/SingleLineEmptyBodyFixer.php>`_
 -  `single_line_throw <./rules/function_notation/single_line_throw.rst>`_
 
    Throwing exception must be done in single line.

+ 23 - 0
doc/rules/basic/single_line_empty_body.rst

@@ -0,0 +1,23 @@
+===============================
+Rule ``single_line_empty_body``
+===============================
+
+Empty body of class or function must be abbreviated as ``{}`` and placed on the
+same line as the previous symbol, separated by a space.
+
+Examples
+--------
+
+Example #1
+~~~~~~~~~~
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php function foo(
+        int $x
+   -)
+   -{
+   -}
+   +) {}

+ 3 - 0
doc/rules/index.rst

@@ -91,6 +91,9 @@ Basic
 - `psr_autoloading <./basic/psr_autoloading.rst>`_ *(risky)*
 
   Classes must be in a path that matches their namespace, be at least one namespace deep and the class name should match the file name.
+- `single_line_empty_body <./basic/single_line_empty_body.rst>`_
+
+  Empty body of class or function must be abbreviated as ``{}`` and placed on the same line as the previous symbol, separated by a space.
 
 Casing
 ------

+ 1 - 0
src/Fixer/Basic/CurlyBracesPositionFixer.php

@@ -150,6 +150,7 @@ $bar = function () { $result = true;
     /**
      * {@inheritdoc}
      *
+     * Must run before SingleLineEmptyBodyFixer.
      * Must run after ControlStructureBracesFixer.
      */
     public function getPriority(): int

+ 78 - 0
src/Fixer/Basic/SingleLineEmptyBodyFixer.php

@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Fixer\Basic;
+
+use PhpCsFixer\AbstractFixer;
+use PhpCsFixer\FixerDefinition\CodeSample;
+use PhpCsFixer\FixerDefinition\FixerDefinition;
+use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
+use PhpCsFixer\Tokenizer\Tokens;
+
+final class SingleLineEmptyBodyFixer extends AbstractFixer
+{
+    public function getDefinition(): FixerDefinitionInterface
+    {
+        return new FixerDefinition(
+            'Empty body of class or function must be abbreviated as `{}` and placed on the same line as the previous symbol, separated by a space.',
+            [new CodeSample('<?php function foo(
+    int $x
+)
+{
+}
+')],
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     *
+     * Must run after ClassDefinitionFixer, CurlyBracesPositionFixer.
+     */
+    public function getPriority(): int
+    {
+        return -1;
+    }
+
+    public function isCandidate(Tokens $tokens): bool
+    {
+        return $tokens->isAnyTokenKindsFound([T_CLASS, T_FUNCTION]);
+    }
+
+    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
+    {
+        for ($index = $tokens->count() - 1; $index > 0; --$index) {
+            if (!$tokens[$index]->isGivenKind([T_CLASS, T_FUNCTION])) {
+                continue;
+            }
+
+            $openBraceIndex = $tokens->getNextTokenOfKind($index, ['{', ';']);
+            if (!$tokens[$openBraceIndex]->equals('{')) {
+                continue;
+            }
+
+            $closeBraceIndex = $tokens->getNextNonWhitespace($openBraceIndex);
+            if (!$tokens[$closeBraceIndex]->equals('}')) {
+                continue;
+            }
+
+            $tokens->ensureWhitespaceAtIndex($openBraceIndex + 1, 0, '');
+
+            $beforeOpenBraceIndex = $tokens->getPrevNonWhitespace($openBraceIndex);
+            if (!$tokens[$beforeOpenBraceIndex]->isGivenKind([T_COMMENT, T_DOC_COMMENT])) {
+                $tokens->ensureWhitespaceAtIndex($openBraceIndex - 1, 1, ' ');
+            }
+        }
+    }
+}

+ 1 - 1
src/Fixer/ClassNotation/ClassDefinitionFixer.php

@@ -104,7 +104,7 @@ $foo = new class(){};
     /**
      * {@inheritdoc}
      *
-     * Must run before BracesFixer.
+     * Must run before BracesFixer, SingleLineEmptyBodyFixer.
      * Must run after NewWithBracesFixer.
      */
     public function getPriority(): int

+ 4 - 0
tests/AutoReview/FixerFactoryTest.php

@@ -369,6 +369,7 @@ final class FixerFactoryTest extends TestCase
             ],
             'class_definition' => [
                 'braces',
+                'single_line_empty_body',
             ],
             'class_keyword_remove' => [
                 'no_unused_imports',
@@ -395,6 +396,9 @@ final class FixerFactoryTest extends TestCase
                 'curly_braces_position',
                 'no_multiple_statements_per_line',
             ],
+            'curly_braces_position' => [
+                'single_line_empty_body',
+            ],
             'declare_strict_types' => [
                 'blank_line_after_opening_tag',
                 'declare_equal_normalize',

+ 297 - 0
tests/Fixer/Basic/SingleLineEmptyBodyFixerTest.php

@@ -0,0 +1,297 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Tests\Fixer\Basic;
+
+use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
+
+/**
+ * @internal
+ *
+ * @covers \PhpCsFixer\Fixer\Basic\SingleLineEmptyBodyFixer
+ */
+final class SingleLineEmptyBodyFixerTest extends AbstractFixerTestCase
+{
+    /**
+     * @dataProvider provideFixCases
+     */
+    public function testFix(string $expected, ?string $input = null): void
+    {
+        $this->doTest($expected, $input);
+    }
+
+    /**
+     * @return iterable<array{0: string, 1?: string}>
+     */
+    public static function provideFixCases(): iterable
+    {
+        yield 'non-empty class' => [
+            '<?php class Foo
+            {
+                public function bar () {}
+            }
+            ',
+        ];
+
+        yield 'non-empty function body' => [
+            '<?php
+                function f1()
+                { /* foo */ }
+                function f2()
+                { /** foo */ }
+                function f3()
+                { // foo
+                }
+                function f4()
+                {
+                    return true;
+                }
+            ',
+        ];
+
+        yield 'classes' => [
+            '<?php
+            class Foo {}
+            class Bar extends BarParent {}
+            class Baz implements BazInteface {}
+            abstract class A {}
+            final class F {}
+            ',
+            '<?php
+            class Foo
+            {
+            }
+            class Bar extends BarParent
+            {}
+            class Baz implements BazInteface    {}
+            abstract class A
+            {}
+            final class F
+            {
+
+            }
+            ',
+        ];
+
+        yield 'multiple functions' => [
+            '<?php
+                function notThis1()    { return 1; }
+                function f1() {}
+                function f2() {}
+                function f3() {}
+                function notThis2(){ return 1; }
+            ',
+            '<?php
+                function notThis1()    { return 1; }
+                function f1()
+                {}
+                function f2() {
+                }
+                function f3()
+                {
+                }
+                function notThis2(){ return 1; }
+            ',
+        ];
+
+        yield 'remove spaces' => [
+            '<?php
+                function f1() {}
+                function f2() {}
+                function f3() {}
+            ',
+            '<?php
+                function f1() { }
+                function f2() {  }
+                function f3() {    }
+            ',
+        ];
+
+        yield 'add spaces' => [
+            '<?php
+                function f1() {}
+                function f2() {}
+                function f3() {}
+            ',
+            '<?php
+                function f1(){}
+                function f2(){}
+                function f3(){}
+            ',
+        ];
+
+        yield 'with return types' => [
+            '<?php
+                function f1(): void {}
+                function f2(): \Foo\Bar {}
+                function f3(): ?string {}
+            ',
+            '<?php
+                function f1(): void
+                {}
+                function f2(): \Foo\Bar    {    }
+                function f3(): ?string {
+
+
+                }
+            ',
+        ];
+
+        yield 'abstract functions' => [
+            '<?php abstract class C {
+                abstract function f1();
+                function f2() {}
+                abstract function f3();
+            }
+            if (true)    {    }
+            ',
+            '<?php abstract class C {
+                abstract function f1();
+                function f2()    {    }
+                abstract function f3();
+            }
+            if (true)    {    }
+            ',
+        ];
+
+        yield 'every token in separate line' => [
+            '<?php
+                function
+                foo
+                (
+                )
+                :
+                void {}
+            ',
+            '<?php
+                function
+                foo
+                (
+                )
+                :
+                void
+                {
+                }
+            ',
+        ];
+
+        yield 'comments before body' => [
+            '<?php
+                function f1()
+                // foo
+                {}
+                function f2()
+                /* foo */
+                {}
+                function f3()
+                /** foo */
+                {}
+                function f4()
+                /** foo */
+                /** bar */
+                {}
+            ',
+            '<?php
+                function f1()
+                // foo
+                {
+                }
+                function f2()
+                /* foo */
+                {
+
+                }
+                function f3()
+                /** foo */
+                {
+                }
+                function f4()
+                /** foo */
+                /** bar */
+                {    }
+            ',
+        ];
+
+        yield 'anonymous class' => [
+            '<?php
+                $o = new class() {};
+            ',
+            '<?php
+                $o = new class() {
+                };
+            ',
+        ];
+
+        yield 'anonymous function' => [
+            '<?php
+                $x = function () {};
+            ',
+            '<?php
+                $x = function () {
+                };
+            ',
+        ];
+    }
+
+    /**
+     * @requires PHP 8.0
+     *
+     * @dataProvider provideFix80Cases
+     */
+    public function testFix80(string $expected, ?string $input = null): void
+    {
+        $this->doTest($expected, $input);
+    }
+
+    /**
+     * @return iterable<array{0: string, 1?: string}>
+     */
+    public static function provideFix80Cases(): iterable
+    {
+        yield 'single-line promoted properties' => [
+            '<?php class Foo
+                {
+                    public function __construct(private int $x, private int $y) {}
+                }
+            ',
+            '<?php class Foo
+                {
+                    public function __construct(private int $x, private int $y)
+                    {
+                    }
+                }
+            ',
+        ];
+
+        yield 'multi-line promoted properties' => [
+            '<?php class Foo
+                {
+                    public function __construct(
+                        private int $x,
+                        private int $y,
+                    ) {}
+                }
+            ',
+            '<?php class Foo
+                {
+                    public function __construct(
+                        private int $x,
+                        private int $y,
+                    ) {
+                    }
+                }
+            ',
+        ];
+    }
+}

+ 14 - 0
tests/Fixtures/Integration/priority/class_definition,single_line_empty_body.test

@@ -0,0 +1,14 @@
+--TEST--
+Integration of fixers: class_definition,single_line_empty_body.
+--RULESET--
+{"class_definition": true, "single_line_empty_body": true}
+--EXPECT--
+<?php
+class Foo {}
+
+--INPUT--
+<?php
+class Foo
+{
+
+}

+ 13 - 0
tests/Fixtures/Integration/priority/curly_braces_position,single_line_empty_body.test

@@ -0,0 +1,13 @@
+--TEST--
+Integration of fixers: curly_braces_position,single_line_empty_body.
+--RULESET--
+{"curly_braces_position": true, "single_line_empty_body": true}
+--EXPECT--
+<?php
+function foo() {}
+
+--INPUT--
+<?php
+function foo()
+{
+}