Skip to content

Commit 8c48853

Browse files
authored
Merge branch 'master' into dialect_api
2 parents 5f61d8d + fad95fa commit 8c48853

File tree

5 files changed

+200
-68
lines changed

5 files changed

+200
-68
lines changed

composer.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"require-dev": {
2222
"symfony/yaml": "^5.4 || ^6.4 || ^7.0",
2323
"phpunit/phpunit": "^10.5",
24-
"cucumber/gherkin-monorepo": "dev-gherkin-v32.1.1",
24+
"cucumber/gherkin-monorepo": "dev-gherkin-v32.1.2",
2525
"friendsofphp/php-cs-fixer": "^3.65",
2626
"phpstan/phpstan": "^2",
2727
"phpstan/extension-installer": "^1",
@@ -56,16 +56,16 @@
5656
"type": "package",
5757
"package": {
5858
"name": "cucumber/gherkin-monorepo",
59-
"version": "dev-gherkin-v32.1.1",
59+
"version": "dev-gherkin-v32.1.2",
6060
"source": {
6161
"type": "git",
6262
"url": "https://github.com/cucumber/gherkin.git",
63-
"reference": "4fad962021cc96b4b006f711527ab07215df2d22"
63+
"reference": "e9ae8a8a7d84e5bb806889d8df2abe45c35fa84a"
6464
},
6565
"dist": {
6666
"type": "zip",
67-
"url": "https://api.github.com/repos/cucumber/gherkin/zipball/4fad962021cc96b4b006f711527ab07215df2d22",
68-
"reference": "4fad962021cc96b4b006f711527ab07215df2d22"
67+
"url": "https://api.github.com/repos/cucumber/gherkin/zipball/e9ae8a8a7d84e5bb806889d8df2abe45c35fa84a",
68+
"reference": "e9ae8a8a7d84e5bb806889d8df2abe45c35fa84a"
6969
}
7070
}
7171
}

src/GherkinCompatibilityMode.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Behat Gherkin Parser.
5+
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
11+
namespace Behat\Gherkin;
12+
13+
enum GherkinCompatibilityMode: string
14+
{
15+
case LEGACY = 'legacy';
16+
17+
/**
18+
* Note: The gherkin-32 parsing mode is not yet complete, and further behaviour changes are expected.
19+
*
20+
* @see https://github.com/Behat/Gherkin/issues?q=is%3Aissue%20state%3Aopen%20label%3Acucumber-parity
21+
*/
22+
case GHERKIN_32 = 'gherkin-32';
23+
24+
/**
25+
* @internal
26+
*/
27+
public function shouldRemoveFeatureDescriptionPadding(): bool
28+
{
29+
return match ($this) {
30+
self::LEGACY => true,
31+
default => false,
32+
};
33+
}
34+
}

src/Parser.php

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,15 @@ class Parser
4949

5050
public function __construct(
5151
private readonly Lexer $lexer,
52+
private GherkinCompatibilityMode $compatibilityMode = GherkinCompatibilityMode::LEGACY,
5253
) {
5354
}
5455

