Browse Source

feature: Introduce `YieldFromArrayToYieldsFixer` (#7114)

Kuba Werłos 1 year ago
parent
commit
7ad47de1a3

+ 8 - 0
doc/list.rst

@@ -3278,6 +3278,14 @@ List of Available Rules
    Part of rule sets `@PhpCsFixer <./ruleSets/PhpCsFixer.rst>`_ `@Symfony <./ruleSets/Symfony.rst>`_
 
    `Source PhpCsFixer\\Fixer\\ArrayNotation\\WhitespaceAfterCommaInArrayFixer <./../src/Fixer/ArrayNotation/WhitespaceAfterCommaInArrayFixer.php>`_
+-  `yield_from_array_to_yields <./rules/array_notation/yield_from_array_to_yields.rst>`_
+
+   Yield from array must be unpacked to series of yields.
+
+   The conversion will make the array in ``yield from`` changed in arrays of 1
+   less dimension.
+
+   `Source PhpCsFixer\\Fixer\\ArrayNotation\\YieldFromArrayToYieldsFixer <./../src/Fixer/ArrayNotation/YieldFromArrayToYieldsFixer.php>`_
 -  `yoda_style <./rules/control_structure/yoda_style.rst>`_
 
    Write conditions in Yoda style (``true``), non-Yoda style (``['equal' => false, 'identical' => false, 'less_and_greater' => false]``) or ignore those conditions (``null``) based on configuration.

+ 34 - 0
doc/rules/array_notation/yield_from_array_to_yields.rst

@@ -0,0 +1,34 @@
+===================================
+Rule ``yield_from_array_to_yields``
+===================================
+
+Yield from array must be unpacked to series of yields.
+
+Description
+-----------
+
+The conversion will make the array in ``yield from`` changed in arrays of 1 less
+dimension.
+
+Examples
+--------
+
+Example #1
+~~~~~~~~~~
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php function generate() {
+   -    yield from [
+   -        1,
+   -        2,
+   -        3,
+   -    ];
+   +     
+   +        yield 1;
+   +        yield 2;
+   +        yield 3;
+   +    
+    }

+ 3 - 0
doc/rules/index.rst

@@ -63,6 +63,9 @@ Array Notation
 - `whitespace_after_comma_in_array <./array_notation/whitespace_after_comma_in_array.rst>`_
 
   In array declaration, there MUST be a whitespace after each comma.
+- `yield_from_array_to_yields <./array_notation/yield_from_array_to_yields.rst>`_
+
+  Yield from array must be unpacked to series of yields.
 
 Basic
 -----

+ 175 - 0
src/Fixer/ArrayNotation/YieldFromArrayToYieldsFixer.php

@@ -0,0 +1,175 @@
+<?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\ArrayNotation;
+
+use PhpCsFixer\AbstractFixer;
+use PhpCsFixer\FixerDefinition\CodeSample;
+use PhpCsFixer\FixerDefinition\FixerDefinition;
+use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
+use PhpCsFixer\Tokenizer\CT;
+use PhpCsFixer\Tokenizer\Token;
+use PhpCsFixer\Tokenizer\Tokens;
+
+/**
+ * @author Kuba Werłos <werlos@gmail.com>
+ */
+final class YieldFromArrayToYieldsFixer extends AbstractFixer
+{
+    public function getDefinition(): FixerDefinitionInterface
+    {
+        return new FixerDefinition(
+            'Yield from array must be unpacked to series of yields.',
+            [new CodeSample('<?php function generate() {
+    yield from [
+        1,
+        2,
+        3,
+    ];
+}
+')],
+            'The conversion will make the array in `yield from` changed in arrays of 1 less dimension.'
+        );
+    }
+
+    public function isCandidate(Tokens $tokens): bool
+    {
+        return $tokens->isTokenKindFound(T_YIELD_FROM);
+    }
+
+    /**
+     * {@inheritdoc}
+     *
+     * Must run before BlankLineBeforeStatementFixer, NoExtraBlankLinesFixer, NoMultipleStatementsPerLineFixer, NoWhitespaceInBlankLineFixer, StatementIndentationFixer.
+     */
+    public function getPriority(): int
+    {
+        return 0;
+    }
+
+    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
+    {
+        /**
+         * @var array<int, Token> $inserts
+         */
+        $inserts = [];
+
+        foreach ($this->getYieldsFromToUnpack($tokens) as $index => [$startIndex, $endIndex]) {
+            $tokens->clearTokenAndMergeSurroundingWhitespace($index);
+
+            if ($tokens[$startIndex]->equals('(')) {
+                $prevStartIndex = $tokens->getPrevMeaningfulToken($startIndex);
+                $tokens->clearTokenAndMergeSurroundingWhitespace($prevStartIndex); // clear `array` from `array(`
+            }
+
+            $tokens->clearTokenAndMergeSurroundingWhitespace($startIndex);
+            $tokens->clearTokenAndMergeSurroundingWhitespace($endIndex);
+
+            $arrayHasTrailingComma = false;
+
+            $inserts[$startIndex] = [new Token([T_YIELD, 'yield']), new Token([T_WHITESPACE, ' '])];
+            foreach ($this->findArrayItemCommaIndex(
+                $tokens,
+                $tokens->getNextMeaningfulToken($startIndex),
+                $tokens->getPrevMeaningfulToken($endIndex),
+            ) as $commaIndex) {
+                $nextItemIndex = $tokens->getNextMeaningfulToken($commaIndex);
+
+                if ($nextItemIndex < $endIndex) {
+                    $inserts[$nextItemIndex] = [new Token([T_YIELD, 'yield']), new Token([T_WHITESPACE, ' '])];
+                    $tokens[$commaIndex] = new Token(';');
+                } else {
+                    $arrayHasTrailingComma = true;
+                    // array has trailing comma - we replace it with `;` (as it's best fit to put it)
+                    $tokens[$commaIndex] = new Token(';');
+                }
+            }
+
+            // there was a trailing comma, so we do not need original `;` after initial array structure
+            if (true === $arrayHasTrailingComma) {
+                $tokens->clearTokenAndMergeSurroundingWhitespace($tokens->getNextMeaningfulToken($endIndex));
+            }
+        }
+
+        $tokens->insertSlices($inserts);
+    }
+
+    /**
+     * @return array<int, array<int>>
+     */
+    private function getYieldsFromToUnpack(Tokens $tokens): array
+    {
+        $yieldsFromToUnpack = [];
+        $tokensCount = $tokens->count();
+        $index = 0;
+        while (++$index < $tokensCount) {
+            if (!$tokens[$index]->isGivenKind(T_YIELD_FROM)) {
+                continue;
+            }
+
+            $prevIndex = $tokens->getPrevMeaningfulToken($index);
+            if (!$tokens[$prevIndex]->equalsAny([';', '{', [T_OPEN_TAG]])) {
+                continue;
+            }
+
+            $arrayStartIndex = $tokens->getNextMeaningfulToken($index);
+
+            if (!$tokens[$arrayStartIndex]->isGivenKind([T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN])) {
+                continue;
+            }
+
+            if ($tokens[$arrayStartIndex]->isGivenKind(T_ARRAY)) {
+                $startIndex = $tokens->getNextTokenOfKind($arrayStartIndex, ['(']);
+                $endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $startIndex);
+            } else {
+                $startIndex = $arrayStartIndex;
+                $endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $startIndex);
+            }
+
+            // is there any nested "yield from"?
+            if ([] !== $tokens->findGivenKind(T_YIELD_FROM, $startIndex, $endIndex)) {
+                continue;
+            }
+
+            $yieldsFromToUnpack[$index] = [$startIndex, $endIndex];
+        }
+
+        return $yieldsFromToUnpack;
+    }
+
+    /**
+     * @return iterable<int>
+     */
+    private function findArrayItemCommaIndex(Tokens $tokens, int $startIndex, int $endIndex): iterable
+    {
+        for ($index = $startIndex; $index <= $endIndex; ++$index) {
+            $token = $tokens[$index];
+
+            // skip nested (), [], {} constructs
+            $blockDefinitionProbe = Tokens::detectBlockType($token);
+
+            if (null !== $blockDefinitionProbe && true === $blockDefinitionProbe['isStart']) {
+                $index = $tokens->findBlockEnd($blockDefinitionProbe['type'], $index);
+
+                continue;
+            }
+
+            if (!$tokens[$index]->equals(',')) {
+                continue;
+            }
+
+            yield $index;
+        }
+    }
+}

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

