Просмотр исходного кода

feature #5745 EmptyLoopBodyFixer - introduction (SpacePossum, keradus)

This PR was squashed before being merged into the master branch.

Discussion
----------

EmptyLoopBodyFixer - introduction

```
possum@aquarium:/home/seb/work/PHP-CS-Fixer$ php php-cs-fixer describe empty_loop_body
Xdebug: [Step Debug] Could not connect to debugging client. Tried: localhost:9003 (fallback through xdebug.client_host/xdebug.client_port) :-(
Description of empty_loop_body rule.
Empty loop-body must be in configured style.

Fixer is configurable using following option:
* style ('braces', 'semicolon'): style of empty loop-bodies; defaults to 'braces'

Fixing examples:
 * Example #1. Fixing with the default configuration.
   ---------- begin diff ----------
   --- Original
   +++ New
   @@ -1,1 +1,1 @@
   -<?php while(foo());
   +<?php while(foo()){}

   ----------- end diff -----------

 * Example #2. Fixing with configuration: ['style' => 'semicolon'].
   ---------- begin diff ----------
   --- Original
   +++ New
   @@ -1,1 +1,1 @@
   -<?php while(foo()){}
   +<?php while(foo());

   ----------- end diff -----------
```

Commits
-------

03ea2914f EmptyLoopBodyFixer - introduction
Dariusz Ruminski 3 лет назад
Родитель
Сommit
62bb793c0c

+ 1 - 0
doc/ruleSets/PhpCsFixer.rst

@@ -15,6 +15,7 @@ Rules
   ``['statements' => ['break', 'case', 'continue', 'declare', 'default', 'exit', 'goto', 'include', 'include_once', 'require', 'require_once', 'return', 'switch', 'throw', 'try']]``
 - `combine_consecutive_issets <./../rules/language_construct/combine_consecutive_issets.rst>`_
 - `combine_consecutive_unsets <./../rules/language_construct/combine_consecutive_unsets.rst>`_
+- `empty_loop_body <./../rules/control_structure/empty_loop_body.rst>`_
 - `escape_implicit_backslashes <./../rules/string_notation/escape_implicit_backslashes.rst>`_
 - `explicit_indirect_variable <./../rules/language_construct/explicit_indirect_variable.rst>`_
 - `explicit_string_variable <./../rules/string_notation/explicit_string_variable.rst>`_

+ 52 - 0
doc/rules/control_structure/empty_loop_body.rst

@@ -0,0 +1,52 @@
+========================
+Rule ``empty_loop_body``
+========================
+
+Empty loop-body must be in configured style.
+
+Configuration
+-------------
+
+``style``
+~~~~~~~~~
+
+Style of empty loop-bodies.
+
+Allowed values: ``'braces'``, ``'semicolon'``
+
+Default value: ``'semicolon'``
+
+Examples
+--------
+
+Example #1
+~~~~~~~~~~
+
+*Default* configuration.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+   -<?php while(foo()){}
+   +<?php while(foo());
+
+Example #2
+~~~~~~~~~~
+
+With configuration: ``['style' => 'braces']``.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+   -<?php while(foo());
+   +<?php while(foo()){}
+
+Rule sets
+---------
+
+The rule is part of the following rule set:
+
+@PhpCsFixer
+  Using the `@PhpCsFixer <./../../ruleSets/PhpCsFixer.rst>`_ rule set will enable the ``empty_loop_body`` rule with the default config.

+ 2 - 0
doc/rules/index.rst

@@ -163,6 +163,8 @@ Control Structure
 
 - `elseif <./control_structure/elseif.rst>`_
     The keyword ``elseif`` should be used instead of ``else if`` so that all control keywords look like single words.
+- `empty_loop_body <./control_structure/empty_loop_body.rst>`_
+    Empty loop-body must be in configured style.
 - `include <./control_structure/include.rst>`_
     Include/Require and file path should be divided with a single space. File path should not be placed under brackets.
 - `no_alternative_syntax <./control_structure/no_alternative_syntax.rst>`_

+ 1 - 1
src/Fixer/Basic/BracesFixer.php

