Repository: PHP-CS-Fixer/PHP-CS-Fixer (php)

Issue:
Fix phpunit provideXxxCases method order to be before/after test
## Feature request

Currently, the order is more or less random. Some controbutors prefer to put method with cases before test method, some after.

I would be happy if the order can be defined/fixed.

Data provider analyser introduced in https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/pull/7057 can be probably reused.

Pull request description:
feat: Add `PhpUnitDataProviderMethodOrderFixer` fixer
~needs #8226, #8240, #8302 to be merged first~ all merged

close #7184

data provider tags will be ordered by https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/pull/8220

Patch:
diff --git a/dev-tools/phpstan/baseline.php b/dev-tools/phpstan/baseline.php
index c33b97384..e8f8ecbd2 100644
--- a/dev-tools/phpstan/baseline.php
+++ b/dev-tools/phpstan/baseline.php
@@ -2725,6 +2725,12 @@
 	'count' => 1,
 	'path' => __DIR__ . '/../../tests/Fixer/PhpTag/NoClosingTagFixerTest.php',
 ];
+$ignoreErrors[] = [
+	'message' => '#^Offset int\\<0, max\\> might not exist on non\\-empty\\-list\\<array\\{start\\: int, visibility\\: string, abstract\\: bool, static\\: bool, readonly\\: bool, type\\: string, name\\: string, end\\: int\\}\\>\\.$#',
+	'identifier' => 'offsetAccess.notFound',
+	'count' => 1,
+	'path' => __DIR__ . '/../../src/Fixer/PhpUnit/PhpUnitDataProviderMethodOrderFixer.php',
+];
 $ignoreErrors[] = [
 	'message' => '#^Generator expects value type array\\{0\\: string, 1\\?\\: string\\}, list\\<string\\> given\\.$#',
 	'identifier' => 'generator.valueType',
diff --git a/doc/ruleSets/PhpCsFixer.rst b/doc/ruleSets/PhpCsFixer.rst
index 2e67679e2..e87a592a5 100644
--- a/doc/ruleSets/PhpCsFixer.rst
+++ b/doc/ruleSets/PhpCsFixer.rst
@@ -54,6 +54,7 @@ Rules
 
 - `ordered_class_elements <./../rules/class_notation/ordered_class_elements.rst>`_
 - `ordered_types <./../rules/class_notation/ordered_types.rst>`_
+- `php_unit_data_provider_method_order <./../rules/php_unit/php_unit_data_provider_method_order.rst>`_
 - `php_unit_internal_class <./../rules/php_unit/php_unit_internal_class.rst>`_
 - `php_unit_test_class_requires_covers <./../rules/php_unit/php_unit_test_class_requires_covers.rst>`_
 - `phpdoc_add_missing_param_annotation <./../rules/phpdoc/phpdoc_add_missing_param_annotation.rst>`_
diff --git a/doc/rules/index.rst b/doc/rules/index.rst
index 22aca27e7..27645251c 100644
--- a/doc/rules/index.rst
+++ b/doc/rules/index.rst
@@ -652,6 +652,9 @@ PHPUnit
 - `php_unit_construct <./php_unit/php_unit_construct.rst>`_ *(risky)*
 
   PHPUnit assertion method calls like ``->assertSame(true, $foo)`` should be written with dedicated method like ``->assertTrue($foo)``.
+- `php_unit_data_provider_method_order <./php_unit/php_unit_data_provider_method_order.rst>`_
+
+  Data provider method must be placed after/before the last/first test where used.
 - `php_unit_data_provider_name <./php_unit/php_unit_data_provider_name.rst>`_ *(risky)*
 
   Data provider names must match the name of the test.
diff --git a/doc/rules/php_unit/php_unit_data_provider_method_order.rst b/doc/rules/php_unit/php_unit_data_provider_method_order.rst
new file mode 100644
index 000000000..b006447ba
--- /dev/null
+++ b/doc/rules/php_unit/php_unit_data_provider_method_order.rst
@@ -0,0 +1,73 @@
+============================================
+Rule ``php_unit_data_provider_method_order``
+============================================
+
+Data provider method must be placed after/before the last/first test where used.
+
+Configuration
+-------------
+
+``placement``
+~~~~~~~~~~~~~
+
+Where to place the data provider relative to the test where used.
+
+Allowed values: ``'after'`` and ``'before'``
+
+Default value: ``'after'``
+
+Examples
+--------
+
+Example #1
+~~~~~~~~~~
+
+*Default* configuration.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+    class FooTest extends TestCase {
+   -    public function dataProvider() {}
+        /**
+         * @dataProvider dataProvider
+         */
+        public function testSomething($expected, $actual) {}
+   +    public function dataProvider() {}
+    }
+
+Example #2
+~~~~~~~~~~
+
+With configuration: ``['placement' => 'before']``.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+    class FooTest extends TestCase {
+   +    public function dataProvider() {}
+        /**
+         * @dataProvider dataProvider
+         */
+        public function testSomething($expected, $actual) {}
+   -    public function dataProvider() {}
+    }
+
+Rule sets
+---------
+
+The rule is part of the following rule set:
+
+- `@PhpCsFixer <./../../ruleSets/PhpCsFixer.rst>`_
+
+References
+----------
+
+- Fixer class: `PhpCsFixer\\Fixer\\PhpUnit\\PhpUnitDataProviderMethodOrderFixer <./../../../src/Fixer/PhpUnit/PhpUnitDataProviderMethodOrderFixer.php>`_
+- Test class: `PhpCsFixer\\Tests\\Fixer\\PhpUnit\\PhpUnitDataProviderMethodOrderFixerTest <./../../../tests/Fixer/PhpUnit/PhpUnitDataProviderMethodOrderFixerTest.php>`_
+
+The test class defines officially supported behaviour. Each test case is a part of our backward compatibility promise.
diff --git a/src/Fixer/ClassNotation/ClassAttributesSeparationFixer.php b/src/Fixer/ClassNotation/ClassAttributesSeparationFixer.php
index c72398d15..81d9f297e 100644
--- a/src/Fixer/ClassNotation/ClassAttributesSeparationFixer.php
+++ b/src/Fixer/ClassNotation/ClassAttributesSeparationFixer.php
@@ -162,7 +162,7 @@ class Sample
      * {@inheritdoc}
      *
      * Must run before BracesFixer, IndentationTypeFixer, NoExtraBlankLinesFixer, StatementIndentationFixer.
-     * Must run after OrderedClassElementsFixer, SingleClassElementPerStatementFixer, VisibilityRequiredFixer.
+     * Must run after OrderedClassElementsFixer, PhpUnitDataProviderMethodOrderFixer, SingleClassElementPerStatementFixer, VisibilityRequiredFixer.
      */
     public function getPriority(): int
     {
diff --git a/src/Fixer/ClassNotation/NoBlankLinesAfterClassOpeningFixer.php b/src/Fixer/ClassNotation/NoBlankLinesAfterClassOpeningFixer.php
index b09c2a2b9..1a8267466 100644
--- a/src/Fixer/ClassNotation/NoBlankLinesAfterClassOpeningFixer.php
+++ b/src/Fixer/ClassNotation/NoBlankLinesAfterClassOpeningFixer.php
@@ -55,7 +55,7 @@ protected function foo()
     /**
      * {@inheritdoc}
      *
-     * Must run after OrderedClassElementsFixer.
+     * Must run after OrderedClassElementsFixer, PhpUnitDataProviderMethodOrderFixer.
      */
     public function getPriority(): int
     {
diff --git a/src/Fixer/ClassNotation/OrderedClassElementsFixer.php b/src/Fixer/ClassNotation/OrderedClassElementsFixer.php
index cff57ae28..0294d97c8 100644
--- a/src/Fixer/ClassNotation/OrderedClassElementsFixer.php
+++ b/src/Fixer/ClassNotation/OrderedClassElementsFixer.php
@@ -220,7 +220,7 @@ public function AWs(){}
     /**
      * {@inheritdoc}
      *
-     * Must run before ClassAttributesSeparationFixer, NoBlankLinesAfterClassOpeningFixer, SpaceAfterSemicolonFixer.
+     * Must run before ClassAttributesSeparationFixer, NoBlankLinesAfterClassOpeningFixer, PhpUnitDataProviderMethodOrderFixer, SpaceAfterSemicolonFixer.
      * Must run after NoPhp4ConstructorFixer, ProtectedToPrivateFixer.
      */
     public function getPriority(): int
@@ -284,8 +284,9 @@ protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
                 continue;
             }
 
+            $endIndex = $elements[array_key_last($elements)]['end'];
+
             $sorted = $this->sortElements($elements);
-            $endIndex = $elements[\count($elements) - 1]['end'];
 
             if ($sorted !== $elements) {
                 $this->sortTokens($tokens, $i, $endIndex, $sorted);
diff --git a/src/Fixer/PhpUnit/PhpUnitDataProviderMethodOrderFixer.php b/src/Fixer/PhpUnit/PhpUnitDataProviderMethodOrderFixer.php
new file mode 100644
index 000000000..fcf2822e2
--- /dev/null
+++ b/src/Fixer/PhpUnit/PhpUnitDataProviderMethodOrderFixer.php
@@ -0,0 +1,288 @@
+<?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\PhpUnit;
+
+use PhpCsFixer\Fixer\AbstractPhpUnitFixer;
+use PhpCsFixer\Fixer\ClassNotation\OrderedClassElementsFixer;
+use PhpCsFixer\Fixer\ConfigurableFixerInterface;
+use PhpCsFixer\Fixer\ConfigurableFixerTrait;
+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\Analyzer\DataProviderAnalyzer;
+use PhpCsFixer\Tokenizer\Tokens;
+
+/**
+ * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
+ *
+ * @phpstan-type _AutogeneratedInputConfiguration array{
+ *  placement?: 'after'|'before'
+ * }
+ * @phpstan-type _AutogeneratedComputedConfiguration array{
+ *  placement: 'after'|'before'
+ * }
+ *
+ * @phpstan-import-type _ClassElement from OrderedClassElementsFixer
+ */
+final class PhpUnitDataProviderMethodOrderFixer extends AbstractPhpUnitFixer implements ConfigurableFixerInterface
+{
+    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
+    use ConfigurableFixerTrait;
+
+    public function getDefinition(): FixerDefinitionInterface
+    {
+        return new FixerDefinition(
+            'Data provider method must be placed after/before the last/first test where used.',
+            [
+                new CodeSample(
+                    '<?php
+class FooTest extends TestCase {
+    public function dataProvider() {}
+    /**
+     * @dataProvider dataProvider
+     */
+    public function testSomething($expected, $actual) {}
+}
+',
+                ),
+                new CodeSample(
+                    '<?php
+class FooTest extends TestCase {
+    /**
+     * @dataProvider dataProvider
+     */
+    public function testSomething($expected, $actual) {}
+    public function dataProvider() {}
+}
+',
+                    [
+                        'placement' => 'before',
+                    ]
+                ),
+            ]
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     *
+     * Must run before ClassAttributesSeparationFixer, NoBlankLinesAfterClassOpeningFixer.
+     * Must run after OrderedClassElementsFixer.
+     */
+    public function getPriority(): int
+    {
+        return 64;
+    }
+
+    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
+    {
+        return new FixerConfigurationResolver([
+            (new FixerOptionBuilder('placement', 'Where to place the data provider relative to the test where used.'))
+                ->setAllowedValues(['after', 'before'])
+                ->setDefault('after')
+                ->getOption(),
+        ]);
+    }
+
+    protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void
+    {
+        $elements = $this->getElements($tokens, $startIndex);
+
+        if (0 === \count($elements)) {
+            return;
+        }
+
+        $endIndex = $elements[array_key_last($elements)]['end'];
+
+        $dataProvidersWithUsagePairs = $this->getDataProvidersWithUsagePairs($tokens, $startIndex, $endIndex);
+        $origUsageDataProviderOrderPairs = $this->getOrigUsageDataProviderOrderPairs($dataProvidersWithUsagePairs);
+
+        $sorted = $elements;
+        $providersPlaced = [];
+        if ('before' === $this->configuration['placement']) {
+            foreach ($origUsageDataProviderOrderPairs as [$usageName, $providerName]) {
+                if (!isset($providersPlaced[$providerName])) {
+                    $providersPlaced[$providerName] = true;
+
+                    $sorted = $this->moveMethodElement($sorted, $usageName, $providerName, false);
+                }
+            }
+        } else {
+            $sameUsageName = false;
+            $sameProviderName = false;
+            foreach ($origUsageDataProviderOrderPairs as [$usageName, $providerName]) {
+                if (!isset($providersPlaced[$providerName])) {
+                    $providersPlaced[$providerName] = true;
+
+                    $sortedBefore = $sorted;
+                    $sorted = $this->moveMethodElement(
+                        $sorted,
+                        $usageName === $sameUsageName // @phpstan-ignore argument.type (https://github.com/phpstan/phpstan/issues/12482)
+                            ? $sameProviderName
+                            : $usageName,
+                        $providerName,
+                        true
+                    );
+
+                    // honor multiple providers order for one test
+                    $sameUsageName = $usageName;
+                    $sameProviderName = $providerName;
+
+                    // keep placement after the first test
+                    if ($sortedBefore !== $sorted) {
+                        unset($providersPlaced[$providerName]);
+                    }
+                }
+            }
+        }
+
+        if ($sorted !== $elements) {
+            $this->sortTokens($tokens, $startIndex, $endIndex, $sorted);
+        }
+    }
+
+    /**
+     * @return list<_ClassElement>
+     */
+    private function getElements(Tokens $tokens, int $startIndex): array
+    {
+        $methodOrderFixer = new OrderedClassElementsFixer();
+
+        return \Closure::bind(static fn () => $methodOrderFixer->getElements($tokens, $startIndex), null, OrderedClassElementsFixer::class)();
+    }
+
+    /**
+     * @param list<_ClassElement> $elements
+     */
+    private function sortTokens(Tokens $tokens, int $startIndex, int $endIndex, array $elements): void
+    {
+        $methodOrderFixer = new OrderedClassElementsFixer();
+
+        \Closure::bind(static fn () => $methodOrderFixer->sortTokens($tokens, $startIndex, $endIndex, $elements), null, OrderedClassElementsFixer::class)();
+    }
+
+    /**
+     * @param list<_ClassElement> $elements
+     *
+     * @return list<_ClassElement>
+     */
+    private function moveMethodElement(array $elements, string $nameKeep, string $nameToMove, bool $after): array
+    {
+        $i = 0;
+        $iKeep = false;
+        $iToMove = false;
+        foreach ($elements as $element) {
+            if ('method' === $element['type']) {
+                if ($element['name'] === $nameKeep) {
+                    $iKeep = $i;
+                } elseif ($element['name'] === $nameToMove) {
+                    $iToMove = $i;
+                }
+            }
+
+            ++$i;
+        }
+        \assert(false !== $iKeep);
+        \assert(false !== $iToMove);
+
+        if ($iToMove === $iKeep + ($after ? 1 : -1)) {
+            return $elements;
+        }
+
+        $elementToMove = $elements[$iToMove]; // @phpstan-ignore offsetAccess.notFound
+        unset($elements[$iToMove]);
+
+        $c = $iKeep
+            + ($after ? 1 : 0)
+            + ($iToMove < $iKeep ? -1 : 0);
+
+        return [
+            ...\array_slice($elements, 0, $c),
+            $elementToMove,
+            ...\array_slice($elements, $c),
+        ];
+    }
+
+    /**
+     * @return list<array{
+     *   array{int, string},
+     *   non-empty-list<array{int, string, int}>
+     * }>
+     */
+    private function getDataProvidersWithUsagePairs(Tokens $tokens, int $startIndex, int $endIndex): array
+    {
+        $dataProvidersWithUsagePairs = [];
+
+        $dataProviderAnalyzer = new DataProviderAnalyzer();
+        foreach ($dataProviderAnalyzer->getDataProviders($tokens, $startIndex, $endIndex) as $dataProviderAnalysis) {
+            $usages = [];
+            foreach ($dataProviderAnalysis->getUsageIndices() as $usageIndex) {
+                $methodNameTokens = $tokens->findSequence([[T_FUNCTION], [T_STRING]], $usageIndex[0], $endIndex);
+                if (null === $methodNameTokens) {
+                    continue;
+                }
+
+                $usages[] = [
+                    array_key_last($methodNameTokens),
+                    end($methodNameTokens)->getContent(),
+                    $usageIndex[1],
+                ];
+            }
+            \assert([] !== $usages);
+
+            $dataProvidersWithUsagePairs[] = [
+                [$dataProviderAnalysis->getNameIndex(), $dataProviderAnalysis->getName()],
+                $usages,
+            ];
+        }
+
+        return $dataProvidersWithUsagePairs;
+    }
+
+    /**
+     * @param list<array{
+     *   array{int, string},
+     *   non-empty-list<array{int, string, int}>
+     * }> $dataProvidersWithUsagePairs
+     *
+     * @return list<array{string, string}>
+     */
+    private function getOrigUsageDataProviderOrderPairs(array $dataProvidersWithUsagePairs): array
+    {
+        $origUsagesOrderPairs = [];
+        foreach ($dataProvidersWithUsagePairs as [$dataProviderPair, $usagePairs]) {
+            foreach ($usagePairs as $usagePair) {
+                $origUsagesOrderPairs[] = [$usagePair, $dataProviderPair[1]];
+            }
+        }
+        uasort($origUsagesOrderPairs, static function (array $a, array $b): int {
+            $cmp = $a[0][0] <=> $b[0][0];
+
+            return 0 !== $cmp
+                ? $cmp
+                : $a[0][2] <=> $b[0][2];
+        });
+
+        $origUsageDataProviderOrderPairs = [];
+        foreach (array_map(static fn (array $v): array => [$v[0][1], $v[1]], $origUsagesOrderPairs) as [$usageName, $providerName]) {
+            $origUsageDataProviderOrderPairs[] = [$usageName, $providerName];
+        }
+
+        return $origUsageDataProviderOrderPairs;
+    }
+}
diff --git a/src/RuleSet/Sets/PhpCsFixerSet.php b/src/RuleSet/Sets/PhpCsFixerSet.php
index 4e62c98af..6b85e00eb 100644
--- a/src/RuleSet/Sets/PhpCsFixerSet.php
+++ b/src/RuleSet/Sets/PhpCsFixerSet.php
@@ -106,6 +106,7 @@ public function getRules(): array
             'no_whitespace_before_comma_in_array' => ['after_heredoc' => true],
             'ordered_class_elements' => true,
             'ordered_types' => true,
+            'php_unit_data_provider_method_order' => true,
             'php_unit_internal_class' => true,
             'php_unit_test_class_requires_covers' => true,
             'phpdoc_add_missing_param_annotation' => true,


Test patch:
diff --git a/tests/AutoReview/FixerFactoryTest.php b/tests/AutoReview/FixerFactoryTest.php
index 9316fc1cd..c27e360c4 100644
--- a/tests/AutoReview/FixerFactoryTest.php
+++ b/tests/AutoReview/FixerFactoryTest.php
@@ -724,6 +724,7 @@ private static function getFixersPriorityGraph(): array
             'ordered_class_elements' => [
                 'class_attributes_separation',
                 'no_blank_lines_after_class_opening',
+                'php_unit_data_provider_method_order',
                 'space_after_semicolon',
             ],
             'ordered_imports' => [
@@ -742,6 +743,10 @@ private static function getFixersPriorityGraph(): array
             'php_unit_construct' => [
                 'php_unit_dedicate_assert',
             ],
+            'php_unit_data_provider_method_order' => [
+                'class_attributes_separation',
+                'no_blank_lines_after_class_opening',
+            ],
             'php_unit_data_provider_name' => [
                 'php_unit_attributes',
             ],
diff --git a/tests/AutoReview/ProjectCodeTest.php b/tests/AutoReview/ProjectCodeTest.php
index 3846850ff..480eec44b 100644
--- a/tests/AutoReview/ProjectCodeTest.php
+++ b/tests/AutoReview/ProjectCodeTest.php
@@ -439,31 +439,6 @@ public function testThatTestDataProvidersAreCorrectlyNamed(string $testClassName
         ));
     }
 
-    /**
-     * @return iterable<string, array{string, string}>
-     */
-    public static function provideDataProviderMethodCases(): iterable
-    {
-        if (null === self::$dataProviderMethodCases) {
-            self::$dataProviderMethodCases = [];
-            foreach (self::provideTestClassCases() as $testClassName) {
-                $testClassName = reset($testClassName);
-                $reflectionClass = new \ReflectionClass($testClassName);
-
-                $dataProviderNames = array_filter(
-                    $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC),
-                    static fn (\ReflectionMethod $reflectionMethod): bool => $reflectionMethod->getDeclaringClass()->getName() === $reflectionClass->getName() && str_starts_with($reflectionMethod->getName(), 'provide')
-                );
-
-                foreach ($dataProviderNames as $dataProviderName) {
-                    self::$dataProviderMethodCases[$testClassName.'::'.$dataProviderName->getName()] = [$testClassName, $dataProviderName->getName()];
-                }
-            }
-        }
-
-        yield from self::$dataProviderMethodCases;
-    }
-
     /**
      * @dataProvider provideTestClassCases
      *
@@ -959,6 +934,31 @@ public function testThatDataFromDataProvidersIsNotDuplicated(string $testClassNa
         self::assertSame([], $duplicates);
     }
 
+    /**
+     * @return iterable<string, array{string, string}>
+     */
+    public static function provideDataProviderMethodCases(): iterable
+    {
+        if (null === self::$dataProviderMethodCases) {
+            self::$dataProviderMethodCases = [];
+            foreach (self::provideTestClassCases() as $testClassName) {
+                $testClassName = reset($testClassName);
+                $reflectionClass = new \ReflectionClass($testClassName);
+
+                $dataProviderNames = array_filter(
+                    $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC),
+                    static fn (\ReflectionMethod $reflectionMethod): bool => $reflectionMethod->getDeclaringClass()->getName() === $reflectionClass->getName() && str_starts_with($reflectionMethod->getName(), 'provide')
+                );
+
+                foreach ($dataProviderNames as $dataProviderName) {
+                    self::$dataProviderMethodCases[$testClassName.'::'.$dataProviderName->getName()] = [$testClassName, $dataProviderName->getName()];
+                }
+            }
+        }
+
+        yield from self::$dataProviderMethodCases;
+    }
+
     /**
      * @dataProvider providePhpUnitFixerExtendsAbstractPhpUnitFixerCases
      *
diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php
index 3ae290d92..4f7673f95 100644
--- a/tests/ConfigTest.php
+++ b/tests/ConfigTest.php
@@ -219,6 +219,18 @@ public function testRegisterCustomFixers(array $expected, iterable $suite): void
         self::assertSame($expected, $config->getCustomFixers());
     }
 
+    public static function provideRegisterCustomFixersCases(): iterable
+    {
+        $fixers = [
+            new NoWhitespaceBeforeCommaInArrayFixer(),
+            new IncludeFixer(),
+        ];
+
+        yield [$fixers, $fixers];
+
+        yield [$fixers, new \ArrayIterator($fixers)];
+    }
+
     public function testConfigDefault(): void
     {
         $config = new Config();
@@ -261,18 +273,6 @@ public function testConfigDefault(): void
         self::assertFalse($config->getUsingCache());
     }
 
-    public static function provideRegisterCustomFixersCases(): iterable
-    {
-        $fixers = [
-            new NoWhitespaceBeforeCommaInArrayFixer(),
-            new IncludeFixer(),
-        ];
-
-        yield [$fixers, $fixers];
-
-        yield [$fixers, new \ArrayIterator($fixers)];
-    }
-
     public function testConfigConstructorWithName(): void
     {
         $anonymousConfig = new Config();
diff --git a/tests/Fixer/PhpUnit/PhpUnitDataProviderMethodOrderFixerTest.php b/tests/Fixer/PhpUnit/PhpUnitDataProviderMethodOrderFixerTest.php
new file mode 100644
index 000000000..46c09bb92
--- /dev/null
+++ b/tests/Fixer/PhpUnit/PhpUnitDataProviderMethodOrderFixerTest.php
@@ -0,0 +1,437 @@
+<?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\PhpUnit;
+
+use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
+
+/**
+ * @internal
+ *
+ * @covers \PhpCsFixer\Fixer\PhpUnit\PhpUnitDataProviderMethodOrderFixer
+ *
+ * @extends AbstractFixerTestCase<\PhpCsFixer\Fixer\PhpUnit\PhpUnitDataProviderMethodOrderFixer>
+ *
+ * @phpstan-import-type _AutogeneratedInputConfiguration from \PhpCsFixer\Fixer\PhpUnit\PhpUnitDataProviderMethodOrderFixer
+ */
+final class PhpUnitDataProviderMethodOrderFixerTest extends AbstractFixerTestCase
+{
+    /**
+     * @param _AutogeneratedInputConfiguration $configuration
+     *
+     * @dataProvider provideFixCases
+     */
+    public function testFix(string $expected, ?string $input = null, array $configuration = []): void
+    {
+        $this->fixer->configure($configuration);
+        $this->doTest($expected, $input);
+    }
+
+    /**
+     * @return iterable<array{0: string, 1?: null|string, 2?: array<string, string>}>
+     */
+    public static function provideFixCases(): iterable
+    {
+        yield 'simple - placement after' => [
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    /** @dataProvider provideFooCases */
+                    public function testFoo(): void {}
+                    public function provideFooCases(): iterable {}
+                }
+                PHP,
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    public function provideFooCases(): iterable {}
+                    /** @dataProvider provideFooCases */
+                    public function testFoo(): void {}
+                }
+                PHP,
+        ];
+
+        yield 'simple - placement before' => [
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    public function provideFooCases(): iterable {}
+                    /** @dataProvider provideFooCases */
+                    public function testFoo(): void {}
+                }
+                PHP,
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    /** @dataProvider provideFooCases */
+                    public function testFoo(): void {}
+                    public function provideFooCases(): iterable {}
+                }
+                PHP,
+            ['placement' => 'before'],
+        ];
+
+        yield 'empty test class' => [
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {}
+                PHP,
+        ];
+
+        yield 'data provider named with different casing' => [
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    /** @dataProvider provideFooCases */
+                    public function testFoo(): void {}
+                    public function PROVIDEFOOCASES(): iterable {}
+                }
+                PHP,
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    public function PROVIDEFOOCASES(): iterable {}
+                    /** @dataProvider provideFooCases */
+                    public function testFoo(): void {}
+                }
+                PHP,
+        ];
+
+        yield 'with test method annotated' => [
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    /**
+                     * @test
+                     * @dataProvider provideFooCases
+                     */
+                    public function foo(): void {}
+                    public function provideFooCases(): iterable {}
+                }
+                PHP,
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    public function provideFooCases(): iterable {}
+                    /**
+                     * @test
+                     * @dataProvider provideFooCases
+                     */
+                    public function foo(): void {}
+                }
+                PHP,
+        ];
+
+        yield 'data provider not found' => [
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    public function provideFooCases(): iterable {}
+                    /** @dataProvider notExistingFunction */
+                    public function testFoo(): void {}
+                }
+                PHP,
+        ];
+
+        yield 'data provider used multiple times - placement after' => [
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    /** @dataProvider provideFooCases */
+                    public function testFoo(): void {}
+                    /** @dataProvider provideFooCases */
+                    public function testFoo2(): void {}
+                    public function provideFooCases(): iterable {}
+                }
+                PHP,
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    public function provideFooCases(): iterable {}
+                    /** @dataProvider provideFooCases */
+                    public function testFoo(): void {}
+                    /** @dataProvider provideFooCases */
+                    public function testFoo2(): void {}
+                }
+                PHP,
+        ];
+
+        yield 'data provider used multiple times - placement after - do not move provider if already after first test where used' => [
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    /** @dataProvider provideFooCases */
+                    public function testFoo(): void {}
+                    public function provideFooCases(): iterable {}
+                    /** @dataProvider provideFooCases */
+                    public function testBar(): void {}
+                }
+                PHP,
+        ];
+
+        yield 'data provider used multiple times - placement before' => [
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    public function provideFooCases(): iterable {}
+                    /** @dataProvider provideFooCases */
+                    public function testFoo(): void {}
+                    /** @dataProvider provideFooCases */
+                    public function testFoo2(): void {}
+                }
+                PHP,
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    /** @dataProvider provideFooCases */
+                    public function testFoo(): void {}
+                    /** @dataProvider provideFooCases */
+                    public function testFoo2(): void {}
+                    public function provideFooCases(): iterable {}
+                }
+                PHP,
+            ['placement' => 'before'],
+        ];
+
+        yield 'multiple data providers for one test method' => [
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    /**
+                     * @dataProvider provideFooCases
+                     * @dataProvider provideFooCases3
+                     * @dataProvider provideFooCases2
+                     */
+                    public function testFoo(): void {}
+                    public function provideFooCases(): iterable {}
+                    public function provideFooCases3(): iterable {}
+                    public function provideFooCases2(): iterable {}
+                }
+                PHP,
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    public function provideFooCases(): iterable {}
+                    /**
+                     * @dataProvider provideFooCases
+                     * @dataProvider provideFooCases3
+                     * @dataProvider provideFooCases2
+                     */
+                    public function testFoo(): void {}
+                    public function provideFooCases2(): iterable {}
+                    public function provideFooCases3(): iterable {}
+                }
+                PHP,
+        ];
+
+        yield 'data provider used multiple times II - placement after' => [
+            <<<'PHP'
+                <?php
+                class FooTest {
+                    /** @dataProvider provideACases */
+                    public function testA1(): void {}
+
+                    /** @dataProvider provideBCases */
+                    public function testB(): void {}
+
+                    public static function provideBCases(): iterable {}
+
+                    /** @dataProvider provideACases */
+                    public function testA2(): void {}
+
+                    public static function provideACases(): iterable {}
+                }
+                PHP,
+        ];
+
+        yield 'data provider used multiple times II - placement before' => [
+            <<<'PHP'
+                <?php
+                class FooTest {
+                    public static function provideACases(): iterable {}
+
+                    /** @dataProvider provideACases */
+                    public function testA2(): void {}
+
+                    public static function provideBCases(): iterable {}
+
+                    /** @dataProvider provideBCases */
+                    public function testB(): void {}
+
+                    /** @dataProvider provideACases */
+                    public function testA1(): void {}
+                }
+                PHP,
+            null,
+            ['placement' => 'before'],
+        ];
+
+        yield 'with other methods - placement after' => [
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    public function testA(): void {}
+                    /** @dataProvider provideFooCases */
+                    public function testFoo(): void {}
+                    public function provideFooCases(): iterable {}
+                    public function testB(): void {}
+                }
+                PHP,
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    public function testA(): void {}
+                    public function provideFooCases(): iterable {}
+                    /** @dataProvider provideFooCases */
+                    public function testFoo(): void {}
+                    public function testB(): void {}
+                }
+                PHP,
+        ];
+
+        yield 'with other methods - placement before' => [
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    public function testA(): void {}
+                    public function provideFooCases(): iterable {}
+                    /** @dataProvider provideFooCases */
+                    public function testFoo(): void {}
+                    public function testB(): void {}
+                }
+                PHP,
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    public function testA(): void {}
+                    /** @dataProvider provideFooCases */
+                    public function testFoo(): void {}
+                    public function provideFooCases(): iterable {}
+                    public function testB(): void {}
+                }
+                PHP,
+            ['placement' => 'before'],
+        ];
+
+        yield 'with other methods II' => [
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    public function testA(): void {}
+                    public function testB(): void {}
+                    /** @dataProvider provideFooCases */
+                    public function testFoo(): void {}
+                    public function provideFooCases(): iterable {}
+                    public function testC(): void {}
+                }
+                PHP,
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    public function testA(): void {}
+                    public function provideFooCases(): iterable {}
+                    public function testB(): void {}
+                    /** @dataProvider provideFooCases */
+                    public function testFoo(): void {}
+                    public function testC(): void {}
+                }
+                PHP,
+        ];
+    }
+
+    /**
+     * @requires PHP ^8.0
+     *
+     * @param _AutogeneratedInputConfiguration $configuration
+     *
+     * @dataProvider provideFix80Cases
+     */
+    public function testFix80(string $expected, ?string $input = null, array $configuration = []): void
+    {
+        $this->fixer->configure($configuration);
+        $this->doTest($expected, $input);
+    }
+
+    /**
+     * @return iterable<array{0: string, 1?: string, 2?: array<string, string>}>
+     */
+    public static function provideFix80Cases(): iterable
+    {
+        yield 'with an attribute between PHPDoc and data provider/test method - placement after' => [
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    /**
+                     * @dataProvider provideFooCases
+                     */
+                    #[CustomTestAttribute]
+                    public function testFoo(): void {}
+                    #[CustomProviderAttribute]
+                    public function provideFooCases(): iterable {}
+                }
+                PHP,
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    #[CustomProviderAttribute]
+                    public function provideFooCases(): iterable {}
+                    /**
+                     * @dataProvider provideFooCases
+                     */
+                    #[CustomTestAttribute]
+                    public function testFoo(): void {}
+                }
+                PHP,
+        ];
+
+        yield 'with an attribute between PHPDoc and data provider/test method - placement before' => [
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    #[CustomProviderAttribute]
+                    public function provideFooCases(): iterable {}
+                    /**
+                     * @dataProvider provideFooCases
+                     */
+                    #[CustomTestAttribute]
+                    public function testFoo(): void {}
+                }
+                PHP,
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    /**
+                     * @dataProvider provideFooCases
+                     */
+                    #[CustomTestAttribute]
+                    public function testFoo(): void {}
+                    #[CustomProviderAttribute]
+                    public function provideFooCases(): iterable {}
+                }
+                PHP,
+            ['placement' => 'before'],
+        ];
+
+        yield 'data provider defined by an attribute' => [ // update expected once https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/pull/8197 is merged
+            <<<'PHP'
+                <?php
+                class FooTest extends TestCase {
+                    public function provideFooCases(): iterable {}
+                    #[DataProvider('provideFooCases')]
+                    public function testFoo(): void {}
+                }
+                PHP,
+        ];
+    }
+}
diff --git a/tests/Fixer/Phpdoc/PhpdocOrderFixerTest.php b/tests/Fixer/Phpdoc/PhpdocOrderFixerTest.php
index f76986bd3..4408609b2 100644
--- a/tests/Fixer/Phpdoc/PhpdocOrderFixerTest.php
+++ b/tests/Fixer/Phpdoc/PhpdocOrderFixerTest.php
@@ -139,6 +139,16 @@ public function testNoAnnotations(array $config): void
         $this->doTest($expected);
     }
 
