Skip to content

Commit

Permalink
feature: Introduce ReturnToYieldFromFixer (PHP-CS-Fixer#7168)
Browse files Browse the repository at this point in the history
  • Loading branch information
kubawerlos authored Jul 28, 2023
1 parent 94ad3e0 commit 319d15d
Show file tree
Hide file tree
Showing 13 changed files with 351 additions and 4 deletions.
5 changes: 5 additions & 0 deletions doc/list.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2824,6 +2824,11 @@ List of Available Rules
Part of rule set `@PhpCsFixer <./ruleSets/PhpCsFixer.rst>`_

`Source PhpCsFixer\\Fixer\\ReturnNotation\\ReturnAssignmentFixer <./../src/Fixer/ReturnNotation/ReturnAssignmentFixer.php>`_
- `return_to_yield_from <./rules/array_notation/return_to_yield_from.rst>`_

If the function explicitly returns an array, and has the return type ``iterable``, then ``yield from`` must be used instead of ``return``.

`Source PhpCsFixer\\Fixer\\ArrayNotation\\ReturnToYieldFromFixer <./../src/Fixer/ArrayNotation/ReturnToYieldFromFixer.php>`_
- `return_type_declaration <./rules/function_notation/return_type_declaration.rst>`_

Adjust spacing around colon in return type declarations and backed enum types.
Expand Down
21 changes: 21 additions & 0 deletions doc/rules/array_notation/return_to_yield_from.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
=============================
Rule ``return_to_yield_from``
=============================

If the function explicitly returns an array, and has the return type
``iterable``, then ``yield from`` must be used instead of ``return``.

Examples
--------

Example #1
~~~~~~~~~~

.. code-block:: diff
--- Original
+++ New
<?php function giveMeData(): iterable {
- return [1, 2, 3];
+ yield from [1, 2, 3];
}
3 changes: 3 additions & 0 deletions doc/rules/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ Array Notation
- `normalize_index_brace <./array_notation/normalize_index_brace.rst>`_

Array index should always be written by using square braces.
- `return_to_yield_from <./array_notation/return_to_yield_from.rst>`_

If the function explicitly returns an array, and has the return type ``iterable``, then ``yield from`` must be used instead of ``return``.
- `trim_array_spaces <./array_notation/trim_array_spaces.rst>`_

Arrays should be formatted like function/method arguments, without leading or trailing single line space.
Expand Down
105 changes: 105 additions & 0 deletions src/Fixer/ArrayNotation/ReturnToYieldFromFixer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

declare(strict_types=1);

/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <[email protected]>
* Dariusz Rumiński <[email protected]>
*
* 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 <[email protected]>
*/
final class ReturnToYieldFromFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'If the function explicitly returns an array, and has the return type `iterable`, then `yield from` must be used instead of `return`.',
[new CodeSample('<?php function giveMeData(): iterable {
return [1, 2, 3];
}
')],
);
}

public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAllTokenKindsFound([T_FUNCTION, T_RETURN]) && $tokens->isAnyTokenKindsFound([T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN]);
}

/**
* {@inheritdoc}
*
* Must run before YieldFromArrayToYieldsFixer.
* Must run after PhpUnitDataProviderReturnTypeFixer, PhpdocToReturnTypeFixer.
*/
public function getPriority(): int
{
return 1;
}

protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($tokens->findGivenKind(T_RETURN) as $index => $token) {
if (!$this->shouldBeFixed($tokens, $index)) {
continue;
}

$tokens[$index] = new Token([T_YIELD_FROM, 'yield from']);
}
}

private function shouldBeFixed(Tokens $tokens, int $returnIndex): bool
{
$arrayStartIndex = $tokens->getNextMeaningfulToken($returnIndex);
if (!$tokens[$arrayStartIndex]->isGivenKind([T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN])) {
return false;
}

if ($tokens[$arrayStartIndex]->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN)) {
$arrayEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $arrayStartIndex);
} else {
$arrayOpenParenthesisIndex = $tokens->getNextTokenOfKind($arrayStartIndex, ['(']);
$arrayEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $arrayOpenParenthesisIndex);
}