@@ -130,7 +130,7 @@ class Foo
      * {@inheritdoc}
      *
      * Must run before ArrayIndentationFixer, MethodArgumentSpaceFixer, MethodChainingIndentationFixer.
-     * Must run after ClassAttributesSeparationFixer, ClassDefinitionFixer, ElseifFixer, LineEndingFixer, NoAlternativeSyntaxFixer, NoEmptyStatementFixer, NoUselessElseFixer, SingleLineThrowFixer, SingleSpaceAfterConstructFixer, SingleTraitInsertPerStatementFixer.
+     * Must run after ClassAttributesSeparationFixer, ClassDefinitionFixer, ElseifFixer, EmptyLoopBodyFixer, LineEndingFixer, NoAlternativeSyntaxFixer, NoEmptyStatementFixer, NoUselessElseFixer, SingleLineThrowFixer, SingleSpaceAfterConstructFixer, SingleTraitInsertPerStatementFixer.
      */
     public function getPriority(): int
     {

+ 140 - 0
src/Fixer/ControlStructure/EmptyLoopBodyFixer.php

@@ -0,0 +1,140 @@
+<?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\ControlStructure;
+
+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;
+use PhpCsFixer\Tokenizer\Token;
+use PhpCsFixer\Tokenizer\Tokens;
+use PhpCsFixer\Tokenizer\TokensAnalyzer;
+
+/**
+ * @author SpacePossum
+ */
+final class EmptyLoopBodyFixer extends AbstractFixer implements ConfigurableFixerInterface
+{
+    private const STYLE_BRACES = 'braces';
+
+    private const STYLE_SEMICOLON = 'semicolon';
+
+    private const TOKEN_LOOP_KINDS = [T_FOR, T_FOREACH, T_WHILE];
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefinition(): FixerDefinitionInterface
+    {
+        return new FixerDefinition(
+            'Empty loop-body must be in configured style.',
+            [
+                new CodeSample("<?php while(foo()){}\n"),
+                new CodeSample(
+                    "<?php while(foo());\n",
+                    [
+                        'style' => 'braces',
+                    ]
+                ),
+            ]
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     *
+     * Must run before BracesFixer, NoExtraBlankLinesFixer.
+     * Must run after NoEmptyStatementFixer.
+     */
+    public function getPriority(): int
+    {
+        return 39;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isCandidate(Tokens $tokens): bool
+    {
+        return $tokens->isAnyTokenKindsFound(self::TOKEN_LOOP_KINDS);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
+    {
+        if (self::STYLE_BRACES === $this->configuration['style']) {
+            $analyzer = new TokensAnalyzer($tokens);
+            $fixLoop = static function (int $index, int $endIndex) use ($tokens, $analyzer): void {
+                if ($tokens[$index]->isGivenKind(T_WHILE) && $analyzer->isWhilePartOfDoWhile($index)) {
+                    return;
+                }
+
+                $semiColonIndex = $tokens->getNextMeaningfulToken($endIndex);
+
+                if (!$tokens[$semiColonIndex]->equals(';')) {
+                    return;
+                }
+
+                $tokens[$semiColonIndex] = new Token('{');
+                $tokens->insertAt($semiColonIndex + 1, new Token('}'));
+            };
+        } else {
+            $fixLoop = static function (int $index, int $endIndex) use ($tokens): void {
+                $braceOpenIndex = $tokens->getNextMeaningfulToken($endIndex);
+
+                if (!$tokens[$braceOpenIndex]->equals('{')) {
+                    return;
+                }
+
+                $braceCloseIndex = $tokens->getNextMeaningfulToken($braceOpenIndex);
+
+                if (!$tokens[$braceCloseIndex]->equals('}')) {
+                    return;
+                }
+
+                $tokens[$braceOpenIndex] = new Token(';');
+                $tokens->clearTokenAndMergeSurroundingWhitespace($braceCloseIndex);
+            };
+        }
+
+        for ($index = $tokens->count() - 1; $index > 0; --$index) {
+            if ($tokens[$index]->isGivenKind(self::TOKEN_LOOP_KINDS)) {
+                $endIndex = $tokens->getNextTokenOfKind($index, ['(']); // proceed to open '('
+                $endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $endIndex); // proceed to close ')'
+                $fixLoop($index, $endIndex); // fix loop if needs fixing
+            }
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
+    {
+        return new FixerConfigurationResolver([
+            (new FixerOptionBuilder('style', 'Style of empty loop-bodies.'))
+                ->setAllowedTypes(['string'])
+                ->setAllowedValues([self::STYLE_BRACES, self::STYLE_SEMICOLON])
+                ->setDefault(self::STYLE_SEMICOLON)
+                ->getOption(),
+        ]);
+    }
+}

+ 1 - 1
src/Fixer/Semicolon/NoEmptyStatementFixer.php

@@ -45,7 +45,7 @@ final class NoEmptyStatementFixer extends AbstractFixer
     /**
      * {@inheritdoc}
      *
-     * Must run before BracesFixer, CombineConsecutiveUnsetsFixer, MultilineWhitespaceBeforeSemicolonsFixer, NoExtraBlankLinesFixer, NoSinglelineWhitespaceBeforeSemicolonsFixer, NoTrailingWhitespaceFixer, NoUselessElseFixer, NoUselessReturnFixer, NoWhitespaceInBlankLineFixer, ReturnAssignmentFixer, SpaceAfterSemicolonFixer, SwitchCaseSemicolonToColonFixer.
+     * Must run before BracesFixer, CombineConsecutiveUnsetsFixer, EmptyLoopBodyFixer, MultilineWhitespaceBeforeSemicolonsFixer, NoExtraBlankLinesFixer, NoSinglelineWhitespaceBeforeSemicolonsFixer, NoTrailingWhitespaceFixer, NoUselessElseFixer, NoUselessReturnFixer, NoWhitespaceInBlankLineFixer, ReturnAssignmentFixer, SpaceAfterSemicolonFixer, SwitchCaseSemicolonToColonFixer.
      * Must run after NoUselessSprintfFixer.
      */
     public function getPriority(): int

+ 1 - 1
src/Fixer/Whitespace/NoExtraBlankLinesFixer.php

@@ -279,7 +279,7 @@ switch($a) {
      * {@inheritdoc}
      *
      * Must run before BlankLineBeforeStatementFixer.
-     * Must run after CombineConsecutiveUnsetsFixer, FunctionToConstantFixer, NoEmptyCommentFixer, NoEmptyPhpdocFixer, NoEmptyStatementFixer, NoUnusedImportsFixer, NoUselessElseFixer, NoUselessReturnFixer, NoUselessSprintfFixer.
+     * Must run after CombineConsecutiveUnsetsFixer, EmptyLoopBodyFixer, FunctionToConstantFixer, NoEmptyCommentFixer, NoEmptyPhpdocFixer, NoEmptyStatementFixer, NoUnusedImportsFixer, NoUselessElseFixer, NoUselessReturnFixer, NoUselessSprintfFixer.
      */
     public function getPriority(): int
     {

+ 1 - 0
src/RuleSet/Sets/PhpCsFixerSet.php

@@ -48,6 +48,7 @@ final class PhpCsFixerSet extends AbstractRuleSetDescription
             ],
             'combine_consecutive_issets' => true,
             'combine_consecutive_unsets' => true,
+            'empty_loop_body' => true,
             'escape_implicit_backslashes' => true,
             'explicit_indirect_variable' => true,
             'explicit_string_variable' => true,

+ 3 - 0
tests/AutoReview/FixerFactoryTest.php

@@ -98,6 +98,8 @@ final class FixerFactoryTest extends TestCase
             [$fixers['doctrine_annotation_array_assignment'], $fixers['doctrine_annotation_spaces']],
             [$fixers['echo_tag_syntax'], $fixers['no_mixed_echo_print']],
             [$fixers['elseif'], $fixers['braces']],
+            [$fixers['empty_loop_body'], $fixers['braces']],
+            [$fixers['empty_loop_body'], $fixers['no_extra_blank_lines']],
             [$fixers['escape_implicit_backslashes'], $fixers['heredoc_to_nowdoc']],
             [$fixers['escape_implicit_backslashes'], $fixers['single_quote']],
             [$fixers['explicit_string_variable'], $fixers['simple_to_complex_string_variable']],
@@ -148,6 +150,7 @@ final class FixerFactoryTest extends TestCase
             [$fixers['no_empty_phpdoc'], $fixers['no_whitespace_in_blank_line']],
             [$fixers['no_empty_statement'], $fixers['braces']],
             [$fixers['no_empty_statement'], $fixers['combine_consecutive_unsets']],
+            [$fixers['no_empty_statement'], $fixers['empty_loop_body']],
             [$fixers['no_empty_statement'], $fixers['multiline_whitespace_before_semicolons']],
             [$fixers['no_empty_statement'], $fixers['no_extra_blank_lines']],
             [$fixers['no_empty_statement'], $fixers['no_singleline_whitespace_before_semicolons']],

+ 143 - 0
tests/Fixer/ControlStructure/EmptyLoopBodyFixerTest.php

@@ -0,0 +1,143 @@
+<?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\ControlStructure;
+
+use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
+
+/**
+ * @author SpacePossum
+ *
+ * @internal
+ *
+ * @covers \PhpCsFixer\Fixer\ControlStructure\EmptyLoopBodyFixer
+ */
+final class EmptyLoopBodyFixerTest extends AbstractFixerTestCase
+{
+    /**
+     * @dataProvider provideFixCases
+     */
+    public function testFixConfig(string $expected, ?string $input = null, ?array $config = null): void
+    {
+        if (null === $config) {
+            $this->doTest($expected, $input);
+
+            $this->fixer->configure(['style' => 'braces']);
+
+            if (null === $input) {
+                $this->doTest($expected, $input);
+            } else {
+                $this->doTest($input, $expected);
+            }
+        } else {
+            $this->fixer->configure($config);
+            $this->doTest($expected, $input);
+        }
+    }
+
+    public function provideFixCases()
+    {
+        yield 'simple "while"' => [
+            '<?php while(foo());',
+            '<?php while(foo()){}',
+        ];
+
+        yield 'simple "for"' => [
+            '<?php for($i = 0;foo();++$i);',
+            '<?php for($i = 0;foo();++$i){}',
+        ];
+
+        yield 'simple "foreach"' => [
+            '<?php foreach (Foo() as $f);',
+            '<?php foreach (Foo() as $f){}',
+        ];
+
+        yield '"while" followed by "do-while"' => [
+            '<?php while(foo(static function(){})); do{ echo 1; }while(bar());',
+            '<?php while(foo(static function(){})){} do{ echo 1; }while(bar());',
+        ];
+
+        yield 'empty "while" after "if"' => [
+            '<?php
+if ($foo) {
+    echo $bar;
+} while(foo());
+',
+            '<?php
+if ($foo) {
+    echo $bar;
+} while(foo()){}
+',
+        ];
+
+        yield 'nested and mixed loops' => [
+            '<?php
+
+do {
+    while($foo()) {
+        while(B()); // fix
+        for($i = 0;foo();++$i); // fix
+
+        for($i = 0;foo();++$i) {
+            foreach (Foo() as $f); // fix
+        }
+    }
+} while(foo());
+',
+            '<?php
+
+do {
+    while($foo()) {
+        while(B()){} // fix
+        for($i = 0;foo();++$i){} // fix
+
+        for($i = 0;foo();++$i) {
+            foreach (Foo() as $f){} // fix
+        }
+    }
+} while(foo());
+',
+        ];
+
+        yield 'not empty "while"' => [
+            '<?php while(foo()){ bar(); };',
+        ];
+
+        yield 'not empty "for"' => [
+            '<?php for($i = 0; foo(); ++$i){ bar(); }',
+        ];
+
+        yield 'not empty "foreach"' => [
+            '<?php foreach (foo() as $f){ echo 1; }',
+        ];
+
+        yield 'test with lot of space' => [
+            '<?php while (foo1())
+;
+
+
+
+echo 1;
+',
+            '<?php while (foo1())
+{
+
+}
+
+echo 1;
+',
+            ['style' => 'semicolon'],
+        ];
+    }
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов