Browse Source

NewWithBracesFixer - add no braces support

Jeremiasz Major 3 years ago
parent
commit
877624a40d

+ 13 - 1
doc/list.rst

@@ -1181,7 +1181,19 @@ List of Available Rules
    `Source PhpCsFixer\\Fixer\\Casing\\NativeFunctionTypeDeclarationCasingFixer <./../src/Fixer/Casing/NativeFunctionTypeDeclarationCasingFixer.php>`_
 -  `new_with_braces <./rules/operator/new_with_braces.rst>`_
 
-   All instances created with new keyword must be followed by braces.
+   All instances created with ``new`` keyword must (not) be followed by braces.
+
+   Configuration options:
+
+   - | ``named_class``
+     | Whether named classes should be followed by parentheses.
+     | Allowed types: ``bool``
+     | Default value: ``true``
+   - | ``anonymous_class``
+     | Whether anonymous classes should be followed by parentheses.
+     | Allowed types: ``bool``
+     | Default value: ``true``
+
 
    Part of rule sets `@PSR12 <./ruleSets/PSR12.rst>`_ `@PhpCsFixer <./ruleSets/PhpCsFixer.rst>`_ `@Symfony <./ruleSets/Symfony.rst>`_
 

+ 1 - 1
doc/rules/index.rst

@@ -504,7 +504,7 @@ Operator
   Use ``&&`` and ``||`` logical operators instead of ``and`` and ``or``.
 - `new_with_braces <./operator/new_with_braces.rst>`_
 
-  All instances created with new keyword must be followed by braces.
+  All instances created with ``new`` keyword must (not) be followed by braces.
 - `no_space_around_double_colon <./operator/no_space_around_double_colon.rst>`_
 
   There must be no space around double colons (also called Scope Resolution Operator or Paamayim Nekudotayim).

+ 61 - 6
doc/rules/operator/new_with_braces.rst

@@ -2,7 +2,28 @@
 Rule ``new_with_braces``
 ========================
 
-All instances created with new keyword must be followed by braces.
+All instances created with ``new`` keyword must (not) be followed by braces.
+
+Configuration
+-------------
+
+``named_class``
+~~~~~~~~~~~~~~~
+
+Whether named classes should be followed by parentheses.
+
+Allowed types: ``bool``
+
+Default value: ``true``
+
+``anonymous_class``
+~~~~~~~~~~~~~~~~~~~
+
+Whether anonymous classes should be followed by parentheses.
+
+Allowed types: ``bool``
+
+Default value: ``true``
 
 Examples
 --------
@@ -10,12 +31,46 @@ Examples
 Example #1
 ~~~~~~~~~~
 
+*Default* configuration.
+
 .. code-block:: diff
 
    --- Original
    +++ New
-   -<?php $x = new X;
-   +<?php $x = new X();
+    <?php
+
+   -$x = new X;
+   -$y = new class {};
+   +$x = new X();
+   +$y = new class() {};
+
+Example #2
+~~~~~~~~~~
+
+With configuration: ``['anonymous_class' => false]``.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+
+   -$y = new class() {};
+   +$y = new class {};
+
+Example #3
+~~~~~~~~~~
+
+With configuration: ``['named_class' => false]``.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+
+   -$x = new X();
+   +$x = new X;
 
 Rule sets
 ---------
@@ -23,10 +78,10 @@ Rule sets
 The rule is part of the following rule sets:
 
 @PSR12
-  Using the `@PSR12 <./../../ruleSets/PSR12.rst>`_ rule set will enable the ``new_with_braces`` rule.
+  Using the `@PSR12 <./../../ruleSets/PSR12.rst>`_ rule set will enable the ``new_with_braces`` rule with the default config.
 
 @PhpCsFixer
-  Using the `@PhpCsFixer <./../../ruleSets/PhpCsFixer.rst>`_ rule set will enable the ``new_with_braces`` rule.
+  Using the `@PhpCsFixer <./../../ruleSets/PhpCsFixer.rst>`_ rule set will enable the ``new_with_braces`` rule with the default config.
 
 @Symfony