$functionEndIndex = $arrayEndIndex;
do {
$functionEndIndex = $tokens->getNextMeaningfulToken($functionEndIndex);
} while ($tokens[$functionEndIndex]->equals(';'));
if (!$tokens[$functionEndIndex]->equals('}')) {
return false;
}

$functionStartIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $functionEndIndex);

$returnTypeIndex = $tokens->getPrevMeaningfulToken($functionStartIndex);
if (!$tokens[$returnTypeIndex]->isGivenKind(T_STRING)) {
return false;
}

if ('iterable' !== strtolower($tokens[$returnTypeIndex]->getContent())) {
return false;
}

$beforeReturnTypeIndex = $tokens->getPrevMeaningfulToken($returnTypeIndex);

return $tokens[$beforeReturnTypeIndex]->isGivenKind(CT::T_TYPE_COLON);
}
}
1 change: 1 addition & 0 deletions src/Fixer/ArrayNotation/YieldFromArrayToYieldsFixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public function isCandidate(Tokens $tokens): bool
* {@inheritdoc}
*
* Must run before BlankLineBeforeStatementFixer, NoExtraBlankLinesFixer, NoMultipleStatementsPerLineFixer, NoWhitespaceInBlankLineFixer, StatementIndentationFixer.
* Must run after ReturnToYieldFromFixer.
*/
public function getPriority(): int
{
Expand Down
2 changes: 1 addition & 1 deletion src/Fixer/FunctionNotation/PhpdocToReturnTypeFixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public function isCandidate(Tokens $tokens): bool
/**
* {@inheritdoc}
*
* Must run before FullyQualifiedStrictTypesFixer, NoSuperfluousPhpdocTagsFixer, PhpdocAlignFixer, ReturnTypeDeclarationFixer.
* Must run before FullyQualifiedStrictTypesFixer, NoSuperfluousPhpdocTagsFixer, PhpdocAlignFixer, ReturnToYieldFromFixer, ReturnTypeDeclarationFixer.
* Must run after AlignMultilineCommentFixer, CommentToPhpdocFixer, PhpdocIndentFixer, PhpdocScalarFixer, PhpdocToCommentFixer, PhpdocTypesFixer.
*/
public function getPriority(): int
Expand Down
2 changes: 1 addition & 1 deletion src/Fixer/NamespaceNotation/CleanNamespaceFixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public function isCandidate(Tokens $tokens): bool
*/
public function getPriority(): int
{
return 1;
return 3;
}

protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
Expand Down
4 changes: 2 additions & 2 deletions src/Fixer/PhpUnit/PhpUnitDataProviderReturnTypeFixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,12 @@ public function provideSomethingCases() {}
/**
* {@inheritdoc}
*
* Must run before ReturnTypeDeclarationFixer.
* Must run before ReturnToYieldFromFixer, ReturnTypeDeclarationFixer.
* Must run after CleanNamespaceFixer.
*/
public function getPriority(): int
{
return 0;
return 2;
}

public function isRisky(): bool
Expand Down
5 changes: 5 additions & 0 deletions tests/AutoReview/FixerFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,7 @@ private static function getFixersPriorityGraph(): array
'php_unit_dedicate_assert',
],
'php_unit_data_provider_return_type' => [
'return_to_yield_from',
'return_type_declaration',
],
'php_unit_dedicate_assert' => [
Expand Down Expand Up @@ -769,6 +770,7 @@ private static function getFixersPriorityGraph(): array
'phpdoc_to_return_type' => [
'fully_qualified_strict_types',
'no_superfluous_phpdoc_tags',
'return_to_yield_from',
'return_type_declaration',
],
'phpdoc_types' => [
Expand All @@ -793,6 +795,9 @@ private static function getFixersPriorityGraph(): array
'return_assignment' => [
'blank_line_before_statement',
],
'return_to_yield_from' => [
'yield_from_array_to_yields',
],
'semicolon_after_instruction' => [
'simplified_if_return',
],
Expand Down
138 changes: 138 additions & 0 deletions tests/Fixer/ArrayNotation/ReturnToYieldFromFixerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php

declare(strict_types=1);

/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <[email protected]>
* Dariusz Rumiński <[email protected]>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace PhpCsFixer\Tests\Fixer\ArrayNotation;

use PhpCsFixer\Tests\Test\AbstractFixerTestCase;

/**
* @internal
*
* @covers \PhpCsFixer\Fixer\ArrayNotation\ReturnToYieldFromFixer
*/
final class ReturnToYieldFromFixerTest extends AbstractFixerTestCase
{
/**
* @dataProvider provideFixCases
*/
public function testFix(string $expected, ?string $input = null): void
{
$this->doTest($expected, $input);
}

/**
* @return iterable<array{0: string, 1?: string}>
*/
public static function provideFixCases(): iterable
{
yield ['<?php function foo() { return [1, 2, 3]; }'];

yield ['<?php function foo(): MyAwesomeIterableType { return [1, 2, 3]; }'];

yield ['<?php function foo(): iterable { if (true) { return [1]; } else { return [2]; } }'];

yield ['<?php function foo(): ?iterable { return [1, 2, 3]; }'];

yield ['<?php
abstract class Foo {
abstract public function bar(): iterable;
public function baz(): array { return []; }
}
'];

yield [
'<?php class Foo {
function bar(): iterable { yield from [1, 2, 3]; }
}',
'<?php class Foo {
function bar(): iterable { return [1, 2, 3]; }
}',
];

yield [
'<?php function foo(): iterable { yield from [1, 2, 3];;;;;;;; }',
'<?php function foo(): iterable { return [1, 2, 3];;;;;;;; }',
];

yield [
'<?php function foo(): iterable { yield from array(1, 2, 3); }',
'<?php function foo(): iterable { return array(1, 2, 3); }',
];

yield [
'<?php function foo(): iterable { $x = 0; yield from [1, 2, 3]; }',
'<?php function foo(): iterable { $x = 0; return [1, 2, 3]; }',
];

yield [
'<?php function foo(): iterable { $x = 0; yield from array(1, 2, 3); }',
'<?php function foo(): iterable { $x = 0; return array(1, 2, 3); }',
];

yield [
'<?php function foo(): ITERABLE { yield from [1, 2, 3]; }',
'<?php function foo(): ITERABLE { return [1, 2, 3]; }',
];

yield [
'<?php $f = function(): iterable { yield from [1, 2, 3]; };',
'<?php $f = function(): iterable { return [1, 2, 3]; };',
];

yield [
'<?php
function foo(): array { return [3, 4]; }
function bar(): iterable { yield from [1, 2]; }
function baz(): int { return 5; }
',
'<?php
function foo(): array { return [3, 4]; }
function bar(): iterable { return [1, 2]; }
function baz(): int { return 5; }
',
];
}

/**
* @dataProvider provideFix80Cases
*
* @requires PHP 8.0
*/
public function testFix80(string $expected, ?string $input = null): void
{
$this->doTest($expected, $input);
}

/**
* @return iterable<array{0: string, 1?: string}>
*/
public static function provideFix80Cases(): iterable
{
yield [
'<?php function foo(): null|iterable { return [1, 2, 3]; }',
];

yield [
'<?php function foo(): iterable|null { return [1, 2, 3]; }',
];

yield [
'<?php function foo(): ITERABLE|null { return [1, 2, 3]; }',
];

yield [
'<?php function foo(): Bar|iterable { return [1, 2, 3]; }',
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
--TEST--
Integration of fixers: php_unit_data_provider_return_type,return_to_yield_from.
--RULESET--
{"php_unit_data_provider_return_type": true, "return_to_yield_from": true}
--EXPECT--
<?php
class FooTest extends TestCase {
/**
* @dataProvider provideFooCases
*/
function testFoo() {}
function provideFooCases(): iterable {
yield from [[1], [2], [3]];
}
}

--INPUT--
<?php
class FooTest extends TestCase {
/**
* @dataProvider provideFooCases
*/
function testFoo() {}
function provideFooCases() {
return [[1], [2], [3]];
}
}
Loading

0 comments on commit 319d15d

Please sign in to comment.