@@ -42,7 +42,7 @@ final class NoMultipleStatementsPerLineFixer extends AbstractFixer implements Wh
      * {@inheritdoc}
      *
      * Must run before CurlyBracesPositionFixer.
-     * Must run after ControlStructureBracesFixer, NoEmptyStatementFixer.
+     * Must run after ControlStructureBracesFixer, NoEmptyStatementFixer, YieldFromArrayToYieldsFixer.
      */
     public function getPriority(): int
     {

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

@@ -238,7 +238,7 @@ if (true) {
     /**
      * {@inheritdoc}
      *
-     * Must run after NoExtraBlankLinesFixer, NoUselessReturnFixer, ReturnAssignmentFixer.
+     * Must run after NoExtraBlankLinesFixer, NoUselessReturnFixer, ReturnAssignmentFixer, YieldFromArrayToYieldsFixer.
      */
     public function getPriority(): int
     {

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

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

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

@@ -38,7 +38,7 @@ final class NoWhitespaceInBlankLineFixer extends AbstractFixer implements Whites
     /**
      * {@inheritdoc}
      *
-     * Must run after AssignNullCoalescingToCoalesceEqualFixer, CombineConsecutiveIssetsFixer, CombineConsecutiveUnsetsFixer, FunctionToConstantFixer, NoEmptyCommentFixer, NoEmptyPhpdocFixer, NoEmptyStatementFixer, NoUselessElseFixer, NoUselessReturnFixer.
+     * Must run after AssignNullCoalescingToCoalesceEqualFixer, CombineConsecutiveIssetsFixer, CombineConsecutiveUnsetsFixer, FunctionToConstantFixer, NoEmptyCommentFixer, NoEmptyPhpdocFixer, NoEmptyStatementFixer, NoUselessElseFixer, NoUselessReturnFixer, YieldFromArrayToYieldsFixer.
      */
     public function getPriority(): int
     {

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

@@ -64,7 +64,7 @@ else {
      * {@inheritdoc}
      *
      * Must run before HeredocIndentationFixer.
-     * Must run after ClassAttributesSeparationFixer, CurlyBracesPositionFixer.
+     * Must run after ClassAttributesSeparationFixer, CurlyBracesPositionFixer, YieldFromArrayToYieldsFixer.
      */
     public function getPriority(): int
     {

+ 7 - 0
tests/AutoReview/FixerFactoryTest.php

@@ -869,6 +869,13 @@ final class FixerFactoryTest extends TestCase
                 'phpdoc_no_empty_return',
                 'return_type_declaration',
             ],
+            'yield_from_array_to_yields' => [
+                'blank_line_before_statement',
+                'no_extra_blank_lines',
+                'no_multiple_statements_per_line',
+                'no_whitespace_in_blank_line',
+                'statement_indentation',
+            ],
         ];
     }
 

Some files were not shown because too many files changed in this diff