56+
public function setGherkinCompatibilityMode(GherkinCompatibilityMode $mode): void
57+
{
58+
$this->compatibilityMode = $mode;
59+
}
60+
5561
/**
5662
* Parses input & returns features array.
5763
*
@@ -218,8 +224,24 @@ protected function parseFeature()
218224
$node = $this->parseExpression();
219225

220226
if (is_string($node)) {
221-
$text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
222-
$description .= ($description !== null ? "\n" : '') . $text;
227+
if ($this->compatibilityMode->shouldRemoveFeatureDescriptionPadding()) {
228+
$text = preg_replace('/^\s{0,' . ($token['indent'] + 2) . '}|\s*$/', '', $node);
229+
$description .= ($description !== null ? "\n" : '') . $text;
230+
continue;
231+
}
232+
233+
if ($node === "\n" && $description === null) {
234+
// Ignore empty lines before the start of the description
235+
continue;
236+
}
237+
238+
// It must be part of the feature description (text & newlines later in the document will be consumed as
239+
// part of parsing Background / Scenario before execution returns to this loop).
240+
$description .= $node;
241+
if ($node !== "\n") {
242+
// Text nodes do not end with a newline, add one. The final trailing newline is rtrimmed below.
243+
$description .= "\n";
244+
}
223245
continue;
224246
}
225247

tests/Cucumber/CompatibilityTest.php

Lines changed: 86 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212

1313
use Behat\Gherkin\Dialect\CucumberDialectProvider;
1414
use Behat\Gherkin\Exception\ParserException;
15+
use Behat\Gherkin\GherkinCompatibilityMode;
1516
use Behat\Gherkin\Lexer;
1617
use Behat\Gherkin\Loader\CucumberNDJsonAstLoader;
17-
use Behat\Gherkin\Node\FeatureNode;
1818
use Behat\Gherkin\Parser;
1919
use FilesystemIterator;
2020
use PHPUnit\Framework\Attributes\DataProvider;
@@ -28,44 +28,75 @@
2828
* Tests the parser against the upstream cucumber/gherkin test data.
2929
*
3030
* @group cucumber-compatibility
31+
*
32+
* @phpstan-type TCucumberParsingTestCase array{mode: GherkinCompatibilityMode, file: SplFileInfo}
33+
* @phpstan-type TKnownIncompatibilityMap array<value-of<GherkinCompatibilityMode>, array<string,string>>
3134
*/
3235
class CompatibilityTest extends TestCase
3336
{
3437
private const GHERKIN_TESTDATA_PATH = __DIR__ . '/../../vendor/cucumber/gherkin-monorepo/testdata';
3538
private const EXTRA_TESTDATA_PATH = __DIR__ . '/extra_testdata';
3639

3740
/**
38-
* @var array<string, string>
41+
* @phpstan-var TKnownIncompatibilityMap
3942
*/
4043
private array $notParsingCorrectly = [
41-
'complex_background.feature' => 'Rule keyword not supported',
42-
'docstrings.feature' => 'Escaped delimiters in docstrings are not unescaped',
43-
'datatables_with_new_lines.feature' => 'Escaped newlines in table cells are not unescaped',
44-
'escaped_pipes.feature' => 'Escaped newlines in table cells are not unescaped',
45-
'rule.feature' => 'Rule keyword not supported',
46-
'rule_with_tag.feature' => 'Rule keyword not supported',
47-
'tags.feature' => 'Rule keyword not supported',
48-
'descriptions.feature' => 'Examples table descriptions not supported',
49-
'descriptions_with_comments.feature' => 'Examples table descriptions not supported',
50-
'feature_keyword_in_scenario_description.feature' => 'Scenario descriptions not supported',
51-
'padded_example.feature' => 'Table padding is not trimmed as aggressively',
52-
'spaces_in_language.feature' => 'Whitespace not supported around language selector',
53-
'rule_without_name_and_description.feature' => 'Rule is wrongly parsed as Description',
54-
'incomplete_background_2.feature' => 'Background descriptions not supported',
44+
'legacy' => [
45+
'complex_background.feature' => 'Rule keyword not supported',
46+
'docstrings.feature' => 'Escaped delimiters in docstrings are not unescaped',
47+
'datatables_with_new_lines.feature' => 'Escaped newlines in table cells are not unescaped',
48+
'escaped_pipes.feature' => 'Escaped newlines in table cells are not unescaped',
49+
'rule.feature' => 'Rule keyword not supported',
50+
'rule_with_tag.feature' => 'Rule keyword not supported',
51+
'tags.feature' => 'Rule keyword not supported',
52+
'descriptions.feature' => 'Examples table descriptions not supported',
53+
'descriptions_with_comments.feature' => 'Examples table descriptions not supported',
54+
'feature_keyword_in_scenario_description.feature' => 'Scenario descriptions not supported',
55+
'padded_example.feature' => 'Table padding is not trimmed as aggressively',
56+
'spaces_in_language.feature' => 'Whitespace not supported around language selector',
57+
'rule_without_name_and_description.feature' => 'Rule is wrongly parsed as Description',
58+
'incomplete_background_2.feature' => 'Background descriptions not supported',
59+
],
60+
'gherkin-32' => [
61+
'complex_background.feature' => 'Rule keyword not supported',
62+
'docstrings.feature' => 'Escaped delimiters in docstrings are not unescaped',
63+
'datatables_with_new_lines.feature' => 'Escaped newlines in table cells are not unescaped',
64+
'escaped_pipes.feature' => 'Escaped newlines in table cells are not unescaped',
65+
'rule.feature' => 'Rule keyword not supported',
66+
'rule_with_tag.feature' => 'Rule keyword not supported',
67+
'tags.feature' => 'Rule keyword not supported',
68+
'descriptions.feature' => 'Examples table descriptions not supported',
69+
'descriptions_with_comments.feature' => 'Examples table descriptions not supported',
70+
'feature_keyword_in_scenario_description.feature' => 'Scenario descriptions not supported',
71+
'padded_example.feature' => 'Table padding is not trimmed as aggressively',
72+
'spaces_in_language.feature' => 'Whitespace not supported around language selector',
73+
'rule_without_name_and_description.feature' => 'Rule is wrongly parsed as Description',
74+
'incomplete_background_2.feature' => 'Background descriptions not supported',
75+
],
5576
];
5677

5778
/**
58-
* @var array<string, string>
79+
* @phpstan-var TKnownIncompatibilityMap
5980
*/
6081
private array $parsedButShouldNotBe = [
61-
'invalid_language.feature' => 'Invalid language is silently ignored',
82+
'legacy' => [
83+
'invalid_language.feature' => 'Invalid language is silently ignored',
84+
],
85+
'gherkin-32' => [
86+
'invalid_language.feature' => 'Invalid language is silently ignored',
87+
],
6288
];
6389

6490
/**
65-
* @var array<string, string>
91+
* @phpstan-var TKnownIncompatibilityMap
6692
*/
6793
private array $deprecatedInsteadOfParseError = [
68-
'whitespace_in_tags.feature' => '/Whitespace in tags is deprecated/',
94+
'legacy' => [
95+
'whitespace_in_tags.feature' => '/Whitespace in tags is deprecated/',
96+
],
97+
'gherkin-32' => [
98+
'whitespace_in_tags.feature' => '/Whitespace in tags is deprecated/',
99+
],
69100
];
70101

71102
private Parser $parser;
@@ -74,10 +105,14 @@ class CompatibilityTest extends TestCase
74105

75106
private static ?StepNodeComparator $stepNodeComparator = null;
76107

108+
private static ?FeatureNodeComparator $featureNodeComparator = null;
109+
77110
public static function setUpBeforeClass(): void
78111
{
79112
self::$stepNodeComparator = new StepNodeComparator();
80113
Factory::getInstance()->register(self::$stepNodeComparator);
114+
self::$featureNodeComparator = new FeatureNodeComparator();
115+
Factory::getInstance()->register(self::$featureNodeComparator);
81116
}
82117

83118
public static function tearDownAfterClass(): void
@@ -86,6 +121,10 @@ public static function tearDownAfterClass(): void
86121
Factory::getInstance()->unregister(self::$stepNodeComparator);
87122
self::$stepNodeComparator = null;
88123
}
124+
if (self::$featureNodeComparator !== null) {
125+
Factory::getInstance()->unregister(self::$featureNodeComparator);
126+
self::$featureNodeComparator = null;
127+
}
89128
}
90129