-  Using the `@Symfony <./../../ruleSets/Symfony.rst>`_ rule set will enable the ``new_with_braces`` rule.
+  Using the `@Symfony <./../../ruleSets/Symfony.rst>`_ rule set will enable the ``new_with_braces`` rule with the default config.

+ 74 - 21
src/Fixer/Operator/NewWithBracesFixer.php

@@ -15,6 +15,10 @@ declare(strict_types=1);
 namespace PhpCsFixer\Fixer\Operator;
 
 use PhpCsFixer\AbstractFixer;
+use PhpCsFixer\Fixer\ConfigurableFixerInterface;
+use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
+use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
+use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
 use PhpCsFixer\FixerDefinition\CodeSample;
 use PhpCsFixer\FixerDefinition\FixerDefinition;
 use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
@@ -25,7 +29,7 @@ use PhpCsFixer\Tokenizer\Tokens;
 /**
  * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  */
-final class NewWithBracesFixer extends AbstractFixer
+final class NewWithBracesFixer extends AbstractFixer implements ConfigurableFixerInterface
 {
     /**
      * {@inheritdoc}
@@ -33,8 +37,18 @@ final class NewWithBracesFixer extends AbstractFixer
     public function getDefinition(): FixerDefinitionInterface
     {
         return new FixerDefinition(
-            'All instances created with new keyword must be followed by braces.',
-            [new CodeSample("<?php \$x = new X;\n")]
+            'All instances created with `new` keyword must (not) be followed by braces.',
+            [
+                new CodeSample("<?php\n\n\$x = new X;\n\$y = new class {};\n"),
+                new CodeSample(
+                    "<?php\n\n\$y = new class() {};\n",
+                    ['anonymous_class' => false]
+                ),
+                new CodeSample(
+                    "<?php\n\n\$x = new X();\n",
+                    ['named_class' => false]
+                ),
+            ]
         );
     }
 
@@ -108,6 +122,7 @@ final class NewWithBracesFixer extends AbstractFixer
                 [CT::T_BRACE_CLASS_INSTANTIATION_OPEN],
                 [CT::T_BRACE_CLASS_INSTANTIATION_CLOSE],
             ];
+
             if (\defined('T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG')) { // @TODO: drop condition when PHP 8.1+ is required
                 $nextTokenKinds[] = [T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG];
                 $nextTokenKinds[] = [T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG];
@@ -120,40 +135,78 @@ final class NewWithBracesFixer extends AbstractFixer
             }
 
             $nextIndex = $tokens->getNextTokenOfKind($index, $nextTokenKinds);
-            $nextToken = $tokens[$nextIndex];
 
             // new anonymous class definition
-            if ($nextToken->isGivenKind(T_CLASS)) {
-                if (!$tokens[$tokens->getNextMeaningfulToken($nextIndex)]->equals('(')) {
-                    $this->insertBracesAfter($tokens, $nextIndex);
+            if ($tokens[$nextIndex]->isGivenKind(T_CLASS)) {
+                $nextIndex = $tokens->getNextMeaningfulToken($nextIndex);
+
+                if ($this->configuration['anonymous_class']) {
+                    $this->ensureBracesAt($tokens, $nextIndex);
+                } else {
+                    $this->ensureNoBracesAt($tokens, $nextIndex);
                 }
 
                 continue;
             }
 
             // entrance into array index syntax - need to look for exit
-            while ($nextToken->equals('[') || $nextToken->isGivenKind(CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN)) {
-                $nextIndex = $tokens->findBlockEnd(Tokens::detectBlockType($nextToken)['type'], $nextIndex) + 1;
-                $nextToken = $tokens[$nextIndex];
-            }
 
-            // new statement has a gap in it - advance to the next token
-            if ($nextToken->isWhitespace()) {
-                $nextIndex = $tokens->getNextNonWhitespace($nextIndex);
-                $nextToken = $tokens[$nextIndex];
+            while ($tokens[$nextIndex]->equals('[') || $tokens[$nextIndex]->isGivenKind(CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN)) {
+                $nextIndex = $tokens->findBlockEnd(Tokens::detectBlockType($tokens[$nextIndex])['type'], $nextIndex);
+                $nextIndex = $tokens->getNextMeaningfulToken($nextIndex);
             }
 
-            // new statement with () - nothing to do
-            if ($nextToken->equals('(') || $nextToken->isObjectOperator()) {
-                continue;
+            if ($this->configuration['named_class']) {
+                $this->ensureBracesAt($tokens, $nextIndex);
+            } else {
+                $this->ensureNoBracesAt($tokens, $nextIndex);
             }
+        }
+    }
 
-            $this->insertBracesAfter($tokens, $tokens->getPrevMeaningfulToken($nextIndex));
+    /**
+     * {@inheritdoc}
+     */
+    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
+    {
+        return new FixerConfigurationResolver([
+            (new FixerOptionBuilder('named_class', 'Whether named classes should be followed by parentheses.'))
+                ->setAllowedTypes(['bool'])
+                ->setDefault(true)
+                ->getOption(),
+            (new FixerOptionBuilder('anonymous_class', 'Whether anonymous classes should be followed by parentheses.'))
+                ->setAllowedTypes(['bool'])
+                ->setDefault(true)
+                ->getOption(),
+        ]);
+    }
+
+    private function ensureBracesAt(Tokens $tokens, int $index): void
+    {
+        $token = $tokens[$index];
+
+        if (!$token->equals('(') && !$token->isObjectOperator()) {
+            $tokens->insertAt(
+                $tokens->getPrevMeaningfulToken($index) + 1,
+                [new Token('('), new Token(')')]
+            );
         }
     }
 
-    private function insertBracesAfter(Tokens $tokens, int $index): void
+    private function ensureNoBracesAt(Tokens $tokens, int $index): void
     {
-        $tokens->insertAt(++$index, [new Token('('), new Token(')')]);
+        if (!$tokens[$index]->equals('(')) {
+            return;
+        }
+
+        $closingIndex = $tokens->getNextMeaningfulToken($index);
+
+        // constructor has arguments - braces can not be removed
+        if (!$tokens[$closingIndex]->equals(')')) {
+            return;
+        }
+
+        $tokens->clearTokenAndMergeSurroundingWhitespace($closingIndex);
+        $tokens->clearTokenAndMergeSurroundingWhitespace($index);
     }
 }

