Browse Source

feature: Introduce `PhpUnitDataProviderNameFixer` (#7057)

Co-authored-by: Greg Korba <wirone@gmail.com>
Kuba Werłos 1 year ago
parent
commit
4976b85821

+ 19 - 0
doc/list.rst

@@ -2431,6 +2431,25 @@ List of Available Rules
    Part of rule sets `@PhpCsFixer:risky <./ruleSets/PhpCsFixerRisky.rst>`_ `@Symfony:risky <./ruleSets/SymfonyRisky.rst>`_
 
    `Source PhpCsFixer\\Fixer\\PhpUnit\\PhpUnitConstructFixer <./../src/Fixer/PhpUnit/PhpUnitConstructFixer.php>`_
+-  `php_unit_data_provider_name <./rules/php_unit/php_unit_data_provider_name.rst>`_
+
+   Data provider names must match the name of the test.
+
+   *warning risky* Fixer could be risky if one is calling data provider by name as function.
+
+   Configuration options:
+
+   - | ``prefix``
+     | Prefix that replaces "test".
+     | Allowed types: ``string``
+     | Default value: ``'provide'``
+   - | ``suffix``
+     | Suffix to be present at the end.
+     | Allowed types: ``string``
+     | Default value: ``'Cases'``
+
+
+   `Source PhpCsFixer\\Fixer\\PhpUnit\\PhpUnitDataProviderNameFixer <./../src/Fixer/PhpUnit/PhpUnitDataProviderNameFixer.php>`_
 -  `php_unit_data_provider_static <./rules/php_unit/php_unit_data_provider_static.rst>`_
 
    Data providers must be static.

+ 3 - 0
doc/rules/index.rst

@@ -600,6 +600,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_name <./php_unit/php_unit_data_provider_name.rst>`_ *(risky)*
+
+  Data provider names must match the name of the test.
 - `php_unit_data_provider_static <./php_unit/php_unit_data_provider_static.rst>`_ *(risky)*
 
   Data providers must be static.

+ 114 - 0
doc/rules/php_unit/php_unit_data_provider_name.rst

@@ -0,0 +1,114 @@
+====================================
+Rule ``php_unit_data_provider_name``
+====================================
+
+Data provider names must match the name of the test.
+
+Warning
+-------
+
+Using this rule is risky
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Fixer could be risky if one is calling data provider by name as function.
+
+Configuration
+-------------
+
+``prefix``
+~~~~~~~~~~
+
+Prefix that replaces "test".
+
+Allowed types: ``string``
+
+Default value: ``'provide'``
+
+``suffix``
+~~~~~~~~~~
+
+Suffix to be present at the end.
+
+Allowed types: ``string``
+
+Default value: ``'Cases'``
+
+Examples
+--------
+
+Example #1
+~~~~~~~~~~
+
+*Default* configuration.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+    class FooTest extends TestCase {
+        /**
+   -     * @dataProvider dataProvider
+   +     * @dataProvider provideSomethingCases
+         */
+        public function testSomething($expected, $actual) {}
+   -    public function dataProvider() {}
+   +    public function provideSomethingCases() {}
+    }
+
+Example #2
+~~~~~~~~~~
+
+With configuration: ``['prefix' => 'data_', 'suffix' => '']``.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+    class FooTest extends TestCase {
+        /**
+   -     * @dataProvider dt_prvdr_ftr
+   +     * @dataProvider data_feature
+         */
+        public function test_feature($expected, $actual) {}
+   -    public function dt_prvdr_ftr() {}
+   +    public function data_feature() {}
+    }
+
+Example #3
+~~~~~~~~~~
+
+With configuration: ``['prefix' => 'provides', 'suffix' => 'Data']``.
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+    class FooTest extends TestCase {
+        /**
+         * @dataProvider dataProviderUsedInMultipleTests
+         */
+        public function testA($expected, $actual) {}
+        /**
+         * @dataProvider dataProviderUsedInMultipleTests
+         */
+        public function testB($expected, $actual) {}
+        /**
+   -     * @dataProvider dataProviderUsedInSingleTest
+   +     * @dataProvider providesCData
+         */
+        public function testC($expected, $actual) {}
+        /**
+         * @dataProvider dataProviderUsedAsFirstInTest
+         * @dataProvider dataProviderUsedAsSecondInTest
+         */
+        public function testD($expected, $actual) {}
+
+        public function dataProviderUsedInMultipleTests() {}
+   -    public function dataProviderUsedInSingleTest() {}
+   +    public function providesCData() {}
+        public function dataProviderUsedAsFirstInTest() {}
+        public function dataProviderUsedAsSecondInTest() {}
+    }

+ 172 - 0
src/Fixer/PhpUnit/PhpUnitDataProviderNameFixer.php

@@ -0,0 +1,172 @@
+<?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\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\Preg;
+use PhpCsFixer\Tokenizer\Analyzer\DataProviderAnalyzer;
+use PhpCsFixer\Tokenizer\Token;
+use PhpCsFixer\Tokenizer\Tokens;
+
+/**
+ * @author Kuba Werłos <werlos@gmail.com>
+ */
+final class PhpUnitDataProviderNameFixer extends AbstractPhpUnitFixer implements ConfigurableFixerInterface
+{
+    public function getDefinition(): FixerDefinitionInterface
+    {
+        return new FixerDefinition(
+            'Data provider names must match the name of the test.',
+            [
+                new CodeSample(
+                    '<?php
+class FooTest extends TestCase {
+    /**
+     * @dataProvider dataProvider
+     */
+    public function testSomething($expected, $actual) {}
+    public function dataProvider() {}
+}
+',
+                ),
+                new CodeSample(
+                    '<?php
+class FooTest extends TestCase {
+    /**
+     * @dataProvider dt_prvdr_ftr
+     */
+    public function test_feature($expected, $actual) {}
+    public function dt_prvdr_ftr() {}
+}
+',
+                    [
+                        'prefix' => 'data_',
+                        'suffix' => '',
+                    ]
+                ),
+                new CodeSample(
+                    '<?php
+class FooTest extends TestCase {
+    /**
+     * @dataProvider dataProviderUsedInMultipleTests
+     */
+    public function testA($expected, $actual) {}
+    /**
+     * @dataProvider dataProviderUsedInMultipleTests
+     */
+    public function testB($expected, $actual) {}
+    /**
+     * @dataProvider dataProviderUsedInSingleTest
+     */
+    public function testC($expected, $actual) {}
+    /**
+     * @dataProvider dataProviderUsedAsFirstInTest
+     * @dataProvider dataProviderUsedAsSecondInTest
+     */
+    public function testD($expected, $actual) {}
+
+    public function dataProviderUsedInMultipleTests() {}
+    public function dataProviderUsedInSingleTest() {}
+    public function dataProviderUsedAsFirstInTest() {}
+    public function dataProviderUsedAsSecondInTest() {}
+}
+',
+                    [
+                        'prefix' => 'provides',
+                        'suffix' => 'Data',
+                    ]
+                ),
+            ],
+            null,
+            'Fixer could be risky if one is calling data provider by name as function.'
+        );
+    }
+
+    public function getConfigurationDefinition(): FixerConfigurationResolverInterface
+    {
+        return new FixerConfigurationResolver([
+            (new FixerOptionBuilder('prefix', 'Prefix that replaces "test".'))
+                ->setAllowedTypes(['string'])
+                ->setDefault('provide')
+                ->getOption(),
+            (new FixerOptionBuilder('suffix', 'Suffix to be present at the end.'))
+                ->setAllowedTypes(['string'])
+                ->setDefault('Cases')
+                ->getOption(),
+        ]);
+    }
+
+    public function getPriority(): int
+    {
+        return 0;
+    }
+
+    public function isRisky(): bool
+    {
+        return true;
+    }
+
+    protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void
+    {
+        $dataProviderAnalyzer = new DataProviderAnalyzer();
+        foreach ($dataProviderAnalyzer->getDataProviders($tokens, $startIndex, $endIndex) as $dataProviderAnalysis) {
+            if (\count($dataProviderAnalysis->getUsageIndices()) > 1) {
+                continue;
+            }
+
+            $usageIndex = $dataProviderAnalysis->getUsageIndices()[0];
+            if (substr_count($tokens[$usageIndex]->getContent(), '@dataProvider') > 1) {
+                continue;
+            }
+
+            $testNameIndex = $tokens->getNextTokenOfKind($usageIndex, [[T_STRING]]);
+
+            $dataProviderNewName = $this->getProviderNameForTestName($tokens[$testNameIndex]->getContent());
+            if (null !== $tokens->findSequence([[T_FUNCTION], [T_STRING, $dataProviderNewName]], $startIndex, $endIndex)) {
+                continue;
+            }
+
+            $tokens[$dataProviderAnalysis->getNameIndex()] = new Token([T_STRING, $dataProviderNewName]);
+
+            $newCommentContent = Preg::replace(
+                sprintf('/(@dataProvider\s+)%s/', $dataProviderAnalysis->getName()),
+                sprintf('$1%s', $dataProviderNewName),
+                $tokens[$usageIndex]->getContent(),
+            );
+
+            $tokens[$usageIndex] = new Token([T_DOC_COMMENT, $newCommentContent]);
+        }
+    }
+
+    private function getProviderNameForTestName(string $name): string
+    {
+        $name = Preg::replace('/^test_*/i', '', $name);
+
+        if ('' === $this->configuration['prefix']) {
+            $name = lcfirst($name);
+        } elseif ('_' !== substr($this->configuration['prefix'], -1)) {
+            $name = ucfirst($name);
+        }
+
+        return $this->configuration['prefix'].$name.$this->configuration['suffix'];
+    }
+}

+ 2 - 2
src/Fixer/PhpUnit/PhpUnitDataProviderStaticFixer.php

@@ -109,7 +109,7 @@ class FooTest extends TestCase {
 
         $inserts = [];
         foreach ($dataProviderAnalyzer->getDataProviders($tokens, $startIndex, $endIndex) as $dataProviderDefinitionIndex) {
-            $methodStartIndex = $tokens->getNextTokenOfKind($dataProviderDefinitionIndex, ['{']);
+            $methodStartIndex = $tokens->getNextTokenOfKind($dataProviderDefinitionIndex->getNameIndex(), ['{']);
             if (null !== $methodStartIndex) {
                 $methodEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $methodStartIndex);
 
@@ -117,7 +117,7 @@ class FooTest extends TestCase {
                     continue;
                 }
             }
-            $functionIndex = $tokens->getPrevTokenOfKind($dataProviderDefinitionIndex, [[T_FUNCTION]]);
+            $functionIndex = $tokens->getPrevTokenOfKind($dataProviderDefinitionIndex->getNameIndex(), [[T_FUNCTION]]);
 
             $methodAttributes = $tokensAnalyzer->getMethodAttributes($functionIndex);
             if (false !== $methodAttributes['static']) {

+ 53 - 0
src/Tokenizer/Analyzer/Analysis/DataProviderAnalysis.php

@@ -0,0 +1,53 @@
+<?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\Tokenizer\Analyzer\Analysis;
+
+final class DataProviderAnalysis
+{
+    private string $name;
+
+    private int $nameIndex;
+
+    /** @var array<int> */
+    private array $usageIndices;
+
+    /**
+     * @param array<int> $usageIndices
+     */
+    public function __construct(string $name, int $nameIndex, array $usageIndices)
+    {
+        $this->name = $name;
+        $this->nameIndex = $nameIndex;
+        $this->usageIndices = $usageIndices;
+    }
+
+    public function getName(): string
+    {
+        return $this->name;
+    }
+
+    public function getNameIndex(): int
+    {
+        return $this->nameIndex;
+    }
+
+    /**
+     * @return array<int>
+     */
+    public function getUsageIndices(): array
+    {
+        return $this->usageIndices;
+    }
+}

+ 10 - 5
src/Tokenizer/Analyzer/DataProviderAnalyzer.php

@@ -15,6 +15,7 @@ declare(strict_types=1);
 namespace PhpCsFixer\Tokenizer\Analyzer;
 
 use PhpCsFixer\Preg;
+use PhpCsFixer\Tokenizer\Analyzer\Analysis\DataProviderAnalysis;
 use PhpCsFixer\Tokenizer\Tokens;
 
 /**
@@ -25,7 +26,7 @@ use PhpCsFixer\Tokenizer\Tokens;
 final class DataProviderAnalyzer
 {
     /**
-     * @return array<int> indices of data provider definitions
+     * @return array<DataProviderAnalysis>
      */
     public function getDataProviders(Tokens $tokens, int $startIndex, int $endIndex): array
     {
@@ -53,18 +54,22 @@ final class DataProviderAnalyzer
             }
         }
 
-        $dataProviderDefinitions = [];
+        $dataProviderAnalyses = [];
         foreach ($dataProviders as $dataProviderName => $dataProviderUsages) {
             $lowercaseDataProviderName = strtolower($dataProviderName);
             if (!\array_key_exists($lowercaseDataProviderName, $methods)) {
                 continue;
             }
-            $dataProviderDefinitions[$methods[$lowercaseDataProviderName]] = $methods[$lowercaseDataProviderName];
+            $dataProviderAnalyses[$methods[$lowercaseDataProviderName]] = new DataProviderAnalysis(
+                $tokens[$methods[$lowercaseDataProviderName]]->getContent(),
+                $methods[$lowercaseDataProviderName],
+                $dataProviderUsages,
+            );
         }
 
-        ksort($dataProviderDefinitions);
+        ksort($dataProviderAnalyses);
 
-        return array_values($dataProviderDefinitions);
+        return array_values($dataProviderAnalyses);
     }
 
     /**

+ 400 - 0
tests/Fixer/PhpUnit/PhpUnitDataProviderNameFixerTest.php

@@ -0,0 +1,400 @@
+<?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\PhpUnitDataProviderNameFixer
+ */
+final class PhpUnitDataProviderNameFixerTest extends AbstractFixerTestCase
+{
+    /**
+     * @param array<string, bool> $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?: string, 2?: array<string, string>}>
+     */
+    public static function provideFixCases(): iterable
+    {
+        yield 'data provider named with different casing' => [
+            '<?php
+class FooTest extends TestCase {
+    /**
+     * @dataProvider provideFooCases
+     */
+    public function testFoo() {}
+    public function provideFooCases() {}
+}',
+            '<?php
+class FooTest extends TestCase {
+    /**
+     * @dataProvider provideFooCases
+     */
+    public function testFoo() {}
+    public function PROVIDEFOOCASES() {}
+}',
+        ];
+
+        yield 'fixing simple scenario with test class prefixed' => [
+            '<?php
+class FooTest extends TestCase {
+    /**
+     * @dataProvider provideFooCases
+     */
+    public function testFoo() {}
+    public function provideFooCases() {}
+}',
+            '<?php
+class FooTest extends TestCase {
+    /**
+     * @dataProvider fooDataProvider
+     */
+    public function testFoo() {}
+    public function fooDataProvider() {}
+}',
+        ];
+
+        yield 'fixing simple scenario with test class annotated' => [
+            '<?php
+class FooTest extends TestCase {
+    /**
+     * @test
+     * @dataProvider provideFooCases
+     */
+    public function foo() {}
+    public function provideFooCases() {}
+}',
+            '<?php
+class FooTest extends TestCase {
+    /**
+     * @test
+     * @dataProvider fooDataProvider
+     */
+    public function foo() {}
+    public function fooDataProvider() {}
+}',
+        ];
+
+        yield 'data provider not found' => [
+            '<?php
+class FooTest extends TestCase {
+    /**
+     * @dataProvider notExistingFunction
+     */
+    public function testFoo() {}
+}',
+        ];
+
+        yield 'data provider used multiple times' => [
+            '<?php
+class FooTest extends TestCase {
+    /**
+     * @dataProvider reusedDataProvider
+     */
+    public function testFoo() {}
+    /**
+     * @dataProvider reusedDataProvider
+     */
+    public function testBar() {}
+    public function reusedDataProvider() {}
+}',
+        ];
+
+        yield 'data provider call without function' => [
+            '<?php
+class FooTest extends TestCase {
+    /**
+     * @dataProvider fooDataProvider
+     */
+    private $prop;
+}',
+        ];
+
+        yield 'data provider target name already used' => [
+            '<?php
+class FooTest extends TestCase {
+    /**
+     * @dataProvider dataProvider
+     */
+    public function testFoo() {}
+    public function dataProvider() {}
+    public function provideFooCases() {}
+}',
+        ];
+
+        yield 'data provider defined for anonymous function' => [
+            '<?php
+class FooTest extends TestCase {
+    public function testFoo()
+    {
+        /**
+         * @dataProvider notDataProvider
+         */
+        function () { return true; };
+    }
+    public function notDataProvider() {}
+}',
+        ];
+
+        yield 'multiple data providers for one test function' => [
+            '<?php
+class FooTest extends TestCase {
+    /**
+     * @dataProvider foo1DataProvider
+     * @dataProvider foo2DataProvider
+     */
+    public function testFoo() {}
+    public function foo1DataProvider() {}
+    public function foo2DataProvider() {}
+}',
+        ];
+
+        yield 'data provider with new name being part of FQCN used in the code' => [
+            '<?php
+class FooTest extends TestCase {
+    /**
+     * @dataProvider provideFooCases
+     */
+    public function testFoo() {
+        $x = Foo\ProvideFooCases::X_DEFAULT;
+    }
+    public function provideFooCases() {}
+}',
+            '<?php
+class FooTest extends TestCase {
+    /**
+     * @dataProvider foo
+     */
+    public function testFoo() {
+        $x = Foo\ProvideFooCases::X_DEFAULT;
+    }
+    public function foo() {}
+}',
+        ];
+
+        yield 'complex example' => [
+            '<?php
+class FooTest extends TestCase {
+    /** @dataProvider notExistingFunction */
+    public function testClosure()
+    {
+        /** Preparing data */
+        $x = 0;
+        /** @dataProvider notDataProvider */
+        function () { return true; };
+    }
+
+    /**
+     * @dataProvider reusedDataProvider
+     * @dataProvider testFooProvider
+     */
+    public function testFoo() {}
+
+    /**
+     * @dataProvider reusedDataProvider
+     * @dataProvider testBarProvider
+     */
+    public function testBar() {}
+
+    public function reusedDataProvider() {}
+
+    /** @dataProvider provideBazCases */
+    public function testBaz() {}
+    public function provideBazCases() {}
+
+    /** @dataProvider provideSomethingCases */
+    public function testSomething() {}
+    public function provideSomethingCases() {}
+    public function testFooProvider() {}
+    public function testBarProvider() {}
+}',
+            '<?php
+class FooTest extends TestCase {
+    /** @dataProvider notExistingFunction */
+    public function testClosure()
+    {
+        /** Preparing data */
+        $x = 0;
+        /** @dataProvider notDataProvider */
+        function () { return true; };
+    }
+
+    /**
+     * @dataProvider reusedDataProvider
+     * @dataProvider testFooProvider
+     */
+    public function testFoo() {}
+
+    /**
+     * @dataProvider reusedDataProvider
+     * @dataProvider testBarProvider
+     */
+    public function testBar() {}
+
+    public function reusedDataProvider() {}
+
+    /** @dataProvider provideBazCases */
+    public function testBaz() {}
+    public function provideBazCases() {}
+
+    /** @dataProvider someDataProvider */
+    public function testSomething() {}
+    public function someDataProvider() {}
+    public function testFooProvider() {}
+    public function testBarProvider() {}
+}',
+        ];
+
+        yield 'fixing when string like expected data provider name is present' => [
+            '<?php
+class FooTest extends TestCase {
+    /**
+     * @dataProvider provideFooCases
+     */
+    public function testFoo() {
+        $foo->provideFooCases(); // do not get fooled that data provider name is already taken
+    }
+    public function provideFooCases() {}
+}',
+            '<?php
+class FooTest extends TestCase {
+    /**
+     * @dataProvider fooDataProvider
+     */
+    public function testFoo() {
+        $foo->provideFooCases(); // do not get fooled that data provider name is already taken
+    }
+    public function fooDataProvider() {}
+}',
+        ];
+
+        foreach (['abstract', 'final', 'private', 'protected', 'static', '/* private */'] as $modifier) {
+            yield sprintf('test function with %s modifier', $modifier) => [
+                sprintf('<?php
+                abstract class FooTest extends TestCase {
+                    /**
+                     * @dataProvider provideFooCases
+                     */
+                    %s function testFoo() %s
+                    public function provideFooCases() {}
+                }', $modifier, 'abstract' === $modifier ? ';' : '{}'),
+                sprintf('<?php
+                abstract class FooTest extends TestCase {
+                    /**
+                     * @dataProvider fooDataProvider
+                     */
+                    %s function testFoo() %s
+                    public function fooDataProvider() {}
+                }', $modifier, 'abstract' === $modifier ? ';' : '{}'),
+            ];
+        }
+
+        foreach (
+            [
+                'custom prefix' => [
+                    'theBestPrefixFooCases',
+                    'testFoo',
+                    ['prefix' => 'theBestPrefix'],
+                ],
+                'custom suffix' => [
+                    'provideFooTheBestSuffix',
+                    'testFoo',
+                    ['suffix' => 'TheBestSuffix'],
+                ],
+                'custom prefix and suffix' => [
+                    'theBestPrefixFooTheBestSuffix',
+                    'testFoo',
+                    ['prefix' => 'theBestPrefix', 'suffix' => 'TheBestSuffix'],
+                ],
+                'empty prefix' => [
+                    'fooDataProvider',
+                    'testFoo',
+                    ['prefix' => '', 'suffix' => 'DataProvider'],
+                ],
+                'empty suffix' => [
+                    'dataProviderForFoo',
+                    'testFoo',
+                    ['prefix' => 'dataProviderFor', 'suffix' => ''],
+                ],
+                'prefix and suffix with underscores' => [
+                    'provide_foo_data',
+                    'test_foo',
+                    ['prefix' => 'provide_', 'suffix' => '_data'],
+                ],
+                'empty prefix and suffix with underscores' => [
+                    'foo_data_provider',
+                    'test_foo',
+                    ['prefix' => '', 'suffix' => '_data_provider'],
+                ],
+                'prefix with underscores and empty suffix' => [
+                    'data_provider_foo',
+                    'test_foo',
+                    ['prefix' => 'data_provider_', 'suffix' => ''],
+                ],
+                'prefix with underscores and empty suffix and test function starting with uppercase' => [
+                    'data_provider_Foo',
+                    'test_Foo',
+                    ['prefix' => 'data_provider_', 'suffix' => ''],
+                ],
+                'prefix and suffix with underscores and test function having multiple consecutive underscores' => [
+                    'provide_foo_data',
+                    'test___foo',
+                    ['prefix' => 'provide_', 'suffix' => '_data'],
+                ],
+                'uppercase naming' => [
+                    'PROVIDE_FOO_DATA',
+                    'TEST_FOO',
+                    ['prefix' => 'PROVIDE_', 'suffix' => '_DATA'],
+                ],
+                'camelCase test function and prefix with underscores' => [
+                    'data_provider_FooBar',
+                    'testFooBar',
+                    ['prefix' => 'data_provider_', 'suffix' => ''],
+                ],
+            ] as $name => [$dataProvider, $testFunction, $config]
+        ) {
+            yield $name => [
+                sprintf('<?php
+                    class FooTest extends TestCase {
+                        /**
+                         * @dataProvider %s
+                         */
+                        public function %s() {}
+                        public function %s() {}
+                    }', $dataProvider, $testFunction, $dataProvider),
+                sprintf('<?php
+                    class FooTest extends TestCase {
+                        /**
+                         * @dataProvider dtPrvdr
+                         */
+                        public function %s() {}
+                        public function dtPrvdr() {}
+                    }', $testFunction),
+                $config,
+            ];
+        }
+    }
+}

+ 35 - 0
tests/Tokenizer/Analyzer/Analysis/DataProviderAnalysisTest.php

@@ -0,0 +1,35 @@
+<?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\Tokenizer\Analyzer\Analysis;
+
+use PhpCsFixer\Tests\TestCase;
+use PhpCsFixer\Tokenizer\Analyzer\Analysis\DataProviderAnalysis;
+
+/**
+ * @covers \PhpCsFixer\Tokenizer\Analyzer\Analysis\DataProviderAnalysis
+ *
+ * @internal
+ */
+final class DataProviderAnalysisTest extends TestCase
+{
+    public function testDataProviderAnalysis(): void
+    {
+        $analysis = new DataProviderAnalysis('Foo', 1, [2, 3]);
+
+        self::assertSame('Foo', $analysis->getName());
+        self::assertSame(1, $analysis->getNameIndex());
+        self::assertSame([2, 3], $analysis->getUsageIndices());
+    }
+}

+ 18 - 7
tests/Tokenizer/Analyzer/DataProviderAnalyzerTest.php

@@ -15,6 +15,7 @@ declare(strict_types=1);
 namespace PhpCsFixer\Tests\Tokenizer\Analyzer;
 
 use PhpCsFixer\Tests\TestCase;
+use PhpCsFixer\Tokenizer\Analyzer\Analysis\DataProviderAnalysis;
 use PhpCsFixer\Tokenizer\Analyzer\DataProviderAnalyzer;
 use PhpCsFixer\Tokenizer\Tokens;
 
@@ -42,12 +43,12 @@ final class DataProviderAnalyzerTest extends TestCase
     }
 
     /**
-     * @return iterable<array{array<int>, string}>
+     * @return iterable<array{array<DataProviderAnalysis>, string}>
      */
     public static function provideGettingDataProvidersCases(): iterable
     {
         yield 'single data provider' => [
-            [28],
+            [new DataProviderAnalysis('provider', 28, [11])],
             '<?php class FooTest extends TestCase {
                 /**
                  * @dataProvider provider
@@ -58,7 +59,7 @@ final class DataProviderAnalyzerTest extends TestCase
         ];
 
         yield 'single data provider with different casing' => [
-            [28],
+            [new DataProviderAnalysis('dataProvider', 28, [11])],
             '<?php class FooTest extends TestCase {
                 /**
                  * @dataProvider dataPROVIDER
@@ -69,7 +70,7 @@ final class DataProviderAnalyzerTest extends TestCase
         ];
 
         yield 'single static data provider' => [
-            [30],
+            [new DataProviderAnalysis('provider', 30, [11])],
             '<?php class FooTest extends TestCase {
                 /**
                  * @dataProvider provider
@@ -80,7 +81,11 @@ final class DataProviderAnalyzerTest extends TestCase
         ];
 
         yield 'multiple data provider' => [
-            [28, 39, 50],
+            [
+                new DataProviderAnalysis('provider1', 28, [11]),
+                new DataProviderAnalysis('provider2', 39, [11]),
+                new DataProviderAnalysis('provider3', 50, [11]),
+            ],
             '<?php class FooTest extends TestCase {
                 /**
                  * @dataProvider provider1
@@ -96,7 +101,11 @@ final class DataProviderAnalyzerTest extends TestCase
 
         foreach (['abstract', 'final', 'private', 'protected', 'static', '/* private */'] as $modifier) {
             yield sprintf('test function with %s modifier', $modifier) => [
-                [54, 65, 76],
+                [
+                    new DataProviderAnalysis('provider1', 54, [37]),
+                    new DataProviderAnalysis('provider2', 65, [11]),
+                    new DataProviderAnalysis('provider3', 76, [24]),
+                ],
                 sprintf('<?php class FooTest extends TestCase {
                     /** @dataProvider provider2 */
                     public function testFoo1() {}
@@ -133,7 +142,9 @@ final class DataProviderAnalyzerTest extends TestCase
         ];
 
         yield 'ignore anonymous function' => [
-            [93],
+            [
+                new DataProviderAnalysis('provider2', 93, [65]),
+            ],
             '<?php class FooTest extends TestCase {
                 public function testFoo0() {}
                 /**