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