+ 359 - 4
tests/Fixer/Operator/NewWithBracesFixerTest.php

@@ -26,16 +26,19 @@ use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
 final class NewWithBracesFixerTest extends AbstractFixerTestCase
 {
     /**
-     * @dataProvider provideFixCases
+     * @dataProvider provideNamedWithDefaultConfigurationCases
      */
-    public function testFix(string $expected, ?string $input = null): void
+    public function testFixNamedWithDefaultConfiguration(string $expected, ?string $input = null): void
     {
         $this->doTest($expected, $input);
     }
 
-    public function provideFixCases(): \Generator
+    public function provideNamedWithDefaultConfigurationCases(): \Generator
     {
         yield from [
+            ['<?php $x = new X(foo(/**/));'],
+            ['<?php $xyz = new X(new Y(new Z(/**/ foo())));'],
+            ['<?php $self = new self(a);'],
             [
                 '<?php class A { public function B(){ $static = new static(new \SplFileInfo(__FILE__)); }}',
             ],
@@ -244,9 +247,306 @@ final class NewWithBracesFixerTest extends AbstractFixerTestCase
                     $a = new Foo ** 1;
                 ',
             ],
+            [
+                '<?php
+                    $a = new Foo() <=> 1;
+                ',
+                '<?php
+                    $a = new Foo <=> 1;
+                ',
+            ],
+        ];
+
+        yield [
+            "<?php \$a = new \$b['class']/* */()\r\n\t ;",
+        ];
+
+        yield [
+            "<?php \$a = new \$b['class'] /* */()\r\n\t ;",
+        ];
+
+        yield [
+            "<?php \$a = new \$b['class']()/* */;",
+            "<?php \$a = new \$b['class']/* */;",
+        ];
+
+        yield [
+            "<?php \$a = new \$b['class']() /* */;",
+            "<?php \$a = new \$b['class'] /* */;",
+        ];
+    }
+
+    /**
+     * @dataProvider provideNamedWithoutBracesCases
+     */
+    public function testFixNamedWithoutBraces(string $expected, ?string $input = null): void
+    {
+        $this->fixer->configure(['named_class' => false]);
+        $this->doTest($expected, $input);
+    }
+
+    public function provideNamedWithoutBracesCases(): \Generator
+    {
+        yield from [
+            ['<?php $x = new X(foo(/**/));'],
+            ['<?php $xyz = new X(new Y(new Z(/**/ foo())));'],
+            ['<?php $self = new self(a);'],
+            [
+                '<?php $bar1 = new $foo->bar["baz"];',
+                '<?php $bar1 = new $foo->bar["baz"]();',
+            ],
+            [
+                '<?php class A { public function B(){ $static = new static(new \SplFileInfo(__FILE__)); }}',
+            ],
+            [
+                '<?php $static = new self(new \SplFileInfo(__FILE__));',
+            ],
+            [
+                '<?php $x = new X/**/ /**/ /**//**//**/ /**//**/   /**/ /**/ /**//**//**/ /**//**//**/ /**/ /**//**//**/ /**//**/;/**/ /**/ /**//**//**/ /**//**/',
+                '<?php $x = new X/**/ /**/ /**//**//**/ /**//**/   (/**/ /**/ /**//**//**/ /**//**/)/**/ /**/ /**//**//**/ /**//**/;/**/ /**/ /**//**//**/ /**//**/',
+            ],
+            [
+                '<?php $x = new X;',
+                '<?php $x = new X();',
+            ],
+            [
+                '<?php $y = new Y ;',
+                '<?php $y = new Y() ;',
+            ],
+            [
+                '<?php $x = new Z /**/;//',
+                '<?php $x = new Z() /**/;//',
+            ],
+            [
+                '<?php $foo = new $foo;',
+                '<?php $foo = new $foo();',
+            ],
+            [
+                '<?php $xyz = new X(new Y(new Z));',
+                '<?php $xyz = new X(new Y(new Z()));',
+            ],
+            [
+                '<?php $foo = (new $bar)->foo;',
+                '<?php $foo = (new $bar())->foo;',
+            ],
+            [
+                '<?php $foo = (new $bar((new Foo)->bar))->foo;',
+                '<?php $foo = (new $bar((new Foo())->bar))->foo;',
+            ],
+            [
+                '<?php $self = new self;',
+                '<?php $self = new self();',
+            ],
+            [
+                '<?php $static = new static;',
+                '<?php $static = new static();',
+            ],
+            [
+                '<?php $a = array( "key" => new DateTime, );',
+                '<?php $a = array( "key" => new DateTime(), );',
+            ],
+            [
+                '<?php $a = array( "key" => new DateTime );',
+                '<?php $a = array( "key" => new DateTime() );',
+            ],
+            [
+                '<?php $a = new $b[$c];',
+                '<?php $a = new $b[$c]();',
+            ],
+            [
+                '<?php $a = new $b[$c][0];',
+                '<?php $a = new $b[$c][0]();',
+            ],
+            [
+                '<?php $a = new $b[$c[$d ? foo() : bar("bar[...]") - 1]];',
+                '<?php $a = new $b[$c[$d ? foo() : bar("bar[...]") - 1]]();',
+            ],
+            [
+                '<?php $a = new $b[\'class\'];',
+                '<?php $a = new $b[\'class\']();',
+            ],
+            [
+                '<?php $a = new $b[\'class\'] ($foo[\'bar\']);',
+            ],
+            [
+                '<?php $a = new $b[\'class\']  ;',
+                '<?php $a = new $b[\'class\'] () ;',
+            ],
+            [
+                '<?php $a = new $b[$c] ($hello[$world]) ;',
+            ],
+            [
+                "<?php \$a = new \$b['class']\r\n\t ;",
+                "<?php \$a = new \$b['class']()\r\n\t ;",
+            ],
+            [
+                '<?php $a = $b ? new DateTime : $b;',
+                '<?php $a = $b ? new DateTime() : $b;',
+            ],
+            [
+                '<?php new self::$adapters[$name]["adapter"];',
+                '<?php new self::$adapters[$name]["adapter"]();',
+            ],
+            [
+                '<?php $a = new \Exception?> <?php echo 1;',
+                '<?php $a = new \Exception()?> <?php echo 1;',
+            ],
+            [
+                '<?php $b = new \StdClass /**/?>',
+                '<?php $b = new \StdClass() /**/?>',
+            ],
+            [
+                '<?php $a = new Foo instanceof Foo;',
+                '<?php $a = new Foo() instanceof Foo;',
+            ],
+            [
+                '<?php
+                    $a = new Foo + 1;
+                    $a = new Foo - 1;
+                    $a = new Foo * 1;
+                    $a = new Foo / 1;
+                    $a = new Foo % 1;
+                ',
+                '<?php
+                    $a = new Foo() + 1;
+                    $a = new Foo() - 1;
+                    $a = new Foo() * 1;
+                    $a = new Foo() / 1;
+                    $a = new Foo() % 1;
+                ',
+            ],
+            [
+                '<?php
+                    $a = new Foo & 1;
+                    $a = new Foo | 1;
+                    $a = new Foo ^ 1;
+                    $a = new Foo << 1;
+                    $a = new Foo >> 1;
+                ',
+                '<?php
+                    $a = new Foo() & 1;
+                    $a = new Foo() | 1;
+                    $a = new Foo() ^ 1;
+                    $a = new Foo() << 1;
+                    $a = new Foo() >> 1;
+                ',
+            ],
+            [
+                '<?php
+                    $a = new Foo and 1;
+                    $a = new Foo or 1;
+                    $a = new Foo xor 1;
+                    $a = new Foo && 1;
+                    $a = new Foo || 1;
+                ',
+                '<?php
+                    $a = new Foo() and 1;
+                    $a = new Foo() or 1;
+                    $a = new Foo() xor 1;
+                    $a = new Foo() && 1;
+                    $a = new Foo() || 1;
+                ',
+            ],
+            [
+                '<?php
+                    if (new DateTime > $this->startDate) {}
+                    if (new DateTime >= $this->startDate) {}
+                    if (new DateTime < $this->startDate) {}
+                    if (new DateTime <= $this->startDate) {}
+                    if (new DateTime == $this->startDate) {}
+                    if (new DateTime != $this->startDate) {}
+                    if (new DateTime <> $this->startDate) {}
+                    if (new DateTime === $this->startDate) {}
+                    if (new DateTime !== $this->startDate) {}
+                ',
+                '<?php
+                    if (new DateTime() > $this->startDate) {}
+                    if (new DateTime() >= $this->startDate) {}
+                    if (new DateTime() < $this->startDate) {}
+                    if (new DateTime() <= $this->startDate) {}
+                    if (new DateTime() == $this->startDate) {}
+                    if (new DateTime() != $this->startDate) {}
+                    if (new DateTime() <> $this->startDate) {}
+                    if (new DateTime() === $this->startDate) {}
+                    if (new DateTime() !== $this->startDate) {}
+                ',
+            ],
+            [
+                '<?php $a = new \stdClass ? $b : $c;',
+                '<?php $a = new \stdClass() ? $b : $c;',
+            ],
+            [
+                '<?php foreach (new Collection as $x) {}',
+                '<?php foreach (new Collection() as $x) {}',
+            ],
+            [
+                '<?php $a = [(string) new Foo => 1];',
+                '<?php $a = [(string) new Foo() => 1];',
+            ],
+            [
+                '<?php $a = [ "key" => new DateTime, ];',
+                '<?php $a = [ "key" => new DateTime(), ];',
+            ],
+            [
+                '<?php $a = [ "key" => new DateTime ];',
+                '<?php $a = [ "key" => new DateTime() ];',
+            ],
+            [
+                '<?php
+                    $a = new Foo ** 1;
+                ',
+                '<?php
+                    $a = new Foo() ** 1;
+                ',
+            ],
+            [
+                '<?php
+                    $a = new Foo <=> 1;
+                ',
+                '<?php
+                    $a = new Foo() <=> 1;
+                ',
+            ],
+        ];
+
+        yield [
+            "<?php \$a = new \$b['class']/* */\r\n\t ;",
+            "<?php \$a = new \$b['class']/* */()\r\n\t ;",
+        ];
+
+        yield [
+            "<?php \$a = new \$b['class'] /* */\r\n\t ;",
+            "<?php \$a = new \$b['class'] /* */()\r\n\t ;",
         ];
 
+        yield [
+            "<?php \$a = new \$b['class']/* */;",
+            "<?php \$a = new \$b['class']()/* */;",
+        ];
+
+        yield [
+            "<?php \$a = new \$b['class'] /* */;",
+            "<?php \$a = new \$b['class']() /* */;",
+        ];
+    }
+
+    /**
+     * @dataProvider provideAnonymousWithDefaultConfigurationCases
+     */
+    public function testFixAnonymousWithDefaultConfiguration(string $expected, ?string $input = null): void
+    {
+        $this->doTest($expected, $input);
+    }
+
+    public function provideAnonymousWithDefaultConfigurationCases(): \Generator
+    {
         yield from [
+            ['<?php $a = new class($a) {use SomeTrait;};'],
+            ['<?php $a = new class(foo(/**/)) implements Foo{};'],
+            ['<?php $a = new class($c["d"]) /**/ extends Bar1{};'],
+            ['<?php $a = new class($e->f  )  extends Bar2 implements Foo{};'],
+            ['<?php $a = new class( /**/ $g )    extends Bar3 implements Foo, Foo2{};'],
+            ['<?php $a = new class( $h  /**/) {}?>'],
             [
                 '<?php
                     $a = new Foo() <=> 1;
@@ -292,11 +592,66 @@ final class NewWithBracesFixerTest extends AbstractFixerTestCase
         ];
     }
 
+    /**
+     * @dataProvider provideAnonymousWithoutBracesCases
+     */
+    public function testFixAnonymousWithoutBraces(string $expected, ?string $input = null): void
+    {
+        $this->fixer->configure(['anonymous_class' => false]);
+        $this->doTest($expected, $input);
+    }
+
+    public function provideAnonymousWithoutBracesCases(): \Generator
+    {
+        yield from [
+            ['<?php $a = new class($a) {use SomeTrait;};'],
+            ['<?php $a = new class(foo(/**/)) implements Foo{};'],
+            ['<?php $a = new class($c["d"]) /**/ extends Bar1{};'],
+            ['<?php $a = new class($e->f  )  extends Bar2 implements Foo{};'],
+            ['<?php $a = new class( /**/ $g )    extends Bar3 implements Foo, Foo2{};'],
+            ['<?php $a = new class( $h  /**/) {}?>'],
+            [
+                '<?php
+                    $a = new class {use SomeTrait;};
+                    $a = new class implements Foo{};
+                    $a = new class /**/ extends Bar1{};
+                    $a = new class  extends Bar2 implements Foo{};
+                    $a = new class    extends Bar3 implements Foo, Foo2{};
+                    $a = new class    {}?>
+                ',
+                '<?php
+                    $a = new class() {use SomeTrait;};
+                    $a = new class() implements Foo{};
+                    $a = new class() /**/ extends Bar1{};
+                    $a = new class()  extends Bar2 implements Foo{};
+                    $a = new class()    extends Bar3 implements Foo, Foo2{};
+                    $a = new class ( )  {}?>
+                ',
+            ],
+            [
+                '<?php
+                    class A {
+                        public function B() {
+                            $static = new static(new class{});
+                        }
+                    }
+                ',
+                '<?php
+                    class A {
+                        public function B() {
+                            $static = new static(new class(){});
+                        }
+                    }
+                ',
+            ],
+        ];
+    }
+
     /**
      * @dataProvider provideFixPre80Cases
      * @requires PHP <8.0
      */
-    public function testFixPre80(string $expected, string $input = null): void
+    public function testFixPre80(string $expected, ?string $input = null): void
     {
         $this->doTest($expected, $input);
     }