From 877624a40d9e2bab18283a1cdd6e30f8cd17fb5a Mon Sep 17 00:00:00 2001 From: Jeremiasz Major Date: Thu, 9 Sep 2021 21:29:14 +0200 Subject: [PATCH] NewWithBracesFixer - add no braces support --- doc/list.rst | 14 +- doc/rules/index.rst | 2 +- doc/rules/operator/new_with_braces.rst | 67 +++- src/Fixer/Operator/NewWithBracesFixer.php | 95 ++++- .../Fixer/Operator/NewWithBracesFixerTest.php | 363 +++++++++++++++++- 5 files changed, 508 insertions(+), 33 deletions(-) diff --git a/doc/list.rst b/doc/list.rst index 67bc57f9200..ba2669e611a 100644 --- a/doc/list.rst +++ b/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>`_ diff --git a/doc/rules/index.rst b/doc/rules/index.rst index 7aa4e51d274..f0acc498b42 100644 --- a/doc/rules/index.rst +++ b/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). diff --git a/doc/rules/operator/new_with_braces.rst b/doc/rules/operator/new_with_braces.rst index 491c366bf69..b82904816f2 100644 --- a/doc/rules/operator/new_with_braces.rst +++ b/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 - - false]``. + +.. code-block:: diff + + --- Original + +++ New + false]``. + +.. code-block:: diff + + --- Original + +++ New + `_ 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. diff --git a/src/Fixer/Operator/NewWithBracesFixer.php b/src/Fixer/Operator/NewWithBracesFixer.php index 16dae573673..83cdccfb66b 100644 --- a/src/Fixer/Operator/NewWithBracesFixer.php +++ b/src/Fixer/Operator/NewWithBracesFixer.php @@ -15,6 +15,10 @@ 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 @@ /** * @author Dariusz RumiƄski */ -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(" false] + ), + new CodeSample( + " false] + ), + ] ); } @@ -108,6 +122,7 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void [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 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void } $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); } } diff --git a/tests/Fixer/Operator/NewWithBracesFixerTest.php b/tests/Fixer/Operator/NewWithBracesFixerTest.php index 3ea22749305..3d436fa3fdb 100644 --- a/tests/Fixer/Operator/NewWithBracesFixerTest.php +++ b/tests/Fixer/Operator/NewWithBracesFixerTest.php @@ -26,16 +26,19 @@ 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 [ + [' 1; + ', + ' 1; + ', + ], + ]; + + yield [ + "fixer->configure(['named_class' => false]); + $this->doTest($expected, $input); + } + + public function provideNamedWithoutBracesCases(): \Generator + { + yield from [ + ['bar["baz"];', + 'bar["baz"]();', + ], + [ + 'foo;', + 'foo;', + ], + [ + 'bar))->foo;', + 'bar))->foo;', + ], + [ + ' new DateTime, );', + ' new DateTime(), );', + ], + [ + ' new DateTime );', + ' new DateTime() );', + ], + [ + ' ', + '', + ], + [ + '> 1; + ', + '> 1; + ', + ], + [ + ' $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) {} + ', + ' $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) {} + ', + ], + [ + ' 1];', + ' 1];', + ], + [ + ' new DateTime, ];', + ' new DateTime(), ];', + ], + [ + ' new DateTime ];', + ' new DateTime() ];', + ], + [ + ' 1; + ', + ' 1; + ', + ], + ]; + + yield [ + "doTest($expected, $input); + } + + public function provideAnonymousWithDefaultConfigurationCases(): \Generator + { yield from [ + ['f ) extends Bar2 implements Foo{};'], + [''], [ ' 1; @@ -292,11 +592,66 @@ public function B() { ]; } + /** + * @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 [ + ['f ) extends Bar2 implements Foo{};'], + [''], + [ + ' + ', + ' + ', + ], + [ + 'doTest($expected, $input); }