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