Skip to content

Commit e260379

Browse files
authored
fix: Reinstate support for legacy tag filters without @ prefix (#407)
As reported by @carlos-granados in #406, the 4.16.0 release broke tag filtering where the tag expression does not contain `@` symbols [e.g. in Behat](https://github.com/Behat/Behat/actions/runs/20028377501/job/57431075976). Behat's filter expression in the failing feature is arguably incorrect: * It uses `new TagFilter('tag2')` (note no leading `@`) - the PHP config object is obviously new but the value has been the same since [the feature was first added in 2014](https://github.com/Behat/Behat/pull/434/files#diff-ca77b0b1e7860ced7eb08cc40f9d6648acc0760a0be45c5fd76a8a19992adbebR92) * All the Behat documentation shows expressing tag filters with the leading `@` e.g. https://docs.behat.org/en/latest/user_guide/configuration/suites.html#suite-filters * The actual implementation of tag filtering is done in Behat/Gherkin. Gherkin's [TagFilterTest only tests tag expressions where the leading `@` is included](https://github.com/Behat/Gherkin/blame/297297343c125d058e18b3f03a24ebb32f70b69d/tests/Filter/TagFilterTest.php) and that appears to have been the case from the beginning. As far as I can see, `TagFilter` has therefore never been meant to accept that expression, and it's been working by accident. However, we obviously don't validate the expression, and we have decided we should reintroduce support for it if even Behat has been relying on it until now. Note that this syntax is officially deprecated and will be removed in the next major - see #408
1 parent 78e7755 commit e260379

File tree

3 files changed

+95
-1
lines changed

3 files changed

+95
-1
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77
This project follows the [Behat release and version support policies]
88
(https://docs.behat.org/en/latest/releases.html).
99

10+
# [4.16.1] - 2025-12-08
11+
12+
### Fixed
13+
14+
* Reinstate support for tag filter expressions without a leading `@` (e.g. `wip&&~slow` instead of `@wip&&~@slow`).
15+
This syntax was never officially supported, but previously worked and was broken by 4.16.0. We have temporarily
16+
fixed this, but it is deprecated and will be removed in the next major version.
17+
1018
# [4.16.0] - 2025-12-05
1119

1220
### Changed
@@ -588,6 +596,7 @@ This project follows the [Behat release and version support policies]
588596
- 47 brand new translations (see i18n)
589597
- Full test suite for everything from AST nodes to translations
590598

599+
[4.16.1]: https://github.com/Behat/Gherkin/compare/v4.16.0...v4.16.1
591600
[4.16.0]: https://github.com/Behat/Gherkin/compare/v4.15.0...v4.16.0
592601
[4.15.0]: https://github.com/Behat/Gherkin/compare/v4.14.0...v4.15.0
593602
[4.14.0]: https://github.com/Behat/Gherkin/compare/v4.13.0...v4.14.0

src/Filter/TagFilter.php

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ class TagFilter extends ComplexFilter
2828

2929
public function __construct(string $filterString)
3030
{
31-
$this->filterString = trim($filterString);
31+
$filterString = trim($filterString);
32+
$fixedFilterString = $this->fixLegacyFilterStringWithoutPrefixes($filterString);
33+
// @todo trigger a deprecation here $filterString !== $fixedFilterString
34+
$this->filterString = $fixedFilterString;
3235

3336
if (preg_match('/\s/u', $this->filterString)) {
3437
trigger_error(
@@ -38,6 +41,40 @@ public function __construct(string $filterString)
3841
}
3942
}
4043

44+
/**
45+
* Fix tag expressions where the filter string does not include the `@` prefixes.
46+
*
47+
* e.g. `new TagFilter('wip&&~slow')` rather than `new TagFilter('@wip&&~@slow')`. These were historically
48+
* supported, although not officially, and have been reinstated to solve a BC issue. This syntax will be deprecated
49+
* and removed in future.
50+
*/
51+
private function fixLegacyFilterStringWithoutPrefixes(string $filterString): string
52+
{
53+
if ($filterString === '') {
54+
return '';
55+
}
56+
57+
$allParts = [];
58+
foreach (explode('&&', $filterString) as $andTags) {
59+
$allParts[] = implode(
60+
',',
61+
array_map(
62+
fn (string $tag): string => match (true) {
63+
// Valid - tag filter contains the `@` prefix
64+
str_starts_with($tag, '@'),
65+
str_starts_with($tag, '~@') => $tag,
66+
// Invalid / legacy cases - insert the missing `@` prefix in the right place
67+
str_starts_with($tag, '~') => '~@' . substr($tag, 1),
68+
default => '@' . $tag,
69+
},
70+
explode(',', $andTags),
71+
),
72+
);
73+
}
74+
75+
return implode('&&', $allParts);
76+
}
77+
4178
/**
4279
* Filters feature according to the filter.
4380
*

tests/Filter/TagFilterTest.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,54 @@ public function testFilterFeatureWithTaggedExamples(): void
279279
$this->assertEquals([$exampleTableNode3], $scenarioInterfaces[1]->getExampleTables());
280280
}
281281

282+
/**
283+
* @phpstan-return list<array{string, list<string>, bool}>
284+
*/
285+
public static function providerMatchWithNoPrefixInFilter(): array
286+
{
287+
// This is officially unsupported (but potentially widespread) use of a filter expression that does not
288+
// contain the `@` prefix. Behat's documentation shows that the `@` prefix should be provided - however Behat's
289+
// own tests include an example where this is not the case, which has been passing. Gherkin has not historically
290+
// validated the tag expression, so we will continue to support these for now.
291+
// These cases rely on the bulk of the coverage being provided by the other tests, and the knowledge that the
292+
// implementation ultimately uses the same logic to compare tags from all types of nodes.
293+
// They are only intended to be temporary until we enforce that filter expressions are valid.
294+
return [
295+
['wip', [], false],
296+
['wip', ['slow'], false],
297+
['wip', ['wip'], true],
298+
['wip', ['slow', 'wip'], true],
299+
['tag1&&~tag2&&tag3', [], false],
300+
['tag1&&~tag2&&tag3', ['tag1'], false],
301+
['tag1&&~tag2&&tag3', ['tag1', 'tag3'], true],
302+
['tag1&&~tag2&&tag3', ['tag1', 'tag2'], false],
303+
['tag1&&~tag2&&tag3', ['tag1', 'tag4'], false],
304+
['tag1&&~tag2&&tag3', ['tag1', 'tag2', 'tag3'], false],
305+
// Also cover when the file was parsed in compatibility mode including the prefix
306+
['wip', [], false],
307+
['wip', ['@slow'], false],
308+
['wip', ['@wip'], true],
309+
['wip', ['@slow', '@wip'], true],
310+
['tag1&&~tag2&&tag3', [], false],
311+
['tag1&&~tag2&&tag3', ['@tag1'], false],
312+
['tag1&&~tag2&&tag3', ['@tag1', '@tag3'], true],
313+
['tag1&&~tag2&&tag3', ['@tag1', '@tag2'], false],
314+
['tag1&&~tag2&&tag3', ['@tag1', '@tag4'], false],
315+
['tag1&&~tag2&&tag3', ['@tag1', '@tag2', '@tag3'], false],
316+
];
317+
}
318+
319+
/**
320+
* @phpstan-param list<string> $tags
321+
*/
322+
#[DataProvider('providerMatchWithNoPrefixInFilter')]
323+
public function testItMatchesWhenFilterDoesNotContainPrefix(string $filter, array $tags, bool $expect): void
324+
{
325+
$feature = new FeatureNode(null, null, $tags, null, [], '', '', null, 1);
326+
$tagFilter = new TagFilter($filter);
327+
$this->assertSame($expect, $tagFilter->isFeatureMatch($feature));
328+
}
329+
282330
public function testFilterWithWhitespaceIsDeprecated(): void
283331
{
284332
$this->expectDeprecationError();

0 commit comments

Comments
 (0)