91130
protected function setUp(): void
@@ -96,36 +135,43 @@ protected function setUp(): void
96135
}
97136

98137
#[DataProvider('goodCucumberFeatures')]
99-
public function testFeaturesParseTheSameAsCucumber(SplFileInfo $file): void
138+
public function testFeaturesParseTheSameAsCucumber(GherkinCompatibilityMode $mode, SplFileInfo $file): void
100139
{
101-
if (isset($this->notParsingCorrectly[$file->getFilename()])) {
102-
$this->markTestIncomplete($this->notParsingCorrectly[$file->getFilename()]);
140+
if (isset($this->notParsingCorrectly[$mode->value][$file->getFilename()])) {
141+
$this->markTestIncomplete($this->notParsingCorrectly[$mode->value][$file->getFilename()]);
103142
}
104143

144+
assert(self::$featureNodeComparator instanceof FeatureNodeComparator);
145+
self::$featureNodeComparator->setGherkinCompatibilityMode($mode);
146+
$this->parser->setGherkinCompatibilityMode($mode);
147+
105148
$gherkinFile = $file->getPathname();
106149
$actual = $this->parser->parse(Filesystem::readFile($gherkinFile), $gherkinFile);
107150
$cucumberFeatures = $this->loader->load($gherkinFile . '.ast.ndjson');
108151

109152
$expected = $cucumberFeatures ? $cucumberFeatures[0] : null;
110153

111154
$this->assertEquals(
112-
$this->normaliseFeature($expected),
113-
$this->normaliseFeature($actual),
114-
Filesystem::readFile($gherkinFile)
155+
$expected,
156+
$actual,
157+
Filesystem::readFile($gherkinFile),
115158
);
116159
}
117160

118161
#[DataProvider('badCucumberFeatures')]
119-
public function testBadFeaturesDoNotParse(SplFileInfo $file): void
162+
public function testBadFeaturesDoNotParse(GherkinCompatibilityMode $mode, SplFileInfo $file): void
120163
{
121-
if (isset($this->parsedButShouldNotBe[$file->getFilename()])) {
122-
$this->markTestIncomplete($this->parsedButShouldNotBe[$file->getFilename()]);
164+
if (isset($this->parsedButShouldNotBe[$mode->value][$file->getFilename()])) {
165+
$this->markTestIncomplete($this->parsedButShouldNotBe[$mode->value][$file->getFilename()]);
123166
}
124167

125168
$gherkinFile = $file->getPathname();
169+
$this->parser->setGherkinCompatibilityMode($mode);
126170

127-
if (isset($this->deprecatedInsteadOfParseError[$file->getFilename()])) {
128-
$this->expectDeprecationErrorMatches($this->deprecatedInsteadOfParseError[$file->getFilename()]);
171+
if (isset($this->deprecatedInsteadOfParseError[$mode->value][$file->getFilename()])) {
172+
$this->expectDeprecationErrorMatches(
173+
$this->deprecatedInsteadOfParseError[$mode->value][$file->getFilename()],
174+
);
129175
} else {
130176
// Note that the exception message is not part of compatibility testing and therefore cannot be checked.
131177
$this->expectException(ParserException::class);
@@ -135,7 +181,7 @@ public function testBadFeaturesDoNotParse(SplFileInfo $file): void
135181
}
136182

137183
/**
138-
* @return iterable<string, array{file: SplFileInfo}>
184+
* @phpstan-return iterable<string, TCucumberParsingTestCase>
139185
*/
140186
public static function goodCucumberFeatures(): iterable
141187
{
@@ -144,7 +190,7 @@ public static function goodCucumberFeatures(): iterable
144190
}
145191

146192
/**
147-
* @return iterable<string, array{file: SplFileInfo}>
193+
* @phpstan-return iterable<string, TCucumberParsingTestCase>
148194
*/
149195
public static function badCucumberFeatures(): iterable
150196
{
@@ -153,7 +199,7 @@ public static function badCucumberFeatures(): iterable
153199
}
154200

155201
/**
156-
* @return iterable<string, array{file: SplFileInfo}>
202+
* @phpstan-return iterable<string, TCucumberParsingTestCase>
157203
*/
158204
private static function getCucumberFeatures(string $folder): iterable
159205
{
@@ -163,37 +209,16 @@ private static function getCucumberFeatures(string $folder): iterable
163209
*/
164210
foreach ($fileIterator as $file) {
165211
if ($file->isFile() && $file->getExtension() === 'feature') {
166-
yield $file->getFilename() => ['file' => $file];
212+
foreach (GherkinCompatibilityMode::cases() as $mode) {
213+
yield $file->getFilename() . ' (' . $mode->value . ')' => [
214+
'mode' => $mode,
215+
'file' => $file,
216+
];
217+
}
167218
}
168219
}
169220
}
170221

171-
/**
172-
* Remove features that aren't present in the cucumber source.
173-
*/
174-
private function normaliseFeature(?FeatureNode $feature): ?FeatureNode
175-
{
176-
if (is_null($feature)) {
177-
return null;
178-
}
179-
180-
return new FeatureNode(
181-
$feature->getTitle(),
182-
// We currently handle whitespace in feature descriptions differently to cucumber
183-
// https://github.com/Behat/Gherkin/issues/209
184-
// We need to be able to ignore that difference so that we can still run cucumber tests that
185-
// include a description but are covering other features.
186-
$feature->getDescription() === null ? null : preg_replace('/^\s+/m', '', $feature->getDescription()),
187-
$feature->getTags(),
188-
$feature->getBackground(),
189-
$feature->getScenarios(),
190-
$feature->getKeyword(),
191-
$feature->getLanguage(),
192-
$feature->getFile(),
193-
$feature->getLine(),
194-
);
195-
}
196-
197222
private function expectDeprecationErrorMatches(string $message): void
198223
{
199224
set_error_handler(

0 commit comments

Comments
 (0)