+    /**
+     * @return iterable<array{array<string, mixed>}>
+     */
+    public static function provideDifferentOrderCases(): iterable
+    {
+        yield [['order' => ['param', 'throw', 'return']]];
+
+        yield [['order' => ['param', 'return', 'throw']]];
+    }
+
     public function testFixBasicCase(): void
     {
         $expected = <<<'EOF'
@@ -388,16 +398,6 @@ public function testExampleFromSymfonyWithLaravelStyle(): void
         $this->doTest($input);
     }
 
-    /**
-     * @return iterable<array{array<string, mixed>}>
-     */
-    public static function provideDifferentOrderCases(): iterable
-    {
-        yield [['order' => ['param', 'throw', 'return']]];
-
-        yield [['order' => ['param', 'return', 'throw']]];
-    }
-
     /**
      * @dataProvider provideFixBasicCaseWithDifferentOrdersCases
      *
diff --git a/tests/Fixer/Whitespace/NoSpacesInsideParenthesisFixerTest.php b/tests/Fixer/Whitespace/NoSpacesInsideParenthesisFixerTest.php
index 53ac26e88..070b9a6a8 100644
--- a/tests/Fixer/Whitespace/NoSpacesInsideParenthesisFixerTest.php
+++ b/tests/Fixer/Whitespace/NoSpacesInsideParenthesisFixerTest.php
@@ -35,29 +35,6 @@ public function testFix(string $expected, ?string $input = null): void
         $this->doTest($expected, $input);
     }
 
-    public function testLeaveNewLinesAlone(): void
-    {
-        $expected = <<<'EOF'
-            <?php
-
-            class Foo
-            {
-                private function bar()
-                {
-                    if (foo(
-                        'foo' ,
-                        'bar'    ,
-                        [1, 2, 3],
-                        'baz' // a comment just to mix things up
-                    )) {
-                        return 1;
-                    };
-                }
-            }
-            EOF;
-        $this->doTest($expected);
-    }
-
     /**
      * @return iterable<array{0: string, 1?: string}>
      */
