diff --git a/README.md b/README.md index 879f3362e..93cee658a 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ This rule spots definitions that are no longer needed, so you can remove them.
-### 4. Find duplicate scenario names (`duplicate-scenario-names`) +### 4. Find duplicate scenario titles (`duplicate-scenario-titles`) In Behat, each scenario should have a unique name to ensure clarity and avoid confusion during test execution and later debugging. This rule identifies scenarios that share the same name within your feature files: diff --git a/composer.json b/composer.json index 9bf539a48..c2008b020 100644 --- a/composer.json +++ b/composer.json @@ -7,8 +7,9 @@ ], "require": { "php": "^8.3", - "nette/utils": "^4.0", + "behat/gherkin": "^4.16", "entropy/entropy": "dev-main", + "nette/utils": "^4.0", "nikic/php-parser": "^5.6", "symfony/finder": "^7.0", "webmozart/assert": "^1.12" @@ -19,6 +20,7 @@ "phpunit/phpunit": "^12.5", "rector/jack": "^0.5.1", "rector/rector": "^2.3.2", + "shipmonk/composer-dependency-analyser": "^1.8", "symplify/easy-coding-standard": "^13.0", "tomasvotruba/class-leak": "^2.1", "tracy/tracy": "^2.10" diff --git a/src/Analyzer/DuplicatedScenarioNamesAnalyzer.php b/src/Analyzer/DuplicatedScenarioTitlesAnalyzer.php similarity index 52% rename from src/Analyzer/DuplicatedScenarioNamesAnalyzer.php rename to src/Analyzer/DuplicatedScenarioTitlesAnalyzer.php index ad69fb98b..ce8a082db 100644 --- a/src/Analyzer/DuplicatedScenarioNamesAnalyzer.php +++ b/src/Analyzer/DuplicatedScenarioTitlesAnalyzer.php @@ -5,14 +5,17 @@ namespace Rector\Behastan\Analyzer; use Entropy\Attributes\RelatedTest; -use Entropy\Utils\Regex; -use Rector\Behastan\Tests\Analyzer\DuplicatedScenarioNamesAnalyzer\DuplicatedScenarioNamesAnalyzerTest; +use Rector\Behastan\Gherkin\GherkinParser; +use Rector\Behastan\Tests\Analyzer\DuplicatedScenarioNamesAnalyzer\DuplicatedScenarioTitlesAnalyzerTest; use Symfony\Component\Finder\SplFileInfo; -#[RelatedTest(DuplicatedScenarioNamesAnalyzerTest::class)] -final class DuplicatedScenarioNamesAnalyzer +#[RelatedTest(DuplicatedScenarioTitlesAnalyzerTest::class)] +final readonly class DuplicatedScenarioTitlesAnalyzer { - private const string SCENARIO_NAME_REGEX = '#\s+Scenario:\s+(?.*?)\n#'; + public function __construct( + private GherkinParser $gherkinParser + ) { + } /** * @param SplFileInfo[] $featureFiles @@ -23,12 +26,10 @@ public function analyze(array $featureFiles): array $scenarioNamesToFiles = []; foreach ($featureFiles as $featureFile) { - // match Scenario: "" - $matches = Regex::matchAll($featureFile->getContents(), self::SCENARIO_NAME_REGEX); + $featureGherkin = $this->gherkinParser->parseFile($featureFile->getRealPath()); - foreach ($matches as $match) { - $scenarioName = $match['name']; - $scenarioNamesToFiles[$scenarioName][] = $featureFile->getRealPath(); + foreach ($featureGherkin->getScenarios() as $scenario) { + $scenarioNamesToFiles[$scenario->getTitle()][] = $featureFile->getRealPath(); } } diff --git a/src/Enum/RuleIdentifier.php b/src/Enum/RuleIdentifier.php index c508a6552..3f89a4dd8 100644 --- a/src/Enum/RuleIdentifier.php +++ b/src/Enum/RuleIdentifier.php @@ -8,7 +8,7 @@ final class RuleIdentifier { public const string DUPLICATED_CONTENTS = 'duplicated-contents'; - public const string DUPLICATED_SCENARIO_NAMES = 'duplicated-scenario-names'; + public const string DUPLICATED_SCENARIO_TITLES = 'duplicated-scenario-titles'; public const string DUPLICATED_PATTERNS = 'duplicated-patterns'; diff --git a/src/Gherkin/GherkinParser.php b/src/Gherkin/GherkinParser.php new file mode 100644 index 000000000..a8dce5118 --- /dev/null +++ b/src/Gherkin/GherkinParser.php @@ -0,0 +1,44 @@ + [ + 'feature' => 'Feature', + 'background' => 'Background', + 'scenario' => 'Scenario', + 'scenario_outline' => 'Scenario Outline|Scenario Template', + 'examples' => 'Examples|Scenarios', + 'given' => 'Given', + 'when' => 'When', + 'then' => 'Then', + 'and' => 'And', + 'but' => 'But', + ], + ]); + + $this->parser = new Parser(new Lexer($arrayKeywords)); + } + + public function parseFile(string $filePath): FeatureNode + { + $featureNode = $this->parser->parseFile($filePath); + Assert::isInstanceOf($featureNode, FeatureNode::class); + + return $featureNode; + } +} diff --git a/src/Rule/DuplicatedScenarioNamesRule.php b/src/Rule/DuplicatedScenarioTitleRule.php similarity index 82% rename from src/Rule/DuplicatedScenarioNamesRule.php rename to src/Rule/DuplicatedScenarioTitleRule.php index 68606fda5..2e424e855 100644 --- a/src/Rule/DuplicatedScenarioNamesRule.php +++ b/src/Rule/DuplicatedScenarioTitleRule.php @@ -4,17 +4,17 @@ namespace Rector\Behastan\Rule; -use Rector\Behastan\Analyzer\DuplicatedScenarioNamesAnalyzer; +use Rector\Behastan\Analyzer\DuplicatedScenarioTitlesAnalyzer; use Rector\Behastan\Contract\RuleInterface; use Rector\Behastan\Enum\RuleIdentifier; use Rector\Behastan\ValueObject\PatternCollection; use Rector\Behastan\ValueObject\RuleError; use Symfony\Component\Finder\SplFileInfo; -final readonly class DuplicatedScenarioNamesRule implements RuleInterface +final readonly class DuplicatedScenarioTitleRule implements RuleInterface { public function __construct( - private DuplicatedScenarioNamesAnalyzer $duplicatedScenarioNamesAnalyzer + private DuplicatedScenarioTitlesAnalyzer $duplicatedScenarioNamesAnalyzer ) { } @@ -47,6 +47,6 @@ public function process( public function getIdentifier(): string { - return RuleIdentifier::DUPLICATED_SCENARIO_NAMES; + return RuleIdentifier::DUPLICATED_SCENARIO_TITLES; } } diff --git a/tests/Analyzer/DuplicatedScenarioNamesAnalyzer/DuplicatedScenarioNamesAnalyzerTest.php b/tests/Analyzer/DuplicatedScenarioNamesAnalyzer/DuplicatedScenarioNamesAnalyzerTest.php deleted file mode 100644 index ec3d2087e..000000000 --- a/tests/Analyzer/DuplicatedScenarioNamesAnalyzer/DuplicatedScenarioNamesAnalyzerTest.php +++ /dev/null @@ -1,36 +0,0 @@ -duplicatedScenarioNamesAnalyzer = $this->make(DuplicatedScenarioNamesAnalyzer::class); - } - - public function test(): void - { - $featureFiles = BehatMetafilesFinder::findFeatureFiles([__DIR__ . '/Fixture']); - $this->assertCount(2, $featureFiles); - - $duplicatedScenarioNamesToFiles = $this->duplicatedScenarioNamesAnalyzer->analyze($featureFiles); - - $this->assertCount(1, $duplicatedScenarioNamesToFiles); - $this->assertArrayHasKey('Same scenario name', $duplicatedScenarioNamesToFiles); - - $givenFiles = $duplicatedScenarioNamesToFiles['Same scenario name']; - - $this->assertSame([__DIR__ . '/Fixture/some.feature', __DIR__ . '/Fixture/another.feature'], $givenFiles); - } -} diff --git a/tests/Analyzer/DuplicatedScenarioNamesAnalyzer/DuplicatedScenarioTitlesAnalyzerTest.php b/tests/Analyzer/DuplicatedScenarioNamesAnalyzer/DuplicatedScenarioTitlesAnalyzerTest.php new file mode 100644 index 000000000..ef4e58f6c --- /dev/null +++ b/tests/Analyzer/DuplicatedScenarioNamesAnalyzer/DuplicatedScenarioTitlesAnalyzerTest.php @@ -0,0 +1,49 @@ +duplicatedScenarioNamesAnalyzer = $this->make(DuplicatedScenarioTitlesAnalyzer::class); + } + + public function testSpot(): void + { + $featureFiles = BehatMetafilesFinder::findFeatureFiles([__DIR__ . '/Fixture/simple']); + $this->assertCount(2, $featureFiles); + + $duplicatedScenarioNamesToFiles = $this->duplicatedScenarioNamesAnalyzer->analyze($featureFiles); + + $this->assertCount(1, $duplicatedScenarioNamesToFiles); + $this->assertArrayHasKey('Same scenario name', $duplicatedScenarioNamesToFiles); + + $givenFiles = $duplicatedScenarioNamesToFiles['Same scenario name']; + + $this->assertSame( + [__DIR__ . '/Fixture/simple/some.feature', __DIR__ . '/Fixture/simple/another.feature'], + $givenFiles + ); + } + + public function testSkipSecondLineDifferent(): void + { + $featureFiles = BehatMetafilesFinder::findFeatureFiles([__DIR__ . '/Fixture/no-multi-line']); + $this->assertCount(2, $featureFiles); + + $duplicatedScenarioNamesToFiles = $this->duplicatedScenarioNamesAnalyzer->analyze($featureFiles); + + $this->assertCount(0, $duplicatedScenarioNamesToFiles); + } +} diff --git a/tests/Analyzer/DuplicatedScenarioNamesAnalyzer/Fixture/no-multi-line/another.feature b/tests/Analyzer/DuplicatedScenarioNamesAnalyzer/Fixture/no-multi-line/another.feature new file mode 100644 index 000000000..1aaa9439b --- /dev/null +++ b/tests/Analyzer/DuplicatedScenarioNamesAnalyzer/Fixture/no-multi-line/another.feature @@ -0,0 +1,4 @@ +Feature: Some feature + + Scenario: Same scenario name But second line is different + Given that diff --git a/tests/Analyzer/DuplicatedScenarioNamesAnalyzer/Fixture/no-multi-line/some.feature b/tests/Analyzer/DuplicatedScenarioNamesAnalyzer/Fixture/no-multi-line/some.feature new file mode 100644 index 000000000..dfd56c829 --- /dev/null +++ b/tests/Analyzer/DuplicatedScenarioNamesAnalyzer/Fixture/no-multi-line/some.feature @@ -0,0 +1,4 @@ +Feature: Another feature + + Scenario: Same scenario name But second line is not the same + Given This diff --git a/tests/Analyzer/DuplicatedScenarioNamesAnalyzer/Fixture/another.feature b/tests/Analyzer/DuplicatedScenarioNamesAnalyzer/Fixture/simple/another.feature similarity index 100% rename from tests/Analyzer/DuplicatedScenarioNamesAnalyzer/Fixture/another.feature rename to tests/Analyzer/DuplicatedScenarioNamesAnalyzer/Fixture/simple/another.feature diff --git a/tests/Analyzer/DuplicatedScenarioNamesAnalyzer/Fixture/some.feature b/tests/Analyzer/DuplicatedScenarioNamesAnalyzer/Fixture/simple/some.feature similarity index 100% rename from tests/Analyzer/DuplicatedScenarioNamesAnalyzer/Fixture/some.feature rename to tests/Analyzer/DuplicatedScenarioNamesAnalyzer/Fixture/simple/some.feature