|
@@ -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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|