@@ -140,6 +117,29 @@ function foo( $bar, $baz )
         ];
     }
 
+    public function testLeaveNewLinesAlone(): void
+    {
+        $expected = <<<'EOF'
+            <?php
+
+            class Foo
+            {
+                private function bar()
+                {
+                    if (foo(
+                        'foo' ,
+                        'bar'    ,
+                        [1, 2, 3],
+                        'baz' // a comment just to mix things up
+                    )) {
+                        return 1;
+                    };
+                }
+            }
+            EOF;
+        $this->doTest($expected);
+    }
+
     /**
      * @dataProvider provideFix80Cases
      *
diff --git a/tests/Fixtures/Integration/priority/ordered_class_elements,php_unit_data_provider_method_order.test b/tests/Fixtures/Integration/priority/ordered_class_elements,php_unit_data_provider_method_order.test
new file mode 100644
index 000000000..0ce97ef70
--- /dev/null
+++ b/tests/Fixtures/Integration/priority/ordered_class_elements,php_unit_data_provider_method_order.test
@@ -0,0 +1,47 @@
+--TEST--
+Integration of fixers: ordered_class_elements,php_unit_data_provider_method_order.
+--RULESET--
+{"ordered_class_elements": {"order": ["method_public_static", "method_public"]}, "php_unit_data_provider_method_order": true}
+--EXPECT--
+<?php
+
+namespace Foo;
+
+use PHPUnit\Framework\TestCase;
+
+class BarTest extends TestCase
+{
+
+    /**
+     * @dataProvider getData
+     */
+    public function testSomething()
+    {
+    }
+    public static function getData(): iterable
+    {
+        yield from range(1, 10);
+    }
+}
+
+--INPUT--
+<?php
+
+namespace Foo;
+
+use PHPUnit\Framework\TestCase;
+
+class BarTest extends TestCase
+{
+    public static function getData(): iterable
+    {
+        yield from range(1, 10);
+    }
+
+    /**
+     * @dataProvider getData
+     */
+    public function testSomething()
+    {
+    }
+}
diff --git a/tests/Fixtures/Integration/priority/php_unit_data_provider_method_order,class_attributes_separation.test b/tests/Fixtures/Integration/priority/php_unit_data_provider_method_order,class_attributes_separation.test
new file mode 100644
index 000000000..6c6236356
--- /dev/null
+++ b/tests/Fixtures/Integration/priority/php_unit_data_provider_method_order,class_attributes_separation.test
@@ -0,0 +1,43 @@
+--TEST--
+Integration of fixers: php_unit_data_provider_method_order,class_attributes_separation.
+--RULESET--
+{"php_unit_data_provider_method_order": true, "class_attributes_separation": true}
+--EXPECT--
+<?php
+
+use PHPUnit\Framework\TestCase;
+
+class BarTest extends TestCase
+{
+    /**
+     * @dataProvider getData
+     */
+    public function testSomething()
+    {
+    }
+
+    public static function getData(): iterable
+    {
+        yield from range(1, 10);
+    }
+}
+
+--INPUT--
+<?php
+
+use PHPUnit\Framework\TestCase;
+
+class BarTest extends TestCase
+{
+    public static function getData(): iterable
+    {
+        yield from range(1, 10);
+    }
+
+    /**
+     * @dataProvider getData
+     */
+    public function testSomething()
+    {
+    }
+}
diff --git a/tests/Fixtures/Integration/priority/php_unit_data_provider_method_order,no_blank_lines_after_class_opening.test b/tests/Fixtures/Integration/priority/php_unit_data_provider_method_order,no_blank_lines_after_class_opening.test
new file mode 100644
index 000000000..14c057273
--- /dev/null
+++ b/tests/Fixtures/Integration/priority/php_unit_data_provider_method_order,no_blank_lines_after_class_opening.test
@@ -0,0 +1,42 @@
+--TEST--
+Integration of fixers: php_unit_data_provider_method_order,no_blank_lines_after_class_opening.
+--RULESET--
+{"php_unit_data_provider_method_order": true, "no_blank_lines_after_class_opening": true}
+--EXPECT--
+<?php
+
+use PHPUnit\Framework\TestCase;
+
+class BarTest extends TestCase
+{
+    /**
+     * @dataProvider getData
+     */
+    public function testSomething()
+    {
+    }
+    public static function getData(): iterable
+    {
+        yield from range(1, 10);
+    }
+}
+
+--INPUT--
+<?php
+
+use PHPUnit\Framework\TestCase;
+
+class BarTest extends TestCase
+{
+    public static function getData(): iterable
+    {
+        yield from range(1, 10);
+    }
+
+    /**
+     * @dataProvider getData
+     */
+    public function testSomething()
+    {
+    }
+}
diff --git a/tests/RuleSet/RuleSetsTest.php b/tests/RuleSet/RuleSetsTest.php
index 1b1f55d51..983c9f37c 100644
--- a/tests/RuleSet/RuleSetsTest.php
+++ b/tests/RuleSet/RuleSetsTest.php
@@ -134,15 +134,6 @@ public function testSetDefinitionsAreSorted(string $setDefinitionName): void
         ));
     }
 
-    public function testSetDefinitionsItselfIsSorted(): void
-    {
-        $setDefinition = array_keys(RuleSets::getSetDefinitions());
-        $sortedSetDefinition = $setDefinition;
-        natsort($sortedSetDefinition);
-
-        self::assertSame($sortedSetDefinition, $setDefinition);
-    }
-
     /**
      * @return iterable<array{string}>
      */
@@ -153,6 +144,15 @@ public static function provideSetDefinitionNameCases(): iterable
         return array_map(static fn (string $setDefinitionName): array => [$setDefinitionName], $setDefinitionNames);
     }
 
+    public function testSetDefinitionsItselfIsSorted(): void
+    {
+        $setDefinition = array_keys(RuleSets::getSetDefinitions());
+        $sortedSetDefinition = $setDefinition;
+        natsort($sortedSetDefinition);
+
+        self::assertSame($sortedSetDefinition, $setDefinition);
+    }
+
     /**
      * @dataProvider providePHPUnitMigrationTargetVersionsCases
      */